※2025年12月時点の情報です。
はじめに。
AWS経験歴2年の神谷です。
経験してきた中で一番便利だと感じたCloudFormationスタックについて紹介します。
CloudFormationはテンプレートを書くところから始まりますが、実際の運用で効いてくるのは「スタックをどう扱うか」です。この記事では、スタックの基本、更新時の安全装置、削除時の振る舞い、ドリフト検出、分割の考え方、そして現場でよくある失敗と対処までをひととおりまとめました。
読者ターゲット
- AWSを業務で使っているが、CloudFormationはまだ部分的にしか触っていない方
- スタック更新で失敗した経験があり、安全な運用方法を知りたい方
- IaCを導入したいが、運用フェーズの落とし穴を理解したいチームリーダー
- AWS認定資格(SAA/DevOps)を勉強中で、実務的なTipsを探している方
目次
- スタックとは?テンプレートとの違い
- スタックのライフサイクル(作成/更新/削除)
- 更新を安全にする仕組み(Change Set/Stack Policy/Termination Protection)
- DeletionPolicyの使い分け(Retain/Snapshot/Delete)
- 更新の落とし穴(依存関係/不可変更属性/Replace)
- ドリフト検出の運用
- StackSets・Nested Stacks・Exports/Importsの設計の考え方
- パラメータ運用のベストプラクティス
- 実務Tips(段階的デプロイ/タグ/ログの読み方)
- よくある失敗例と再発防止策
- まとめ
1. スタックとは?テンプレートとの違い
-
テンプレートは設計図(YAML/JSON)。
ResourcesやParametersなどの宣言を持ちます。 - スタックはテンプレートに具体のパラメータを与えて作ったリソースの集合体。イベントや状態、履歴を持つ“運用単位”です。
- 同じテンプレートでも、パラメータを変えれば環境(dev/stg/prod)ごとに別スタックを作れます。運用視点では「テンプレートの書き方」だけでなく「スタックをどう分けて、どう更新するか」が重要です。
2. スタックのライフサイクル(作成/更新/削除)
作成(create-stack)
aws cloudformation create-stack \
--stack-name app-network \
--template-body file://network.yaml \
--parameters \
ParameterKey=VpcCidr,ParameterValue=10.0.0.0/16 \
ParameterKey=PublicSubnetCidr,ParameterValue=10.0.1.0/24 \
--capabilities CAPABILITY_NAMED_IAM
- コンソールのEventsで
CREATE_IN_PROGRESS → CREATE_COMPLETEの流れを追います。 - 失敗時は既定でロールバックします(
ROLLBACK_IN_PROGRESS → ROLLBACK_COMPLETE)。
更新(update-stack)
aws cloudformation update-stack \
--stack-name app-network \
--template-body file://network.yaml \
--parameters ParameterKey=PublicSubnetCidr,ParameterValue=10.0.2.0/24
- 一部の属性は**不可変更(immutable)で、変更を試みると置換(Replace)**が発生します。
- Replaceは「新規作成→切り替え→旧リソース削除」のため、IPやDNSの変更、停止時間に影響します。
削除(delete-stack)
aws cloudformation delete-stack --stack-name app-network
- 個々のリソースの扱いはテンプレート側の
DeletionPolicyで決まります(後述)。
3. 更新を安全にする仕組み(Change Set/Stack Policy/Termination Protection)
Change Set(事前プレビュー)
更新時に何が起きるかを先に確認できます。運用では習慣化したいところです。
aws cloudformation create-change-set \
--stack-name app-network \
--change-set-name preview-202512 \
--template-body file://network.yaml
aws cloudformation describe-change-set \
--stack-name app-network \
--change-set-name preview-202512
- 追加・変更・削除・置換の差分が見えます。特に本番では必須に近い扱いにしたいです。
Stack Policy(クリティカル保護)
重要リソース(例:S3バケット、IAMロール)への削除や置換を防ぐポリシーです。
{
"Statement": [
{ "Effect": "Allow", "Action": "Update:*", "Principal": "*", "Resource": "*" },
{ "Effect": "Deny", "Action": ["Update:Delete","Update:Replace"], "Principal": "*", "Resource": "LogicalResourceId/ProdBucket" }
]
}
適用:
aws cloudformation set-stack-policy \
--stack-name app-prod \
--stack-policy-body file://stack-policy.json
Termination Protection(削除防止)
誤削除を物理的にブロックします。
aws cloudformation update-termination-protection \
--stack-name app-prod \
--enable-termination-protection
4. DeletionPolicyの使い分け(Retain/Snapshot/Delete)
Resources:
ProdBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain # スタック削除でも残す(データ保護)
Database:
Type: AWS::RDS::DBInstance
DeletionPolicy: Snapshot # 削除時に自動スナップショット
TempQueue:
Type: AWS::SQS::Queue
DeletionPolicy: Delete # デフォルト相当。痕跡を残さない
実務の感覚
- 本番のデータ系は基本的にRetainかSnapshot。
- Retainはリソースが孤立しやすいので、命名規約・タグ設計で追跡できるようにしておきます。
- PRレビューでDeletion差分(Delete指定の追加など)を確認する習慣をつけると事故が減ります。
5. 更新の落とし穴(依存関係/不可変更属性/Replace)
- 暗黙依存はCloudFormationが推測してくれますが、要所は明示した方が安全です(
DependsOnやRef/Fn::GetAtt)。 - 不可変更属性に触れるとReplaceを誘発します。影響(IP変更、DNS切替、ダウンタイム)を見積もり、必要ならblue/greenを前提に設計します。
- セキュリティグループの相互参照や、LambdaとIAMロールのポリシー生成などは循環依存になりがち。片方向参照に整理するか、固定CIDRで一時回避するなどの対処を考えます。
6. ドリフト検出の運用
aws cloudformation detect-stack-drift --stack-name app-prod
aws cloudformation describe-stack-drift-detection-status --stack-drift-detection-id <ID>
aws cloudformation describe-stack-resource-drifts --stack-name app-prod
- 本番は基本的に手動変更禁止。やむを得ない場合は例外運用を記録し、速やかにテンプレートへ反映します。
- 許容したい差分はParametersやSSM Parameterに逃がす設計にします。
7. StackSets・Nested Stacks・Exports/Importsの設計の考え方
StackSets(複数アカウント/リージョン展開)
組織横断のガイドラインや標準VPCなどの一斉配布に向いています。更新の波及範囲が大きいので、テンプレートは小粒にしておく方が扱いやすいです。
Nested Stacks(テンプレート分割)
大規模構成は、network/security/compute/dataなどに分けてモジュール化しておくと、変更の影響範囲が読みやすくなります。親→子のOutputs連携で疎結合に。
Exports/Imports(クロススタック参照)
Outputs:
VpcId:
Value: !Ref Vpc
Export:
Name: !Sub "${AWS::StackName}-VpcId"
共有値の再利用に便利ですが、Export名の変更や削除には制約が強めです。長期運用ではSSM Parameter Storeなども選択肢に入ります。
8. パラメータ運用のベストプラクティス
- Defaultは最小限(誤デプロイ防止)。機密値はNoEcho: true。
-
AllowedValuesやAllowedPatternで入力制約をかけておくと事故が減ります。 - SSMパラメータ参照で環境差分を外に出す:
Parameters:
AmiId:
Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
Default: '/prod/amis/web'
-
MetadataのParameterGroups/Labelsでコンソールの入力UIを整えておくと、共同作業がラクになります。
9. 実務Tips(段階的デプロイ/タグ/ログの読み方)
段階的デプロイ
- Change Setで差分を確認
- 非破壊変更のみ適用
- 破壊的変更はblue/green(新スタック→切替→旧削除)
タグ設計
-
Stack=app-prod/Env=prod/Owner=team-xなどを全リソースへ付与。料金可視化や監査、Retainした孤立リソースの探索に効きます。
ログの読み方(Events)
- 失敗時は最初のERRORではなく、その直前のイベントで原因を探します。
- 物理IDから各サービスのコンソールへ飛んで詳細を確認します。
- CloudTrailで権限不足(AccessDenied)が出ていないかも併せて見ると早いです。
10. よくある失敗例と再発防止策
-
本番S3を誤削除:
DeletionPolicy: Deleteのまま適用。→ Stack Policy+DeletionPolicy: Retainを標準に。PRで削除差分をチェック。 - ALBの設定変更でReplaceが発生:不可変更属性に触れた。→ 新リスナーを用意して段階的に切替(blue/green)。Change Setで事前に検知。
- 手動でSGを調整して疎通OK→後の更新で消えた:ドリフトをテンプレートへ還元していない。→ ドリフト検出を定期運用、変更は必ずテンプレート経由。
- Nested Stackの子で失敗して親が大きくロールバック:大変更を一括適用。→ 小刻みに適用、子スタック単位で先に検証。
11. まとめ
- Change Set/Stack Policy/Termination Protection/DeletionPolicyを組み合わせて安全性を担保します。
- Replaceの可能性は常に見積もり、必要ならblue/greenを前提に設計します。
- ドリフト検出で“手動の誘惑”からスタックを守り、IaCの一貫性を維持します。
- 分割(Nested/StackSets)とタグ設計で、長期運用に耐える形に整えます。
付録:最小構成テンプレート(抜粋)
AWSTemplateFormatVersion: '2010-09-09'
Description: Simple VPC with one public subnet and internet access
Parameters:
VpcCidr:
Type: String
Default: 10.0.0.0/16
AllowedPattern: '^([0-9]{1,3}\\.){3}[0-9]{1,3}\\/([0-9]|[1-2][0-9]|3[0-2])$'
PublicSubnetCidr:
Type: String
Default: 10.0.1.0/24
Resources:
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-vpc'
InternetGateway:
Type: AWS::EC2::InternetGateway
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref Vpc
InternetGatewayId: !Ref InternetGateway
PublicSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: !Ref PublicSubnetCidr
MapPublicIpOnLaunch: true
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
PublicRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
DependsOn: AttachGateway
PublicSubnetRtAssoc:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet
RouteTableId: !Ref PublicRouteTable
Outputs:
VpcId:
Value: !Ref Vpc
Export:
Name: !Sub '${AWS::StackName}-VpcId'
PublicSubnetId:
Value: !Ref PublicSubnet