はじめに
AWSリソースをCloudFormationで管理すると便利というのは分かってはいるけど、LambdaのソースをCloudFormationで管理するのは色々ハードルがあってなかなか手が出せないという自分のような層向けに、まずは最小限のところからやってみようという意識低めの記事です。
まずはこのあたりから始めつつ、より良いベストプラクティスなどは勉強しながら成長させていきたい。
今回やりたいこと
- LambdaのソースコードをCloudFormationテンプレートから分離する
- Lambda環境変数をソースリポジトリから追い出す
今回触れないこと
- CodeDeployなどのCIとの統合
- SAMのお話
検証したバージョン
$ aws --version
aws-cli/1.16.28 Python/2.7.15 Darwin/18.6.0 botocore/1.12.18
Lambdaリソースを作成するCloudFormationテンプレート
ディレクトリ構造
今回は以下のような構造のNodeJS関数について考えます。
/project-root
|- hello-http #<= lambdaソースディレクトリ
|- node_modules
|- index.js
|- package.json
|- package-lock.json
|- template.yml #<= CloudFormationテンプレートファイル
|- settings.conf #<= パラメータ変数定義
|- package.sh #<= packageコマンドスクリプト
|- deploy.sh #<= deployコマンドスクリプト
パラメータ
CloudFormationでは以下のようにパラメータ定義することでaws cloudformation deploy
コマンド実行時にパラメータを渡すことができます。
AWSTemplateFormatVersion: 2010-09-09
Description: Lambda CloudFormation Template
Parameters:
BaseStackName:
Type: String
Description: Base Stack Name
ApiUrl:
Type: String
Description: API Url
Default: https://api.example.net/api
ApiToken:
Type: String
Description: API Token
NoEcho: true
CloudFormationスタック内であまり表示したくないパラメータについては、NoEcho: true
を設定しておくとパスワード扱いとなり、CloudFormationスタック上では非表示になります。
Lambda関数に環境変数として渡した場合はLambda関数上では見れてしまうので、今回のケースに関しては気休め程度という気はします。
Lambda関数内でよりセキュアにトークンなどを扱うにはやはりSecretsManagerあたりを使うのがよいのでしょうかね。
このあたりはまだ勉強中です。
ベースとなるLambdaリソース
Resources:
HttpCallFunction:
Type: 'AWS::Lambda::Function'
Properties:
Code: hello-http
Description: Lambda Function
FunctionName: !Sub hello-http-${BaseStackName}
Role:
Fn::GetAtt: [ LambdaExecutionRole, Arn ]
Runtime: nodejs8.10
Timeout: 25
Handler: index.handler
ポイントとなるのは [Properties]の[Code]。
ローカルの相対パスディレクトリを指定しておきます。
この状態で aws cloudformation package
コマンドを実行すると、
- ローカルファイルを固めてS3にアップロード
- CloudFormationのテンプレートファイル内のLambdaのコードをS3パスに変更
してくれます。
他には、!Sub hello-http-${BaseStackName}
の部分はCloudFormationパラメータで指定して、デプロイ環境ごとにLambda関数を別々にデプロイすることを想定しています。
Role:
Fn::GetAtt: [ LambdaExecutionRole, Arn ]
の部分は、別途作成するLambdaの実行ロールのARNを参照します。
packageコマンド
aws cloudformation package --s3-bucket $BUCKET --s3-prefix $PREFIX --template-file template.yml --output-template-file template-packaged.yml
パラメータ | 内容 |
---|---|
--s3-bucket | ソースをアップロードするバケット名 |
--s3-prefix | ソースをアップロードするプレフィックス(任意) |
--template-file | 入力元となるテンプレートファイル |
--output-template-file | 変換後のテンプレート出力先ファイル |
変換後のパラメータ
Properties:
Code:
S3Bucket: $BUCKET
S3Key: $PREFIX/0123456ea0dab860708bb7c79c676278
実行ロールの設定
Lambdaファンクションの実行ロールも作成しておきます。
LambdaExecutionRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
-
Effect: 'Allow'
Principal:
Service:
- 'lambda.amazonaws.com'
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
最低限のロールとしてlambdaへの信頼関係の付与と、CloudWatchLogsへログを書き込むマネージドポリシーを設定しておきます。
他の権限が必要な場合はここに追加していきます。
環境変数のパラメータ化
CloudFormationパラメータをそのまま環境変数に渡すことができます。
Properties:
...
Environment:
Variables:
ApiUrl: !Ref ApiUrl
ApiToken: !Ref ApiToken
これで、deploy時に渡したパラメータがLambdaの環境変数として設定されます。
deployコマンド
aws cloudformation deploy --template-file template-packaged.yml --stack-name hello-http-develop --capabilities CAPABILITY_NAMED_IAM CAPABILITY_IAM --parameter-overrides BaseStackName=develop ApiUrl=https://example.com/ ApiToken=xxxxx
パラメータ | 内容 |
---|---|
--template-file | リソースを作成するテンプレートファイル |
--stack-name | CloudFormationスタック名 |
--parameter-overrides | パラメータに渡す値。Key=Value形式でスペース区切りで複数指定可能。 |
package/deployシェルの作成
packageコマンド、deployコマンドはシェルスクリプトにしてしておくと便利です。
deployは引数に渡す環境変数を開発/本番環境ごとに分離しておくと事故も減らせて安心です。
packageスクリプト
#!/bin/sh
aws cloudformation package --s3-bucket $BUCKET --s3-prefix $PREFIX --template-file template.yml --output-template-file template-packaged.yml $OPTIONS
deployスクリプト
#!/bin/sh
CONF=$1
CONF=${CONF:=settings.conf}
source $CONF
aws cloudformation deploy --template-file template-packaged.yml --stack-name hello-http-$BaseStackName --capabilities CAPABILITY_NAMED_IAM CAPABILITY_IAM --parameter-overrides $PARAMS $OPTIONS
confファイル (settings.conf)
BaseStackName=develop
OPTIONS=""
PARAMS="BaseStackName=develop ApiVersion=1.0 ApiUrl=http://localhost ApiToken=xxx"
本番環境用にはsettings-production.conf
などを用意し、
$ ./deploy.sh settings-production.conf
を実行すると、本番環境用トークンなどが環境変数に設定されたLambdaが新しくデプロイするようにしておきます。
今回検証したテンプレートファイル全体
AWSTemplateFormatVersion: 2010-09-09
Description: Lambda CloudFormation Template
Parameters:
BaseStackName:
Type: String
Description: Base Stack Name
ApiVersion:
Type: String
Description: API Version
Default: v1
ApiUrl:
Type: String
Description: API Url
Default: https://api.example.net/api
ApiToken:
Type: String
Description: API Token
NoEcho: true
Resources:
HttpCallFunction:
Type: 'AWS::Lambda::Function'
Properties:
Code: hello-http
Description: Lambda Function
FunctionName: !Sub hello-http-${BaseStackName}
Role:
Fn::GetAtt: [ LambdaExecutionRole, Arn ]
Runtime: nodejs8.10
Timeout: 25
Handler: index.handler
Environment:
Variables:
ApiVersion: !Ref ApiVersion
ApiUrl: !Ref ApiUrl
ApiToken: !Ref ApiToken
LambdaExecutionRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
-
Effect: 'Allow'
Principal:
Service:
- 'lambda.amazonaws.com'
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
今回作ったコードサンプル全体はこちらにも置いてあります。
まとめ
ここまでできると、今まで手作業で作成していたLambdaの管理がCloudFormationで出来そうな気になってきました。
SAMとか関係ない細々したLambdaをどう管理するのかがよく分かっていなかったので、多少整理できた気がします。
sam package
で出来ることはおおよそaws cloudformation package
でも出来るようですね。