CloudFormationネステッドスタックの作成・分割はどのように行うのか?
CloudFormationで同じテンプレートで親スタック・子スタックのデプロイの記載方法や、別のテンプレートでデプロイしたパラメータの参照方法に困ったことがあったため、備忘として残します。
目次
- スタック分割設計の考え方
- 親スタック・子スタックの実装パターン
- パラメータとアウトプットの効果的な活用
- 条件分岐とマッピングの活用
- カスタムリソースの実装
- スタック更新戦略とロールバック対応
- セキュリティベストプラクティス
- 運用監視とトラブルシューティング
基本的なスタック構成
ネストスタックとは?
CloudFormationでは、「Aテンプレートの中で、別のB,Cのリソースを作成する」というような形でスタックの中にスタックを作成する、入れ子構造を作ることが出来ます。
この全てのテンプレートの大元となるAテンプレートのことを親スタック、Aテンプレートから作られるB,Cなどのスタックを子スタック(ネステッドスタック)と呼びます。
細かいスタックの分割方針については省略しますが、大体以下のような部分を考慮した上でテンプレートを分割すると良いです。
過度な分割を避ける
- 1つのスタックに1つのリソースという極端な分割はNG
- 密結合なリソースは同一スタックに配置する
- スタックの分割粒度を揃える
- 親テンプレートで直接リソースのデプロイを行わない、一つの子テンプレートに関連リソースを纏めるなど
依存関係の明確化
- スタック間の依存関係を図式化しておく(なるべく依存関係が多くなりすぎないように)
- 循環依存を避ける設計に
スタック
スタックを分割する際は、親スタックには、作成するリソースの枠組みのみを記載します。
子スタックにどのようなパラメータを受け渡すのかを記載します。
# parent-stack.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Main stack that orchestrates child stacks'
Parameters:
Environment:
Type: String
Default: dev
ProjectName:
Type: String
Default: my-project
Resources:
# ネットワークスタック
NetworkStack:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: !Sub 'https://my-bucket.s3.amazonaws.com/templates/network.yaml'
# パラメータは、親スタックで定義したものを子スタックへ受け渡す
Parameters:
Environment: !Ref Environment
ProjectName: !Ref ProjectName
Tags:
- Key: Environment
Value: !Ref Environment
# セキュリティスタック(ネットワークスタック後に作成)
SecurityStack:
Type: AWS::CloudFormation::Stack
DependsOn: NetworkStack
Properties:
TemplateURL: !Sub 'https://my-bucket.s3.amazonaws.com/templates/security.yaml'
Parameters:
Environment: !Ref Environment
# ネットワークスタックで作られたパラメータを参照
VpcId: !GetAtt NetworkStack.Outputs.VpcId
PrivateSubnetIds: !GetAtt NetworkStack.Outputs.PrivateSubnetIds
# アプリケーションスタック
ApplicationStack:
Type: AWS::CloudFormation::Stack
DependsOn:
- NetworkStack
- SecurityStack
Properties:
TemplateURL: !Sub 'https://my-bucket.s3.amazonaws.com/templates/application.yaml'
Parameters:
Environment: !Ref Environment
VpcId: !GetAtt NetworkStack.Outputs.VpcId
SubnetIds: !GetAtt NetworkStack.Outputs.PrivateSubnetIds
SecurityGroupId: !GetAtt SecurityStack.Outputs.AppSecurityGroupId
Outputs:
ApplicationUrl:
Description: 'Application URL'
Value: !GetAtt ApplicationStack.Outputs.ApplicationUrl
Export:
Name: !Sub '${ProjectName}-${Environment}-app-url'
子スタックの実装例
子スタックでは、親スタックから受け取ったパラメータを受け取って
# network.yaml(子スタック)
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Network infrastructure'
Parameters:
Environment:
Type: String
ProjectName:
Type: String
Mappings:
EnvironmentConfig:
dev:
VpcCidr: '10.0.0.0/16'
PublicSubnetCidr1: '10.0.1.0/24'
PrivateSubnetCidr1: '10.0.10.0/24'
prod:
VpcCidr: '10.1.0.0/16'
PublicSubnetCidr1: '10.1.1.0/24'
PrivateSubnetCidr1: '10.1.10.0/24'
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !FindInMap [EnvironmentConfig, !Ref Environment, VpcCidr]
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${Environment}-vpc'
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !FindInMap [EnvironmentConfig, !Ref Environment, PublicSubnetCidr1]
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${Environment}-public-subnet-1'
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !FindInMap [EnvironmentConfig, !Ref Environment, PrivateSubnetCidr1]
AvailabilityZone: !Select [0, !GetAZs '']
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${Environment}-private-subnet-1'
Outputs:
VpcId:
Description: 'VPC ID'
Value: !Ref VPC
Export:
Name: !Sub '${ProjectName}-${Environment}-vpc-id'
PublicSubnetIds:
Description: 'Public Subnet IDs'
Value: !Ref PublicSubnet1
Export:
Name: !Sub '${ProjectName}-${Environment}-public-subnet-ids'
PrivateSubnetIds:
Description: 'Private Subnet IDs'
Value: !Ref PrivateSubnet1
Export:
Name: !Sub '${ProjectName}-${Environment}-private-subnet-ids'
スタック間連携のベストプラクティス
1. Cross-Stack参照 vs Nested Stack
Cross-Stack参照の使用例:
# 他のスタックのExportを参照
VpcId: !ImportValue 'my-project-dev-vpc-id'
Nested Stackの使用例:
# 親スタック内で子スタックの出力を直接参照
VpcId: !GetAtt NetworkStack.Outputs.VpcId
2. パラメータファイルの活用
// parameters/dev.json
[
{
"ParameterKey": "Environment",
"ParameterValue": "dev"
},
{
"ParameterKey": "InstanceType",
"ParameterValue": "t3.micro"
}
]
パラメータとアウトプットの効果的な活用
パラメータの設計パターン
1. 階層化されたパラメータ構造
Parameters:
# 環境固有パラメータ
Environment:
Type: String
AllowedValues: [dev, staging, prod]
# アプリケーション固有パラメータ
App:
Type: AWS::SSM::Parameter::Value<String>
Default: /myapp/config/app-name
# インフラ固有パラメータ
InstanceType:
Type: String
Default: t3.micro
AllowedValues: [t3.micro, t3.small, t3.medium]
2. Systems Manager Parameter Storeとの連携
Parameters:
# Parameter Storeから動的に値を取得
DatabasePassword:
Type: AWS::SSM::Parameter::Value<String>
Default: /myapp/database/password
NoEcho: true
# SecureStringパラメータの取得
ApiKey:
Type: AWS::SSM::Parameter::Value<String>
Default: /myapp/external/api-key
NoEcho: true
アウトプットの設計パターン
1. 他スタックでの再利用を考慮したアウトプット
Outputs:
# リソースID
DatabaseEndpoint:
Description: 'RDS endpoint'
Value: !GetAtt Database.Endpoint.Address
Export:
Name: !Sub '${AWS::StackName}-db-endpoint'
# ARN
LambdaFunctionArn:
Description: 'Lambda function ARN'
Value: !GetAtt ProcessorFunction.Arn
Export:
Name: !Sub '${AWS::StackName}-lambda-arn'
# 複合値(JSON)
DatabaseConfig:
Description: 'Database configuration'
Value: !Sub |
{
"endpoint": "${Database.Endpoint.Address}",
"port": "${Database.Endpoint.Port}",
"database": "${DatabaseName}"
}
Export:
Name: !Sub '${AWS::StackName}-db-config'
条件分岐とマッピングの活用
条件分岐の実装パターン
Conditions:
# 単純な条件
IsProduction: !Equals [!Ref Environment, prod]
# 複合条件
IsProductionOrStaging: !Or
- !Equals [!Ref Environment, prod]
- !Equals [!Ref Environment, staging]
# リソース存在確認
HasKmsKey: !Not [!Equals [!Ref KmsKeyId, '']]
Resources:
# 条件付きリソース作成
ProductionOnlyResource:
Type: AWS::S3::Bucket
Condition: IsProduction
Properties:
BucketName: !Sub '${ProjectName}-prod-only-bucket'
# 条件付きプロパティ
Database:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceClass: !If [IsProduction, db.t3.medium, db.t3.micro]
MultiAZ: !If [IsProduction, true, false]
KmsKeyId: !If [HasKmsKey, !Ref KmsKeyId, !Ref 'AWS::NoValue']
マッピングの活用パターン
Mappings:
# 環境別設定
EnvironmentConfig:
dev:
InstanceType: t3.micro
MinSize: 1
MaxSize: 2
DesiredCapacity: 1
staging:
InstanceType: t3.small
MinSize: 1
MaxSize: 3
DesiredCapacity: 2
prod:
InstanceType: t3.medium
MinSize: 2
MaxSize: 10
DesiredCapacity: 4
# リージョン別AMI ID
RegionAmiMap:
us-east-1:
AMI: ami-0abcdef1234567890
us-west-2:
AMI: ami-0fedcba0987654321
ap-northeast-1:
AMI: ami-0123456789abcdef0
Resources:
LaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateData:
ImageId: !FindInMap [RegionAmiMap, !Ref 'AWS::Region', AMI]
InstanceType: !FindInMap [EnvironmentConfig, !Ref Environment, InstanceType]
カスタムリソースの実装
Lambda-backed カスタムリソース
# カスタムリソース用Lambda関数
CustomResourceFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub '${AWS::StackName}-custom-resource'
Runtime: python3.9
Handler: index.lambda_handler
Code:
ZipFile: |
import json
import boto3
import urllib3
def lambda_handler(event, context):
response_url = event['ResponseURL']
stack_id = event['StackId']
request_id = event['RequestId']
logical_resource_id = event['LogicalResourceId']
request_type = event['RequestType']
response_data = {}
status = 'SUCCESS'
try:
if request_type == 'Create':
# カスタム作成ロジック
result = create_custom_resource(event['ResourceProperties'])
response_data['Result'] = result
elif request_type == 'Update':
# カスタム更新ロジック
result = update_custom_resource(event['ResourceProperties'])
response_data['Result'] = result
elif request_type == 'Delete':
# カスタム削除ロジック
delete_custom_resource(event['ResourceProperties'])
except Exception as e:
print(f'Error: {str(e)}')
status = 'FAILED'
response_data['Error'] = str(e)
# CloudFormationに応答を送信
send_response(response_url, status, response_data, stack_id,
request_id, logical_resource_id)
def create_custom_resource(properties):
# カスタム作成ロジックを実装
return "Created successfully"
def send_response(response_url, status, response_data, stack_id,
request_id, logical_resource_id):
response_body = {
'Status': status,
'Reason': f'See CloudWatch Log Stream: {context.log_stream_name}',
'PhysicalResourceId': logical_resource_id,
'StackId': stack_id,
'RequestId': request_id,
'LogicalResourceId': logical_resource_id,
'Data': response_data
}
http = urllib3.PoolManager()
response = http.request('PUT', response_url,
body=json.dumps(response_body),
headers={'Content-Type': 'application/json'})
# カスタムリソースの使用
CustomResource:
Type: Custom::MyCustomResource
Properties:
ServiceToken: !GetAtt CustomResourceFunction.Arn
CustomProperty1: !Ref SomeParameter
CustomProperty2: !Ref AnotherParameter
スタック更新戦略とロールバック対応
更新ポリシーの設定
AutoScalingGroup:
Type: AWS::AutoScaling::AutoScalingGroup
UpdatePolicy:
AutoScalingRollingUpdate:
MinInstancesInService: 1
MaxBatchSize: 2
PauseTime: PT5M
WaitOnResourceSignals: true
SuspendProcesses:
- AlarmNotification
- AZRebalance
Properties:
# ASG設定...
# Lambda関数の段階的デプロイ
LambdaFunction:
Type: AWS::Lambda::Function
Properties:
# Lambda設定...
LambdaAlias:
Type: AWS::Lambda::Alias
UpdatePolicy:
CodeDeployLambdaAliasUpdate:
ApplicationName: !Ref CodeDeployApplication
DeploymentGroupName: !Ref CodeDeployDeploymentGroup
BeforeAllowTrafficHook: !Ref PreTrafficHook
AfterAllowTrafficHook: !Ref PostTrafficHook
変更セットを活用した安全な更新
# 変更セットの作成
aws cloudformation create-change-set \
--stack-name my-stack \
--change-set-name my-changeset \
--template-body file://template.yaml \
--parameters file://parameters.json \
--capabilities CAPABILITY_IAM
# 変更内容の確認
aws cloudformation describe-change-set \
--stack-name my-stack \
--change-set-name my-changeset
# 変更セットの実行
aws cloudformation execute-change-set \
--stack-name my-stack \
--change-set-name my-changeset
セキュリティベストプラクティス
IAMロールとポリシーの最小権限原則
# CloudFormation実行用ロール
CloudFormationExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: cloudformation.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/PowerUserAccess
Policies:
- PolicyName: CloudFormationSpecificPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- iam:CreateRole
- iam:DeleteRole
- iam:AttachRolePolicy
- iam:DetachRolePolicy
Resource: !Sub 'arn:aws:iam::${AWS::AccountId}:role/${ProjectName}-*'
# アプリケーション用IAMロール
ApplicationRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: ApplicationPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource: !Sub '${S3Bucket}/*'
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:Query
Resource: !GetAtt DynamoDBTable.Arn
機密情報の管理
# Secrets Managerの活用
DatabaseSecret:
Type: AWS::SecretsManager::Secret
Properties:
Description: 'Database credentials'
GenerateSecretString:
SecretStringTemplate: '{"username": "admin"}'
GenerateStringKey: 'password'
PasswordLength: 16
ExcludeCharacters: '"@/\'
# Parameter Store SecureStringの活用
ApiKeyParameter:
Type: AWS::SSM::Parameter
Properties:
Name: !Sub '/myapp/${Environment}/api-key'
Type: SecureString
Value: !Ref ApiKeyValue
Description: 'External API Key'
運用監視とトラブルシューティング
CloudWatch Eventsを活用したスタック監視
# スタック状態変更の監視
StackStateChangeRule:
Type: AWS::Events::Rule
Properties:
Description: 'Monitor CloudFormation stack state changes'
EventPattern:
source: [aws.cloudformation]
detail-type: [CloudFormation Stack Status Change]
detail:
stack-id: [!Ref 'AWS::StackId']
status-details:
status:
- CREATE_FAILED
- UPDATE_FAILED
- DELETE_FAILED
- ROLLBACK_FAILED
Targets:
- Arn: !Ref AlertTopic
Id: StackFailureAlert
# SNS通知
AlertTopic:
Type: AWS::SNS::Topic
Properties:
DisplayName: 'CloudFormation Alerts'
Subscription:
- Protocol: email
Endpoint: admin@example.com
ドリフト検出の自動化
# ドリフト検出用Lambda関数
DriftDetectionFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub '${AWS::StackName}-drift-detection'
Runtime: python3.9
Handler: index.lambda_handler
Code:
ZipFile: |
import boto3
import json
def lambda_handler(event, context):
cf_client = boto3.client('cloudformation')
stack_name = event['stack_name']
# ドリフト検出の開始
response = cf_client.detect_stack_drift(StackName=stack_name)
return {
'statusCode': 200,
'body': json.dumps({
'drift_detection_id': response['StackDriftDetectionId']
})
}
# 定期実行用EventBridge
DriftDetectionSchedule:
Type: AWS::Events::Rule
Properties:
Description: 'Run drift detection daily'
ScheduleExpression: 'rate(1 day)'
Targets:
- Arn: !GetAtt DriftDetectionFunction.Arn
Id: DriftDetectionTarget
Input: !Sub |
{
"stack_name": "${AWS::StackName}"
}
デバッグとトラブルシューティングのTips
1. 詳細なログ出力の設定
# Lambda関数でのデバッグログ
Resources:
ProcessorFunction:
Type: AWS::Lambda::Function
Properties:
Environment:
Variables:
LOG_LEVEL: !If [IsProduction, INFO, DEBUG]
STACK_NAME: !Ref 'AWS::StackName'
2. タグ付けによるリソース管理
# 共通タグの定義
Globals:
Function:
Tags:
Environment: !Ref Environment
Project: !Ref ProjectName
StackName: !Ref 'AWS::StackName'
ManagedBy: CloudFormation
まとめ
CloudFormationを本格的に運用する際は、以下のポイントを意識することが重要です:
- 適切なスタック分割: ライフサイクルと責任範囲を考慮した設計
- 親子スタック構成: 依存関係を明確にした階層構造
- パラメータ化: 環境間の差異を適切に吸収
- セキュリティ: 最小権限の原則と機密情報の適切な管理
- 監視とメンテナンス: 継続的な監視とドリフト検出
これらの実践により、スケーラブルで保守性の高いインフラストラクチャコードを構築できます。次回は、具体的なユースケースに基づいたアーキテクチャパターンについて解説予定です。