[AWS] API Gateway + Lambda for Ruby + S3 + Slack通知をSAMで構築

これは[Fusic Advent Calendar 2018](https://qiita.com/advent-calendar/2018/fusic)の8日目の記事です。
昨日の記事は...なんか上がってないので、もう少しお待ちくださいませ。

ちなみに一昨日[6日目の記事](https://tech.fusic.co.jp/cloud/sayonara-mesh-hellp-transit-gateway/)はAWS re:Invent2018に参加してきたメンバーが発表されたサービスの中で一番ときめいたという「Transit Gateway」について説明しています。どうぞ併せてご覧ください。

## 社内ツール『バボちゃん3』は今日も元気です
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f32323536372f62376132643263642d663037352d356134352d353664352d6533393533646234313030392e706e67.png](https://qiita-image-store.s3.amazonaws.com/0/22567/687faaf1-1f34-7097-6d52-a9d1ffa1422c.png)
[Backlog WebhookをAmazon API Gateway+AWS LambdaでSlackに通知する](https://qiita.com/withelmo/items/70d0617e95080aff64da)
昔こんな記事を書きましたが、弊社ではプロジェクトで[Backlog](https://backlog.com/ja/)と[Slack](https://slack.com/intl/ja-jp/)をよく使用します。
Backlogでのチケット起票やコメントは知りたいけど、逐一メールチェックはしたくない、そんな気持ちから最初は[Heroku](https://jp.heroku.com/)上で稼働するアプリとして生まれました。
その後、Herokuの無料枠が変わって、24時間稼働が難しくなったタイミングでAWS Lambda for Node.jsで作り直したのが『バボちゃん2』でした。
※ちなみにバボちゃんは、`バ`ックログ・`ボ`ットの略称です^^

その後、AWS LambdaがPython3に対応したので、嬉しくなってつい作り直しちゃったのが『バボちゃん3』で、いまも元気にたくさんのプロジェクトのBacklogチケットをせっせとSlackにお届けしております。

## AWS re:Invent2018で一番ときめいたのはLambda for Ruby
[Announcing Ruby Support for AWS Lambda](https://aws.amazon.com/jp/blogs/compute/announcing-ruby-support-for-aws-lambda/)
ラスベガスに行っていた弊社メンバーから「なんかLambdaでなんかあるかも！」という情報が来たのが日本時間11/29 お昼すぎ。そわそわしながらも熟睡して翌朝(11/30)起きると社内Slackチャンネルでリリースを知りました。喜びですね。

### やるしかない！『バボちゃん4』  
ということで、本ポストではバボちゃんで必要な要素を一通り試してみようと思います。
さらにやろうやろうと思いつつ、やれていなかったAWS SAMも使ってみました。

## 環境構築
開発・デプロイ環境はMacです。  

### AWS SAM
以前はaws-sam-localというやつでしたが、先日バージョンアップしてaws-sam-cliになったようです。 
https://aws.amazon.com/about-aws/whats-new/2018/04/aws-sam-cli-releases-new-init-command/

そのため以前使っていた場合はアンインストールする必要があります。
自分はここでちょっとハマりました。後述するbrewでのインストールは問題なくできてしまいますが、バージョンが0.2.11のままでRuntimeにRubyが使えません。

```shell

$ sam
A newer version of the AWS SAM CLI is available!
Your version:   0.2.11
Latest version: 0.8.1
See https://github.com/awslabs/aws-sam-local for upgrade instructions
(snip)

$ $ npm -g ls | grep sam
├─┬ aws-sam-local@0.2.11
```
自分の場合はnpmでglobalにインストールしていたようです。
入れるだけ入れて何もやってなかったので、記憶になく、ここに至るまでちょっと手間取りました（汗

####  aws-sam-localのアンインストール
```shell
$ npm uninstall -g aws-sam-local
```

#### aws-sam-cliインストール
```shell

$ brew update
$ brew tap aws/tap
$ brew install aws-sam-cli
$ sam --version
SAM CLI, version 0.8.1
```
（参考）

- https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html  
- https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-install-mac.html

### Docker for macOS
こちらは公式やその他記事を参照ください。  
（参考）

- https://store.docker.com/editions/community/docker-ce-desktop-mac

### Ruby2.5+
AWS Lambda上では `ruby2.5` と書いてますが、手元にあった `2.5.1` でも特に問題はなく 
こちらもインストールについては公式やその他記事を参照ください。
bundlerのインストールもお忘れなく。

（参考）

- https://github.com/rbenv/rbenv

### aws-cli
これは不要なのかもしれないですが、あったほうが検証とかもろもろしやすいですよね。あとsamでパッケージしたり、デプロイしたりするときのクレデンシャル情報としてaws-cliのprofileを使用しました。

```shell
$ brew install awscli
```

## Hello world
まずはsamでテンプレートとかをgenerateします。

```shell
$ sam init --runtime ruby2.5 --name hello_ruby
[+] Initializing project structure...
[SUCCESS] - Read hello_ruby/README.md for further instructions on how to proceed
[*] Project initialization is now complete
$ tree .
.
└── hello_ruby
    ├── Gemfile
    ├── README.md
    ├── hello_world
    │   └── app.rb
    ├── template.yaml
    └── tests
        └── unit
            └── test_handler.rb

4 directories, 5 files
```
おぉ、RuntimeでRubyを選択できた！
更に`template.yaml`を見ると、デフォルトでAPI Gateway + Lambda連携になってました。便利。
丁寧にREAMD.mdが書かれているので、その通りにすれば大体うまくいきます。
せっかくなので、このサンプルでローカル実行とデプロイして動作確認してみましょう。

### ローカル実行
生成されたRubyアプリを見ると、

```rb
require 'httparty'
require 'json'

def lambda_handler(event:, context:)
begin
    response = HTTParty.get('http://checkip.amazonaws.com/')
  rescue HTTParty::Error => error
    puts error.inspect
    raise error
  end
        		
  return {
    :statusCode => response.code,
    :body => {
      :message => "Hello World!",
      :location => response.body
    }.to_json
  }
end
```

`httparty`が使われているので、Gemインストールが必要ですね。  

```
$ bundle install --path hello_world/vendor/bundle
```

ではLambda単体で動かしてみましょう。

```
$ sam local invoke HelloWorldFunction --no-event
2018-12-07 08:32:15 Invoking app.lambda_handler (ruby2.5)

Fetching lambci/lambda:ruby2.5 Docker container image......
2018-12-07 08:32:19 Mounting /.../hello_ruby/hello_world as /var/task:ro inside runtime container
START RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72 Version: $LATEST
END RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72
REPORT RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72  Duration: 527.55 ms     Billed Duration: 600 ms Memory Size: 128 MB     Max Memory Used: 30 MB
{"statusCode":200,"body":"{\"message\":\"Hello World!\",\"location\":\"himitsu-no-akko-chan\\n\"}"}
```
アプリは自身のIPアドレスを取得して返却するもののようですね。

次にAPI Gateway経由で実行してみましょう。
aws-sam-cliにはローカルで発火できる疑似Eventを簡単に生成することができます。便利！  

```
$ sam local generate-event apigateway aws-proxy > event_file_apigateway.json
{
  "body": "eyJ0ZXN0IjoiYm9keSJ9",
  "resource": "/{proxy+}",
  "path": "/path/to/resource",
  "httpMethod": "POST",
  "isBase64Encoded": true,
  "queryStringParameters": {
    "foo": "bar"
  },
  "pathParameters": {
    "proxy": "/path/to/resource"
  },
  "stageVariables": {
    "baz": "qux"
  },
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate, sdch",
    "Accept-Language": "en-US,en;q=0.8",
    "Cache-Control": "max-age=0",
    "CloudFront-Forwarded-Proto": "https",
    "CloudFront-Is-Desktop-Viewer": "true",
    "CloudFront-Is-Mobile-Viewer": "false",
    "CloudFront-Is-SmartTV-Viewer": "false",
    "CloudFront-Is-Tablet-Viewer": "false",
    "CloudFront-Viewer-Country": "US",
    "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
    "Upgrade-Insecure-Requests": "1",
    "User-Agent": "Custom User Agent String",
    "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
    "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
    "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
    "X-Forwarded-Port": "443",
    "X-Forwarded-Proto": "https"
  },
  "requestContext": {
    "accountId": "123456789012",
    "resourceId": "123456",
    "stage": "prod",
    "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
    "requestTime": "09/Apr/2015:12:34:56 +0000",
    "requestTimeEpoch": 1428582896000,
    "identity": {
      "cognitoIdentityPoolId": null,
      "accountId": null,
      "cognitoIdentityId": null,
      "caller": null,
      "accessKey": null,
      "sourceIp": "127.0.0.1",
      "cognitoAuthenticationType": null,
      "cognitoAuthenticationProvider": null,
      "userArn": null,
      "userAgent": "Custom User Agent String",
      "user": null
    },
    "path": "/prod/path/to/resource",
    "resourcePath": "/{proxy+}",
    "httpMethod": "POST",
    "apiId": "1234567890",
    "protocol": "HTTP/1.1"
  }
}
```

ではAPI GatewayにEventを送ってローカル実行してみましょう。  

```
$ sam local invoke HelloWorldFunction -e event_file_apigateway.json
2018-12-07 08:38:13 Invoking app.lambda_handler (ruby2.5)

Fetching lambci/lambda:ruby2.5 Docker container image......
2018-12-07 08:38:17 Mounting /.../hello_ruby/hello_world as /var/task:ro inside runtime container
START RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72 Version: $LATEST
END RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72
REPORT RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72  Duration: 490.94 ms     Billed Duration: 500 ms Memory Size: 128 MB     Max Memory Used: 30 MB
{"statusCode":200,"body":"{\"message\":\"Hello World!\",\"location\":\"higashino-keigo\\n\"}"}
```
まあ、直接lambda動かしたのと差はないですね。

## パッケージ&デプロイ
これもsamでちょちょいと。  
その前に、パッケージするためのS3バケットが必要なので、ご準備ください。

### パッケージ用にbundle install
```shell
$ bundle install --deployment --path hello_world/vendor/bundle
```
gemもパッケージするために `--deployment` を付けてinstallします。

### パッケージ
```
$ sam package --template-file template.yaml --output-template-file packaged.yaml --s3-bucket <YOUR_BUCKET_NAME> --profile <YOUR_PROFILE_NAME>
Uploading to xxxxxxxxxxxxxxxxxxx  643003 / 643003.0  (100.00%)
Successfully packaged artifacts and wrote output template to file packaged.yaml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /.../hello_ruby/packaged.yaml --stack-name <YOUR STACK NAME>
```

### デプロイ
```
$ sam deploy --template-file packaged.yaml --stack-name <YOUR_STACK_NAME> --capabilities CAPABILITY_IAM --profile <YOUR_PROFILE_NAME>

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - ServerlessFunctionHelloRuby
```

CloudFormationのスタック名は任意でOKです。

Amazon Management Consoleで確認したらちゃんとできていました

<img width="1019" alt="API_Gateway_2.png" src="https://qiita-image-store.s3.amazonaws.com/0/22567/2c864083-6961-c451-231e-339895e9cc68.png">


できたAPIをブラウザでアクセスしてみたらちゃんと動きましたー！

<img width="645" alt="apigateway_lambda_hello.png" src="https://qiita-image-store.s3.amazonaws.com/0/22567/e8c8f6a0-202e-92dd-0f66-84eb964639f0.png">


## （バボちゃん4を意識して）S3 + Slack通知を追加
### tempalte.yaml
使用するS3バケットとLambdaからの権限付与するのと、Bucket名とSlack Incoming WebhookのURLをアプリで扱えるように環境変数で渡します。  
またBacklogからのWebhookはPOSTになるので、API GatewayのMethodも変更します。  

```yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  babo_ruby
  Sample SAM Template for babo_ruby

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3
Resources:
  Bucket:
    Type: AWS::S3::Bucket
  BaboFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: babo/
      Handler: app.lambda_handler
      Runtime: ruby2.5
      Policies:
        - S3CrudPolicy:
            BucketName: !Ref Bucket
      Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object
        Variables:
          BUCKET_NAME: !Ref Bucket
          SLACK_URL: https://hooks.slack.com/services/xxxxxxxxxxxxx
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /webhook
            Method: post
Outputs:
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/webhook/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt BaboFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt BaboFunction.Arn

```

### Gemfile

```rb
source "https://rubygems.org"

gem "aws-sdk-s3"
gem 'slack-notifier'
```

### app.rb
```rb
require 'aws-sdk-s3'
require 'slack-notifier'
require 'json'

def lambda_handler(event:, context:)
  begin
    bucket_name = ENV['BUCKET_NAME']
    slack_url   = ENV['SLACK_URL']

    bl_body = JSON.parse(event['body'])
    bl_project_id = bl_body['project']['id']

    s3 = Aws::S3::Resource.new
    s3_obj = s3.bucket(bucket_name).object("#{bl_project_id}.conf")
    s3_body = JSON.parse(s3_obj.get.body.read)

  rescue => error
    puts error
    raise error
  end

  notifier = Slack::Notifier.new(slack_url, username: 'babo_tester', channel: s3_body['channel_name'])
  res = notifier.ping("『#{bl_body['project']['name']}』で変化がありました")
  return res
end
```

## Backlogの設定
### Webhookエンドポイントの
前述したパッケージ&デプロイします。
実行されたCloudFormation StackのOutputで表示れているAPI Gatewayのエンドポイントをコピペして、Backlogの『プロジェクト設定＞インテグレーション＞Webhook』のWebHook URLに登録して全てのイベントを通知するようにします。

BacklogプロジェクトのIDを確認して、{ProjectID番号}.confにして、作成されたS3バケットにアップロードします。（連携の仕組みは[昔のポスト](https://qiita.com/withelmo/items/70d0617e95080aff64da)を参照ください）  


## 動作確認
Backlogでチケットを登録すると、Slackに通知が来ました！やったね！
<img width="358" alt="Screen Shot 2018-12-08 at 18.51.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/22567/8feadae0-fa73-e0e3-b3d3-f285fd6b4c93.png">

## まとめ
すでにちゃんと動いているものを別言語とか別サービスで作り直すのは無駄かなと思いつつ、新しい技術を覚えるのにはコストが低く、ただHello worldだけに終わらず、もう少し使える知見になりやすいなと考えています。  

アドベントカレンダー担当日までに、バボちゃんのロジック部分を移行するまではできなかったので、今回はここまでです。
Fusicでは普段、[Tech Blog](https://tech.fusic.co.jp/)で分野・大小様々な技術発信をしているので、バボちゃんのロジックを移行して、更に汎用性を持たせるところまでできたらそこでソース公開するポストでもを書こうと目論んでします。
もし使ってみたいなーと思っている方がいたらぜひウォッチしていただければ幸いです。
さて正月休みに気分転換でやるかな。

最後までお読みいただき、ありがとうございました。
それでは良いRuby Lifeを！