AWS CloudFormationでLambda Layerを使用したSlack通知定期実行Lambdaを構築しよう

# はじめに
AWS CloudFormationを利用してLambda Layerを使用したSlack通知定期実行Lambdaの構築のテンプレートのサンプルです。
CloudWatchにて、Lambdaを定期実行します。また、Lambdaは、S3アーティファクトを使用します。
通知内容は起動しているEC2インスタンスを停止する際の情報になります。

テンプレートの概要が分からない場合は、[はじめてのAWS CloudFormationテンプレートを理解する](https://qiita.com/miyabiz/items/eb7b0fa1675b5199b9b7)を参考にしてください。

コードは[GitHub](https://github.com/miyabiz-room/CloudFormation-Template/tree/lambda-02)にもあります。

今回は、akane というシステムの dev 環境を想定しています。
同じ構成で違う環境を作成する場合は、{環境名}-parameters.jsonを別途作成します。

```text:ディレクトリ構成
akane (システム)
  ├── lambda (スタック)
  │   ├── code
  │   │   └── getEC2InfoToSlack.py (S3アーティファクトソース)
  │   ├── code-lambda-getEC2InfoToSlack.zip (S3アーティファクト)
  │   ├── delete_artifact.dev.sh (S3アーティファクト削除シェル)
  │   ├── dev-parameters.json (dev 環境のパラメータ)
  │   ├── lambda.yml (CFnテンプレート)
  │   ├── mkzip.sh (S3アーティファクト作成シェル)
  │   └── upload_artifact.dev.sh (S3アーティファクトアップロードシェル)
  ├── lambda-layer
  │   ├── all-parameters.json (all 環境のパラメータ)
  │   ├── code
  │   │   └── python
  │   │       ├── requirements.txt
  │   │       └── setSlackWebHook.py (S3アーティファクトソース)
  │   ├── code-lambda-setSlackWebHook.zip (S3アーティファクト)
  │   ├── delete_artifact.all.sh (S3アーティファクト削除シェル)
  │   ├── lambda-layer.yml (CFnテンプレート)
  │   ├── mkzip.sh (S3アーティファクト作成シェル)
  │   └── upload_artifact.all.sh (S3アーティファクトアップロードシェル)
  └─ s3 (スタック)
      ├─ s3.yml (CFnテンプレート)
      └─ all-parameters.json (all 環境のパラメータ)
```

# AWS リソース構築内容
  1. lambdaスタック
      - Lambdaロール
      - Lambda
  2. lambda-layerスタック
      - Lambda-layer
  3. s3スタック
      - s3バケット (akane-all-s3-artifacts)
      - バケットポリシー (s3:GetObject)

# 実行環境の準備
[AWS CloudFormationを動かすためのAWS CLIの設定](https://qiita.com/miyabiz/items/fed11796f0ea2b7608f4)を参考にしてください。

# AWS リソース構築準備
1.  ```akane/lambda/dev-parameters.json``` の ```SlackWebHookUrl```にSlack Incoming WebhookのURLを記述する。

    ※ [incoming-webhook](https://slack.com/services/new/incoming-webhook)にて作成する

2.  ```akane/lambda/dev-parameters.json``` の ```SlackChannel```にSlack通知のチェンネル名を記述する。

    ```
    #test_slack
    ```

# AWS リソース構築手順
1. 下記を実行してスタックを作成

    ```
    ./create_stacks.sh
    ```

    ※ 実行後、AWS Certificate Manager にて、対象ドメインを選択し、```Route 53 でのレコード作成```を押下する。

2. 下記を実行してLambdaの動作を確認

    ```
    ./test_lambda.sh
    ```

3. 下記を実行してスタックを削除

    ```
    ./delete_stacks.sh
    ```

# 構築テンプレート
**1. s3スタック**
 
```yaml:s3.yml
AWSTemplateFormatVersion: 2010-09-09
Description: S3 For Akane

# Metadata:

Parameters:
  SystemName:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-]*'
  EnvType:
    Description: Environment type.
    Type: String
    AllowedValues: [all, dev, stg, prod]
    ConstraintDescription: must specify all, dev, stg, or prod.

# Mappings

# Conditions

# Transform

Resources:
  # S3 Bucket作成
  akaneS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      AccessControl: Private
      BucketName: !Sub
        - ${SystemName}-${EnvType}-s3-artifacts
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-s3-artifacts
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  # S3 BucketPolicy作成
  akaneS3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    DependsOn: akaneS3Bucket
    Properties:
      Bucket: !Ref akaneS3Bucket
      PolicyDocument:
        Statement:
          - Action:
              - s3:GetObject
            Effect: Allow
            Resource: !Join
              - ''
              - - 'arn:aws:s3:::'
                - !Ref akaneS3Bucket
                - /*
            Principal:
              AWS: '*'

Outputs:
  akaneS3Bucket:
    Value: !Ref akaneS3Bucket
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-s3-artifacts
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
```

```json:all-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "all"
        }
    ]
}
```

**2. lambda-layerスタック**
 
```yaml:lambda-layer.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Lambda Layer For Akane

# Metadata:

Parameters:
  SystemName:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-]*'
  EnvType:
    Description: Environment type.
    Type: String
    AllowedValues: [all, dev, stg, prod]
    ConstraintDescription: must specify all, dev, stg, or prod.
  ArtifactS3Bucket:
    Type: String
  LambdaLayerName:
    Type: String
  CompatibleRuntime1:
    Type: String

# Mappings

# Conditions

# Transform

Resources:
  # LambdaLayer 作成
  akaneLambdaLayerSetSlackWebHook:
    Type: AWS::Lambda::LayerVersion
    Properties:
      CompatibleRuntimes:
        - !Ref CompatibleRuntime1
      Content:
        S3Bucket: !Ref ArtifactS3Bucket
        S3Key: !Sub
          - code-lambda-${LambdaLayerName}.zip
          - {LambdaLayerName: !Ref LambdaLayerName}
      Description: !Sub
        - ${SystemName}-${EnvType}-lambda-layer-${LambdaLayerName}
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType, LambdaLayerName: !Ref LambdaLayerName}
      LayerName: !Ref LambdaLayerName
      LicenseInfo: MIT

Outputs:
  akaneLambdaLayerSetSlackWebHook:
    Value: !Ref akaneLambdaLayerSetSlackWebHook
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-lambda-layer-arn-${LambdaLayerName}
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType, LambdaLayerName: !Ref LambdaLayerName}
```

```json:all-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "all"
        },
        {
            "ParameterKey": "ArtifactS3Bucket",
            "ParameterValue": "akane-all-s3-artifacts"
        },
        {
            "ParameterKey": "LambdaLayerName",
            "ParameterValue": "setSlackWebHook"
        },
        {
            "ParameterKey": "CompatibleRuntime1",
            "ParameterValue": "python3.8"
        }
    ]
}
```

**3. lambdaスタック**

```yaml:lambda.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Lambda For Akane

# Metadata:

Parameters:
  SystemName:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-]*'
  EnvType:
    Description: Environment type.
    Type: String
    AllowedValues: [all, dev, stg, prod]
    ConstraintDescription: must specify all, dev, stg, or prod.
  EnvTypeCommon:
    Description: Environment type.
    Type: String
    AllowedValues: [all, dev, stg, prod]
    ConstraintDescription: must specify all, dev, stg, or prod.
  ArtifactS3Bucket:
    Type: String
  LambdaName:
    Type: String
  LambdaMemorySize:
    Type: Number
  LambdaRuntime:
    Type: String
  SlackWebHookUrl:
    Type: String
  SlackChannel:
    Type: String
  LambdaLayerName:
    Type: String

# Mappings

# Conditions

# Transform

Resources:
  # ロール 作成
  akaneRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Description: !Sub
        - ${SystemName}-${EnvType}-role-lambda-${AWS::Region}
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess
      Path: /
      Policies:
        - PolicyName: ec2
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ec2:Start*
                  - ec2:Stop*
                Resource: '*'
      RoleName: !Sub
        - ${SystemName}-${EnvType}-role-lambda-${AWS::Region}
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-role-lambda-${AWS::Region}
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  # Lambda 作成
  akaneLambdaGetEC2InfoToSlack:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: !Ref ArtifactS3Bucket
        S3Key: !Sub
          - code-lambda-${LambdaName}.zip
          - {LambdaName: !Ref LambdaName}
      Description: !Sub
        - ${SystemName}-${EnvType}-lambda-${LambdaName}
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType, LambdaName: !Ref LambdaName}
      Environment:
        Variables:
          SLACK_WEB_HOOK_URL: !Ref SlackWebHookUrl
          SLACK_CHANNEL: !Ref SlackChannel
      FunctionName: !Sub
        - ${SystemName}-${EnvType}-lambda-${LambdaName}
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType, LambdaName: !Ref LambdaName}
      Handler: !Sub
        - ${LambdaName}.handler
        - {LambdaName: !Ref LambdaName}
      Layers:
        - Fn::ImportValue: !Sub
          - ${SystemName}-${EnvTypeCommon}-lambda-layer-arn-${LambdaLayerName}
          - {SystemName: !Ref SystemName, EnvTypeCommon: !Ref EnvTypeCommon, LambdaLayerName: !Ref LambdaLayerName}
      MemorySize: !Ref LambdaMemorySize
      Role: !GetAtt akaneRole.Arn
      Runtime: !Ref LambdaRuntime
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-lambda-${LambdaName}
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType, LambdaName: !Ref LambdaName}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
      Timeout: 900
      TracingConfig:
        Mode: PassThrough
  # スケジュール 作成
  akaneEventsRule:
    Type: AWS::Events::Rule
    Properties:
      Description: !Sub
        - ${SystemName}-${EnvType}-rule-lambda-${LambdaName}
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType, LambdaName: !Ref LambdaName}
      Name: !Sub
        - ${SystemName}-${EnvType}-rule-lambda-${LambdaName}
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType, LambdaName: !Ref LambdaName}
      ScheduleExpression: "rate(24 hours)"
      State: ENABLED
      Targets:
        - Arn: !GetAtt akaneLambdaGetEC2InfoToSlack.Arn
          Id: !Sub
            - ${SystemName}-${EnvType}-lambda-${LambdaName}
            - {SystemName: !Ref SystemName, EnvType: !Ref EnvType, LambdaName: !Ref LambdaName}
  # LambdaPermission 作成
  akaneLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt akaneLambdaGetEC2InfoToSlack.Arn
      Principal: events.amazonaws.com
      SourceArn: !GetAtt akaneEventsRule.Arn

Outputs:
  akaneLambdaGetEC2InfoToSlackArn:
    Value: !GetAtt akaneLambdaGetEC2InfoToSlack.Arn
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-lambda-arn
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
```

```json:dev-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "dev"
        },
        {
            "ParameterKey": "EnvTypeCommon",
            "ParameterValue": "all"
        },
        {
            "ParameterKey": "ArtifactS3Bucket",
            "ParameterValue": "akane-all-s3-artifacts"
        },
        {
            "ParameterKey": "LambdaName",
            "ParameterValue": "getEC2InfoToSlack"
        },
        {
            "ParameterKey": "LambdaMemorySize",
            "ParameterValue": "128"
        },
        {
            "ParameterKey": "LambdaRuntime",
            "ParameterValue": "python3.8"
        },
        {
            "ParameterKey": "SlackWebHookUrl",
            "ParameterValue": "https://hooks.slack.com/services/XXXXXXXXXXX/XXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX"
        },
        {
            "ParameterKey": "SlackChannel",
            "ParameterValue": "#test_slack"
        },
        {
            "ParameterKey": "LambdaLayerName",
            "ParameterValue": "setSlackWebHook"
        }
    ],
    "Capabilities": [
        "CAPABILITY_NAMED_IAM"
    ]
}
```

**4. 実行ファイル**

```sh:create_stacks.sh
#!/bin/sh

cd `dirname $0`

SYSTEM_NAME=akane

create_stack () {
    ENV_TYPE=$1
    STACK_NAME=$2
    aws cloudformation create-stack \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME} \
    --template-body file://./${SYSTEM_NAME}/${STACK_NAME}/${STACK_NAME}.yml \
    --cli-input-json file://./${SYSTEM_NAME}/${STACK_NAME}/${ENV_TYPE}-parameters.json

    aws cloudformation wait stack-create-complete \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}
}

create_stack all s3
./akane/lambda-layer/mkzip.sh
./akane/lambda-layer/upload_artifact.all.sh
create_stack all lambda-layer
./akane/lambda/mkzip.sh
./akane/lambda/upload_artifact.dev.sh
create_stack dev lambda

exit 0
```

```sh:test_lambda.sh
#!/bin/sh

cd `dirname $0`

LAMBDA_NAME=akane-dev-lambda-getEC2InfoToSlack
OUTPUT_FILE=response.json

aws lambda invoke --function-name ${LAMBDA_NAME} --log-type Tail ${OUTPUT_FILE} --query 'LogResult'  --output text |  base64 -D

cat ${OUTPUT_FILE}

exit 0
```

```sh:delete_stacks.sh
#!/bin/sh

cd `dirname $0`

SYSTEM_NAME=akane

delete_stack () {
    ENV_TYPE=$1
    STACK_NAME=$2
    aws cloudformation delete-stack \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}

    aws cloudformation wait stack-delete-complete \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}
}

delete_stack dev lambda
./akane/lambda/delete_artifact.dev.sh
delete_stack all lambda-layer
./akane/lambda-layer/delete_artifact.all.sh
delete_stack all s3

exit 0
```

**4. アーティファクト関連ファイル**

```sh:mkzip.sh
#!/bin/sh

cd `dirname $0`

ENV_TYPE=dev
LAMBDA_NAME=$(cat ${ENV_TYPE}-parameters.json | jq -r '.Parameters[] | select(.ParameterKey == "LambdaName").ParameterValue')

FILE=../code-lambda-${LAMBDA_NAME}.zip

cd ./code

ls . |  grep -v -E ".gitignore|${LAMBDA_NAME}.py|requirements.txt" | xargs rm -rf

if [ -e "requirements.txt" ]; then
  pip install -r requirements.txt -t .
fi

rm -f ${FILE}
zip -r ${FILE} ./*

exit 0
```

```sh:upload_artifact.dev.sh
#!/bin/sh

cd `dirname $0`

BASENAME=$(basename $0)
FILENAME=${BASENAME%.*}
ENV_TYPE=${FILENAME##*.}

BUCKET_NAME=$(cat ${ENV_TYPE}-parameters.json | jq -r '.Parameters[] | select(.ParameterKey == "ArtifactS3Bucket").ParameterValue')

ARTIFACT_NAME=code-lambda-$(cat ${ENV_TYPE}-parameters.json | jq -r '.Parameters[] | select(.ParameterKey == "LambdaName").ParameterValue')

aws s3 cp ${ARTIFACT_NAME}.zip s3://${BUCKET_NAME}/${ARTIFACT_NAME}.zip

exit 0
```

```sh:delete_artifact.dev.sh
#!/bin/sh

cd `dirname $0`

BASENAME=$(basename $0)
FILENAME=${BASENAME%.*}
ENV_TYPE=${FILENAME##*.}

BUCKET_NAME=$(cat ${ENV_TYPE}-parameters.json | jq -r '.Parameters[] | select(.ParameterKey == "ArtifactS3Bucket").ParameterValue')

ARTIFACT_NAME=code-lambda-$(cat ${ENV_TYPE}-parameters.json | jq -r '.Parameters[] | select(.ParameterKey == "LambdaName").ParameterValue')

aws s3 rm s3://${BUCKET_NAME}/${ARTIFACT_NAME}.zip

exit 0
```