これは TECHSCORE Advent Calendar 2018 の7日目の記事です。
こんにちは、土屋です。
今年1月に AWS Lambda が Go のサポートを開始しましたね。
Gopher たちが賑わう中、私も Go のアプリを Lambda で作りたいなあと思い、 AWS SAMを使って環境構築をしてみました。
環境構築はチャチャッと終わらせたい!と思いつつも、実際にSAMを使ってみると、自分がやりたいことを実現する方法がわからず、調べるのに意外に時間がかかってしまいました。
そこで今回は「先に知ってればもっと工数削減できたなー」と思った、AWS SAM に関する小ネタを4つをまとめました。
※AWS SAM は公式ドキュメントの Benefits of Using AWS SAM に書かれている通り、AWS CloudFormation の拡張です。本記事の内容は、CloudFormation にも当てはめることができます。
1. テンプレート内で環境ごとに異なる値を渡したい
開発環境と本番環境はできるだけ同じ条件にしたいものですが、どうしても環境ごとに異なる値を渡したいこともありますよね。
「環境ごとにテンプレートファイルを作る!」という方法でも解決できますが、ほぼ同じファイルの内容をいくつも管理することになりますし、開発環境のテンプレートで行った変更を本番環境のテンプレートに反映し忘れていた...などのリスクを考えるとあまり嬉しくないです。
そんな時は Parameters と Mappings を使うと便利です。
ざっくりとした使い方は
- Parameters で環境のリストを作成
- Mappings でリストごとにマップを定義
- マップの値を使いたい箇所で埋め込み
- 環境を指定してデプロイ
という流れになります。
例として環境ごとに異なるアラートの通知先を SNS に設定してみます。
まず Parameters を使った環境のリストは次のように定義します。ここでは開発環境、ステージング環境、本番環境を用意してみました。
1 2 3 4 5 6 7 8 |
Parameters: Environment: Type: String AllowedValues: - development # 開発環境 - staging # ステージング環境 - production # 本番環境 Default: development |
Mappings は次のように定義します。ここでは環境ごとに通知先を設定しています。
1 2 3 4 5 6 7 8 |
Mappings: EnvironmentMap: development: staging: production: |
使いたい箇所に FindInMap を使って値を埋め込みます。ここでは SNS のエンドポイントとして、アラート先を設定しています。
1 2 3 4 5 6 7 8 |
Resources: SampleNotice: Type: "AWS::SNS::Topic" Properties: # DisplayName などのプロパティは省略 Subscription: - Endpoint: !FindInMap [ EnvironmentMap, !Ref Environment, alertEndpoint] # ここ Protocol: email |
あとはデプロイ時に --parameter-overrides を使って環境を指定すればOKです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# cloudformationのテンプレートを自動生成 $ aws cloudformation package \ --template-file template.yml \ --s3-bucket sample \ --s3-prefix sample \ --output-template-file packaged_template.yml # デプロイ $ aws cloudformation deploy \ --template-file packaged_template.yml \ --stack-name sample \ --capabilities CAPABILITY_IAM \ --parameter-overrides Environment=development # ここ |
2. テンプレート内でAWSのアカウントIDを埋め込みたい
環境ごとにAWSアカウントを作成して、1つのテンプレートファイルで管理したい時、サービスの名前を同じにしておけば、AWSのアカウントIDを変更するだけでアクセスすることができます。
例) AとBのアカウントでそれぞれ sample.fifo という名前のSQSを定義した場合
- https://sqs.ap-northeast-1.amazonaws.com/[AのアカウントID]/sample.fifo
- https://sqs.ap-northeast-1.amazonaws.com/[BのアカウントID]/sample.fifo
でアクセスすることできます。
このような場合のテンプレート内のアカウントIDの指定方法として、公式ドキュメントのマッピングなしの Fn::Subにスッキリと書く方法が紹介されています。
環境ごとに異なる sample.fifo という SQS にアクセスするなら、次のように指定します。
1 |
QUEUE_URL: !Sub 'https://sqs.ap-northeast-1.amazonaws.com/${AWS::AccountId}/sample.fifo' |
ただしアカウントIDは、デプロイ前に設定する aws configure で指定したIAMユーザーのアカウントに依存するため、環境ごとにデプロイ用のIAMユーザーを作成し、 aws configure でデプロイ前に設定する必要があります。
デプロイ用のIAMユーザーとは異なるアカウントIDを指定したい場合は、先に紹介した Parameters と Mappings を使って、アカウントIDを埋め込む方法が良いと思います。
3. テンプレート内でロール名を指定してロールを定義したい
実際にハマったことなのですが、テンプレートでロールを定義する際にロール名を指定してデプロイすると、設定は間違えていないのに、なぜかデプロイできなかったことがありました。
原因はデプロイ時に --capabilities の値に "CAPABILITY_NAMED_IAM" を指定していなかったからでした。
1 2 3 4 |
aws cloudformation deploy \ --template-file packaged_template.yml \ --stack-name sample \ --capabilities CAPABILITY_NAMED_IAM # ここ |
公式ドキュメントのAWS CloudFormation テンプレートでの IAM リソースの承認を見ると、CloudFormation の仕様として、AWSアカウントのアクセス権限に影響するリソース(Roleの作成など)を含む可能性があるテンプレートを指定する場合、明示的に --capabilities を使ってテンプレート機能の承認を行う必要があります。
渡す値は、通常 "CAPABILITY_IAM" で問題ないのですが、今回のように自分でRole名をつける場合、 "CAPABILITY_NAMED_IAM" を渡さなければなりません。
公式ドキュメントの RoleName の説明に CAPABILITY_NAMED_IAM を指定するよう記載があり、完全に見逃していただけなのですが、ググるとロールの定義でハマった記事が多くヒットしたので、意外にここの設定を忘れている人は多いのかもしれません...
4. デプロイ失敗時のログを見やすくしたい
例えば sample という stack 名を指定してデプロイした際に、デプロイに失敗すると "aws cloudformation describe-stack-events --stack-name sample" というメッセージがターミナルに表示されます。
「このコマンドを実行して stack のイベントを見て原因を見つけてね」ということなのですが、そのまま実行すると、次のように全てのイベント情報が表示されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
$ aws cloudformation describe-stack-events --stack-name sample { "StackEvents": [ { "StackId": "arn:aws:cloudformation:ap-northeast-1:*************:stack/sample/dc2d7f10-f3a6-11e8-b88e-0e7d1a719a58", "EventId": "8bdcd240-f43c-11e8-9edc-500c44f24c82", "StackName": "sample", "LogicalResourceId": "sample", "PhysicalResourceId": "arn:aws:cloudformation:ap-northeast-1:*************:stack/sample/dc2d7f10-f3a6-11e8-b88e-0e7d1a719a58", "ResourceType": "AWS::CloudFormation::Stack", "Timestamp": "2018-11-30T01:09:11.611Z", "ResourceStatus": "UPDATE_ROLLBACK_COMPLETE" }, { "StackId": "arn:aws:cloudformation:ap-northeast-1:*************:stack/sample/dc2d7f10-f3a6-11e8-b88e-0e7d1a719a58", "EventId": "Alarm-b1238910-4cca-4605-b22e-3b1cf5c3a109", "StackName": "sample", "LogicalResourceId": "Alarm", "PhysicalResourceId": "", "ResourceType": "AWS::CloudWatch::Alarm", "Timestamp": "2018-11-30T01:09:11.211Z", "ResourceStatus": "DELETE_COMPLETE" }, { "StackId": "arn:aws:cloudformation:ap-northeast-1:*************:stack/sample/dc2d7f10-f3a6-11e8-b88e-0e7d1a719a58", "EventId": "8a95ac40-f43c-11e8-a355-503a6ff78efe", "StackName": "sample", "LogicalResourceId": "sample", "PhysicalResourceId": "arn:aws:cloudformation:ap-northeast-1:*************:stack/sample/dc2d7f10-f3a6-11e8-b88e-0e7d1a719a58", "ResourceType": "AWS::CloudFormation::Stack", "Timestamp": "2018-11-30T01:09:09.478Z", "ResourceStatus": "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS" }, { "StackId": "arn:aws:cloudformation:ap-northeast-1:*************:stack/sample/dc2d7f10-f3a6-11e8-b88e-0e7d1a719a58", "EventId": "87cb41a0-f43c-11e8-a355-503a6ff78efe", "StackName": "sample", "LogicalResourceId": "sample", "PhysicalResourceId": "arn:aws:cloudformation:ap-northeast-1:*************:stack/sample/dc2d7f10-f3a6-11e8-b88e-0e7d1a719a58", "ResourceType": "AWS::CloudFormation::Stack", "Timestamp": "2018-11-30T01:09:04.789Z", "ResourceStatus": "UPDATE_ROLLBACK_IN_PROGRESS", "ResourceStatusReason": "The following resource(s) failed to create: [Alarm]. " }, { "StackId": "arn:aws:cloudformation:ap-northeast-1:*************:stack/sample/dc2d7f10-f3a6-11e8-b88e-0e7d1a719a58", "EventId": "Alarm-CREATE_FAILED-2018-11-30T01:09:03.197Z", "StackName": "sample", "LogicalResourceId": "Alarm", "PhysicalResourceId": "", "ResourceType": "AWS::CloudWatch::Alarm", "Timestamp": "2018-11-30T01:09:03.197Z", "ResourceStatus": "CREATE_FAILED", # 見たいのはここだけ↓ "ResourceStatusReason": "Value of property AlarmActions must be of type List of String" }, { ... # イベントの数だけ配列が続く |
デプロイに失敗した時(上記の例では CREATE_FAILED した時)だけの理由だけを知りたいのに、情報が多すぎて見るのが少し辛いですね...
そこで、次のように jq を使って失敗した箇所だけ表示されるよう実行すると、いい感じに表示されます。
1 2 3 |
$ aws cloudformation describe-stack-events --stack-name sample | jq -r '.StackEvents[] | select(.ResourceStatus == "CREATE_FAILED") | .ResourceStatusReason' Value of property AlarmActions must be of type List of String |
※--stack-name と ResourceStatus で指定する値(上記の sample や CREATE_FAILED)は、実行時の状態に応じて変更してください。
最後に
今回の記事では
- Parameters と Mappings を使用した値の渡し方
- マッピングを使わない Fn::Sub の使い方
- ロール名を指定したロール定義のデプロイ方法
- デプロイ失敗時にのエラーメッセージだけ表示する方法
を紹介しました。
実際に AWS SAM を使ってみて、テンプレートの書き方など、慣れるまでに少し時間がかかってしまいましたが、一度慣れると気軽に設定を変更できて使いやすいと思いました。
リリースノートを見ると、執筆している現在もどんどんアップデートされていますし、環境構築でAWS SAMを使ったことがない人は、試しに使ってみてはいかがでしょうか。
最後まで読んでいただきありがとうございました。