0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CloudFormationネステッドスタックの作成・分割はどのように行うのか?

Posted at

CloudFormationネステッドスタックの作成・分割はどのように行うのか?

CloudFormationで同じテンプレートで親スタック・子スタックのデプロイの記載方法や、別のテンプレートでデプロイしたパラメータの参照方法に困ったことがあったため、備忘として残します。

目次

  1. スタック分割設計の考え方
  2. 親スタック・子スタックの実装パターン
  3. パラメータとアウトプットの効果的な活用
  4. 条件分岐とマッピングの活用
  5. カスタムリソースの実装
  6. スタック更新戦略とロールバック対応
  7. セキュリティベストプラクティス
  8. 運用監視とトラブルシューティング

基本的なスタック構成

ネストスタックとは?
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を本格的に運用する際は、以下のポイントを意識することが重要です:

  1. 適切なスタック分割: ライフサイクルと責任範囲を考慮した設計
  2. 親子スタック構成: 依存関係を明確にした階層構造
  3. パラメータ化: 環境間の差異を適切に吸収
  4. セキュリティ: 最小権限の原則と機密情報の適切な管理
  5. 監視とメンテナンス: 継続的な監視とドリフト検出

これらの実践により、スケーラブルで保守性の高いインフラストラクチャコードを構築できます。次回は、具体的なユースケースに基づいたアーキテクチャパターンについて解説予定です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?