この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは、CX事業本部の夏目です。
Lambdaに最初から入ってるAWS-SDKのバージョンが気になったので、一日一回TweetしてくれるBotを作ったので、構成や工夫を紹介します。
構成
- AWS-SDKのバージョンを取得するLambdaはCloudWatchのScheduleで一日一回動く
- AWS-SDKのバージョンを取得するLambdaとTweetするLambda(Tweeter)は
Lambda Destination
でつないで、別々に実装する - AWS-SDKのバージョンを取得するLambdaはログを出力させない
- TwitterのAPI KeyなどはSystems Manager Parameter StoreにKMSで暗号化して保存
- TweetするLambda(Tweeter)については簡易的なエラー通知を行う
AWS-SDKのバージョンを取得するLambdaはログを出力させない
すごくシンプルな処理かつ通信を行うわけでもないので、ログ出力そのものをさせない。
(今回の使い方であればCloudWatch Logsの料金は気になるほどでないが、そういう使い方もあるんだよという例と思って欲しい)
TweetするLambda(Tweeter)については簡易的なエラー通知を行う
TweeterのLambdaについてはエラーが起きた際に通知を行うようにしている。
MetricsFilterを使用してCloudWatch Logsにエラーログが出力されると、CustomMetricsを投げるようにしている。
投げられたCustomMetricsをCloudWatch Alarmで監視をして、何かあればSNS TopicにPublishをする。
残念ながら、AWSの規約の関係上SNS Topicの次に使っているサービスについては紹介できない。
しかし、SNS TopicをサブスクライブしてSlackへの通知処理を実装すると考えてもらえばよい。
リポジトリ
.
├── .circleci
│ └── config.yml # deployはCircleCIで
├── .gitignore
├── LICENSE # GitHubが生成してくれるやつ
├── Makefile # Task Runnerとして使う
├── README.md
├── poetry.lock # poetryのlockファイル
├── pyproject.toml # poetryの設定ファイル
├── sam.yml
└── src
├── layer
│ ├── Dockerfile
│ └── requirements.txt
├── node
│ └── index.js
├── python
│ └── index.py
├── ruby
│ └── index.rb
└── tweet
├── index.py
└── main.py
6 directories, 13 files
リポジトリの構成は上記のようになっている。
src/tweet
ディレクトリにTweeterの実装を、src/layer
ディレクトリにLambda Layerの実装を置く想定。
(Lambda Layerの実装は make build
実行時に置かれる)
すべてを説明していたら長くなってしまうので、一部項目について説明する。
Makefile
SHELL = /usr/bin/env bash -xeuo pipefail
stack_name:=lambda-runtime-tweeter-lambda
template_path:=packaged.yml
isort:
poetry run isort -rc src
black:
poetry run black src
format: isort black
build:
cd src/layer; \
docker build -t my-build .; \
docker run --name my-container my-build pip3 install -r requirements.txt -t ./python; \
docker cp my-container:/workdir/python .; \
docker rm my-container; \
docker rmi my-build; \
cd ../../
package:
poetry run sam package \
--s3-bucket $$ARTIFACT_BUCKET \
--output-template-file $(template_path) \
--template-file sam.yml
deploy: package
poetry run sam deploy \
--stack-name $(stack_name) \
--template-file $(template_path) \
--capabilities CAPABILITY_IAM \
--no-fail-on-empty-changeset \
--role-arn $$CLOUDFORMATION_DEPLOY_ROLE_ARN
.PHONY: \
deploy \
package \
build \
isort \
black \
format
SAMのデプロイ等はmakeコマンドを使って実行する。
実際のデプロイはCircleCI上で行うため、Lambdaのデプロイパッケージを保存するS3 Bucket名やCloudFormationを実行するIAM RoleのARNは環境変数で渡す。
(CircleCIの環境変数を設定できるのはリポジトリでAdminの権限を持ってる人だけ、だったはず。自信ない)
(いつの間にか、実行時のログにも出力しないようになってた)
こうやって、隠すべき情報はGitに載せないことで、パブリックリポジトリに安心して置くことができる。
build
ではCircleCI上での動作を想定して、Docker Imageの作成と実行、実行したコンテナからのファイルコピーで依存ファイルを手元に持ってきている。
(CircleCIではdockerコマンドが実行されるホストが別なので、-v
オプションとかでファイルをマウントすることができない)
(Docker Imageを作成する際には別ホストでもローカルのファイルをコピーしてくれるのを利用してファイルを送り込んでいる)
.circleci/config.yml
version: 2
jobs:
deploy:
docker:
- image: circleci/python:3.8.1
steps:
- run:
name: poetry in-project true
command: |
set -x
poetry config virtualenvs.in-project true
- checkout
- setup_remote_docker
- restore_cache:
keys:
- layer-{{ checksum "src/layer/Dockerfile" }}-{{ checksum "src/layer/requirements.txt" }}
- restore_cache:
keys:
- poetry-{{ checksum "pyproject.toml" }}-{{ checksum "poetry.lock" }}
- run:
name: install dependencies
command: |
set -x
poetry install
if [ ! -d src/layer/python ]; then
make build
fi
- save_cache:
paths:
- .venv
key: poetry-{{ checksum "pyproject.toml" }}-{{ checksum "poetry.lock" }}
- save_cache:
paths:
- src/layer/python
key: layer-{{ checksum "src/layer/Dockerfile" }}-{{ checksum "src/layer/requirements.txt" }}
- run:
name: install dependencies
command: |
set -x
make deploy
workflows:
version: 2
deploy:
jobs:
- deploy:
filters:
branches:
only: master
pipモジュールの管理にはpoetryを使用しています。
poetry config virtualenvs.in-project true
と設定しておくとprojectのルートディレクトリに.venv
という仮想環境を作成するので、CircleCIでキャッシュさせておく。
キャッシュが残っていればpoetryはpipモジュール再インストールを行わない。
キャッシュのキーとして、pyproject.toml
とpoetry.lock
のチェックサムを使用しているので、変更があった際にはキャッシュがアップデートされる。
Lambda LayerにするpipモジュールもCircleCIでキャッシュさせておく。
src/tweet/main.py
import boto3
from botocore.client import BaseClient
from jeffy.framework import setup
from twitter import Api
app = setup()
def main(event: dict, ssm_client: BaseClient = boto3.client("ssm")):
app.logger.info({"name": "event", "value": event})
keys = get_keys(ssm_client)
message = get_message(event)
tweet(message, keys)
def get_keys(ssm_client: BaseClient) -> dict:
option = {"Path": "/twitter/keys", "WithDecryption": True}
resp = ssm_client.get_parameters_by_path(**option)
keys = {x["Name"]: x["Value"] for x in resp["Parameters"]}
return {
"consumer_key": keys["/twitter/keys/consumer_key"],
"consumer_secret": keys["/twitter/keys/consumer_secret"],
"access_token_key": keys["/twitter/keys/access_token_key"],
"access_token_secret": keys["/twitter/keys/access_token_secret"],
}
def get_message(event: dict) -> str:
return event["responsePayload"]
def tweet(message: str, keys: dict):
resp = Api(**keys).PostUpdate(message)
app.logger.info({"name": "tweet response", "value": resp})
TwitterのAPI Keyは4つ必要になるのだが、それぞれ Systems Manager Parameter StoreにKMSで暗号化して保存している。
- consumer_key:
/twitter/keys/consumer_key
- consumer_secret:
/twitter/keys/consumer_secret
- access_token_key:
/twitter/keys/access_token_key
- access_token_secret:
/twitter/keys/access_token_secret
それぞれのキーを上記名前で保存しているのだが、このときGetParametersByPath
というAPIを使用してまとめて値を取得している。
(正確にはまとめて値を取得できるようにこのような名前にした)
また、取得時に"WithDecryption": True
を指定することで、復号化された値を取得できる。
まとめ
ということで、以上作ったものの工夫等でした。
これらの工夫が何かのやくにたてば幸いです。