CloudFormationでネストされたスタックをつくる

CloudFormationのネスト機能を利用してみます。イメージは下記の公式資料より。

f:id:goodbyegangster:20190826051706p:plain

ネストされたスタックの操作

ユースケース

ネスト機能を利用するユースケースは、以下の2パターンがあります。

単一のCloudFormationスタックでは、テンプレートが長大となってしまう場合

単一のCloudFormationテンプレートで宣言できるResourceの最大数は200個までとなっているので、それを超過してしまうような場合です。下記のQiitaの投稿がそのケースにあたります。VPC, Subnet, EC2でテンプレートを別け、それを包括ような形で上位のテンプレートを作成している例となります。

CloudFormationでスタックをネストする

作成するResourceの共通設定内容をテンプレート化して管理する方法

例えば、多数のALBを作成する場合、共通となる設定内容をテンプレート化して、各ALBで参照しながら作成する方法となります。

AWS CloudFormation のベストプラクティス - ネストされたスタックを使用して共通テンプレートパターンを再利用する

今回はこちらのユースケースでの、ネスト化されたスタック作成を試してみます。

作業

複数のALBを作成するにあたり、各ALBリソースで共通設定となるパラメータを、テンプレート化して作成してみます。

共通設定内容をテンプレート化

共通設定となるResourceを定義、各ALBごとに動的に変更されるだろう内容をParameterとして外出ししたCloudFormationを作成します。以下が、そのCloudFormationテンプレートとなります。

template-alb.yml

AWSTemplateFormatVersion: "2010-09-09"
Description: "CloudFormation Template for ALB"
Parameters:
  VpcId:
    Type: "AWS::EC2::VPC::Id"
  Subnet:
    Type: "List<AWS::EC2::Subnet::Id>"
  LoadBalancerName:
    Type: "String"
  EC2Instance:
    Type: "AWS::EC2::Instance::Id"
