はじめに
今回は自分が過去にAWSのインフラ構築をAWS CloudFormation経由で実施した際に
上手くいかなかった下記2つの事象と解決策等のノウハウをまとめたブログになります。
事象① 作成するリソースの順序の不備による構築失敗
事象② 承認遅延タイムアウト発生
AWS CloudFormationは単純なコード作成スキルだけじゃなく、デプロイ戦略を考慮するスキルも必要となるため、
過去にハマったエラーについて纏めてみました。
事象① 作成するリソースの順序の不備による構築失敗
AWS CloudFormation経由でCloudTrailの証跡ログを作成しようとしたところ、エラーが発生して構築に失敗しました。
※CloudTrailの構築を例にしていますが、NAT Gatewayとルートテーブル作成の場合等別の構築作業でも同様の事象が発生したこともあります。
■使用したAWS CloudFormationのコード(yaml形式)
AWSTemplateFormatVersion: '2010-09-09'
Description: This template create CloudTrail.
Parameters:
Env:
Description: select the environment name.
Type: String
Default: env1
AllowedValues:
- env1
- env2
- env3
EnvLowerCase:
Description: select the environment name.
Type: String
Default: env1
AllowedValues:
- env1
- env2
- env3
SystemName:
Type: String
MinLength: 1
MaxLength: 100
Default: CLOUD TRAIL
SystemNameLowerCase:
Type: String
MinLength: 1
MaxLength: 100
Default: cloudtrail
CloudTrailLogGroupName:
Description: select CloudTrail LogGroupName.
Type: String
Default: "CloudTrail/DefaultLogGroup"
Resources:
# ------------------------------------------------------------------
# Create S3 Bucket & Policy for CloudTrail
# - BucketName : バケット名
# - AccessControl : バケットへのACL(デフォルトは Private: 所有者以外にはアクセス許可なし)
# - BucketEncryption : バケットの暗号化 ※SSE-S3の場合AES256 SSE-KMSの場合KMS
# - LifecycleConfiguration : バケットのライフサイクル
# - Status : ルールの有効化
# - ExpirationInDays : 削除されるまでの経過日数
# - Transitions : s3標準から別のストレージクラスへ移行されるルール
# - PublicAccessBlockConfiguration : すべてブロックでパブリックアクセスをすべてブロック
#
# ------------------------------------------------------------------
CloudTrailS3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "${SystemNameLowerCase}-${EnvLowerCase}-s3-${AWS::AccountId}-cloudtrail-bucket"
AccessControl: Private
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
VersioningConfiguration:
Status: Enabled
ObjectLockEnabled: true
ObjectLockConfiguration:
ObjectLockEnabled: Enabled
Rule:
DefaultRetention:
Mode: GOVERNANCE
Days: '2563'
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
LifecycleConfiguration:
Rules:
- Id: !Sub "${SystemNameLowerCase}-${Env}-s3-${AWS::AccountId}-lifecycle-7years-delete"
Prefix: ''
Status: Enabled
AbortIncompleteMultipartUpload:
DaysAfterInitiation: '7'
ExpirationInDays: '2563'
Tags:
- Key: "Name"
Value: !Sub "${SystemNameLowerCase}-${Env}-${AWS::AccountId}-s3-cloudtrail-bucket"
LoggingConfiguration:
DestinationBucketName: !Sub "${SystemNameLowerCase}-${EnvLowerCase}-s3-${AWS::AccountId}-cloudtrail-bucket"
LogFilePrefix: !Sub "s3://${SystemNameLowerCase}-${EnvLowerCase}-s3-${AWS::AccountId}-cloudtrail-bucket/accesslog"
CloudTrailBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref CloudTrailS3Bucket
PolicyDocument:
Id: CloudTrailBucketPolicy
Version: 2012-10-17
Statement:
- Sid: AWSCloudTrailAclCheck
Effect: Allow
Principal:
Service: cloudtrail.amazonaws.com
Action: 's3:GetBucketAcl'
Resource:
- !Join [ "", [ 'arn:aws:s3:::', !Ref CloudTrailS3Bucket ] ]
- Sid: AWSCloudTrailWrite
Effect: Allow
Principal:
Service: cloudtrail.amazonaws.com
Action: 's3:PutObject'
Resource:
- !Join [ "", [ 'arn:aws:s3:::', !Ref CloudTrailS3Bucket, !Sub "/AWSLogs/${AWS::AccountId}/*"] ]
Condition:
StringEquals:
's3:x-amz-acl': 'bucket-owner-full-control'
CloudTrailIAMRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service:
- "cloudtrail.amazonaws.com"
Action:
- "sts:AssumeRole"
Policies:
- PolicyName: !Sub "${SystemNameLowerCase}-${Env}-iam-policy-CloudTrailPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "logs:CreateLogStream"
Resource: !Join [ "", [ "arn:aws:logs:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":log-group:", !Ref CloudTrailLogGroupName, ":log-stream:", !Ref "AWS::AccountId", "_CloudTrail_", !Ref "AWS::Region", "*" ] ]
- Effect: "Allow"
Action:
- "logs:PutLogEvents"
Resource: !Join [ "", [ "arn:aws:logs:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":log-group:", !Ref CloudTrailLogGroupName, ":log-stream:", !Ref "AWS::AccountId", "_CloudTrail_", !Ref "AWS::Region", "*" ] ]
Path: "/"
RoleName: !Sub "${SystemNameLowerCase}-${Env}-iam-role-CloudTrailRole"
CloudTrail:
Type: AWS::CloudTrail::Trail
Properties:
S3BucketName: !Ref CloudTrailS3Bucket
EnableLogFileValidation: true
IncludeGlobalServiceEvents: true
IsLogging: true
IsMultiRegionTrail: true
TrailName: !Sub "${SystemNameLowerCase}-${Env}-cloudtrail"
Tags:
- Key: "Name"
Value: !Sub "${SystemNameLowerCase}-${Env}-cloudtrail"
パラメータは下記で、その他のオプションはデフォルトのまま実行しました。

下記のように「Invalid request provided: Incorrect S3 bucket policy is detected for bucket」というエラーが出て、AWS CloudFormationの処理が失敗になりました。

事象①の原因 作成するリソースの順序の不備による構築失敗
CloudTrailで参照しているS3バケットのバケットポリシー作成が完了する前に、
CloudTrailの構築をしようとしたことが原因となります。

ここで一つ罠もあります。AWS CloudFormationの仕様で、作成するリソースの順序については、指定がない限り基本的にはAWS側でコントロールすることになります。
その際全く同じAWSアカウント、同じAWS CloudFormationのコードを利用しても成功/失敗どちらも発生する可能性があります。
過去にテストした際は10回中8回成功、2回失敗ということがありました。
事前にテスト用環境で実行して問題ないことを担保した資材を
本番環境に持っていって実行したらエラーになるということもありました。(泣)
事象①の解決策 作成するリソースの順序の不備による構築失敗
解決策としては、AWS CloudFormationのDependsOnオプションをつけることで、
作成するリソースの順序を制御することができます。
修正方針としては下記のようになります。①②③の処理が終わってから④の処理が動き出すという仕様になっています。

実際に、DependsOnを記載した改良版のコードは下記になります。
AWSTemplateFormatVersion: '2010-09-09'
Description: This template create CloudTrail.
Parameters:
Env:
Description: select the environment name.
Type: String
Default: env1
AllowedValues:
- env1
- env2
- env3
EnvLowerCase:
Description: select the environment name.
Type: String
Default: env1
AllowedValues:
- env1
- env2
- env3
SystemName:
Type: String
MinLength: 1
MaxLength: 100
Default: CLOUD TRAIL
SystemNameLowerCase:
Type: String
MinLength: 1
MaxLength: 100
Default: cloudtrail
CloudTrailLogGroupName:
Description: select CloudTrail LogGroupName.
Type: String
Default: "CloudTrail/DefaultLogGroup"
Resources:
# ------------------------------------------------------------------
# Create S3 Bucket & Policy for CloudTrail
# - BucketName : バケット名
# - AccessControl : バケットへのACL(デフォルトは Private: 所有者以外にはアクセス許可なし)
# - BucketEncryption : バケットの暗号化 ※SSE-S3の場合AES256 SSE-KMSの場合KMS
# - LifecycleConfiguration : バケットのライフサイクル
# - Status : ルールの有効化
# - ExpirationInDays : 削除されるまでの経過日数
# - Transitions : s3標準から別のストレージクラスへ移行されるルール
# - PublicAccessBlockConfiguration : すべてブロックでパブリックアクセスをすべてブロック
#
# ------------------------------------------------------------------
CloudTrailS3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "${SystemNameLowerCase}-${EnvLowerCase}-s3-${AWS::AccountId}-cloudtrail-bucket"
AccessControl: Private
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
VersioningConfiguration:
Status: Enabled
ObjectLockEnabled: true
ObjectLockConfiguration:
ObjectLockEnabled: Enabled
Rule:
DefaultRetention:
Mode: GOVERNANCE
Days: '2563'
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
LifecycleConfiguration:
Rules:
- Id: !Sub "${SystemNameLowerCase}-${Env}-s3-${AWS::AccountId}-lifecycle-7years-delete"
Prefix: ''
Status: Enabled
AbortIncompleteMultipartUpload:
DaysAfterInitiation: '7'
ExpirationInDays: '2563'
Tags:
- Key: "Name"
Value: !Sub "${SystemNameLowerCase}-${Env}-${AWS::AccountId}-s3-cloudtrail-bucket"
LoggingConfiguration:
DestinationBucketName: !Sub "${SystemNameLowerCase}-${EnvLowerCase}-s3-${AWS::AccountId}-cloudtrail-bucket"
LogFilePrefix: !Sub "s3://${SystemNameLowerCase}-${EnvLowerCase}-s3-${AWS::AccountId}-cloudtrail-bucket/accesslog"
CloudTrailBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref CloudTrailS3Bucket
PolicyDocument:
Id: CloudTrailBucketPolicy
Version: 2012-10-17
Statement:
- Sid: AWSCloudTrailAclCheck
Effect: Allow
Principal:
Service: cloudtrail.amazonaws.com
Action: 's3:GetBucketAcl'
Resource:
- !Join [ "", [ 'arn:aws:s3:::', !Ref CloudTrailS3Bucket ] ]
- Sid: AWSCloudTrailWrite
Effect: Allow
Principal:
Service: cloudtrail.amazonaws.com
Action: 's3:PutObject'
Resource:
- !Join [ "", [ 'arn:aws:s3:::', !Ref CloudTrailS3Bucket, !Sub "/AWSLogs/${AWS::AccountId}/*"] ]
Condition:
StringEquals:
's3:x-amz-acl': 'bucket-owner-full-control'
CloudTrailIAMRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service:
- "cloudtrail.amazonaws.com"
Action:
- "sts:AssumeRole"
Policies:
- PolicyName: !Sub "${SystemNameLowerCase}-${Env}-iam-policy-CloudTrailPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "logs:CreateLogStream"
Resource: !Join [ "", [ "arn:aws:logs:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":log-group:", !Ref CloudTrailLogGroupName, ":log-stream:", !Ref "AWS::AccountId", "_CloudTrail_", !Ref "AWS::Region", "*" ] ]
- Effect: "Allow"
Action:
- "logs:PutLogEvents"
Resource: !Join [ "", [ "arn:aws:logs:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":log-group:", !Ref CloudTrailLogGroupName, ":log-stream:", !Ref "AWS::AccountId", "_CloudTrail_", !Ref "AWS::Region", "*" ] ]
Path: "/"
RoleName: !Sub "${SystemNameLowerCase}-${Env}-iam-role-CloudTrailRole"
CloudTrail:
DependsOn: ## ★先に作成しておく必要があるAWSリソースを明示的に追加
- CloudTrailS3Bucket
- CloudTrailIAMRole
- CloudTrailBucketPolicy
Type: AWS::CloudTrail::Trail
Properties:
S3BucketName: !Ref CloudTrailS3Bucket
EnableLogFileValidation: true
IncludeGlobalServiceEvents: true
IsLogging: true
IsMultiRegionTrail: true
TrailName: !Sub "${SystemNameLowerCase}-${Env}-cloudtrail"
Tags:
- Key: "Name"
Value: !Sub "${SystemNameLowerCase}-${Env}-cloudtrail"
改良後のコードをAWS CloudFormationで実行すると、処理が正常完了しました。
タイムラインビューを確認すると、①S3バケット、②S3バケットポリシー、③IAMロールの作成が完了した後に、
④CloudTrailの処理が実行される仕様と変更になっていることが確認できます。

この例だと作成するAWSリソースの数が少ないので、DependsOnオプション一つだけで大丈夫ですが、何十個、何百個のリソースを作成するコードを作成する際は、大量のDependsOnオプションの指定が必要になり、管理がとても大変になってしまいます。
その場合は、作成するAWSリソースを小分けにして、AWS CloudFormationのコードを分割してデプロイする戦略が有効です。
ファイルを分割する際の実装方式のイメージは下記となります。


事象② 承認遅延タイムアウト発生
この時は、自チームが所有しているVPCと対向先の他チームが持っているAWS Transit Gatewayの構築をしようとしました。
作業手順としては下記の予定をしていました。
①自チームでAWS Transit Gateway Attachmentの作成を行い、他チームのAWSアカウントに承認を依頼する。
②対向の他チーム側で承認を行い、自チームのVPCと他チームのAWS Transit Gatewayの接続を行う。
VPC用のAWS CloudFormationのスタックにて下記のコードを追記して、Transit Gateway Attachmentの作成を行いました。
~~~~
#------------------------------
# Transit Gateway Attachment
#------------------------------
TgwAttachment:
Type: AWS::EC2::TransitGatewayAttachment
Properties:
SubnetIds:
- !Ref TransitGatewaySubnet1
- !Ref TransitGatewaySubnet2
TransitGatewayId: !Ref TgwGWID
VpcId: !Ref VPC
~~~
しかしながら、対向側の②の作業が完了する前(AWS CloudFormationの処理実行をしてから約20分後)に、AWS CloudFormationで「Exceeded attempts to wait」とエラーが表示された後、
スタックがロールバックしてしまいました。
事象②の原因 承認遅延タイムアウト発生
AWS CloudFormationの仕様として、リソース処理内のハンドラーにて、「承認待ちで安定化しないリソース」は、一定時間内に処理が安定しない場合、ハンドラーで FAILEDを返すという仕様となっていました。
具体的なタイムアウト時間は最小2分、最大36時間となっています。AWSリソースの種類によって変わります。
※残念ながら今回のシチュエーションの具体的なタイムアウト値は公式ドキュメントから見つけることはできませんでした。
この事象では、上記AWS CloudFormation側の処理で、リソース作成に失敗扱いとなり、スタックがロールバックしてしまいました。
事象②の解決策 承認遅延タイムアウト発生
タイムアウトが発生する前に、対向側の承認が完了するようオペレーションを改善しました。
※エラー発生した時は、諸事情で他チームへの連携が遅れていたのでそこを改善しました。
これはあくまで時間が20分以内かつ当時は2チームが同時に作業をする前提の体制となってから実現ができました。
AWS Transit Gateway AttachmentやVPCPeering構築等の外部の人間承認を待つ必要があるリソースの構築については、
リリースの方針/体制によってはAWS CloudFormationを使わずに手動構築することも検討が必要になります。
(承認依頼を出すチーム/承認をするチームが別で同日に作業できず、承認依頼を出すところまで作業をやって、承認は後日に実施したい場合等)
手動で構築した場合のAWSリソースの有効期間を公式ドキュメントで調べたところ、下記になっていました。
手動であれば、他チームと一緒に作業をするための厳密な調整を行う必要もなく、期限内に承認を行うだけで作業が完了します。
・AWS Transit Gateway Attachmentのpending acceptance: 明確な自動失効期限はなく、手動でAccept/Rejectするか、Transit Gateway側を削除するまでpendingAcceptanceのまま残り続ける仕様
・VPCPeering接続のpending-acceptance: デフォルト7日間で失効
最後に
今回はAWS CloudFormationのエラーでハマった話をまとめました。
AWS CloudFormationを使いこなすためにはAWSリソースのデプロイ戦略を考えるスキルが重要になってきます。
今回はわかりやすい例を取り上げましたが、実際のシステムでは、何百、何千のAWSリソースがあり作成するCloudFormationの資材も大量に必要なると思います。
必要に応じてAWS構成図の状態遷移を整理しながら準備するとデプロイ戦略の考えが楽になると思います。
おまけ AWS CloudFormationのExpressモード
最近実装されたAWS CloudFormationのExpressモードも少し検証してみました。
AWS CloudFormationのスタック作成/更新gな面で下記のオプションを有効化すると利用することがd家います。

事象①のコードをExpressモードで実行すると下記のようになりました。

通常モードであれば、①リソース作成②一貫性チェックの完了③次の処理の開始という順番に処理が行われるのですが、
②の一貫性チェックを行わないまま、次の処理を行うという仕様となってます。
Expressモードですが、テスト環境で検証用にAWSリソース(AWS Lambda等)を一つだけデプロイしたいという用途でデプロイの高速化は期待できます。
一方で、複数のAWSリソースの一括デプロイや、スピードよりも確実性を求められる本番環境でのデプロイでは注意が必要なモードなので、利用する際は気をつけましょう。


