CloudFormationでスタックをネストし、EC2を起動させてみた

本記事では、CloudFormationのベストプラクティスの1つである「ネストされたスタックを使用して共通テンプレートパターンを再利用する」という項目について実践してみます。
2021.10.29

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

CloudFormationでのスタックのネスト

CloudFormationでは、スタックをネストさせること可能です。
ネストさせる際には、テンプレート(ファイル)を分割して記述することができるので、一度作成したテンプレートを使い回すことができます。
本記事では、実際にスタックをネストし、EC2の起動を行なっていきます。

作成する構成

EC2をPublic Subnet上に立てます。今回は使用しませんが、Private Subnetも構築します。

各リソースのテンプレート作成

ファイル構成は以下のように行います。

├── ec2
│   ├── ec2.yaml
│   └── sg.yaml
├── main.yaml
├── subnet
│   ├── private
│   │   └── subnet.yaml
│   └── public
│       └── subnet.yaml
└── vpc
    └── vpc.yaml

EC2を作成する際に、セキュリティグループのテンプレートを呼び出すようにします。

親テンプレートの作成

各テンプレートを呼び出す親テンプレートを作成します。

main.yaml

AWSTemplateFormatVersion: "2010-09-09"
Description: nesting stack 

Resources:

  VPC:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: vpc/vpc.yaml
      Parameters:
        TagName : "VPCTEST"
  
  PrivateSubnet:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: subnet/private/subnet.yaml
      Parameters:
        VPC: !GetAtt VPC.Outputs.VPCID
  
  PublicSubnet:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: subnet/public/subnet.yaml
      Parameters:
        VPC: !GetAtt VPC.Outputs.VPCID

  EC2Instance:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ec2/ec2.yaml
      Parameters:
        VPC: !GetAtt VPC.Outputs.VPCID
        KeyName: hogehoge
        Subnet: !GetAtt PublicSubnet.Outputs.SubnetID

VPCテンプレートの作成

VPCのIDを渡すのでOutputsに指定しています。

vpc/vpc.yaml

AWSTemplateFormatVersion: '2010-09-09'
Description: 'VPC for nesting'
Parameters:
  VPCCIDR:
    Type: String
    Default: "10.1.0.0/16"
  TagName :
    Type: String
    Default: "nesting"
  
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      Tags:
        - Key: Name
          Value: !Ref TagName

Outputs:
  VPCID: 
    Value: !Ref VPC

サブネットテンプレートの作成

パブリック、プライベートサブネットそれぞれの作成を行います。

subnet/private/subnet.yaml

AWSTemplateFormatVersion: '2010-09-09'
Description: 'private subnet for nesting'

Parameters:
  AZ:
    Type: AWS::EC2::AvailabilityZone::Name
    Default: ap-northeast-1a
  TagName:
    Type: String
    Default: "subnetnesting"
  VPC:
    Type: AWS::EC2::VPC::Id

Resources:
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties: 
      AvailabilityZone: !Ref AZ
      VpcId: !Ref VPC
      CidrBlock: 10.1.1.0/24
      Tags:
        - Key: Name
          Value: !Ref TagName
  PrivateRouteTable: 
    Type: "AWS::EC2::RouteTable"
    Properties: 
      VpcId: !Ref VPC 
      Tags: 
        - Key: Name
          Value: !Ref TagName
  PrivateSubnetTableAssociation: 
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties: 
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateRouteTable

Outputs:
  SubnetID: 
    Value: !Ref PrivateSubnet

subnet/public/subnet.yaml

AWSTemplateFormatVersion: '2010-09-09'
Description: 'public subnet for nesting'

Parameters:
  AZ:
    Type: AWS::EC2::AvailabilityZone::Name
    Default: ap-northeast-1a
  TagName:
    Type: String
    Default: "subnetnesting"
  VPC:
    Type: AWS::EC2::VPC::Id

Resources:
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties: 
      AvailabilityZone: !Ref AZ
      VpcId: !Ref VPC
      CidrBlock: 10.1.2.0/24
      Tags:
        - Key: Name
          Value: !Ref TagName
  
  PublicRouteTable: 
    Type: "AWS::EC2::RouteTable"
    Properties: 
      VpcId: !Ref VPC 
      Tags: 
        - Key: Name
          Value: !Ref TagName
  
  InternetGateway: 
    Type: "AWS::EC2::InternetGateway"
    Properties: 
      Tags: 
        - Key: Name
          Value: !Ref TagName
  
  InternetGatewayAttachment: 
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties: 
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC 

  PublicRoute: 
    Type: "AWS::EC2::Route"
    Properties: 
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway 

  PublicSubnetTableAssociation: 
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties: 
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable

Outputs:
  SubnetID: 
    Value: !Ref PublicSubnet

EC2テンプレートの作成

EC2では、セキュリティグループのテンプレートを呼び出すように記述します。
動作確認のため、LAMPを構築するようにユーザデータを指定します。

