0
0

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

Last updated at Posted at 2021-05-12

はじめに

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

テンプレートの概要が分からない場合は、はじめてのAWS CloudFormationテンプレートを理解するを参考にしてください。

コードはGitHubにもあります。

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

ディレクトリ構成
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の設定を参考にしてください。

AWS リソース構築準備

  1. akane/lambda/dev-parameters.jsonSlackWebHookUrlにSlack Incoming WebhookのURLを記述する。

    incoming-webhookにて作成する

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

    #test_slack
    

AWS リソース構築手順

  1. 下記を実行してスタックを作成

    ./create_stacks.sh
    

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

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

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

    ./delete_stacks.sh
    

構築テンプレート

1. s3スタック

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}
all-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "all"
        }
    ]
}

2. lambda-layerスタック

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}
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スタック

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}
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. 実行ファイル

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
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
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. アーティファクト関連ファイル

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
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
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
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0