LoginSignup
10
6

More than 1 year has passed since last update.

AWS CDK で Lambda + CloudWatch Event の 日次 Slack 通知を作る

Last updated at Posted at 2021-05-18

はじめに

Lambda Function + CloudWatch Event な日次Slack通知を AWS CDK を使って作りました。
Python で作ってます。
CDKを触るにはちょうどいい規模でした。

作るもの

毎日9時にAWSの利用料金をSlack通知してくれるやつ。
スクリーンショット 2021-05-18 14.01.09.png

環境

% sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H2

開始

AWS CKD のインストール

terminal
npm install -g aws-cdk

バージョン確認

terminal
cdk --version
1.104.0 (build 44d3383)

※詳細は以下を参照

CDK プロジェクトの作成

プロジェクト用フォルダ作成

terminal
mkdir mycdksample && cd mycdksample

CDK プロジェクト作成

terminal
cdk init --language=python

フォルダ構成

terminal
% tree -I ".DS_Store|.venv|.git|.gitignore"  -La 4 --dirsfirst
.
├── mycdksample
│   ├── __init__.py
│   └── mycdksample_stack.py
├── README.md
├── app.py
├── cdk.json
├── requirements.txt
├── setup.py
└── source.bat

Python の仮想環境を Activate

terminal
python3 -m venv .venv && source .venv/bin/activate

先に必要なパッケージをインストール

terminal
pip install aws_cdk.core aws_cdk.aws_lambda aws_cdk.aws_events aws_cdk.aws_events_targets requests

Lambda Fucntion を作成

terminal
mkdir lambda && vi lambda/app.py # コードは参考程度に
lambda/app.py
# encoding: utf-8
import json
import datetime
import requests
import boto3
import os
import logging

TODAY = datetime.datetime.utcnow()
BEGINING_OF_THE_MONTH = TODAY - datetime.timedelta(days=TODAY.day - 1)
START_DATE = BEGINING_OF_THE_MONTH.strftime('%Y/%m/%d').replace('/', '-')
END_DATE = TODAY.strftime('%Y/%m/%d').replace('/', '-')

SLACK_POST_URL = os.environ['SLACK_POST_URL']
SLACK_CHANNEL = os.environ['SLACK_CHANNEL']

logger = logging.getLogger()
logger.setLevel(logging.INFO)

client = boto3.client('ce')

def get_total_cost():
    response = client.get_cost_and_usage(
        TimePeriod={
            'Start': START_DATE,
            'End': END_DATE
        },
        Granularity='MONTHLY',
        Metrics=[
            'UnblendedCost',
        ],
    )

    total_cost = response["ResultsByTime"][0]["Total"]["UnblendedCost"]["Amount"]
    return total_cost

def handler(event, context):
    text = "{}までのAWS合計料金 : ${}".format(END_DATE, get_total_cost())
    content = {"text": text}

    slack_message = {
        'channel': SLACK_CHANNEL,
        "attachments": [content],
    }

    try:
        requests.post(SLACK_POST_URL, data=json.dumps(slack_message))
    except requests.exceptions.RequestException as e:
        logger.error("Request failed: %s", e)

Lambda Layer を作成

requests モジュールを使うので、 Lambda Layer を利用
Lambda Layer 用のディレクトリを作成

terminal
mkdir lambda_layer

requirements.txt を作成

terminal
pip freeze | grep requests > lambda_layer/requirements.txt

Makefile を作成

terminal
vi Makefile
Makefile
layer_build:
    docker run --rm \
        -v ${CURDIR}/lambda_layer:/var/task \
        lambci/lambda:build-python3.8 \
        pip install -r requirements.txt -t python/

Make

terminal
make layer_build

lambda_layer 配下に python ディレクトリが作成されます

% tree -I ".DS_Store|.venv|.git|.gitignore"  -La 3 --dirsfirst
.
├── lambda
│   └── app.py
├── lambda_layer
│   ├── python
│   │   ├── bin
│   │   ├── certifi
│   │   ├── certifi-2020.12.5.dist-info
│   │   ├── chardet
│   │   ├── chardet-4.0.0.dist-info
│   │   ├── idna
│   │   ├── idna-2.10.dist-info
│   │   ├── requests
│   │   ├── requests-2.25.1.dist-info
│   │   ├── urllib3
│   │   └── urllib3-1.26.4.dist-info
│   └── requirements.txt
(省略)

