😎

CloudFormationのコマンドラインツールRainを使用してWordPress環境を構築してみた

2021/02/23に公開

はじめに

Classmethod様の記事を拝見してRainを使ってみたかったのと、VPC関連リソースをCloudFormation化して好きな時に作って壊したいなと思っていたので、重い腰を上げてやってみました。

環境↓

$ sw_vers
ProductName:	macOS
ProductVersion:	11.2.1
BuildVersion:	20D74

$ rain --version
Rain v1.1.1 darwin/amd64

Rainとは

CloudFormationのためのコマンドラインツールです。
使ってみて嬉しかった機能は以下の3つです。

  • デプロイのリアルタイム情報が表示される
  • リソースを指定したら、そのリソースに合わせたテンプレートを作成してくれる
  • デプロイ失敗後の再デプロイがコンソールと比べて激速

詳細はGitHubをご覧ください。
https://github.com/aws-cloudformation/rain

私がRainを知ったきっかけはClassmethod様の記事なのでこちらもどうぞ。
https://dev.classmethod.jp/articles/aws-cloudformation-rain/

WordPress環境の完成図

Rainを使ってCloudFormationテンプレートを作成する

rain build | ネットワーク系テンプレート作成

rain buildコマンドでリソースタイプを指定すると、そのリソースのプロパティを記述したテンプレートの雛形を作成してくれます。
まず、VPCなどのネットワーク系リソースのテンプレートを作成し、その後にEC2インスタンス、RDSを作成していきます。

必要なリソースタイプはこちらの公式ドキュメントから探しながら、
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html
こちらのAWS側で提供しているテンプレートなども参考にしました。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-sample-templates.html

$ rain build AWS::EC2::VPC AWS::EC2::InternetGateway AWS::EC2::VPCGatewayAttachment AWS::EC2::Subnet AWS::EC2::Subnet AWS::EC2::Subnet AWS::EC2::Subnet AWS::EC2::RouteTable AWS::EC2::Route AWS::EC2::SubnetRouteTableAssociation > rain/Network.yaml
Network.yaml
AWSTemplateFormatVersion: "2010-09-09"

Description: Template generated by rain

Resources:
  MyInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: CHANGEME
          Value: CHANGEME

  MyRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: CHANGEME # Optional
      DestinationIpv6CidrBlock: CHANGEME # Optional
      EgressOnlyInternetGatewayId: CHANGEME # Optional
      GatewayId: CHANGEME # Optional
      InstanceId: CHANGEME # Optional
      NatGatewayId: CHANGEME # Optional
      NetworkInterfaceId: CHANGEME # Optional
      RouteTableId: CHANGEME
      TransitGatewayId: CHANGEME # Optional
      VpcPeeringConnectionId: CHANGEME # Optional

  MyRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: CHANGEME
          Value: CHANGEME
      VpcId: CHANGEME
  
  MySubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AssignIpv6AddressOnCreation: false # Optional
      AvailabilityZone: CHANGEME # Optional
      CidrBlock: CHANGEME
      Ipv6CidrBlock: CHANGEME # Optional
      MapPublicIpOnLaunch: false # Optional
      Tags:
        - Key: CHANGEME
          Value: CHANGEME
      VpcId: CHANGEME

  MySubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AssignIpv6AddressOnCreation: false # Optional
      AvailabilityZone: CHANGEME # Optional
      CidrBlock: CHANGEME
      Ipv6CidrBlock: CHANGEME # Optional
      MapPublicIpOnLaunch: false # Optional
      Tags:
        - Key: CHANGEME
          Value: CHANGEME
      VpcId: CHANGEME

  MySubnet3:
    Type: AWS::EC2::Subnet
    Properties:
      AssignIpv6AddressOnCreation: false # Optional
      AvailabilityZone: CHANGEME # Optional
      CidrBlock: CHANGEME
      Ipv6CidrBlock: CHANGEME # Optional
      MapPublicIpOnLaunch: false # Optional
      Tags:
        - Key: CHANGEME
          Value: CHANGEME
      VpcId: CHANGEME

  MySubnet4:
    Type: AWS::EC2::Subnet
    Properties:
      AssignIpv6AddressOnCreation: false # Optional
      AvailabilityZone: CHANGEME # Optional
      CidrBlock: CHANGEME
      Ipv6CidrBlock: CHANGEME # Optional
      MapPublicIpOnLaunch: false # Optional
      Tags:
        - Key: CHANGEME
          Value: CHANGEME
      VpcId: CHANGEME

  MySubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: CHANGEME
      SubnetId: CHANGEME

  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: CHANGEME
      EnableDnsHostnames: false # Optional
      EnableDnsSupport: false # Optional
      InstanceTenancy: CHANGEME # Optional
      Tags:
        - Key: CHANGEME
          Value: CHANGEME

  MyVPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: CHANGEME # Optional
      VpcId: CHANGEME
      VpnGatewayId: CHANGEME # Optional

Outputs:
  MySubnet1AvailabilityZone:
    Value: !GetAtt MySubnet1.AvailabilityZone

  MySubnet1Ipv6CidrBlocks:
    Value: !GetAtt MySubnet1.Ipv6CidrBlocks

  MySubnet1NetworkAclAssociationId:
    Value: !GetAtt MySubnet1.NetworkAclAssociationId

  MySubnet1VpcId:
    Value: !GetAtt MySubnet1.VpcId

  MySubnet2AvailabilityZone:
    Value: !GetAtt MySubnet2.AvailabilityZone

  MySubnet2Ipv6CidrBlocks:
    Value: !GetAtt MySubnet2.Ipv6CidrBlocks

  MySubnet2NetworkAclAssociationId:
    Value: !GetAtt MySubnet2.NetworkAclAssociationId

  MySubnet2VpcId:
    Value: !GetAtt MySubnet2.VpcId

  MySubnet3AvailabilityZone:
    Value: !GetAtt MySubnet3.AvailabilityZone

  MySubnet3Ipv6CidrBlocks:
    Value: !GetAtt MySubnet3.Ipv6CidrBlocks

  MySubnet3NetworkAclAssociationId:
    Value: !GetAtt MySubnet3.NetworkAclAssociationId

  MySubnet3VpcId:
    Value: !GetAtt MySubnet3.VpcId

  MySubnet4AvailabilityZone:
    Value: !GetAtt MySubnet4.AvailabilityZone

  MySubnet4Ipv6CidrBlocks:
    Value: !GetAtt MySubnet4.Ipv6CidrBlocks

  MySubnet4NetworkAclAssociationId:
    Value: !GetAtt MySubnet4.NetworkAclAssociationId

  MySubnet4VpcId:
    Value: !GetAtt MySubnet4.VpcId

  MyVPCCidrBlock:
    Value: !GetAtt MyVPC.CidrBlock

  MyVPCCidrBlockAssociations:
    Value: !GetAtt MyVPC.CidrBlockAssociations

  MyVPCDefaultNetworkAcl:
    Value: !GetAtt MyVPC.DefaultNetworkAcl

  MyVPCDefaultSecurityGroup:
    Value: !GetAtt MyVPC.DefaultSecurityGroup

  MyVPCIpv6CidrBlocks:
    Value: !GetAtt MyVPC.Ipv6CidrBlocks

