Slackに定期通知するLambda関数をCloudFormationとAWS CLIを使ってデプロイしてみた

2021.07.07

7月からアノテーション テクニカルサポートチームにJOINしました 川崎です。

新しいチームで、心機一転がんばりたいと思います。

早速OJTという形で、テクニカルサポートの業務を学ぶ機会をいただいております。

日々蓄積されてきたナレッジを活用し、効率的に業務が運営されている現場に接することができ、おかげさまで、刺激的な毎日を送っています。

そこには、働き方改革のヒントがつまっています。

やってみた

さて、今回のブログでは、Lambda関数をCloudFormationとAWS CLIを使ってデプロイする方法を試してみました。

Pythonスクリプトをテンプレートに埋め込む方法や、あらかじめzip圧縮してS3バケットにアップロードする方法などは知っていましたが、 今回、ファイルとして用意するPythonスクリプトを、デプロイ時にcloudformation packageコマンドを使ってアップロードする方法を調べてみました。

1.準備するもの

Webhook URL

Slackで通知に使うIncoming WebhookのURLを作成しておきます。

S3バケット

CloudFormationのデプロイに使うS3バケットを作成しておきます。

CloudFormation用 IAMロール

CloudFormation を使用して、スタックのリソースを作成、変更、削除する IAM ロールを作成しておきます。

2.Lambda関数にするPythonファイル

Webhook URLを使用して、対象のSlackチャンネルに通知のメッセージを送付します。

import json
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError


def lambda_handler(event, context):
    # 後でパラメータストアから取得する形にする
    slack_hook_url = "https://hooks.slack.com/services/XXXXXXXXX/YYYYYYYYYYY/zzzzzzzzzzzzzzzzzzzzzzzz"
    message = "Hello from Lambda!"
    slack_message = {"attachments": [{"text": message}]}
    req = Request(slack_hook_url, json.dumps(slack_message).encode("utf-8"))
    try:
        response = urlopen(req)
        response.read()
        print("Message posted to %s", "****" + slack_hook_url[-5:])
    except HTTPError as e:
        print("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        print("Server connection failed: %s", e.reason)

3.テンプレートファイル準備

CloudFormationのテンプレートは2つ用意します。

1つ目のテンプレートファイル

ファイル名:template4package.yml

1つ目のテンプレートファイルは「aws cloudformation package」コマンドで、PythonスクリプトをS3バケットにアップロードするのに利用します。

AWSTemplateFormatVersion : '2010-09-09'
Description: Lambda function template for package command.

Resources:
  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code: source/

2つ目のテンプレートファイル

ファイル名:template_lambda.yml

2つ目のテンプレートファイルに、Lambdaの設定と、スケジュール実行のためのCloudWatch Eventsのルールを定義します。

AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda Template
Parameters:
  CodeS3Key:
    Type: String
  DeploymentBucket:
    Type: String

Resources:
  SlackNotifyLambdaFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        S3Bucket: !Ref DeploymentBucket
        S3Key: !Ref CodeS3Key
      FunctionName: !Sub notify
      Handler: handler/lambda_function.lambda_handler
      MemorySize: 128
      Role: !GetAtt 
        - SlackNotifyLambdaRole
        - Arn
      Runtime: python3.7
      Timeout: 15
      TracingConfig:
        Mode: Active
    DependsOn:
      - SlackNotifyLambdaRole
  SlackNotifyEvent:
    Type: 'AWS::Events::Rule'
    Properties:
      Name: !Sub slack-notify-SlackNotifyEvent
      ScheduleExpression: cron(0 0 * * ? *)
      State: ENABLED
      Targets:
        - Arn: !GetAtt SlackNotifyLambdaFunction.Arn
          Id: SlackNotifyLambdaFunction
  SlackNotifyLambdaRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        - !Ref SlackNotifyLambdaRolePolicy
        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
  SlackNotifyLambdaRolePolicy:
    Type: 'AWS::IAM::ManagedPolicy'
    Properties:
      Path: /slack-notify/
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - 'sts:AssumeRole'
            Resource: '*'
  LambdaInvokePermissionSlackNotifyEvent:
    Type: 'AWS::Lambda::Permission'
    Properties:
      FunctionName: !Ref SlackNotifyLambdaFunction
      Action: 'lambda:InvokeFunction'
      Principal: events.amazonaws.com
      SourceArn: !GetAtt SlackNotifyEvent.Arn

4.スクリプトを作成する

CloudFormation のテンプレートを作成、更新するためのbashスクリプトを作成します。

#!/bin/bash
set -eu
if [ $# -ne 1 ]; then
  echo "指定された引数は$#個です。" 1>&2
  echo "実行するには1個の引数が必要です。" 1>&2
  echo "$0 [AWS CLI Profile]"
  exit 1
fi
Profile=$1
echo "指定されたProfileは $Profile です。"
unset AWS_PROFILE

DeploymentBucket=slack-notify-s3-xxxxxx
cfnRole=arn:aws:iam::xxxxxxxxxxxx:role/cfn-role
s3Key="slack-notify"
STACK_NAME="slack-notify"
TEMPLATE_FILE="template4package.yml"
StackTemplateUrl="template_lambda.yml"

echo "$(date "+%Y/%m/%d %H:%M:%S") CloudFormation Stack: create "${STACK_NAME}" started."
TOOL_DIR=$(dirname $0)
cd ${TOOL_DIR}
mkdir -p source/handler
cp handler/lambda_function.py ./source/handler/
echo ${TEMPLATE_FILE}
regexp="S3Key\: ([^ ]+)"
CF_STACK_STATUS=$( \
aws --profile ${Profile} cloudformation package --template-file ${TEMPLATE_FILE} \
--s3-bucket "${DeploymentBucket}" --s3-prefix "${s3Key}" \
) \
&& echo ${CF_STACK_STATUS}

if [[  "${CF_STACK_STATUS}"  =~  $regexp  ]] ; then 
  CODES3KEY="${BASH_REMATCH[1]}"
  echo ${CODES3KEY}
else 
  echo "no match"
fi

echo "Lambdaを構築するCloudFormationを実行します"
result=$(aws --profile ${Profile} cloudformation list-stacks --stack-status-filter CREATE_COMPLETE ROLLBACK_COMPLETE UPDATE_COMPLETE UPDATE_ROLLBACK_COMPLETE | jq '[.StackSummaries[]][]|select(.StackName=="'${STACK_NAME}'")')
set +u
if [ -n "$result" ]; then
    echo "Stackが存在していたのでStackの更新を行います"
    aws --profile ${Profile} cloudformation update-stack --stack-name ${STACK_NAME} --template-body "file://${StackTemplateUrl}" \
    --capabilities CAPABILITY_NAMED_IAM \
    --role-arn ${cfnRole} \
    --parameters \
    ParameterKey=CodeS3Key,ParameterValue=${CODES3KEY} \
    ParameterKey=DeploymentBucket,ParameterValue=${DeploymentBucket}
    echo "Stackの更新完了を待ちます。"
    aws --profile ${Profile} cloudformation wait stack-update-complete --stack-name ${STACK_NAME}
    echo "Stackの更新が完了しました。"
else
    echo "Stackが存在していないのでStackの作成を行います"
    aws --profile ${Profile} cloudformation create-stack --stack-name ${STACK_NAME} --template-body "file://${StackTemplateUrl}" \
    --capabilities CAPABILITY_NAMED_IAM \
    --role-arn ${cfnRole} \
    --parameters \
    ParameterKey=CodeS3Key,ParameterValue=${CODES3KEY} \
    ParameterKey=DeploymentBucket,ParameterValue=${DeploymentBucket}

    echo "Stackの作成完了を待ちます。"
    aws --profile ${Profile} cloudformation wait stack-create-complete --stack-name ${STACK_NAME} 
    echo "Stackの作成が完了しました。"
fi

5.デプロイを実行する

指定されたProfileは xxdev です。
2021/07/07 22:28:39 CloudFormation Stack: create slack-notify started.
template4package.yml
Uploading to slack-notify/7eafd8db81a46ea7dc3eff236aadbc18 215 / 215.0 (100.00%) AWSTemplateFormatVersion: '2010-09-09' Description: Lambda function template for package command. Resources: LambdaFunction: Type: AWS::Lambda::Function Properties: Code: S3Bucket: slack-notify-s3-xxxxxx S3Key: slack-notify/7eafd8db81a46ea7dc3eff236aadbc18
slack-notify/7eafd8db81a46ea7dc3eff236aadbc18
Lambdaを構築するCloudFormationを実行します
Stackが存在していないのでStackの作成を行います
{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:xxxxxxxxxxxx:stack/slack-notify/27a88940-df26-11eb-82f6-0a255f541851"
}
Stackの作成完了を待ちます。
Stackの作成が完了しました。

6.Lambda関数をテスト実行する

Lambda関数をテスト実行し、Slackの指定のチャンネルに通知がされるか、確認します。

まとめ

Lambda関数をCloudFormationとAWS CLIを使ってデプロイする方法を試してみました。

今回、ファイルとして用意するPythonスクリプトを、デプロイ時にcloudformation packageコマンドを使ってアップロードする方法を試してみました。

CloudFormationのテンプレートファイルを2つ用意するなど、 工夫が必要な箇所があるものの、無事Lambda関数をデプロイし、スケジュール実行させることができました。