はじめに
Lambda Function + CloudWatch Event な日次Slack通知を AWS CDK を使って作りました。
Python で作ってます。
CDKを触るにはちょうどいい規模でした。
作るもの
環境
% sw_vers
ProductName: Mac OS X
ProductVersion: 10.15.7
BuildVersion: 19H2
開始
AWS CKD のインストール
npm install -g aws-cdk
バージョン確認
cdk --version
1.104.0 (build 44d3383)
※詳細は以下を参照
CDK プロジェクトの作成
プロジェクト用フォルダ作成
mkdir mycdksample && cd mycdksample
CDK プロジェクト作成
cdk init --language=python
フォルダ構成
% 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
python3 -m venv .venv && source .venv/bin/activate
先に必要なパッケージをインストール
pip install aws_cdk.core aws_cdk.aws_lambda aws_cdk.aws_events aws_cdk.aws_events_targets requests
Lambda Fucntion を作成
mkdir lambda && vi 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 用のディレクトリを作成
mkdir lambda_layer
requirements.txt を作成
pip freeze | grep requests > lambda_layer/requirements.txt
Makefile を作成
vi 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
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 を編集
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 を作成
cdk synth --no-staging > template.yaml
ローカルでテスト実行
sam local invoke cdkFunctionMycdksampleStack{YOUR ID※} --no-event
※cdkFunctionMycdksampleStack{YOUR ID※} は template.yaml
を参照
SAM CLI のインストールは以下を参照
デプロイ
bootstrap を実行
cdk bootstrap
デプロイ
cdk deploy
bootstrap, deploy は以下を参照
後始末
cdk destroy
最後に
やっぱりPythonが好き