#1、はじめに
今回の記事では忘備録として、AWS-SAM-CLIのビルド・デプロイ時の設定ファイルであるテンプレートファイルについて私のわかる範囲で説明させていただきます。
前回の記事でAWS CLIを用いてLambda関数の情報とコードの一覧を取得するという記事を書かせていただきました。
アプリ毎のAPIGatewayとlambdaの管理を楽にしたかったのですが、正直あまり便利になりませんでした。例えば、ver1だったらこのapiとlambdaを紐づけ、ver2だったらこのapiとlambdaを紐づけ、アプリをver3に更新したけどやっぱver2の時に戻したい、等を簡単に実現したかったのですが、前回の記事の内容では中々うまくいきません。
それらの実現のために、使えたらかっこいいからIaCを用いることにしました。AWS内ではCloudFormationというIaCが提供されています。今回はSAMというCloudFormationの中でも、サーバーレスアプリケーション(lambda, APIGateway等)を利用する部分に特化したIaCを使います。
まずは環境構築の話が出ると思うのですが、そちらはまた次の機会に記事にします。(現環境はvirtualbox, vagrant, docker等勉強不足なものだらけなのでもう少し勉強してからにしたいです。何回か環境ぶっ壊したし、なんか知らんけど動いてるし手順としてpython3.8でSAMを使える環境を書き記すのは現状の知識では厳しい。。。)
とりあえず、環境構築で参考にさせていただいたサイト様だけでも紹介させていただきます。
・aws-sam-cliのインストールとHello Worldの実行(Ubuntu 18.04 LTS, CentOS 7)
・Windows10で VirtualBoxとVagrantをインストールして仮想環境の構築
・【Vagrant/Mac】ローカルと仮想マシンのフォルダを共有/同期する方法
それではテンプレートファイルの説明に入る前に、まずはsamを用いて実際にlambdaとAPIGatewayを作成してみましょう。
#2、SAMを用いてlambdaとAPIGateway作成
SAMを使えばこんなことができる、こんなのが作成できるといわれても実際にやらないといまいちピンとこないと思います。というわけでSAM公式が用意してくれているhello-worldテンプレートを使ってlamdaとAPIGatewayをデプロイしてみましょう。
##環境
vagrant内の環境は以下のようになっております。
・ubuntu/bionic64
・Docker version 20.10.5
・SAM CLI, version 1.22.0
##プロジェクトの作成
以下コマンドで任意の名前でプロジェクト作成できます。
sam init --name <プロジェクト名>
今回は testPj という名前でやっていきたいと思います。
runtimeはpython3.8、template は Hello World Exmapleを用います。
$ sam init --name testPj
Which template source would you like to use?
1 - AWS Quick Start Templates
2 - Custom Template Location
Choice: 1
What package type would you like to use?
1 - Zip (artifact is a zip uploaded to S3)
2 - Image (artifact is an image uploaded to an ECR image repository)
Package type: 1
Which runtime would you like to use?
1 - nodejs14.x
2 - python3.8
3 - ruby2.7
4 - go1.x
5 - java11
6 - dotnetcore3.1
7 - nodejs12.x
8 - nodejs10.x
9 - python3.7
10 - python3.6
11 - python2.7
12 - ruby2.5
13 - java8.al2
14 - java8
15 - dotnetcore2.1
Runtime: 2
Cloning app templates from https://github.com/aws/aws-sam-cli-app-templates
AWS quick start application templates:
1 - Hello World Example
2 - EventBridge Hello World
3 - EventBridge App from scratch (100+ Event Schemas)
4 - Step Functions Sample App (Stock Trader)
5 - Elastic File System Sample App
Template selection: 1
-----------------------
Generating application:
-----------------------
Name: testPj
Runtime: python3.8
Dependency Manager: pip
Application Template: hello-world
Output Directory: .
Next steps can be found in the README file at ./testPj/README.md
testPjディレクトリが作成されました。この中に関数のコードやlambda, APIGatewayの設定ファイルが入っています。
$ ls
testPj
$ cd testPj/
/testPj$ ls
README.md __init__.py events hello_world template.yaml tests
一旦説明は省きますが、 テンプレートファイル:'template.yaml' 内に 関数:'HelloWorldFunction' が定義されています。samとdockerを用いればこの関数をデプロイせずにローカルで実行することができます。
さっそくローカル環境でこの関数を動かしてみましょう。
/testPj$ sam local invoke "HelloWorldFunction" -e events/event.json
Invoking app.lambda_handler (python3.8)
Image was not found.
Building image......................................................................
Skip pulling image and use local one: amazon/aws-sam-cli-emulation-image-python3.8:rapid-1.22.0.
Mounting /home/vagrant/testPj/hello_world as /var/task:ro,delegated inside runtime container
START RequestId: 81dd12f6-e529-4f38-8190-37711ea15a7b Version: $LATEST
END RequestId: 81dd12f6-e529-4f38-8190-37711ea15a7b
REPORT RequestId: 81dd12f6-e529-4f38-8190-37711ea15a7b Init Duration: 0.46 ms Duration: 166.21 ms Billed Duration: 200 ms Memory Size: 128 MB Max Memory Used: 128 MB
{"statusCode": 200, "body": "{\"message\": \"hello world\"}"}
最後の'{"statusCode": 200, "body": "{"message": "hello world"}"}'の部分が関数のreturnになります。hello_world/app.pyの中にコードが定義されているので気になったら見てみましょう。
末尾の'-e events/event.json' ですが、こちらはlambda関数のテストイベントになります。今回は特にいじらずデフォルトで。
##プロジェクトのデプロイ
何もいじってはいませんが、無事にローカルでの動作が確認できたため、ビルドしてデプロイしちゃいましょう
ビルドは以下コマンドで実行できます。
~/testPj$ sam build -t template.yaml
Building codeuri: ~/testPj/hello_world runtime: python3.8 metadata: {} functions: ['HelloWorldFunction']
Running PythonPipBuilder:ResolveDependencies
Running PythonPipBuilder:CopySource
Build Succeeded
Built Artifacts : .aws-sam/build
Built Template : .aws-sam/build/template.yaml
Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided
'-t' でテンプレートファイルを指定できます。テンプレートファイルにはlambda, APIGateway等の設定が書かれています。複数のテンプレートファイルを用いて運用することもありえると判断し紹介させていただきます。
では、続いてデプロイします。
~/testPj$ sam deploy --guided
Configuring SAM deploy
======================
Looking for config file [samconfig.toml] : Not found
Setting default arguments for 'sam deploy'
=========================================
Stack Name [sam-app]:
AWS Region [ap-northeast-1]:
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
Confirm changes before deploy [y/N]: y
#SAM needs permission to be able to create roles to connect to the resources in your template
Allow SAM CLI IAM role creation [Y/n]: y
HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y
Save arguments to configuration file [Y/n]: y
SAM configuration file [samconfig.toml]:
SAM configuration environment [default]:
いくつか入力を求められるのでその項目を説明します。(--guidedを記入しなければこれらの値はデフォルトで行われます。)
Stack Name:
CloudFormation内でのストックの名前です。CloudFormationで作成されたリソース(lambda, APIGateway等)は、このストック名に紐づいて作成されます。例えば、テンプレートファイルでAPI Gatewayとlambdaの名前を変更したと仮定します。この時、ストック名を変更してデプロイすれば__その前に作成したAPIGatewayとlambdaは残った状態で新しいAPIGatewayとlambdaは作成されます。一方、ストック名を変更せずにデプロイした場合は__その前に作成したAPIGatewayとlambdaが削除され、新しいAPIGatewayとlambdaは作成されます。
この辺はCloudFormationの仕様の話ですが、__勘違いすると後で痛い目に合う__のでよく頭に入れておいてください。現行のシステムが全部消して上書きしちゃうなんて笑い話にならないですから。私も気を付けます。。。
AWS Region:
AWSのリージョンです。この記事を読んでいる方なら多分'ap-northeast-1'でいいかと
Confirm changes before deploy:
こちらを'Y'しておくとデプロイ前に変更されるリソースの一覧を表示した上で本当のデプロイするかどうか聞いてくれるようになります。それ上で間違えそうな心折設計 新設設計です。
Allow SAM CLI IAM role creation:
SAM CLIがIAMロールを作っても良いかどうかです。'Y'で問題ありません。
HelloWorldFunction may not have authorization defined, Is this okay?:
関数'HelloWorldFunction'に認証設定がないけど良いですかって聞いてます。とりあえず'Y'です。(この項目は正しく認証を設定すると消えます。)
Save arguments to configuration file:
samconfig.tomlを作成してその中にデフォルトのデプロイパラメータを記入・保存するかどうか。'Y'でOKです。
では、コンソールでlambdaとAPIGatewayが作成されたか確認してみます。
作成できていますね。
ちなみにlambdaとAPIGatewayの削除はCloudFormationコンソール上でストックの削除を行うか以下コマンドで実行できます。
aws cloudformation delete-stack --stack-name <ストック名>
#3、テンプレートファイルの説明
ここからが本題です。ここからはテンプレートファイルの中身の見方、書き方についてみていきたいと思います。今からの内容は、こちらの公式ドキュメントの内容の一部になります。英語が得意な方はそちらをご覧ください。
また、SAMのテンプレートファイルの書き方の基本はCloudFormationと同じになります。その上で、一部の書き方がSAMにしかできないという認識を持って読んでください。
SAMのテンプレートファイルでは、最初に 'AWSTemplateFormatVersion' と 'Transform' が定義されています。こちらはSAMによって定義されたリソースを、CloudFormationで定義しなおすために必ず書く必要があります。特に意識する必要はありません。
テンプレートファイルは以下のオプションにわけられます。
###Description
こちらはCloudFormationストックの説明文です。コンソール上から確認できます。必要に応じ適宜変更するようにしましょう。
###Globals(SAM特有)
こちらには、ストック全体の共通設定を書きます。lambda, APIGateway, DynamoDB の共通設定をこちらで書くことができます。後に説明する__Resources__では一つ一つこれらの設定を定義する必要がありますが、__Globals__を用いることでまとめて設定することができます。__ただし、Globalsで設定できる項目には制限があります。__詳しくはこちらのページをご覧ください。
###Metadata
(SAMを使用しているとあんまり意識しませんが)、CloudFormationのデザイナー上のリソース表示位置が記載されています。
###Parameters
デプロイコマンド実行時にテンプレートに渡すことができる値を定義できます。定義した値はResourcesとOutputsのパラメータとして使用できます。sam deployコマンドについてはこちらを参照ください。
###Mappings
任意のキーと値とを対応付けることができます。例えば、リージョンに基づく値を設定する場合、リージョン名をキーとして必要な値を保持することができます。FindInMap関数を用いることで必要な値を取り出したり、__Parameters__の入力パラメータに応じキーを選択することができます。詳しくはこちらのサイトをご覧ください。
###Conditions
条件分岐を設定することができます。例えば、条件を__Resources__または__Outputs__に紐づけることで、条件が__true__の時のみ__Resources__、Outputs__を出力するようにできます。
ただし__スタックの更新時に、条件を単独で更新することはできません。条件を更新できるのは、リソースを追加、変更、または削除する変更を含める場合だけです。
###Resources(必須)
lambdaやAPIGatewayを定義することができます。__Globals__にいくら設定をかいても、こちらに何も書かなければCloudFormationでリソースは作成されません。詳しい書き方は後述します。
###Outputs
こちらでは、CloudFormationコンソール上で表示する出力値や、他のストックにインポート・応答として返す出力値を宣言できます。
では、testPj のテンプレートファイルの中身を見ていきましょう。(余計な部分は消しております)
~/testPj$ more template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
testPj
Sample SAM Template for testPj
Globals:
Function:
Timeout: 3
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.8
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: get
Outputs:
HelloWorldApi:
Description: "API Gateway endpoint URL for Prod stage for Hello World function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
HelloWorldFunction:
Description: "Hello World Lambda Function ARN"
Value: !GetAtt HelloWorldFunction.Arn
HelloWorldFunctionIamRole:
Description: "Implicit IAM Role created for Hello World function"
Value: !GetAtt HelloWorldFunctionRole.Arn
上から見ていきます。AWSTemplateFormatVersionとTransformを飛ばすと、'Globals: Function: Timeout: 3' と書かれています。日本語で言うと__Resourcesに記述のあるすべての'Type: AWS::Serverless::Function'のタイムアウトを3秒に設定する__という意味です。
Globalsではそれぞれ以下のようにResourcesの共通設定を記述することができます。詳しくはこちらのページをご覧ください。
Function: → Type: AWS::Serverless::Function
Api: → Type: AWS::Serverless::Api
HttpApi: → Type: AWS::Serverless::HttpApi
SimpleTable: → Type:AWS::Serverless::SimpleTable
続けて、__'Resources: HelloWorldFunction:'__と書いてあります。こちらは、__今から'HelloWorldFunction'という名前のリソースの設定を記述します__という宣言になります。また、この'HelloWorldFunction'はあくまでCloudFormationとSAM内での呼び名になります。例えばAPIGatewayとこの関数を結び付ける場合、この'HelloWorldFunction'という名前をテンプレート内に記述して結びつけますが、実際の関数の名前とは異なる場合があります。
続けてその'HelloWorldFunction'に対し、__'Type'と'Properties'__という二つのオプションが与えられています。テンプレートファイルにはあらかじめリソースタイプというのが定義されており、Typeはその中から選択します。SAM特有のリソースタイプは以下6種類が当てはまります。CloudFormationのリソースタイプは公式ドキュメントをご覧ください。
AWS::Serverless::Function
AWS::Serverless::Api
AWS::Serverless::HttpApi
AWS::Serverless::Application
AWS::Serverless::SimpleTable
AWS::Serverless::LayerVersion
Propertiesには'HelloWorldFunction'の細かい設定が記述されています。Type毎に設定できるPropertiesが決まっており、このテンプレートを日本語に訳すとこのようになります。
CodeUri: hello_world/
→ ディレクトリ'hello_world/'内を参照して 'HelloWorldFunction' を作成します。
Handler: app.lambda_handler
→ app.py内で定義されている関数lambda_handlerをlambdaで実行する関数とする。
Runtime: python3.8
→ python3.8を使います。
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: get
→ この関数のトリガーである'HelloWorld'の中身を設定します。
'Type: Api'でHelloWorldがAPIGatewayを通じてlambdaを起動することを宣言しています。
'Proparties'はその設定です。
ちなみに、私はかなりハマったのですが、__'Runtime: python3.8'の記述は、SAMの環境上で python3.8 が使用できる状態になっていないとbuild・deployできませんでした。__Dockerをうまく使えばかわせそうだったのですが、結局環境にpython3.8をインストールすることになってしまいとても大変でした。(だれかDockerの使い方教えてください。)
最後に'Outputs'です。こちらはテンプレートを見るよりコンソールを直接見た方が早いと思います。このようにテンプレートで記述したOutputsが出力されています。
さて、ここまで読んでいただければテンプレートファイルに何が書けるのか、どのように書くのかが大体わかったと思います。では、テンプレートファイルへの追記を開始しましょう。
#4、テンプレートファイルの変更
今回行う設定は以下の通りです。簡単なものから説明していきます。
1,Lambdaにroleを追加
2,APIGatewayの設定を記述できるよう修正
3,APIGatewayにCORSを設定
以上の3つができるようになれば、テンプレート内でリソースを作成し、それを利用して他のリソースを作成できるようになります。
###1,Lambdaにroleを追加
まず、Lambdaにroleを追加してみましょう。使用するroleはあらかじめ作成済みのものを使用します。こちらは簡単で'Resources: HelloWorldFunction: Properties:'に以下の記述を追加すれば完了になります。
Role: <追加したいroleのarn>
ただし、__'Globals: Function:'に記述しても意味がない__ことに注意してください。Propertiesには、Globalsに設定できるものと個別のResourcesに記述の必要なものがあります。Roleは後者の当たります。
###2,APIGatewayの設定を記述できるよう修正
'Resources: HelloWorldAPIGateway' を追加して、APIGatewayの設定を細かく記述できるようにします。'AWS::Serverless::Api' は Properties のStageNameが必須であるため、それも一緒に記述します。
Resources:
~~HelloWorldFunctionの記述~~
HelloWorldAPIGateway:
Type: AWS::Serverless::Api
Properties:
StageName: <任意のステージ名>
念のため、うまくいったか確認のため、ビルドしてデプロイしてみましょう。。。
Error: Failed to create/update the stack: sam-app, Waiter StackCreateComplete failed: Waiter encountered a terminal failure state: For expression "Stacks[].StackStatus" we matched expected path: "ROLLBACK_COMPLETE" at least once
あれ!? うまくいきません!!
。
。
。
そうです。勘の良い方ならお気づきでしょうか。
HelloWorldAPIGatewayとHelloWorldFunctionが紐づいていません。
HelloWorldFunction側に 'Event: HelloWorldの詳しい説明はHelloWorldAPIGatewayに書いてある'、と追記しなければなりません。
Resources:
~~HelloWorldFunctionの記述~~
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: any
RestApiId:
Ref: HelloWorldAPIGateway
HelloWorldAPIGateway:
Type: AWS::Serverless::Api
Properties:
StageName: <任意のステージ名>
これでHelloWorldAPIGatewayとHelloWorldFunctionが紐づきました。
ちなみにあまり本編とは関係ありませんが、このままデプロイしてもうまくいきません。OutputsのHelloWorldApi:の記述を以下のように変更してください。
Outputs:
HelloWorldAPIGateway:
Description: "API Gateway endpoint URL for Prod stage for Hello World function"
Value: !Sub "https://${HelloWorldAPIGateway}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
~~ HelloWorldFunctionの記述 ~~
~~ HelloWorldFunctionIamRoleの記述 ~~
###3,APIGatewayにCORSを設定
いよいよ最後の設定です。APIGatewayにCORSの設定を加えます。CORS設定はGlobals, Resourcesどちらでも記述できますが、今回はGlobalsに設定します。以下の記述を追加します。
Globals:
Function:
~~ Functionの共通設定 ~~
Api:
Cors:
AllowMethods: "'<任意のメソッド>'"
AllowHeaders: "'<任意のヘッダー>'"
AllowOrigin: "'<任意のオリジン>'"
これでCORSの設定が無事にできました。
めでたしめでたし。。。とはいきません!!
こちらのCORSの設定、なんとOPTIONSメソッドにしか記述がされていません。私はアプリでPOSTを使いたかったのですが、__最初のPreflightは通過したのに本命のPOSTではじかれる__という苦い経験をさせられました。
じゃあ今度はPOSTのCORSの設定を記述する必要が出てきました。それにはPropertiesの 'DefinitionBody' を使います。こちらの 'DefinitionBody' はパスのメソッド毎に細かい設定ができるという優れもの~~(記述が多い)~~になります。これを 'Resources: HelloWorldAPIGateway: Properties:' にぶち込みます。長いので説明は割愛。
Resources:
~~HelloWorldFunctionの記述~~
HelloWorldAPIGateway:
Type: AWS::Serverless::Api
Properties:
StageName: <任意のステージ名>
DefinitionBody:
swagger: '2.0'
info:
version: '1.0'
title: !Ref 'AWS::StackName'
paths:
/hello:
post:
x-amazon-apigateway-integration:
httpMethod: POST
type: aws
uri: !Sub >-
arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations
responses:
default:
statusCode: '200'
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'<任意のオリジン>'"
method.response.header.Access-Control-Allow-Methods: "'<任意のメソッド>'"
method.response.header.Access-Control-Allow-Headers: "'<任意のヘッダー>'"
responses:
'200':
headers:
Access-Control-Allow-Origin:
type: string
Access-Control-Allow-Headers:
type: string
Access-Control-Allow-Methods:
type: string
これでCORSの設定は完璧です。
最終的にテンプレートファイルはこのようになりました。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
testPj
Sample SAM Template for testPj
Globals:
Function:
Timeout: 3
Api:
Cors:
AllowMethods: "'<任意のメソッド>'"
AllowHeaders: "'<任意のヘッダー>'"
AllowOrigin: "'<任意のオリジン>'"
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.8
Role: <追加したいroleのarn>
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: any
RestApiId:
Ref: HelloWorldAPIGateway
HelloWorldAPIGateway:
Type: AWS::Serverless::Api
Properties:
StageName: <任意のステージ名>
DefinitionBody:
swagger: '2.0'
info:
version: '1.0'
title: !Ref 'AWS::StackName'
paths:
/hello:
post:
x-amazon-apigateway-integration:
httpMethod: POST
type: aws
uri: !Sub >-
arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations
responses:
default:
statusCode: '200'
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'<任意のオリジン>'"
method.response.header.Access-Control-Allow-Methods: "'<任意のメソッド>'"
method.response.header.Access-Control-Allow-Headers: "'<任意のヘッダー>'"
responses:
'200':
headers:
Access-Control-Allow-Origin:
type: string
Access-Control-Allow-Headers:
type: string
Access-Control-Allow-Methods:
type: string
Outputs:
HelloWorldAPIGateway:
Description: "API Gateway endpoint URL for Prod stage for Hello World function"
Value: !Sub "https://${HelloWorldAPIGateway}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
HelloWorldFunction:
Description: "Hello World Lambda Function ARN"
Value: !GetAtt HelloWorldFunction.Arn
HelloWorldFunctionIamRole:
Description: "Implicit IAM Role created for Hello World function"
Value: !GetAtt HelloWorldFunctionRole.Arn
#5、最後に
ここまで読んでいただきありがとうございました。AWSを触り始めて半年も経ってないペーペーですので、書いてあることには誤りがあるかもしれません。
何か気付いたことがあればぜひコメントでご指摘頂けると幸いです。
CloudFormation・SAMの知識が全くない中やってみたので中々に大変でした。そもそも最初はCloudFormationとSAMが別物だと思っていましたし。。。CloudFormation・SAM・CKDについてもう少し勉強してからまた取り組みたいと思います。