そのプロパティがオプションか分かるように表示してくれます。
誤字の心配がなく、Outputsまでカバー。
rain build --bareとすれば、必須のプロパティのみ表示してくれます。
神ですね。

リソースの並び順はアルファベット順のようです。
個人的には指定した引数順に並べてくれたらなお嬉しいです。
では、このテンプレートを活用して肉付けしていきます。

VPC・InternetGatewayの設定

Parameters:
  VpcCidrIpv4:
    Type: String
    Default: 10.0.0.0/16
    ConstraintDescription: Malformed input-Parameter VpcCidrIpv4 must match pattern (x.x.x.x/16-28)
    AllowedPattern: ^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/1[6-9]|2[0-8]$

  InstanceTenancy:
    Type: String
    Default: default
    AllowedValues:
      - default
      - dedicated
      - host

Resources:
# VPC
  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidrIpv4
      EnableDnsHostnames: true # Optional
      EnableDnsSupport: true # Optional
      InstanceTenancy: !Ref InstanceTenancy # Optional

# InternetGateway
  MyInternetGateway:
    Type: AWS::EC2::InternetGateway

  MyVPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref MyInternetGateway # Optional
      VpcId: !Ref MyVPC

VPCのCidrBlockInstanceTenancyをパラメータで渡すことで、カスタマイズできるようにしています。
CIDRはAllowedPatternにより正規表現に一致する値しか入力できないようにしています。
ここまでする必要があるかは疑問ですが、正規表現の勉強がてらしてみました。

なお、CloudFormationの正規表現はJavaの正規表現構文に準拠しています。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-regexes.html
正規表現の参考・チェックに使用したサイトはこちら。
https://www.geolocation.co.jp/learn/program/07.html
https://regex-testdrive.com/ja/

Subnetの設定

Parameters:
  SubnetCidrIpv4:
    Type: CommaDelimitedList
    Default: 10.0.0.0/24, 10.0.1.0/24, 10.0.2.0/24, 10.0.3.0/24
    Description: Comma-delimited list of four CIDR blocks. The first half is Public and the second half is Private。

  AvailabilityZoneSubnet1:
    Type: AWS::EC2::AvailabilityZone::Name
    Default: ap-northeast-1a

  AvailabilityZoneSubnet2:
    Type: AWS::EC2::AvailabilityZone::Name
    Default: ap-northeast-1c

Resources:
# Subnet
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZoneSubnet1 # Optional
      CidrBlock: !Select
        - 0
        - !Ref SubnetCidrIpv4
      MapPublicIpOnLaunch: true # Optional
      VpcId: !Ref MyVPC

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZoneSubnet2 # Optional
      CidrBlock: !Select
        - 1
        - !Ref SubnetCidrIpv4
      MapPublicIpOnLaunch: true # Optional
      VpcId: !Ref MyVPC

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZoneSubnet1 # Optional
      CidrBlock: !Select
        - 2
        - !Ref SubnetCidrIpv4
      MapPublicIpOnLaunch: false # Optional
      VpcId: !Ref MyVPC

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZoneSubnet2 # Optional
      CidrBlock: !Select
        - 3
        - !Ref SubnetCidrIpv4
      MapPublicIpOnLaunch: false # Optional
      VpcId: !Ref MyVPC

サブネットのCidrBlockはパラメータ型CommaDelimitedListと組み込み関数Fn::Selectを使用し、一つのパラメータから値を受け取れるようにしました。

Routeの設定

# Route
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: CHANGEME
          Value: CHANGEME
      VpcId: !Ref MyVPC

  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      GatewayId: !Ref MyInternetGateway # Optional
      RouteTableId: !Ref PublicRouteTable

  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet1

  PublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet2

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: CHANGEME
          Value: CHANGEME
      VpcId: !Ref MyVPC

  PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet1

  PrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet2

Route関連のリソースは多少ややこしく感じました。
まずRouteTableを作成し、そのテーブルにRouteを追加して、そのRouteTableを各サブネットにAssociationします。

これでリソース部分は完成しました。
後はコンソールでパラメータの見た目を良くするためのMetadataの設定と、他のテンプレートでVPCIDなどを使用するためのOutputsの設定をしたらネットワーク系テンプレートの作成は完了です。

Metadata

Metadata
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: VPC Configuration
        Parameters:
          - VPCCidrIpv4
          - InstanceTenancy
      - Label:
          default: Subnet Configuration
        Parameters:
          - SubnetCidrIpv4
          - AvailabilityZoneSubnet1
          - AvailabilityZoneSubnet2
      - Label:
          default: Tag
        Parameters:
          - PrefixNameTagValue

Outputs