Stack を作成

mycdksample/mycdksample_stack.py を編集

mycdksample/mycdksample_stack.py
from aws_cdk import core as cdk
# For consistency with other languages, `cdk` is the preferred import name for
# the CDK's core module.  The following line also imports it as `core` for use
# with examples from the CDK Developer's Guide, which are in the process of
# being updated to use `cdk`.  You may delete this import if you don't need it.
from aws_cdk import (
    core,
    aws_s3 as _s3,
    aws_lambda as _lambda,
    aws_events as _events,
    aws_events_targets as _event_targets,
    aws_iam as _iam
)
import os

class MycdksampleStack(cdk.Stack):
    def __init__(self, scope: cdk.Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)
        # S3 Bucket
        my_stack_bucket = _s3.Bucket(
            self, f"cdk-S3-{construct_id}",
            removal_policy=cdk.RemovalPolicy.DESTROY
        )
        # IAM Role
        my_function_role = _iam.Role(
            self, f"cdk-Role-{construct_id}",
            assumed_by=_iam.ServicePrincipal("lambda.amazonaws.com")
        )
        # Managed Policy
        my_function_role.add_managed_policy(
            _iam.ManagedPolicy.from_managed_policy_arn(
                self, f"cdk-Policy-{construct_id}",
                managed_policy_arn="arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
            )
        )
        # Add to Policy
        my_function_role.add_to_policy(
            _iam.PolicyStatement(
                effect=_iam.Effect.ALLOW,
                resources=[
                    "arn:aws:ce:us-east-1:{YOUR AWS ACCOUNT ID}:/GetCostAndUsage"
                ],
                actions=[
                    "ce:GetCostAndUsage",
                ]
            )
        )
        # Lambda Layer
        layer = _lambda.LayerVersion(
            self,
            f'cdk-Layer-{construct_id}',
            compatible_runtimes=[_lambda.Runtime.PYTHON_3_8],
            code=_lambda.AssetCode("lambda_layer"),
        )
        # Lambda Function
        my_function = _lambda.Function(
            self, f'cdk-Function-{construct_id}',
            runtime=_lambda.Runtime.PYTHON_3_8,
            code=_lambda.Code.asset('lambda'),
            handler='app.handler',
            environment={
                'TZ': "Asia/Tokyo",
                'SLACK_POST_URL': "{YOUR SLACK URL}",
                'SLACK_CHANNEL': "{YOUR CHANNEL}"
            },
            layers=[layer],
            timeout=cdk.Duration.seconds(60),
            role=my_function_role
        )
        # CloudWatch Event
        my_events = _events.Rule(
            self, "cdk-Event-{construct_id}",
            schedule=_events.Schedule.cron(
                minute='00',
                hour='00',
                day='*',
                month='*',
                # week_day='?', <- issue, don't add
                year='*'),
            enabled=True,
        )
        my_events.add_target(_event_targets.LambdaFunction(my_function))

def main():
    app = core.App()
    MycdksampleStack(
        app,
        "MycdksampleStack",
        env={
            "region": os.environ["CDK_DEFAULT_REGION"],
            "account": os.environ["CDK_DEFAULT_ACCOUNT"]
        }
    )
    app.synth()

if __name__ == "__main__":
    main()

Slack の URL と Channel はこちらから取得

ServicePrincipal を探すのも苦労したので、こちらも記録

ローカルで動作確認

AWS CloudFormation template を作成

terminal
cdk synth --no-staging > template.yaml

ローカルでテスト実行

terminal
sam local invoke cdkFunctionMycdksampleStack{YOUR ID※} --no-event

以下の通知が来たら成功
スクリーンショット 2021-05-18 13.52.06.png

※cdkFunctionMycdksampleStack{YOUR ID※} は template.yaml を参照
SAM CLI のインストールは以下を参照

デプロイ

bootstrap を実行

terminal
cdk bootstrap

デプロイ

terminal
cdk deploy

スクリーンショット 2021-05-18 11.34.49.png

bootstrap, deploy は以下を参照

後始末

terminal
cdk destroy

最後に

やっぱりPythonが好き :snake:

10
6
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
10
6