はじめに
Lambda ベースのアプリケーション開発を効率化できる Python フレームワークの AWS Chalice1、面倒な設定はフレームワークにお任せしてコードにひたすら集中することができとても重宝しています。また、アプリケーション内で多くの関数が必要になったり、他の AWS サービスとの連携が増えたりしても、CloudFormation でのスマートな構成管理も可能2で安心です。でも、、、Chalice プロジェクトを CloudFormation で構成管理するときにはちょっとした困りごとが。。。それは、Lambda 関数名の扱いです。
例えば以下のように、helloworld
というアプリケーションで my_function
という関数名の Lambda 関数3を定義することを考えます。
from chalice import Chalice
app = Chalice(app_name='helloworld')
@app.lambda_function()
def my_function(event, context):
return {'hello': 'world'}
これを、Chalice のコマンド chalice deploy
でデプロイすると、Chalice が自動的に命名してくれる Lambda 関数名は {app_name}-{stage}-{func_name}
となります。
では、同じ Lambda 関数を、CloudFormation を使って helloworld-dev
という名前のスタックにデプロイしてみます。コマンドは、少し複雑になりますが以下の通りです。
chalice package out/
aws cloudformation package \
--template-file out/sam.json \
--s3-bucket 'パッケージを出力するバケット名' \
--output-template-file out/packaged.yaml
aws cloudformation deploy \
--stack-name helloworld-dev \
--template-file out/packaged.yaml \
--capabilities CAPABILITY_NAMED_IAM
このとき、Chalice と CloudFormation が自動的に命名してくれる関数名は、
{app_name}-{stage}-{func_name をアッパーキャメルケースに変換したもの}-{ランダム文字列}
となって、chalice deploy
で作られる名前と違うだけでなく、名前にランダム文字列が入ってしまいます。
これ、Chalice で作った Lambda 関数を、AWS CLI や Step Functions などから直接 invoke
したいときには、関数名を推測できず少し面倒です。マネージメントコンソールや AWS CLI で生成された関数名を確認するか、もしくは、chalice package --merge-template
でマージする CloudFormation のテンプレート内で Outputs
に関数名を出力して機械的に参照できるようにする、など、何らかの手間が必要になります。
そこで今回は、Chalice プロジェクトを CloudFormation を使ってデプロイしても、chalice deploy
で設定されるのと同じ Lambda 関数名(すなわち、{app_name}-{stage}-{func_name}
)が設定されるよう、対策してみます。
方針
調べると、github の公式リポジトリの Issue に同様の事例が挙がっていました。
Use function name in the SAM template generation with package command · Issue #935 · aws/chalice
2018年に投稿されたもので、Chalice 自体のソースコードに手を入れる対策も提案されているのですが、現時点で反映はされていません。3年以上経っても反映されていないことには理由があるはずで、関連する言及もあります4。要は、CloudFormation では、リソースに明示的に固定の名前を付けないほうがいいよね、というような考え方なのですが、、、明示的に固定の名前を付けたいときも、やっぱりありますよね。
他方、この Issue では別の提案もされていて、それは、chalice package
で出力される CloudFormation テンプレートを Python スクリプトで後処理して、テンプレートに Outputs
を自動追加してしまう、というものです。
そこで今回はもう一歩踏み込んで、chalice package
で出力される CloudFormation テンプレートを Python スクリプトで後処理して、テンプレートに Lambda 関数名(FunctionName
)を自動追加してしまう、というやり方で対策してみたいと思います5。
実装
上記 Issue で提案されている Python スクリプト6では、CloudFormation テンプレートの処理に troposphere という Python ライブラリ7を使っていますが、今回はこのライブラリは使わず、CloudFormation テンプレート(JSON or YAML)を dict
として読み込んで直接処理します。
やることを割り切ってしまえば実装は単純で、chalice package
が出力する sam.json
(もしくは sam.yaml
)に Lambda 関数名(FunctionName
)を自動追加する Python スクリプト「augment.py
」は、以下のようになります。
import sys
import json, yaml
import codecs
import argparse
def main():
parser = argparse.ArgumentParser()
parser.add_argument('template_path')
args = parser.parse_args()
return _run(args)
def _run(args):
template_path = args.template_path
template = _load_template(template_path)
_inject_funcname(template)
_write_template(template_path, template)
return 0
def _load_template(path):
data = {}
with open(path, 'r') as f:
if path.endswith('.json'):
data = json.load(f)
else:
data = yaml.safe_load(f)
return data
def _inject_funcname(template):
for lid, res in template.get('Resources', {}).items():
prop = res.get('Properties', {})
arr1 = prop.get('Handler', '').split('.')
if arr1[0] == 'app':
data = {'func': arr1[1]}
for s in prop.get('Tags', {}).get('aws-chalice', '').split(':'):
arr2 = s.split('=')
data[arr2[0]] = arr2[1]
if 'app' in data and 'stage' in data:
res['Properties']['FunctionName'] = (
'{}-{}-{}'.format(data['app'], data['stage'], data['func'])
)
def _write_template(path, template):
with codecs.open(path, 'w', 'utf-8') as f:
if path.endswith('.json'):
json.dump(template, f, indent=2)
else:
yaml.dump(template, f)
if __name__ == '__main__':
sys.exit(main())
_inject_funcname
関数で CloudFormation テンプレートを更新しているのですが、更新の手がかりは以下です。
- Chalice がテンプレートに出力してくれる
Handler
に含まれる関数名(app.{func_name}
) - Chalice がテンプレートに出力してくれる
Properties.Tags.aws-chalice
に含まれるアプリケーション名とステージ(version={version}:stage={stage}:app={app_name}
)
試しに、はじめに で提示したプロジェクトで chalice package
すると出力される CloudFormation テンプレート sam.json
について、augment.py
での処理前後を比較してみます(my_function
に関する部分だけ)。
"MyFunction": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Runtime": "python3.7",
"Handler": "app.my_function",
"CodeUri": "./deployment.zip",
"Tags": {
"aws-chalice": "version=1.26.2:stage=dev:app=helloworld"
},
"Tracing": "PassThrough",
"Timeout": 60,
"MemorySize": 128,
"Role": {
"Fn::GetAtt": [
"DefaultRole",
"Arn"
]
}
}
}
"MyFunction": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Runtime": "python3.7",
"Handler": "app.my_function",
"CodeUri": "./deployment.zip",
"Tags": {
"aws-chalice": "version=1.26.2:stage=dev:app=helloworld"
},
"Tracing": "PassThrough",
"Timeout": 60,
"MemorySize": 128,
"Role": {
"Fn::GetAtt": [
"DefaultRole",
"Arn"
]
},
"FunctionName": "helloworld-dev-my_function"
}
}
テンプレート内の(かつ Lambda 関数リソース内の)情報だけで FunctionName
をうまく設定できる、ということが分かると思います。
使い方
はじめに で提示したプロジェクトを例にすると、デプロイするためのコマンドは以下となります8。chalice package
の直後に augment.py
を実行しています。
chalice package out/
python tool/augment.py out/sam.json
aws cloudformation package \
--template-file out/sam.json \
--s3-bucket 'パッケージを出力するバケット名' \
--output-template-file out/packaged.yaml
aws cloudformation deploy \
--stack-name helloworld-dev \
--template-file out/packaged.yaml \
--capabilities CAPABILITY_NAMED_IAM
デプロイの結果、スタックに {app_name}-{stage}-{func_name}
の名前が付いた Lambda 関数が確かに作成されました。
ちなみに、既存の CloudFormation テンプレート(この例では resources.yaml
としています)と統合してデプロイする場合には、コマンドは以下のようになります。
chalice package --merge-template resources.yaml out/
python tool/augment.py out/sam.yaml
aws cloudformation package \
--template-file out/sam.yaml \
--s3-bucket 'パッケージを出力するバケット名' \
--output-template-file out/packaged.yaml
aws cloudformation deploy \
--stack-name helloworld-dev \
--template-file out/packaged.yaml \
--capabilities CAPABILITY_NAMED_IAM
おわりに
リソースのバッティングが起きないよう気をつける必要はありますが、Chalice で作ったプロジェクトも CloudFormation でスマートに構成管理しながら、chalice deploy
での命名規則と同じ推測可能な Lambda 関数名を設定できるようになりました。直接 invoke
しやすくて便利です。
-
'The general caveat of explicitly naming CloudFormation resources is that you lose the ability to perform updates on the stack that require replacement of the resource.'
kadrach commented on 17 Aug 2018 ↩ -
Chalice プロジェクト外に重複する Lambda 関数名が存在していたとき等に問題が発生し得ます。ご注意ください。 ↩
-
For example something like this:
stealthycoin commented on 26 Sep 2018 ↩ -
cloudtools/troposphere: troposphere - Python library to create AWS CloudFormation descriptions ↩
-
Makefile や CodeBuild に設定して使ってます。 ↩