1
0

More than 1 year has passed since last update.

AWS Chalice プロジェクトを CloudFormation でデプロイすると Lambda 関数名がランダムに設定されてしまう、を回避する

Last updated at Posted at 2021-12-11

はじめに

Lambda ベースのアプリケーション開発を効率化できる Python フレームワークの AWS Chalice1、面倒な設定はフレームワークにお任せしてコードにひたすら集中することができとても重宝しています。また、アプリケーション内で多くの関数が必要になったり、他の AWS サービスとの連携が増えたりしても、CloudFormation でのスマートな構成管理も可能2で安心です。でも、、、Chalice プロジェクトを CloudFormation で構成管理するときにはちょっとした困りごとが。。。それは、Lambda 関数名の扱いです。

例えば以下のように、helloworld というアプリケーションで my_function という関数名の Lambda 関数3を定義することを考えます。

app.py
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} となります。
chalice1.png
では、同じ 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 で作られる名前と違うだけでなく、名前にランダム文字列が入ってしまいます。
chalice2.png
これ、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」は、以下のようになります。

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 に関する部分だけ)。

更新前のsam.json
    "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"
          ]
        }
      }
    }
更新後のsam.json
    "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 をうまく設定できる、ということが分かると思います。

使い方

はじめに で提示したプロジェクトを例にすると、デプロイするためのコマンドは以下となります8chalice package の直後に augment.py を実行しています。

helloworldをデプロイ
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 関数が確かに作成されました。
chalice3.png
ちなみに、既存の CloudFormation テンプレート(この例では resources.yaml としています)と統合してデプロイする場合には、コマンドは以下のようになります。

helloworldを既存テンプレートと統合してデプロイ
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 しやすくて便利です。


  1. Documentation — AWS Chalice 

  2. AWS CloudFormation Support — AWS Chalice 

  3. Pure Lambda Functions — AWS Chalice 

  4. '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 

  5. Chalice プロジェクト外に重複する Lambda 関数名が存在していたとき等に問題が発生し得ます。ご注意ください。 

  6. For example something like this: stealthycoin commented on 26 Sep 2018 

  7. cloudtools/troposphere: troposphere - Python library to create AWS CloudFormation descriptions 

  8. Makefile や CodeBuild に設定して使ってます。 

1
0
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
1
0