【AWS Lambda】Slackに通知を送信する

今回はCloudFormationでLambdaを触ってみるということで、Slackに通知を送信するLambda関数を構築していきたいと思います。

調べてみたところ、Lambdaを構築する場合はCloudFormationの `AWS::Serverless` というマクロ機能を利用するみたいです。
[AWS::Serverless変換](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/transform-aws-serverless.html)

マクロを利用すると「AWS SAM 構文」なるものでLambdaを定義できるようになり、 `aws cloudformation package` コマンド実行時に、通常のCloudFormationテンプレートに展開されます。

マクロ機能を利用するには、テンプレート内で下記のように宣言します。

```template.yml
Transform: AWS::Serverless-2016-10-31
```

# SlackのWebhook URLを取得する
Slack通知を行うためのWebhookURLを取得します。
[Slack での Incoming Webhook の利用](https://slack.com/intl/ja-jp/help/articles/115005265063)

### 1. 「アプリを追加する」から Incoming Webhookを追加

![スクリーンショット 2021-10-05 233317.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92383/2241b14f-b57b-a0db-c215-4fec3b1f62ff.png)

![スクリーンショット 2021-10-05 233201.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92383/2bbce68b-a84b-be3f-a017-878159225fcd.png)

### 2. 投稿したいチャンネルを選択し、インテグレーションの追加をクリック
![スクリーンショット 2021-10-05 234312.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92383/9b8a1408-8ad6-1147-b8b9-0b6abd8001a1.png)

### 3. Webhook URLをコピー

![スクリーンショット 2021-10-05 234335.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92383/fb0db9ec-9c74-f091-6781-909d13b5d637.png)


# テンプレートの実装

ディレクトリ構成

```
- project-notify-slack.yml
- src/
   - lambda_function.py
```

## 1. CloudFormationテンプレート実装

下記3つのリソースを作成します

- Lambda発火の起点となるトピック
- LambdaのDLQ用トピック
- Slackに通知を送信するLambda  
[AWS::Serverless::Function](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html)


```project-notify-slack.yml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Slack notification app

Parameters:
  SlackWebhookUrl:
    Type: String
  SlackChannel:
    Type: String
  Email:
    Type: String

Outputs:
  SlackNotifySnsTopicArn:
    Value: !Ref SlackNotifyTopic

Resources:
  # Lambda発火の起点となるトピック
  # https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-sns-topic.html
  SlackNotifyTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Sub ${AWS::StackName}-topic-notify

  # lambdaのDLQ用トピック
  # https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-sns-topic.html
  DlqTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Sub ${AWS::StackName}-topic-dlq
      # https://docs.aws.amazon.com/sns/latest/api/API_Subscribe.html
      Subscription:
        - Endpoint: !Ref Email
          Protocol: email

  # Slackに通知を送信するLambda
  # https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html
  SlackNotifyFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${AWS::StackName}-function
      CodeUri: ./src
      # lambdaのエントリーポイント(ファイル名.関数名)
      Handler: lambda_function.lambda_handler
      Runtime: python3.8
      MemorySize: 128
      Timeout: 180
      # 環境変数
      Environment:
        Variables:
          SLACK_WEBHOOK_URL: !Ref SlackWebhookUrl
          SLACK_CHANNEL: !Ref SlackChannel
      # Lambdaの処理が規定回数以上失敗した場合の通知先
      # https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-deadletterqueue.html
      DeadLetterQueue:
        Type: SNS
        TargetArn: !Ref DlqTopic
      # 関数実行の起点となるイベントソースを定義
      # https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-eventsource.html
      Events:
        Rule:
          Type: SNS
          Properties:
            Topic: !Ref SlackNotifyTopic
```


## 2. Lambda関数の実装

Slackに通知を送信する関数を実装します。（公式のコピペです)

https://aws.amazon.com/jp/premiumsupport/knowledge-center/sns-lambda-webhooks-chime-slack-teams/


```/src/lambda_function.py
#!/usr/bin/python3.8

import urllib3
import json
import os

http = urllib3.PoolManager()

def lambda_handler(event, context):
    url = os.getenv("SLACK_WEBHOOK_URL")
    channel = os.getenv("SLACK_CHANNEL")
    print("url={}, channel={}".format(url, channel))
    msg = {
        "channel": channel,
        "text": event['Records'][0]['Sns']['Message'],
        "icon_emoji": ""
    }
    encoded_msg = json.dumps(msg).encode('utf-8')
    resp = http.request('POST',url, body=encoded_msg)
    print({
        "message": event['Records'][0]['Sns']['Message'],
        "status_code": resp.status,
        "response": resp.data
    })
```


# デプロイ


`aws cloudformation package` コマンドで、AWS SAM形式のテンプレート(マクロ）を展開します。
デプロイに必要な資材をs3にアップロードする必要があるので、適当なバケットを作ってオプションに渡します。

```bash
# lambdaのデプロイ資材をアップロードするバケット
S3_BUCKET="sample-bucket"
# デプロイ資材の格納パス
S3_PREFIX="path/to/package"

# lambda関数をパッケージ
# --output-template-file に指定したファイル名に展開後のテンプレートが保存されます。
aws cloudformation package \
  --region "ap-northeast-1" \
  --profile "default" \
  --template-file "project-notify-slack.yml" \
  --output-template-file "project-notify-slack-package.yml" \
  --s3-bucket "${S3_BUCKET}" \
  --s3-prefix "${S3_PREFIX}"

```

展開されたテンプレートをデプロイします。

```bash
# スタック名任意
STACK_NAME="sample-stack"
# 先ほど取得したWebhook URLを定義
SLACK_WEBHOOK_URL="https://hooks.slack.com/services/xxxxxxxxx/YYYYYYYYYYY/ZZZZZZZZZZZ"
# 通知を飛ばしたいチャンネル名を指定
SLACK_CHANNEL="#test-midorikawa"
# lambda処理失敗時に通知を飛ばすメールアドレスを指定
EMAIL="hogehoge@example.com"

# デプロイ
aws cloudformation deploy \
  --region "ap-northeast-1" \
  --profile "default" \
  --stack-name "${STACK_NAME}" \
  --template-file "project-notify-slack-package.yml" \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides \
    SlackWebhookUrl="${SLACK_WEBHOOK_URL}" \
    SlackChannel="${SLACK_CHANNEL}" \
    Email="${EMAIL}"
```

# 動作確認

CLIからトピックにpublishしてSlackに通知が送信されるか確認します。

```bash
# TopicArnを確認
TOPIC_ARN=$(aws cloudformation describe-stacks --stack-name $STACK_NAME | jq '.Stacks[].Outputs[]')

# メッセージを送信
aws sns publish --topic-arn "${TOPIC_ARN}" --subject testSubject --message testMessage

```


# 参考

https://aws.amazon.com/jp/premiumsupport/knowledge-center/sns-lambda-webhooks-chime-slack-teams/