SAMでSlack通知Lambdaをデプロイ

## はじめに
[AWS Lambda Container Image Support](https://aws.amazon.com/jp/blogs/news/new-for-aws-lambda-container-image-support/) や EventBridgeのcron設定、Secrets Managerからシークレット情報の取得を試してみたかったので、以下のような構成図で一気に試してみます。

## 構成図

- SAMでSlackにメッセージを通知するLambdaをデプロイする
- Lambdaはイメージ化してECRで管理する
- LambdaはEventBridgeで定期実行する
- SlackのWebhook URLをSecrets Managerで管理する

![sam.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/917499/eed96002-2bda-33c2-64bf-cfdac785faed.png)

## 手順
1. ECRにリポジトリを作成しておく
2. Secrets ManagerにSlackのWebhook URLを設定する
3. sam initでテンプレート作成
4. template.yamlの修正
5. Slackにメッセージ通知するLambda関数を作成
6. デプロイ
7. テスト

## 1. ECRにリポジトリを作成しておく

イメージのプッシュ先のリポジトリを事前に登録しておきます。
![screencapture-ap-northeast-1-console-aws-amazon-ecr-repositories-private-901071604628-notify-slack-edit-2021-03-29-02_14_19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/917499/6192880b-d2b2-cc3b-ce8b-2f4863628afe.png)

## 2. Secrets ManagerにSlackのWebhook URLを設定する

SlackのWebhook URLをSecrets Managerに登録します。
![スクリーンショット_2021-03-29_2_18_58（3）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/917499/bb5c797f-9050-70e7-2279-338e780302cd.png)

設定が完了すると、シークレット情報取得のサンプルコードが確認できるので、後でこれをベースにLambda関数を実装します。
![スクリーンショット 2021-03-29 2.24.31（3）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/917499/f1b1b94e-db76-1cf8-37d3-9af26bf0d4c7.png)

## 3. sam initでテンプレート作成
事前準備ができたので、次はSAMのテンプレートを作成していきます。

適当にディレクトリを作成し、`$ sam init` を実行します。

```bash
$ sam init
Which template source would you like to use?
	1 - AWS Quick Start Templates
	2 - Custom Template Location
Choice: 1
What package type would you like to use?
	1 - Zip (artifact is a zip uploaded to S3)
	2 - Image (artifact is an image uploaded to an ECR image repository)
Package type: 2

Which base image would you like to use?
	1 - amazon/nodejs14.x-base
	2 - amazon/nodejs12.x-base
	3 - amazon/nodejs10.x-base
	4 - amazon/python3.8-base
	5 - amazon/python3.7-base
	6 - amazon/python3.6-base
	7 - amazon/python2.7-base
	8 - amazon/ruby2.7-base
	9 - amazon/ruby2.5-base
	10 - amazon/go1.x-base
	11 - amazon/java11-base
	12 - amazon/java8.al2-base
	13 - amazon/java8-base
	14 - amazon/dotnet5.0-base
	15 - amazon/dotnetcore3.1-base
	16 - amazon/dotnetcore2.1-base
Base image: 10

Project name [sam-app]: notification

Cloning app templates from https://github.com/aws/aws-sam-cli-app-templates

    -----------------------
    Generating application:
    -----------------------
    Name: notification
    Base Image: amazon/go1.x-base
    Dependency Manager: mod
    Output Directory: .

    Next steps can be found in the README file at ./notification/README.md
```

先ほど作成したECRにイメージをプッシュしたいので、 `What package type would you like to use?` は `2 - Image (artifact is an image uploaded to an ECR image repository)` を選択します。
ランタイムについては、goを選択しました。

`$ sam init` 実行後のディレクトリ構成は以下です。（notificationディレクトリを作成し、その配下で `$ sam init` を実行しています。）

```bash
notification/
├── README.md
├── samconfig.toml
├── hello-world
│   ├── Dockerfile
│   ├── go.mod
│   ├── go.sum
│   ├── main.go
│   └── main_test.go
└── template.yaml
```

package typeにimageを選択したため、Dockerfileが作成されています。
中身は以下のようになっています。

```bash:Dockerfile
FROM golang:1.14 as build-image

WORKDIR /go/src
COPY go.mod main.go ./

RUN go build -o ../bin

FROM public.ecr.aws/lambda/go:1

COPY --from=build-image /go/bin/ /var/task/

# Command can be overwritten by providing a different command in the template directly.
CMD ["hello-world"]
```

## 4. template.yamlの修正

次に、`$ sam init` で作成されたtemplate.yamlに修正を加え、以下のようになりました。


```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: notify slack

Globals:
  Function:
    Timeout: 5

Resources:
  SlackRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: "sts:AssumeRole"
      Policies:
        - PolicyName: "GetSecretsPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                  - "secretsmanager:GetSecretValue"
                Resource: "*"
  SlackFunction:
    Type: AWS::Serverless::Function
    Properties:
      Role: !GetAtt SlackRole.Arn
      PackageType: Image
      Events:
        Notification:
          Type: Schedule
          Properties:
            Schedule: cron(0 0 * * ? *) # JST 9:00
    Metadata:
      DockerTag: go1.x-v1
      DockerContext: ./slack
      Dockerfile: Dockerfile

Outputs:
  SlackFunction:
    Description: "Notify Slack Lambda Function ARN"
    Value: !GetAtt SlackFunction.Arn
  SlackFunctionIamRole:
    Description: "Implicit IAM Role created for Slack function"
    Value: !GetAtt SlackRole.Arn
```

大きな修正点としては以下です。

- Role
- 実行スケジュール

**Role**
デフォルトで生成されるCloudWatch用のポリシーに、 Secrets Managerからシークレット情報取得用の`secretsmanager:GetSecretValue`を加えたロールを作成します。
（ロールの設定をここで記述しない場合、デフォルトでLambda実行に必要なロールが付与されます。）

```yaml
Resources:
  SlackRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: "sts:AssumeRole"
      Policies:
        - PolicyName: "GetSecretsPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                  - "secretsmanager:GetSecretValue"
                Resource: "*"
  SlackFunction:
    Type: AWS::Serverless::Function
    Properties:
      Role: !GetAtt SlackRole.Arn
```

**実行スケジュール**

EventBridgeのスケジュールをトリガーにしたいので、以下のように設定します。

```yaml
Events:
  Notification:
    Type: Schedule
    Properties:
      Schedule: cron(0 0 * * ? *) # JST 9:00
```

## 5. Slackにメッセージ通知するLambda関数を作成
Secrets ManagerからSlackのWebhook URLを取得して、Slackにメッセージを送信します。

```golang
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"

	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/secretsmanager"
)

// Secrets Managerで設定したキーとリージョン
var (
	secretName string = "SlackWebhookUrl"
	region     string = "ap-northeast-1"
)

type SlackEvent struct {
	Text string `json:"text"`
}

func Modify(se *SlackEvent) {
	se.Text = "test message"
}

// シークレット情報取得用function
func getSecret() (string, error) {
	svc := secretsmanager.New(session.New(),
		aws.NewConfig().WithRegion(region))

	input := &secretsmanager.GetSecretValueInput{
		SecretId:     aws.String(secretName),
		VersionStage: aws.String("AWSCURRENT"),
	}

    // GetSecretValue関数にキー名を渡して、値を取得
	result, err := svc.GetSecretValue(input)
	if err != nil {
		return "", err
	}

    // SecretString配下にJSON形式でシークレット情報がレスポンスされます
	secretString := aws.StringValue(result.SecretString)
	res := make(map[string]interface{})
	if err := json.Unmarshal([]byte(secretString), &res); err != nil {
		return "", err
	}

	return res["WEBHOOK_URL"].(string), nil
}

func handler() error {
	webhoolUrl, err := getSecret()
	if err != nil {
		return err
	}

	se := &SlackEvent{}
	Modify(se)

	params, err := json.Marshal(se)
	if err != nil {
		return err
	}

    // slackにメッセージ送信
	resp, err := http.PostForm(
		webhoolUrl,
		url.Values{"payload": {string(params)}},
	)
	if err != nil {
		return err
	}

	body, _ := ioutil.ReadAll(resp.Body)

	defer resp.Body.Close()

	fmt.Printf("response code:%s, response body:%s, hook:%s\n", resp.Status, body, webhoolUrl)

	return nil
}

func main() {
	lambda.Start(handler)
}
```

## 6. デプロイ
ここまで設定ができたら、デプロイしていきます。
`$ sam build` → `$ sam deploy` の順に実行します。

修正内容に不備がなければ、以下のように`$ sam build` が成功します。

```bash
Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided
```

ビルドが完了したら、`$ sam deploy` を実行してデプロイします。
はじめての実行の場合、以下のように `--guided` オプションを付けることで、対話形式でデプロイの設定ができるので便利です。

```bash
$ sam deploy --guided

Configuring SAM deploy
======================

	Looking for config file [samconfig.toml] :  Found
	Reading default arguments  :  Success

	Setting default arguments for 'sam deploy'
	=========================================
	Stack Name [notify-slack]:
	AWS Region [ap-northeast-1]:
	Image Repository for SlackFunction [xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/notify-slack]:
	  slackfunction:go1.x-v1 to be pushed to xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/notify-slack:slackfunction-eb1817a25a39-go1.x-v1

	#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
	Confirm changes before deploy [Y/n]: Y
	#SAM needs permission to be able to create roles to connect to the resources in your template
	Allow SAM CLI IAM role creation [Y/n]: Y
	Save arguments to configuration file [Y/n]: Y
	SAM configuration file [samconfig.toml]:
	SAM configuration environment [default]:
```

これで、CloudFormationにスタックが作成されます。

## 7. テスト

Scheduleを適当な時間に調整し、Webhook URLに設定したSlackチャンネルにメッセージが届けば確認完了です。

```yaml
Events:
  Notification:
    Type: Schedule
    Properties:
      Schedule: cron(0 0 * * ? *) # JST 9:00
```

## 備考

コンソールのLambda関数の画面からテスト実行することもできます。
![スクリーンショット 2021-03-29 3.35.41（3）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/917499/9112b882-db5b-8638-528a-90c7125fefdc.png)


`secretsmanager:GetSecretValue`のポリシーをロールにアタッチしていない場合、エラーとなりAPIが呼び出せないので注意が必要です。

```
AccessDeniedException: User: arn:aws:sts::901071604628:assumed-role/notify-slack-SlackFunctionRole-1IND13I9826OF/notify-slack-SlackFunction-AL8RRMO0EQ8H is not authorized to perform: secretsmanager:GetSecretValue on resource: SlackWebhookUrl
```