dshimizu/blog/alpha

とりとめのないITブログ

ネストされたCFnテンプレートを作ってみる

CFnのテンプレートを1つにしておくと可読性が悪くなったりなどはよく言われているのでNested Stackを使って分割するサンプルを書いてみた。

ネストされたスタック

CloudFormationの最上位のテンプレート内で、別のCloudFormationテンプレートをリソースとして定義することができる、というもの。

これによってテンプレートを分割できる。

試す

VPCの作成とEC2の作成を別テンプレートにして試す。

ディレクトリ構成は以下のようにしてみる。 直下の./master.ymlの中に./ec2/master.yml./vpc/master.ymlを呼び出すように定義する。

./
┗ master.yml
  ec2/
  ┗ master.yml
  vpc/
  ┗ master.yml

master.ymlの中身は以下のような感じにしている。

  • ./master.yml
  • AWSTemplateFormatVersion: '2010-09-09'
    
    Resources:
      vpcStack:
        Type: AWS::CloudFormation::Stack
        Properties:
          TemplateURL: ./vpc/master.yml
      EC2Stack:
        Type: AWS::CloudFormation::Stack
        Properties:
          TemplateURL: ./ec2/master.yml

TemplateURLのところでネストするテンプレートを定義する。 通常この部分はアップロードする先のS3のURLを指定しなければならない。

  • ./vpc/master.yml
  • AWSTemplateFormatVersion: 2010-09-09
    Description: VPC Template
    
    Parameters:
      ProjectName:
        Type: String
        Default: 'sample-cfn'
        Description: Project name for tag
    
    Resources:
      # VPC
      VPC:
        Type: 'AWS::EC2::VPC'
        Properties:
          CidrBlock: 10.1.0.0/16
          InstanceTenancy: default
          EnableDnsSupport: 'true'
          EnableDnsHostnames: 'false'
          Tags:
            - Key: Name
              Value: !Sub "${ProjectName}-vpc"
    
      # Inernet Gateway
      InternetGateway:
        Type: 'AWS::EC2::InternetGateway'
        Properties:
          Tags:
            - Key: Name
              Value: !Sub "${ProjectName}-igw"
    
      # DHCP Options
      DHCPOptions:
        Type: 'AWS::EC2::DHCPOptions'
        Properties:
          Tags:
            - Key: Name
              Value: !Sub "${ProjectName}-dopt"
          DomainName: ap-northeast-1.compute.internal
          DomainNameServers:
            - AmazonProvidedDNS
    
      # RouteTable
      RouteTable:
        Type: 'AWS::EC2::RouteTable'
        Properties:
          VpcId: !Ref VPC
          Tags:
            - Key: Name
              Value: !Sub "${ProjectName}-rt"
    
      # NetworkACL
      NetworkACL:
        Type: 'AWS::EC2::NetworkAcl'
        Properties:
          VpcId: !Ref VPC
    
      # NetworkACL Entry
      NetworkAclEntryEgress:
        Type: 'AWS::EC2::NetworkAclEntry'
        Properties:
          CidrBlock: 0.0.0.0/0
          Egress: 'true'
          Protocol: '-1'
          RuleAction: allow
          RuleNumber: '100'
          NetworkAclId: !Ref NetworkACL
      NetworkAclEntryIngress:
        Type: 'AWS::EC2::NetworkAclEntry'
        Properties:
          CidrBlock: 0.0.0.0/0
          Protocol: '-1'
          RuleAction: allow
          RuleNumber: '100'
          NetworkAclId: !Ref NetworkACL
    
      # InternetGateway
      VPCGatewayAttachment:
        Type: 'AWS::EC2::VPCGatewayAttachment'
        Properties:
          VpcId: !Ref VPC
          InternetGatewayId: !Ref InternetGateway
    
      # RouteTable
      Route:
        Type: 'AWS::EC2::Route'
        Properties:
          DestinationCidrBlock: 0.0.0.0/0
          RouteTableId: !Ref RouteTable
          GatewayId: !Ref InternetGateway
        DependsOn: VPCGatewayAttachment
    
      # DHCP Options Association
      DHCPOptionsAssociation:
        Type: 'AWS::EC2::VPCDHCPOptionsAssociation'
        Properties:
          VpcId: !Ref VPC
          DhcpOptionsId: !Ref DHCPOptions
    
      # Subnet
      PublicSubnetA:
        Type: 'AWS::EC2::Subnet'
        Properties:
          CidrBlock: 10.1.1.0/24
          AvailabilityZone: ap-northeast-1a
          VpcId: !Ref VPC
          Tags:
            - Key: Name
              Value: !Sub "${ProjectName}-public-subnet-1a"
      SubnetRouteTableAssociation:
        Type: 'AWS::EC2::SubnetRouteTableAssociation'
        Properties:
          SubnetId: !Ref PublicSubnetA
          RouteTableId: !Ref RouteTable
      SubnetRouteTableAssociation:
        Type: 'AWS::EC2::SubnetRouteTableAssociation'
        Properties:
          SubnetId: !Ref PublicSubnetA
          NetworkAclId: !Ref NetworkACL
    
    ### Outputs
    Outputs:
      VPCId:
        Value: !Ref VPC
        Export:
          Name: !Sub "${ProjectName}-vpc"
      RouteTableId:
        Value: !Ref rt1
        Export:
          Name: !Sub "${ProjectName}-rt"
      NetworkAclId:
        Value: !Ref NetworkACL
        Export:
          Name: !Sub "${ProjectName}-nacl"
      PublicSubnetA:
        Value: !Ref subnet1
        Export:
          Name: !Sub "${ProjectName}-public-subnet-1a"
    
  • ./ec2/master.yml
  • AWSTemplateFormatVersion: 2010-09-09
    Description: Create EC2 Template
    
    Parameters:
      ProjectName:
        Type: String
        Default: 'sample-cfn'
        Description: Project name for tag
    Resources:
      EC2:
        Type: 'AWS::EC2::Instance'
        Properties:
          DisableApiTermination: 'false'
          InstanceInitiatedShutdownBehavior: stop
          ImageId: ami-00f9d04b3b3092052
          InstanceType: t2.micro
          KeyName: ec2-key-pair-apn1
          Monitoring: 'false'
          Tags:
            - Key: Name
              Value: !Sub "${ProjectName}-ec2"
          NetworkInterfaces:
            - DeleteOnTermination: 'true'
              Description: Primary network interface
              DeviceIndex: 0
              SubnetId:
                "Fn::ImportValue": !Sub "${ProjectName}-public-subnet-1a"
              GroupSet:
                - !Ref SecurityGroup
              AssociatePublicIpAddress: 'true'
    
      SecurityGroup:
        Type: 'AWS::EC2::SecurityGroup'
        Properties:
          GroupDescription: 'Sample SG'
          VpcId:
            "Fn::ImportValue": !Sub "${ProjectName}-vpc"
    
      SecurityGroupIngress:
        Type: 'AWS::EC2::SecurityGroupIngress'
        Properties:
          GroupId: !Ref SecurityGroup
          IpProtocol: tcp
          FromPort: '22'
          ToPort: '22'
          CidrIp: 0.0.0.0/0
    
      SecurityGroupEgress:
        Type: 'AWS::EC2::SecurityGroupEgress'
        Properties:
          GroupId: !Ref SecurityGroup
          IpProtocol: '-1'
          CidrIp: 0.0.0.0/0
    

実行

テンプレートファイルをアップロードするS3バケットを作成します。

% aws s3 mb s3://cfn-nested-stack

S3バケット作成後、下記コマンドでアップロードします。

% aws cloudformation package --template-file master.yaml \
    --s3-bucket cfn-nested-stack \
    --output-template-file master-package.yml

チェンジセットを作成します。

aws cloudformation execute-change-set \
    --stack-name cfn-nested-stack \
    --change-set-name cfn-nested-change-set

チェンジセットを実行します。

aws cloudformation execute-change-set \
    --stack-name cfn-nested-stack \
    --change-set-name cfn-nested-change-set

まとめ

ネストされたスタックをaws cliで扱うやり方について書いた。親テンプレートに定義するTemplateURLは本来、子テンプレートが置いてあるs3のバケットURLを指定する必要があるが、ローカルのディレクトリを指定してaws cliを使ってパッケージングすると、子テンプレートをS3にアップロードした上でTemplateURLをs3のURLに書き換えてくれるのでその点は便利だと思う。 ただ、テンプレートが肥大化すると結局変数の受け渡しが煩雑になる気はする。