Resources:
  SecurityGroup:
  # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-security-group-rule.html
    Type: "AWS::EC2::SecurityGroup"
    DeletionPolicy: "Delete"
    Properties:
      GroupName: !Sub for-alb-${LoadBalancerName}
      GroupDescription: !Sub for-alb-${LoadBalancerName}
      # SecurityGroupEgress:
      #   - Security Group Rule
      SecurityGroupIngress:
        - 
          CidrIp: "0.0.0.0/0"
          # CidrIpv6: String
          # Description: String
          IpProtocol: "tcp"
          FromPort: "80"
          ToPort: "80"
          # SourceSecurityGroupId: String
          # SourceSecurityGroupName: String
          # SourceSecurityGroupOwnerId: String
      Tags:
        - Key: "Name"
          Value: !Sub for-alb-${LoadBalancerName}
      VpcId: !Ref "VpcId"
  S3Bucket:
  # https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html
    Type: "AWS::S3::Bucket"
    DeletionPolicy: "Delete"
    DependsOn: "SecurityGroup"
    Properties:
      # AccessControl: String
      # AccelerateConfiguration:
      #   AccelerateConfiguration
      # AnalyticsConfigurations:
      #   - AnalyticsConfiguration
      # BucketEncryption: 
      #   BucketEncryption
      BucketName: !Sub logs-elb-${LoadBalancerName}-${AWS::AccountId}
      # CorsConfiguration:
      #   CorsConfiguration
      # InventoryConfigurations:
      #   - InventoryConfiguration 
      LifecycleConfiguration:
        Rules:
          -
            # AbortIncompleteMultipartUpload: 
            #   AbortIncompleteMultipartUpload
            # ExpirationDate: String
            ExpirationInDays: "30"
            Id: !Sub s3-lifecycle-rule-${LoadBalancerName}
            NoncurrentVersionExpirationInDays: "30"
            # NoncurrentVersionTransitions:
            #   - NoncurrentVersionTransition
            # Prefix: String
            Status: "Enabled"
            # TagFilters: 
            #   - TagFilter
            # Transitions:
            #   - Transition
      # LoggingConfiguration:
      #   LoggingConfiguration
      # MetricsConfigurations: 
      #   - MetricsConfiguration
      # NotificationConfiguration:
      #   NotificationConfiguration
      # PublicAccessBlockConfiguration: 
      #   PublicAccessBlockConfiguration
      # ReplicationConfiguration:
      #   ReplicationConfiguration
      # Tags:
      #   - Key: String
      #     Value: String
      # VersioningConfiguration:
      #   VersioningConfiguration
      # WebsiteConfiguration:
      #   WebsiteConfiguration
  S3BucketPolicy:
  # https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html
    Type: "AWS::S3::BucketPolicy"
    DeletionPolicy: "Delete"
    DependsOn: "S3Bucket"
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Statement: 
          - 
            Action: 
              - "s3:PutObject"
            Effect: "Allow"
            Resource: !Sub arn:aws:s3:::${S3Bucket}/${LoadBalancerName}/AWSLogs/${AWS::AccountId}/*
            Principal:
              AWS: "582318560864"
  LoadBalancer:
  # https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-loadbalancer.html
    Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
    DependsOn: "S3BucketPolicy"
    DeletionPolicy: "Delete"
    Properties:
      IpAddressType: "ipv4"
      LoadBalancerAttributes:
        - Key: "access_logs.s3.enabled"
          Value: "true"
        - Key: "access_logs.s3.bucket"
          Value: !Ref "S3Bucket"
        - Key: "access_logs.s3.prefix"
          Value: !Ref "LoadBalancerName"
        - Key: "deletion_protection.enabled"
          Value: "false"
        - Key: "idle_timeout.timeout_seconds"
          Value: "120"
        - Key: "routing.http2.enabled"
          Value: "true"
      Name: !Ref "LoadBalancerName"
      Scheme: "internet-facing"
      SecurityGroups:
        - !Ref "SecurityGroup"
      # SubnetMappings:
      #   - SubnetMapping
      Subnets: !Ref "Subnet"
      # Tags:
      #   - Key: String
      #     Value: String
      Type: "application"
  TargetGroup:
  # https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-targetgroup.html
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    DependsOn: "LoadBalancer"
    DeletionPolicy: "Delete"
    Properties:
      HealthCheckIntervalSeconds: "30"
      HealthCheckPath: "/"
      HealthCheckPort: "traffic-port"
      HealthCheckProtocol: "HTTP"
      HealthCheckTimeoutSeconds: "5"
      HealthyThresholdCount: "5"
      Matcher:
        HttpCode: "200"
      Name: !Sub tg-${LoadBalancerName}
      Port: "80"
      Protocol: "HTTP"
      # Tags:
      #   - Key: String
      #     Value: String
      TargetGroupAttributes:
        - Key: "deregistration_delay.timeout_seconds"
          Value: "300"
      Targets:
        - 
          # AvailabilityZone: String
          Id: !Ref EC2Instance
          # Port: Integer
      TargetType: "instance"
      UnhealthyThresholdCount: "2"
      VpcId: !Ref "VpcId"
  Listener:
  # https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-listener.html
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    DependsOn: "TargetGroup"
    DeletionPolicy: "Delete"
    Properties:
      # Certificates:
      #   - CertificateArn: String
      DefaultActions:
        -
          # AuthenticateCognitoConfig: AuthenticateCognitoConfig
          # AuthenticateOidcConfig: AuthenticateOidcConfig
          # FixedResponseConfig: FixedResponseConfig
          Order: "1"
          # RedirectConfig: RedirectConfig
          TargetGroupArn: !Ref TargetGroup
          Type: "forward"
      LoadBalancerArn: !Ref "LoadBalancer"
      Port: "80"
      Protocol: "HTTP"
      # SslPolicy: String

長々書いていますが、以下のような定義をしています。

  • Paramter
    • ALBを配置するVPC
    • ALBを配置するSubnet
    • ALB名
    • ALB配下となるEC2
  • Resource

参照されるテンプレートはS3にアップロードする必要があります。今回は s3://cloudformation-template-zunpero/alb/template-alb.yml というS3パスにアップロードしています。

実際に作成するALBを定義したテンプレートを作成

上記でS3上にアップロードしたテンプレートファイルを参照しつつ、Parameterとした項目に値を設定するテンプレートを作成します。テンプレートのResourceTypeは AWS::CloudFormation::Stack というものを利用します。

CloudFormation - AWS::CloudFormation::Stack

各パラメータについは上記の公式を見てもらうとして、1つ目のALB用。

sample1.yml

AWSTemplateFormatVersion: "2010-09-09"
Description: "sample1"
Resources:
  sample1:
    Type: AWS::CloudFormation::Stack
    DeletionPolicy: "Delete"
    Properties:
      # NotificationARNs:
      #   - String
      Parameters:
        VpcId: "vpc-aaaaaaaa"
        Subnet: "subnet-11111111,subnet-22222222"
        LoadBalancerName: "sample1"
        EC2Instance: "i-000000000000000"
      # Tags:
      #   - Resource Tag
      TemplateURL: "https://s3.amazonaws.com/cloudformation-template-zunpero/alb/template-alb.yml"
      # TimeoutInMinutes: Integer

2つ目のALB用。

sample2.yml

AWSTemplateFormatVersion: "2010-09-09"
Description: "sample2"
Resources:
  sample1:
    Type: AWS::CloudFormation::Stack
    DeletionPolicy: "Delete"
    Properties:
      # NotificationARNs:
      #   - String
      Parameters:
        VpcId: "vpc-bbbbbbbb"
        Subnet: "subnet-33333333,subnet-44444444"
        LoadBalancerName: "sample2"
        EC2Instance: "i-111111111111111"
      # Tags:
      #   - Resource Tag
      TemplateURL: "https://s3.amazonaws.com/cloudformation-template-zunpero/alb/template-alb.yml"
      # TimeoutInMinutes: Integer

作成/更新

用意した sample1.ymlsample2.yml のCloudFormationを実行すれば、AWSリソースが作成されます。

S3上にアップロードしたテンプレートファイルを更新して、CloudFormationスタックを更新すれば、当然AWSリソースが更新されることになります。困る点としては、 Change Sets が全く利用できないという点ですね。。。参照側、被参照側のどちらのテンプレートを更新したとしても、Change Setsで変更点を表示してくれないです。