ec2/ec2.yaml

AWSTemplateFormatVersion: '2010-09-09'
Description: 'VPC for nesting'
Parameters:
  ImageId:
    Type: String
    Default: "ami-0701e21c502689c31"
  KeyName:
    Type: String
  Subnet:
    Type: AWS::EC2::Subnet::Id
  VPC:
    Type: AWS::EC2::VPC::Id
  TagName :
    Type: String
    Default: "nesting"

Resources:
  EC2SG:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: sg.yaml
      Parameters:
        VPC: !Ref VPC
  
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties: 
      ImageId: !Ref ImageId
      KeyName: !Ref KeyName
      InstanceType: t2.micro
      NetworkInterfaces: 
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          SubnetId: !Ref Subnet
          GroupSet:
            - !GetAtt EC2SG.Outputs.SGID
      UserData: !Base64 |
        #!/bin/bash
        yum update -y
        amazon-linux-extras install -y lamp-mariadb10.2-php7.2 php7.2
        yum install -y httpd mariadb-server
        systemctl start httpd
        systemctl enable httpd
        usermod -a -G apache ec2-user
        chown -R ec2-user:apache /var/www
        chmod 2775 /var/www && find /var/www -type d -exec sudo chmod 2775 {} \;
        find /var/www -type f -exec sudo chmod 0664 {} \;
        echo "<?php phpinfo(); ?>" > /var/www/html/phpinfo.php
        systemctl start mariadb
        mysqladmin password hogehoge1111
        yum install php-mbstring php-xml -y
        systemctl restart httpd
        systemctl restart php-fpm
        wget https://www.phpmyadmin.net/downloads/phpMyAdmin-latest-all-languages.tar.gz
        mkdir /var/www/html/phpMyAdmin && tar -xvzf phpMyAdmin-latest-all-languages.tar.gz -C /var/www/html/phpMyAdmin --strip-components 1
        rm phpMyAdmin-latest-all-languages.tar.gz
        systemctl start mariadb
        
      Tags:
          - Key: Name
            Value: !Ref TagName

セキュリティグループテンプレートの作成

セキュリティグループのIDをEC2に渡す必要があるため、Outputsで指定します。

ec2/sg.yaml

AWSTemplateFormatVersion: '2010-09-09'
Description: 'SG for nesting'
Parameters:
  MyIP:
    Type: String
    Default: "0.0.0.0/0"
  VPC:
    Type: AWS::EC2::VPC::Id
  TagName :
    Type: String
    Default: "nesting"

Resources:
  EC2SG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "test demo"
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref MyIP
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref MyIP
      Tags:
        - Key: Name
          Value: !Ref TagName

Outputs:
  SGID: 
    Value: !Ref EC2SG

スタック作成の準備をする

このままの記述では、ローカルファイルを参照できないので、S3 バケットへアップロードします。

バケットの作成

$ aws s3 mb s3://nested-stack-artifact

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

aws cloudformation package --template-file main.yaml \
    --s3-bucket nested-stack-artifact \
    --output-template-file artifact.yml

正常に実行できると、artifact.ymlが作成されています。
中身を見てみると、先ほどまでローカルファイルを指定していたものがs3のファイル参照に変更されていることが確認できます。

artifact.yml

AWSTemplateFormatVersion: '2010-09-09'
Description: nesting stack
Resources:
  VPC:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.ap-northeast-1.amazonaws.com/nested-stack-artifact/hogehoge.template
      Parameters:
        TagName: VPCTEST
  PrivateSubnet:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.ap-northeast-1.amazonaws.com/nested-stack-artifact/hogehoge.template
      Parameters:
        VPC:
          Fn::GetAtt:
          - VPC
          - Outputs.VPCID
  PublicSubnet:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.ap-northeast-1.amazonaws.com/nested-stack-artifact/hogehoge.template
      Parameters:
        VPC:
          Fn::GetAtt:
          - VPC
          - Outputs.VPCID
  EC2Instance:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.ap-northeast-1.amazonaws.com/nested-stack-artifact/hogehoge.template
      Parameters:
        VPC:
          Fn::GetAtt:
          - VPC
          - Outputs.VPCID
        KeyName: hogehoge
        Subnet:
          Fn::GetAtt:
          - PublicSubnet
          - Outputs.SubnetID

スタックの作成

では、最後にスタックの作成を行います。先程のartifact.ymlを指定します。

aws cloudformation deploy --template-file artifact.yml --stack-name nested-stack-demo

コンソール上を確認すると、作成したスタックとネストされたスタックが確認できます。
しばらく待つと、CREATE_COMPLETEとなります。

まとめ

本記事では、ネスタされたスタックを作成してみましたが、再利用できるのは非常に魅力的だと感じました。
一方で、どこまで分割していくかが悩ましいところです。細かく分割してしまうと、逆にファイルの管理が大変になりそうですので、機能ごとにスタックをまとめて、再利用していくのが良さそうです。