Outputs
Outputs:
  MyVPCId:
    Value: !Ref MyVPC
    Export:
      Name: !Sub ${AWS::StackName}-VPCId

  PublicSubnet1Id:
    Value: !Ref PublicSubnet1
    Export:
      Name: !Sub ${AWS::StackName}-PublicSubnet1Id

  PublicSubnet2Id:
    Value: !Ref PublicSubnet2
    Export:
      Name: !Sub ${AWS::StackName}-PublicSubnet2Id

  PrivateSubnet1Subnet1Id:
    Value: !Ref PrivateSubnet1
    Export:
      Name: !Sub ${AWS::StackName}-PrivateSubnet1Id

  PrivateSubnet2Subnet1Id:
    Value: !Ref PrivateSubnet2
    Export:
      Name: !Sub ${AWS::StackName}-PrivateSubnet2Id

EC2インスタンスとRDSで各IDを使用するので、クロススタック参照するために出力しておきます。

rain fmt | テンプレートのフォーマット

テンプレートが完成したので、フォーマットしたらどうなるのか試してみます。
デフォルトでは標準出力でフォーマットされるため、ファイルに書き込みます。

$ rain fmt Network.yaml > fmt-Network.yaml

以下の点が変化していました。

  • パラメータのプロパティ順序
  • コメント位置

以下のように変わっていました。

Parameters:

  InstanceTenancy:
    Type: String
    Default: default
    AllowedValues:
      - default
      - dedicated
      - host
Resources:
# VPC
  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidrIpv4
      EnableDnsHostnames: true # Optional
      EnableDnsSupport: true # Optional
      InstanceTenancy: !Ref InstanceTenancy # Optional

# InternetGateway

Parameters:

  InstanceTenancy:
    Type: String
    AllowedValues:
      - default
      - dedicated
      - host
    Default: default

Resources:
  # VPC

  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidrIpv4
      EnableDnsHostnames: true # Optional
      EnableDnsSupport: true # Optional
      InstanceTenancy: !Ref InstanceTenancy # Optional
      # InternetGateway

コメントが変なところに行っているので、rain fmtは使わない方がいいかもしれません。
yaml⇄JSONの変更もできますが、yaml→JSONにしようとしたらSemantic difference after formatting: とエラーが発生してできませんでした。
リストに-ではなく[]を使用する必要があるかもしれません。

rain deploy | テンプレートデプロイ

Stackをデプロイするため、rain deployを使用します。
rain deployを実行すると、以下のことがわかりました。

  • 初回はrain用のS3バケットが作成される
  • Stack名を指定しない場合、テンプレートの名前がStackの名前となる(拡張子除く)
  • 入力するパラメータの順序は毎回異なる
  • 変更セットが表示
  • RollBack時のエラー理由表示
  • デプロイ失敗後の再デプロイ時、失敗したStackを自動的に削除するが、入力したパラメータは流用できる
  • デプロイに成功するとOutputsも表示

特にデプロイ失敗後の再デプロイ時に、自動的にStackを削除してくれることが嬉しいです。
コンソールだとこの作業が面倒なので助かります。

rain deploy実行時のターミナルは以下のとおりです(私が原因のプロパティの誤字でエラーが起きています)。

$ rain deploy --profile $PROFILE Network.yaml NetworkStack

Rain needs to create an S3 bucket called 'rain-artifacts-300805321587-ap-northeaRain needs to create an S3 bucket called 'rain-artifacts-300805321587-ap-northeaRain needs to create an S3 bucket called 'rain-artifacts-300805321587-ap-northeast-1'. Continue? (Y/n) Y

Enter a value for parameter 'InstanceTenancy' (default value: default): 
Enter a value for parameter 'SubnetCidrIpv4' "Comma-delimited list of four CIDR blocks. The first half is Public and the second half is Private。" (default valuEnter a value for parameter 'SubnetCidrIpv4' "Comma-delimited list of four CIDR blocks. The first half is Public and the second half is Private。" (default valuEnter a value for parameter 'SubnetCidrIpv4' "Comma-delimited list of four CIDR blocks. The first half is Public and the second half is Private。" (default value: 10.0.0.0/24, 10.0.1.0/24, 10.0.2.0/24, 10.0.3.0/24): 
Enter a value for parameter 'AvailabilityZoneSubnet1' (default value: ap-northeaEnter a value for parameter 'AvailabilityZoneSubnet1' (default value: ap-northeaEnter a value for parameter 'AvailabilityZoneSubnet1' (default value: ap-northeaEnter a value for parameter 'AvailabilityZoneSubnet1' (default value: ap-northeast-1a): 
Enter a value for parameter 'AvailabilityZoneSubnet2' (default value: ap-northeaEnter a value for parameter 'AvailabilityZoneSubnet2' (default value: ap-northeaEnter a value for parameter 'AvailabilityZoneSubnet2' (default value: ap-northeast-1c): 
Enter a value for parameter 'VPCCidrIpv4' (default value: 10.0.0.0/16): 

CloudFormation will make the following changes:
Stack NetworkStack:
  + AWS::EC2::InternetGateway MyInternetGateway
  + AWS::EC2::VPCGatewayAttachment MyVPCGatewayAttachment
  + AWS::EC2::VPC MyVPC
  + AWS::EC2::RouteTable PrivateRouteTable
  + AWS::EC2::SubnetRouteTableAssociation PrivateSubnet1RouteTableAssociation
  + AWS::EC2::Subnet PrivateSubnet1
  + AWS::EC2::SubnetRouteTableAssociation PrivateSubnet2RouteTableAssociation
  + AWS::EC2::Subnet PrivateSubnet2
  + AWS::EC2::RouteTable PublicRouteTable
  + AWS::EC2::Route PublicRoute
  + AWS::EC2::SubnetRouteTableAssociation PublicSubnet1RouteTableAssociation
  + AWS::EC2::Subnet PublicSubnet1
  + AWS::EC2::SubnetRouteTableAssociation PublicSubnet2RouteTableAssociation
  + AWS::EC2::Subnet PublicSubnet2
Do you wish to continue? (Y/n) 
Deploying template 'Network.yaml' as stack 'NetworkStack' in ap-northeast-1.
Stack NetworkStack: ROLLBACK_COMPLETE
Messages:
  - MyVPCGatewayAttachment: Encountered unsupported property VPCId
  - PrivateRouteTable: Encountered unsupported property VPCId
  - PrivateSubnet1: Encountered unsupported property VPCId
  - PrivateSubnet2: Encountered unsupported property VPCId
  - PublicRouteTable: Encountered unsupported property VPCId
  - PublicSubnet1: Encountered unsupported property VPCId
  - PublicSubnet2: Encountered unsupported property VPCId
failed deploying stack 'NetworkStack'

以下は失敗したStackを自動的に削除した後、デプロイが成功している状態です。

$ rain deploy --profile $PROFILE Network.yaml NetworkStack

Deleted existing, empty stack.

省略

Deploying template 'Network.yaml' as stack 'NetworkStack' in ap-northeast-1.
Stack NetworkStack: CREATE_COMPLETE
  Outputs:
    PrivateSubnet2Subnet1Id: subnet-0368fc284b9639f4c # exported as NetworkStack-PrivateSubnet2Id
    MyVPCId: vpc-056ab2c7c9dc91a8f # exported as NetworkStack-VPCId
    PublicSubnet1Id: subnet-08d4895141b51b8be # exported as NetworkStack-PublicSubnet1Id
    PublicSubnet2Id: subnet-08aa1bbb0a9d3a82f # exported as NetworkStack-PublicSubnet2Id
    PrivateSubnet1Subnet1Id: subnet-07581ce346dfe2ebb # exported as NetworkStack-PrivateSubnet1Id
Successfully deployed NetworkStack

コンソールを確認すると、リソースは無事に作成できていました。

EC2インスタンステンプレート作成

以下のテンプレートを作成し、Rainでデプロイしました。

インスタンスの設定

Parameters:
  ImageId:
    Type: AWS::EC2::Image::Id
    Default: ami-0992fc94ca0f1415a

  InstanceType:
    Type: String
    Default: t2.micro

  KeyName:
    Type: AWS::EC2::KeyPair::KeyName

  PrefixNameTagValue:
    Type: String
    Default: CloudTech

  NetworkStackName:
    Type: String
    Default: NetworkStack

Resources:
  MyInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref ImageId # Optional
      InstanceType: !Ref InstanceType # Optional
      KeyName: !Ref KeyName # Optional
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeleteOnTermination: true
          DeviceIndex: 0
          GroupSet:
            - !Ref MySecurityGroup
          SubnetId:
            Fn::ImportValue: !Sub ${NetworkStackName}-PublicSubnet1Id
      UserData:
        Fn::Base64: |
          #!/bin/bash
          yum -y update

          amazon-linux-extras install php7.2 -y
          yum -y install mysql httpd php-mbstring php-xml gd php-gd

          systemctl enable httpd.service
          systemctl start httpd.service

          wget -P /home/ec2-user http://ja.wordpress.org/latest-ja.tar.gz
          cd /home/ec2-user
          tar zxvf latest-ja.tar.gz
          cp -r wordpress/* /var/www/html/
          chown apache:apache -R /var/www/html
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - Web-Server

ImageId,InstanceType,KeyNameはカスタムできるようにパラメータで渡しています。
ボリュームは設定しなければデフォルトのものがアタッチされるようです。
NetworkInterfacesプロパティでセキュリティグループやサブネットを指定します。
Fn::ImportValueで他のテンプレートでExportしている値を参照しています(クロススタック参照)。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/walkthrough-crossstackref.html

セキュリティグループの設定

Parameters:
  SSHLocation:
    Type: String
    Default: 0.0.0.0/32
    
Resources:
  MySecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: ssh-myip-http-full
      GroupName: ssh-myip-http-full # Optional
      SecurityGroupIngress:
        - CidrIp: !Ref SSHLocation # Optional
          FromPort: 22 # Optional
          IpProtocol: tcp
          ToPort: 22 # Optional
        - CidrIp: 0.0.0.0/0 # Optional
          FromPort: 80 # Optional
          IpProtocol: tcp
          ToPort: 80 # Optional
        - CidrIp: 0.0.0.0/0 # Optional
          FromPort: 443 # Optional
          IpProtocol: tcp
          ToPort: 443 # Optional
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - Web-Server
      VpcId:
        Fn::ImportValue: !Sub ${NetworkStackName}-VPCId # Optional

Outputs:
  MySecurityGroup:
    Value: !Ref MySecurityGroup
    Export:
      Name: !Sub ${AWS::StackName}-AllowWebServer

WebServer用のセキュリティグループを作成しています。
sshは自分のIPアドレスのみをパラメータで渡し、http,httpsはフルオープンです。

WordPress画面の確認

インスタンスのパブリックIPアドレスを確認すると、以下のように表示されたのでWordPressのインストールは成功です。

次はRDSのテンプレートを作成します。

RDSのテンプレート作成

以下のテンプレートをRainでデプロイしました。

AWSTemplateFormatVersion: "2010-09-09"

Description: Template generated by rain

Parameters:
  DBInstanceClass:
    Type: String
    Default: db.t2.micro

  DBName:
    Type: String
    Default: wordpress

  DBUser:
    Type: String
    NoEcho: true
    MinLength: 1
    MaxLength: 16
    AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*"
    ConstraintDescription: must begin with a letter and contain only alphanumeric characters.

  DBPassword:
    Type: String
    NoEcho: true
    MinLength: 8
    MaxLength: 41
    AllowedPattern: "[a-zA-Z0-9]*"
    ConstraintDescription: must contain only alphanumeric characters.

  AvailabilityZone:
    Type: AWS::EC2::AvailabilityZone::Name
    Default: ap-northeast-1a

  NetworkStackName:
    Type: String
    Default: NetworkStack

  InstanceStackName:
    Type: String
    Default: InstanceStack

  PrefixNameTagValue:
    Type: String
    Default: CloudTech

Resources:
  MyDBInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      AllocatedStorage: 20
      AvailabilityZone: !Ref AvailabilityZone # Optional
      BackupRetentionPeriod: 0 # Optional
      DBInstanceClass: !Ref DBInstanceClass
      DBName: !Ref DBName # Optional
      DBSubnetGroupName: !Ref MyDBSubnetGroup # Optional
      Engine: mysql # Optional
      MasterUserPassword: !Ref DBPassword # Optional
      MasterUsername: !Ref DBUser # Optional
      PubliclyAccessible: false # Optional
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - mysql
      VPCSecurityGroups:
        - !Ref MySecurityGroup

  MyDBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: !Sub ${AWS::StackName}-SubnetGroup
      SubnetIds:
        - Fn::ImportValue: !Sub ${NetworkStackName}-PrivateSubnet1Id
        - Fn::ImportValue: !Sub ${NetworkStackName}-PrivateSubnet2Id
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - SubnetGroup

  MySecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: db-SG
      GroupName: db-SG # Optional
      SecurityGroupIngress:
        - SourceSecurityGroupId:
            Fn::ImportValue: !Sub ${InstanceStackName}-AllowWebServer # Optional
          FromPort: 3306 # Optional
          IpProtocol: tcp
          ToPort: 3306 # Optional
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - mysql
      VpcId:
        Fn::ImportValue: !Sub ${NetworkStackName}-VPCId # Optional

Outputs:
  MyDBInstanceEndpointAddress:
    Value: !GetAtt MyDBInstance.Endpoint.Address

  MyDBInstanceEndpointPort:
    Value: !GetAtt MyDBInstance.Endpoint.Port

  MySecurityGroupGroupId:
    Value: !GetAtt MySecurityGroup.GroupId

  MySecurityGroupVpcId:
    Value: !GetAtt MySecurityGroup.VpcId

RDSはプロパティが多いので、業務でテンプレート化すると大変そうです。
パラメータのNoEchoは値をマスクしてコンソール、CLI、APIに表示させないようにできます。
もっとセキュアにするにはSSMのパラメータストアやSecret Managerを使うとよいでしょう。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/security-best-practices.html#creds

仕上げにWordPressの画面からインストールを完了させて、ログインをします。
成功しました。

rain rm | Stackの削除

テンプレート化すると設定項目が多くて大変ですね。
これでWordPress環境構築が終了したので、Stackを削除して終了します。

$ rain rm InstanceStack --profile $PROFILE 
Stack InstanceStack: CREATE_COMPLETE
Are you sure you want to delete this stack? (Y/n) 
Successfully deleted stack 'InstanceStack'

他のStackも同様に削除できたら、後片付け完了です。

最後に、使用していないRainの各コマンドを試して終わりにします。

その他のrainコマンド

rain ls | 実行中Stackを表示

$ rain ls --profile $PROFILE
CloudFormation stacks in ap-northeast-1:
  ConfigS3: UPDATE_COMPLETE
  EnableConfig: UPDATE_COMPLETE
  EnableGuardDuty: CREATE_COMPLETE
  StackSet-EnableAccessAnalyzer-828997b8-17d8-47a5-a734-52e2ecce44f3: CREATE_COMPLETE
  
$ rain ls ConfigS3 --profile $PROFILE
Stack ConfigS3: UPDATE_COMPLETE
  Outputs:
    ConfigBucketOutput: enableconfig-configbucket-xxx # S3 (exported as ConfigS3-S3BucketName)

$ rain ls --all EnableConfig --profile $PROFILE
Stack EnableConfig: UPDATE_COMPLETE
  Parameters:
    IncludeGlobalResourceTypes: true
    ResourceTypes: <All>
    DeliveryChannelName: <Generated>
    S3StackName: ConfigS3
    Frequency: 24hours
    AllSupported: true
  Resources:
    ConfigDeliveryChannel: CREATE_COMPLETE
      EnableConfig-ConfigDeliveryChannel-xxxx
    ConfigRecorder: CREATE_COMPLETE
      EnableConfig-ConfigRecorder-xxxx
    ConfigRecorderRole: CREATE_COMPLETE
      EnableConfig-ConfigRecorderRole-xxxx
      
$ rain ls --all --profile $PROFILE 
CloudFormation stacks in ap-northeast-1:
  ConfigS3: UPDATE_COMPLETE
  EnableConfig: UPDATE_COMPLETE
  EnableGuardDuty: CREATE_COMPLETE
  StackSet-EnableAccessAnalyzer-828997b8-17d8-47a5-a734-52e2ecce44f3: CREATE_COMPLETE
CloudFormation stacks in ap-northeast-2:
  StackSet-EnableAccessAnalyzer-85e9246d-a979-4035-a5be-e15a43df73da: CREATE_COMPLETE
CloudFormation stacks in ap-south-1:
  StackSet-EnableAccessAnalyzer-9b3bd2af-4ffd-43b6-978a-6de54760bfdd: CREATE_COMPLETE
CloudFormation stacks in ap-southeast-1:
  StackSet-EnableAccessAnalyzer-423b7c20-a875-4142-a2a1-9dafda0a1129: CREATE_COMPLETE
CloudFormation stacks in ap-southeast-2:
  StackSet-EnableAccessAnalyzer-745381f2-4287-4d16-a648-2fbd8cc82815: CREATE_COMPLETE
CloudFormation stacks in ca-central-1:
  StackSet-EnableAccessAnalyzer-64740f3b-eb80-4d16-b680-3a047fa19a4e: CREATE_COMPLETE
CloudFormation stacks in eu-central-1:
  StackSet-EnableAccessAnalyzer-7598a6b5-0ade-4b44-949f-6c21e1c7431b: CREATE_COMPLETE
CloudFormation stacks in eu-north-1:
  StackSet-EnableAccessAnalyzer-0dbf73f3-058f-4d9e-9e9e-414ceab6658e: CREATE_COMPLETE
CloudFormation stacks in eu-west-1:
  StackSet-EnableAccessAnalyzer-fa6692d2-8c2b-4316-a0bb-6713f8a0f18a: CREATE_COMPLETE
CloudFormation stacks in eu-west-2:
  StackSet-EnableAccessAnalyzer-d354463c-30ef-4640-94b2-2918c50b4ff3: CREATE_COMPLETE
CloudFormation stacks in eu-west-3:
  StackSet-EnableAccessAnalyzer-4cac3e6d-fe04-4431-bf76-91a9e7df1cee: CREATE_COMPLETE
CloudFormation stacks in sa-east-1:
  StackSet-EnableAccessAnalyzer-7e5f58c7-e7bd-4621-b49d-36219c15e68b: CREATE_COMPLETE
CloudFormation stacks in us-east-1:
  StackSet-EnableAccessAnalyzer-22576926-73ca-4132-a5a5-001bc20dfde6: CREATE_COMPLETE
CloudFormation stacks in us-east-2:
  StackSet-EnableAccessAnalyzer-f4a61f4b-8a41-4976-ba4d-7f506286a5b8: CREATE_COMPLETE
CloudFormation stacks in us-west-1:
  StackSet-EnableAccessAnalyzer-78b1e93f-e88f-4ada-bc4e-a0a0c71711cc: CREATE_COMPLETE
CloudFormation stacks in us-west-2:
  StackSet-EnableAccessAnalyzer-7179d02f-ce1b-4539-878c-b657381e8a3d: CREATE_COMPLETE

rain cat | 実行中Stackからテンプレート取得

$ rain cat StackSet-EnableAccessAnalyzer-828997b8-17d8-47a5-a734-52e2ecce44f3 --profile $PROFILE
AWSTemplateFormatVersion: "2010-09-09"

Description: EnableAccessAnalyzer

Parameters:
  AccountID:
    Description: Account ID to be archived
    Type: String
    MaxLength: 12
    MinLength: 12

Resources:
  AccessAnalyzer:
    Type: AWS::AccessAnalyzer::Analyzer
    Properties:
      Type: ACCOUNT
      ArchiveRules:
        - Filter:
            - Property: principal.AWS
              Eq:
                - !Ref AccountID
            - Property: isPublic
              Eq:
                - false
          RuleName: ArchiveOfTrustedID

Outputs:
  AccessAnalyzerOutput:

rain console | コンソールにログイン

$ rain console ConfigS3 --profile $PROFILE
sign-in URLs can only be constructed for assumed roles

rain diff | テンプレートの比較

Network.yamlをコピーしたBackUp-Network.yamlにリソースを一つ追加してみました。
このように違いが表示されます。

$ rain diff Network.yaml BackUp-Network.yaml 
(|) Resources:
(+)   rivateSubnet2RouteTableAssociation:
(+)     Properties:
(+)       RouteTableId:
(+)         Ref: PrivateRouteTable
(+)       SubnetId:
(+)         Ref: PrivateSubnet2
(+)     Type: AWS::EC2::SubnetRouteTableAssociation

rain info | 現在の設定を表示

$ rain info --profile $PROFILE
Account:  xxxxx
Region:   xxxxx
Identity: arn:aws:iam::xxxxxxx:user/xxxxx
Profile:  xxxxx

rain logs | Stackのイベントログを表示

デフォルトではStackの有用なメッセージを含むログを表示します。

$ rain logs ConfigS3 --profile $PROFILE
Jan 23 12:34:32 ConfigS3/ConfigBucket (AWS::S3::Bucket) UPDATE_IN_PROGRESS "Apply stack-level tags to imported resource if applicable."
Jan 23 12:34:31 ConfigS3/ConfigBucket (AWS::S3::Bucket) IMPORT_COMPLETE "Resource import completed."
Jan 23 12:34:30 ConfigS3/ConfigBucket (AWS::S3::Bucket) IMPORT_IN_PROGRESS "Resource import started."

$ rain logs EnableConfig --profile $PROFILE
No interesting log messages to display. To see everything, use the --all flag

$ rain logs --all EnableConfig --profile $PROFILE
Jan 23 14:38:12 EnableConfig/EnableConfig (AWS::CloudFormation::Stack) UPDATE_COMPLETE
Jan 23 14:38:12 EnableConfig/ConfigBucketPolicy (AWS::S3::BucketPolicy) DELETE_SKIPPED
Jan 23 14:38:10 EnableConfig/EnableConfig (AWS::CloudFormation::Stack) UPDATE_COMPLETE_CLEANUP_IN_PROGRESS
Jan 23 14:38:03 EnableConfig/EnableConfig (AWS::CloudFormation::Stack) UPDATE_IN_PROGRESS "User Initiated"
Jan 23 14:31:41 EnableConfig/EnableConfig (AWS::CloudFormation::Stack) UPDATE_COMPLETE
Jan 23 14:31:40 EnableConfig/EnableConfig (AWS::CloudFormation::Stack) UPDATE_COMPLETE_CLEANUP_IN_PROGRESS
Jan 23 14:31:37 EnableConfig/ConfigBucketPolicy (AWS::S3::BucketPolicy) UPDATE_COMPLETE
Jan 23 14:31:32 EnableConfig/EnableConfig (AWS::CloudFormation::Stack) UPDATE_IN_PROGRESS "User Initiated"
Jan 23 13:32:50 EnableConfig/EnableConfig (AWS::CloudFormation::Stack) CREATE_COMPLETE
Jan 23 13:32:49 EnableConfig/ConfigRecorder (AWS::Config::ConfigurationRecorder) CREATE_COMPLETE
Jan 23 13:32:43 EnableConfig/ConfigDeliveryChannel (AWS::Config::DeliveryChannel) CREATE_COMPLETE
Jan 23 13:32:42 EnableConfig/ConfigDeliveryChannel (AWS::Config::DeliveryChannel) CREATE_IN_PROGRESS "Resource creation Initiated"
Jan 23 13:32:34 EnableConfig/ConfigRecorder (AWS::Config::ConfigurationRecorder) CREATE_IN_PROGRESS "Resource creation Initiated"
Jan 23 13:32:34 EnableConfig/ConfigRecorder (AWS::Config::ConfigurationRecorder) CREATE_IN_PROGRESS
Jan 23 13:32:31 EnableConfig/ConfigRecorderRole (AWS::IAM::Role) CREATE_COMPLETE
Jan 23 13:32:16 EnableConfig/ConfigDeliveryChannel (AWS::Config::DeliveryChannel) CREATE_IN_PROGRESS
Jan 23 13:32:14 EnableConfig/ConfigBucketPolicy (AWS::S3::BucketPolicy) CREATE_COMPLETE
Jan 23 13:32:14 EnableConfig/ConfigBucketPolicy (AWS::S3::BucketPolicy) CREATE_IN_PROGRESS "Resource creation Initiated"
Jan 23 13:32:13 EnableConfig/ConfigRecorderRole (AWS::IAM::Role) CREATE_IN_PROGRESS "Resource creation Initiated"
Jan 23 13:32:12 EnableConfig/ConfigRecorderRole (AWS::IAM::Role) CREATE_IN_PROGRESS
Jan 23 13:32:12 EnableConfig/ConfigBucketPolicy (AWS::S3::BucketPolicy) CREATE_IN_PROGRESS
Jan 23 13:32:08 EnableConfig/EnableConfig (AWS::CloudFormation::Stack) CREATE_IN_PROGRESS "User Initiated"

rain merge | テンプレートのマージ

指定したテンプレート同士がセクションごとにまとめられて標準出力に出ます。

rain tree | テンプレート内の依存関係表示

依存関係が分かります。

$ rain tree RDS-Instance.yaml 
Resources:
  MyDBSubnetGroup:
    DependsOn:
      Parameters:
        - AWS::StackName
        - NetworkStackName
        - PrefixNameTagValue
  MySecurityGroup:
    DependsOn:
      Parameters:
        - InstanceStackName
        - NetworkStackName
        - PrefixNameTagValue
  MyDBInstance:
    DependsOn:
      Parameters:
        - AvailabilityZone
        - DBInstanceClass
        - DBName
        - DBPassword
        - DBUser
        - PrefixNameTagValue
      Resources:
        - MyDBSubnetGroup
        - MySecurityGroup
Outputs:
  MySecurityGroupGroupId:
    DependsOn:
      Resources:
        - MySecurityGroup
  MySecurityGroupVpcId:
    DependsOn:
      Resources:
        - MySecurityGroup
  MyDBInstanceEndpointAddress:
    DependsOn:
      Resources:
        - MyDBInstance
  MyDBInstanceEndpointPort:
    DependsOn:
      Resources:
        - MyDBInstance

rain watch | Stackの更新状態を監視

Stackのステータスを繰り返し表示します。
外部から開始された進行状況を監視に役立ちます。

$ rain watch ConfigS3 --profile $PROFILE
Fetching stack status  .˙Stack ConfigS3: UPDATE_COMPLETE
not watching unchanging stack

さいごに

今回初めてCloudFormationのツールを使用しました。
Rain以外にも色々とあるので、他のツールも試してみたいです。

CloudFormation化は後の設定変更などを考えなければ簡単に行えますが、業務で使うとなると色々と考えることが多くて大変そうです。

この記事は主に私の勉強のために執筆していますが、他の誰かのお役に立てれば幸いです。

テンプレート

ネットワーク

Network.yaml
AWSTemplateFormatVersion: "2010-09-09"

Description: Template generated by rain

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: VPC Configuration
        Parameters:
          - VPCCidrIpv4
          - InstanceTenancy
      - Label:
          default: Subnet Configuration
        Parameters:
          - SubnetCidrIpv4
          - AvailabilityZoneSubnet1
          - AvailabilityZoneSubnet2
      - Label:
          default: Tag
        Parameters:
          - PrefixNameTagValue

Parameters:
  VPCCidrIpv4:
    Type: String
    Default: 10.0.0.0/16
    ConstraintDescription: Malformed input-Parameter VPCCidrIpv4 must match pattern (x.x.x.x/16-28)
    AllowedPattern: ^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/1[6-9]|2[0-8]$

  InstanceTenancy:
    Type: String
    Default: default
    AllowedValues:
      - default
      - dedicated
      - host

  SubnetCidrIpv4:
    Type: CommaDelimitedList
    Default: 10.0.0.0/24, 10.0.1.0/24, 10.0.2.0/24, 10.0.3.0/24
    Description: Comma-delimited list of four CIDR blocks. The first half is Public and the second half is Private.

  AvailabilityZoneSubnet1:
    Type: AWS::EC2::AvailabilityZone::Name
    Default: ap-northeast-1a

  AvailabilityZoneSubnet2:
    Type: AWS::EC2::AvailabilityZone::Name
    Default: ap-northeast-1c

  PrefixNameTagValue:
    Type: String
    Default: CloudTech

Resources:
# VPC
  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidrIpv4
      EnableDnsHostnames: true # Optional
      EnableDnsSupport: true # Optional
      InstanceTenancy: !Ref InstanceTenancy # Optional
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - VPC

# InternetGateway
  MyInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - InternetGateway

  MyVPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref MyInternetGateway # Optional
      VpcId: !Ref MyVPC

# Subnet
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZoneSubnet1 # Optional
      CidrBlock: !Select
        - 0
        - !Ref SubnetCidrIpv4
      MapPublicIpOnLaunch: true # Optional
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - PublicSubnet1
      VpcId: !Ref MyVPC

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZoneSubnet2 # Optional
      CidrBlock: !Select
        - 1
        - !Ref SubnetCidrIpv4
      MapPublicIpOnLaunch: true # Optional
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - PublicSubnet2
      VpcId: !Ref MyVPC

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZoneSubnet1 # Optional
      CidrBlock: !Select
        - 2
        - !Ref SubnetCidrIpv4
      MapPublicIpOnLaunch: false # Optional
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - PraivateSubnet1
      VpcId: !Ref MyVPC

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZoneSubnet2 # Optional
      CidrBlock: !Select
        - 3
        - !Ref SubnetCidrIpv4
      MapPublicIpOnLaunch: false # Optional
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - PraivateSubnet2
      VpcId: !Ref MyVPC

# Route
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - Public-Route-Table
      VpcId: !Ref MyVPC

  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref MyInternetGateway # Optional
      RouteTableId: !Ref PublicRouteTable

  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet1

  PublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet2

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - Praivate-Route-Table
      VpcId: !Ref MyVPC

  PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet1

  PrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet2

Outputs:
  MyVPCId:
    Value: !Ref MyVPC
    Export:
      Name: !Sub ${AWS::StackName}-VPCId

  PublicSubnet1Id:
    Value: !Ref PublicSubnet1
    Export:
      Name: !Sub ${AWS::StackName}-PublicSubnet1Id

  PublicSubnet2Id:
    Value: !Ref PublicSubnet2
    Export:
      Name: !Sub ${AWS::StackName}-PublicSubnet2Id

  PrivateSubnet1Subnet1Id:
    Value: !Ref PrivateSubnet1
    Export:
      Name: !Sub ${AWS::StackName}-PrivateSubnet1Id

  PrivateSubnet2Subnet1Id:
    Value: !Ref PrivateSubnet2
    Export:
      Name: !Sub ${AWS::StackName}-PrivateSubnet2Id

EC2インスタンス

Instance.yaml
AWSTemplateFormatVersion: "2010-09-09"

Description: Template generated by rain

Parameters:
  ImageId:
    Type: AWS::EC2::Image::Id
    Default: ami-0992fc94ca0f1415a

  InstanceType:
    Type: String
    Default: t2.micro

  KeyName:
    Type: AWS::EC2::KeyPair::KeyName

  SSHLocation:
    Type: String
    Default: 0.0.0.0/32

  PrefixNameTagValue:
    Type: String
    Default: CloudTech

  NetworkStackName:
    Type: String
    Default: NetworkStack

Resources:
  MyInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref ImageId # Optional
      InstanceType: !Ref InstanceType # Optional
      KeyName: !Ref KeyName # Optional
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeleteOnTermination: true
          DeviceIndex: 0
          GroupSet:
            - !Ref MySecurityGroup
          SubnetId:
            Fn::ImportValue: !Sub ${NetworkStackName}-PublicSubnet1Id
      UserData:
        Fn::Base64: |
          #!/bin/bash
          yum -y update

          amazon-linux-extras install php7.2 -y
          yum -y install mysql httpd php-mbstring php-xml gd php-gd

          systemctl enable httpd.service
          systemctl start httpd.service

          wget -P /home/ec2-user http://ja.wordpress.org/latest-ja.tar.gz
          cd /home/ec2-user
          tar zxvf latest-ja.tar.gz
          cp -r wordpress/* /var/www/html/
          chown apache:apache -R /var/www/html
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - Web-Server

  MySecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: ssh-myip-http-full
      GroupName: ssh-myip-http-full # Optional
      SecurityGroupIngress:
        - CidrIp: !Ref SSHLocation # Optional
          FromPort: 22 # Optional
          IpProtocol: tcp
          ToPort: 22 # Optional
        - CidrIp: 0.0.0.0/0 # Optional
          FromPort: 80 # Optional
          IpProtocol: tcp
          ToPort: 80 # Optional
        - CidrIp: 0.0.0.0/0 # Optional
          FromPort: 443 # Optional
          IpProtocol: tcp
          ToPort: 443 # Optional
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - Web-Server
      VpcId:
        Fn::ImportValue: !Sub ${NetworkStackName}-VPCId # Optional

Outputs:
  MySecurityGroup:
    Value: !Ref MySecurityGroup
    Export:
      Name: !Sub ${AWS::StackName}-AllowWebServer

RDS

RDS.yaml
AWSTemplateFormatVersion: "2010-09-09"

Description: Template generated by rain

Parameters:
  DBInstanceClass:
    Type: String
    Default: db.t2.micro

  DBName:
    Type: String
    Default: wordpress

  DBUser:
    Type: String
    NoEcho: true
    MinLength: 1
    MaxLength: 16
    AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*"
    ConstraintDescription: must begin with a letter and contain only alphanumeric characters.

  DBPassword:
    Type: String
    NoEcho: true
    MinLength: 8
    MaxLength: 41
    AllowedPattern: "[a-zA-Z0-9]*"
    ConstraintDescription: must contain only alphanumeric characters.

  AvailabilityZone:
    Type: AWS::EC2::AvailabilityZone::Name
    Default: ap-northeast-1a

  NetworkStackName:
    Type: String
    Default: NetworkStack

  InstanceStackName:
    Type: String
    Default: InstanceStack

  PrefixNameTagValue:
    Type: String
    Default: CloudTech

Resources:
  MyDBInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      AllocatedStorage: 20
      AvailabilityZone: !Ref AvailabilityZone # Optional
      BackupRetentionPeriod: 0 # Optional
      DBInstanceClass: !Ref DBInstanceClass
      DBName: !Ref DBName # Optional
      DBSubnetGroupName: !Ref MyDBSubnetGroup # Optional
      Engine: mysql # Optional
      MasterUserPassword: !Ref DBPassword # Optional
      MasterUsername: !Ref DBUser # Optional
      PubliclyAccessible: false # Optional
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - mysql
      VPCSecurityGroups:
        - !Ref MySecurityGroup

  MyDBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: !Sub ${AWS::StackName}-SubnetGroup
      SubnetIds:
        - Fn::ImportValue: !Sub ${NetworkStackName}-PrivateSubnet1Id
        - Fn::ImportValue: !Sub ${NetworkStackName}-PrivateSubnet2Id
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - SubnetGroup

  MySecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: db-SG
      GroupName: db-SG # Optional
      SecurityGroupIngress:
        - SourceSecurityGroupId:
            Fn::ImportValue: !Sub ${InstanceStackName}-AllowWebServer # Optional
          FromPort: 3306 # Optional
          IpProtocol: tcp
          ToPort: 3306 # Optional
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref PrefixNameTagValue
              - mysql
      VpcId:
        Fn::ImportValue: !Sub ${NetworkStackName}-VPCId # Optional

Outputs:
  MyDBInstanceEndpointAddress:
    Value: !GetAtt MyDBInstance.Endpoint.Address

  MyDBInstanceEndpointPort:
    Value: !GetAtt MyDBInstance.Endpoint.Port

  MySecurityGroupGroupId:
    Value: !GetAtt MySecurityGroup.GroupId

  MySecurityGroupVpcId:
    Value: !GetAtt MySecurityGroup.VpcId

Discussion