LoginSignup
0
0

More than 3 years have passed since last update.

CloudFormation のスタックネスト機能をいろいろ考えながら使ってみる

Posted at

はじめに

_s__o_ です。

便利な CloudFormatin ですが、あれもこれもと記載しているうち、コード量がついつい多くなってしまいます。そこで便利なのが スタックのネスト機能 。スタックを入れ子にすることで、スタック (コード) を分割することができます。たとえば、子スタックとして VPC 用スタック & EC2 用スタックを、親スタックとしてラッパースタックを準備します。この場合、ラッパースタックだけをキックすれば、VPC 用スタックも EC2 用スタックも実行されます。

今回は、このネスト機能を、色々考えながら使ってみようと思います。

スタック分割の考え方

まず初めにスタック分割の考え方、つまりはスタックの設計について少し検討してみたいと思います。単純にコードを分割するだけもいいと思いますが、折角なのでスタック内の要素の特性なども考慮しながら、良さげな分割方法を考えてみたいと思います。

分割対象の要素

とりあえず下記とします。① AWS 環境を構築する ② Client VPN Endpoint によるアクセスを提供する ③ AWS 環境からインターネットへの通信を許可する……だけの、シンプルなオブジェクト群です。
- AWS::EC2::VPC
- AWS::EC2::Subnet
- AWS::EC2::SecurityGroup
- AWS::EC2::NetworkInterface
- AWS::EC2::InternetGateway
- AWS::EC2::VPCGatewayAttachment
- AWS::EC2::EIP
- AWS::EC2::NatGateway
- AWS::EC2::RouteTable
- AWS::EC2::Route
- AWS::EC2::SubnetRouteTableAssociation
- AWS::EC2::Instance
- AWS::EC2::ClientVpnEndpoint
- AWS::EC2::ClientVpnTargetNetworkAssociation
- AWS::EC2::ClientVpnAuthorizationRule

分割の方針

試行錯誤の結果、下記 3 点の方針としました。
- 参照可能性のあるオブジェクトは親スタックで作成する (例 : VPC、サブネットなど)
- 参照可能性のないオブジェクトは子スタックで作成する (例 : ルートテーブルなど)
- 子スタックは、さらにオブジェクト種類で分割する (例 : EC2 用、ルートテーブル用など)

分割結果

上記の方針に基づいて仕分けた結果が下記となります。必ずしも下記が最適ではないですが、個人的には一番しっくりきました。
01_スタック分解案.png

コード

コードを記載するにあたっては こちらの記事 を参考にさせていただきました。非常によくまとまっており、とてもわかりやすい記事です。

親スタック

親スタックです。このコードを CloudFormation にアップロードして、実行します。1 環境をまるっと作成するので、指定するパラメータは非常に多くなっています。

AWSTemplateFormatVersion: 2010-09-09

# ------------------------------------------------------------#
#  Parameter
# ------------------------------------------------------------#

Parameters:

  000001templateRouteURL:
    Type: String
    Description: "Specify the URL where the route cfTemplate is placed."
    Default: "https://s3-ap-northeast-1.amazonaws.com/xxxxx/route.yml"
  000002templateCvpnURL:
    Type: String
    Description: "Specify the URL where the Client VPN Endpoint cfTemplate is placed."
    Default: "https://s3-ap-northeast-1.amazonaws.com/xxxxx/cvpn.yml"
  000003templateEc2URL:
    Type: String
    Description: "Specify the URL where the EC2 (AWS) cfTemplate is placed."
    Default: "https://s3-ap-northeast-1.amazonaws.com/xxxxx/ec2.yml"
  000101vpcName:
    Type: String
    Description: "Specify a VPC name."
    Default: "test-vpc"
  000102vpcCIDR:
    Type: String
    Description: "Specify a VPC CIDR."
    Default: "192.168.100.0/24"
  000201nwPubCIDR:
    Type: String
    Description: "Specify a Public NW CIDR."
    Default: "192.168.100.0/26"
  000202nwPriv1CIDR:
    Type: String
    Description: "Specify a Private 1 NW CIDR."
    Default: "192.168.100.128/26"
  000203nwPriv2CIDR:
    Type: String
    Description: "Specify a Private 2 NW CIDR."
    Default: "192.168.100.192/26"
  000302eniApIP:
    Type: String
    Description: "Specify a AP server IP address."
    Default: "192.168.100.136"
  000303eniDbIP:
    Type: String
    Description: "Specify a DB server IP address."
    Default: "192.168.100.137"
  020201cvpnCIDR:
    Type: String
    Description: "Specify a Cline VPN user CIDR."
    Default: "100.64.0.0/22"
  020202cvpnCertARN:
    Type: String
    Description: "Specify a cert ARN for Cline VPN."
    Default: "xxxxx"
  020203cvpnDnsIP:
    Type: String
    Description: "Specify a DNS IP which provided for user."
    Default: "192.168.100.2"
  030101AmiName:
    Type: String
    Description: "Specify a AMI name."
    Default: "Win2019Std20200610"
    AllowedValues:
      - "Win2019Std20200610"
      - "amazonlinux2"
  030102InstanceType:
    Type: String
    Description: "Specify a instance type."
    Default: "t3.small"
    AllowedValues:
      - "t3.nano"
      - "t3.small"
  030103KeyName:
    Type: String
    Description: "Specify a private key name."
    Default: "kp-xxxxx"

# ------------------------------------------------------------#
#  Mappings
# ------------------------------------------------------------#

Mappings:
  EC2:
    AMI:
      Win2019Std20200610: "ami-06f39b98503ed7f4e"
      amazonlinux2: "ami-0a1c2ec61571737db"

# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#

Resources:

# ------------------------------------------------------------#
#  AWS::EC2::VPC
# ------------------------------------------------------------#

  vpc:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: !Ref 000102vpcCIDR
      EnableDnsSupport: "true"
      EnableDnsHostnames: "true"
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Ref 000101vpcName

# ------------------------------------------------------------#
#  AWS::EC2::Subnet
# ------------------------------------------------------------#

  nwPublic1:
    Type: "AWS::EC2::Subnet"
    DependsOn:
      - vpc
    Properties:
      VpcId: !Ref vpc
      CidrBlock: !Ref 000201nwPubCIDR
      MapPublicIpOnLaunch: 'true'
      AvailabilityZone: "ap-northeast-1a"
      Tags:
        - Key: Name
          Value: !Sub "nw-${000101vpcName}-public-a"

  nwPrivate1:
    Type: "AWS::EC2::Subnet"
    DependsOn:
      - vpc
    Properties:
      VpcId: !Ref vpc
      CidrBlock: !Ref 000202nwPriv1CIDR
      MapPublicIpOnLaunch: 'false'
      AvailabilityZone: "ap-northeast-1a"
      Tags:
        - Key: Name
          Value: !Sub "nw-${000101vpcName}-private-a"

  nwPrivate2:
    Type: "AWS::EC2::Subnet"
    DependsOn:
      - vpc
    Properties:
      VpcId: !Ref vpc
      CidrBlock: !Ref 000203nwPriv2CIDR
      MapPublicIpOnLaunch: 'false'
      AvailabilityZone: "ap-northeast-1c"
      Tags:
        - Key: Name
          Value: !Sub "nw-${000101vpcName}-private-c"

# ------------------------------------------------------------#
#  AWS::EC2::SecurityGroup
# ------------------------------------------------------------#

  # See the URL below for the reason for using "GetAtt".
  # https://dev.classmethod.jp/articles/careful-when-using-security-group-with-cloud-formation/

  sgCvpn01:
    Type: "AWS::EC2::SecurityGroup"
    DependsOn:
      - vpc
    Properties:
      GroupName: !Sub "sgr-${000101vpcName}-cvpn01"
      GroupDescription: !Sub "sgr-${000101vpcName}-cvpn01"
      VpcId: !Ref vpc
      Tags:
        - Key: Name
          Value: !Sub "sgr-${000101vpcName}-cvpn01"

  sgAp01:
    Type: "AWS::EC2::SecurityGroup"
    DependsOn:
      - vpc
      - sgCvpn01
    Properties:
      GroupName: !Sub "sgr-${000101vpcName}-ap01"
      GroupDescription: !Sub "sgr-${000101vpcName}-ap01"
      VpcId: !Ref vpc
      SecurityGroupIngress:
        - IpProtocol: "-1"
          SourceSecurityGroupId: !GetAtt sgCvpn01.GroupId
      Tags:
        - Key: Name
          Value: !Sub "sgr-${000101vpcName}-ap01"

  sgDb01:
    Type: "AWS::EC2::SecurityGroup"
    DependsOn:
      - vpc
      - sgCvpn01
    Properties:
      GroupName: !Sub "sgr-${000101vpcName}-db01"
      GroupDescription: !Sub "sgr-${000101vpcName}-db01"
      VpcId: !Ref vpc
      SecurityGroupIngress:
        - IpProtocol: "-1"
          SourceSecurityGroupId: !GetAtt sgCvpn01.GroupId
      Tags:
        - Key: Name
          Value: !Sub "sgr-${000101vpcName}-db01"

# ------------------------------------------------------------#
#  AWS::EC2::SecurityGroupIngress
# ------------------------------------------------------------#

  # "AWS::EC2::SecurityGroupIngress" is used to avoid circular dependency.
  sgAp01In:
    Type: "AWS::EC2::SecurityGroupIngress"
    DependsOn:
      - sgAp01
      - sgDb01
    Properties:
      GroupId: !GetAtt sgAp01.GroupId
      IpProtocol: "-1"
      SourceSecurityGroupId: !GetAtt sgDb01.GroupId

  sgDb01In:
    Type: "AWS::EC2::SecurityGroupIngress"
    DependsOn:
      - sgAp01
      - sgDb01
    Properties:
      GroupId: !GetAtt sgDb01.GroupId
      IpProtocol: "-1"
      SourceSecurityGroupId: !GetAtt sgAp01.GroupId

# ------------------------------------------------------------#
#  AWS::EC2::NetworkInterface
# ------------------------------------------------------------#

  eniAp01:
    Type: "AWS::EC2::NetworkInterface"
    DependsOn:
      - sgAp01
      - nwPrivate1
    Properties:
      SourceDestCheck: "true"
      GroupSet:
        - !GetAtt sgAp01.GroupId
      SubnetId: !Ref nwPrivate1
      PrivateIpAddress: !Ref 000302eniApIP
      Tags:
        - Key: Name
          Value: !Sub "eni-${000101vpcName}-ap01"

  eniDb01:
    Type: "AWS::EC2::NetworkInterface"
    DependsOn:
      - sgDb01
      - nwPrivate1
    Properties:
      SourceDestCheck: "true"
      GroupSet:
        - !GetAtt sgDb01.GroupId
      SubnetId: !Ref nwPrivate1
      PrivateIpAddress: !Ref 000303eniDbIP
      Tags:
        - Key: Name
          Value: !Sub "eni-${000101vpcName}-db01"

# ------------------------------------------------------------#
#  AWS::CloudFormation::Stack - route
# ------------------------------------------------------------#

# Create IGW, NGW, Routetable.

  stackRoute:
    Type: AWS::CloudFormation::Stack
    DependsOn:
      - nwPublic1
      - nwPrivate1
      - nwPrivate2
    Properties:
      TemplateURL: !Ref 000001templateRouteURL
      Parameters:
        vpcID: !Ref vpc
        vpcName: !Ref 000101vpcName
        pub1SubnetID: !Ref nwPublic1
        priv1SubnetID: !Ref nwPrivate1
        priv2SubnetID: !Ref nwPrivate2

# ------------------------------------------------------------#
#  AWS::CloudFormation::Stack - cvpn
# ------------------------------------------------------------#

# Create Client VPN Endpoint.

  stackCvpn:
    Type: AWS::CloudFormation::Stack
    DependsOn:
      - nwPrivate1
      - nwPrivate2
      - sgCvpn01
    Properties:
      TemplateURL: !Ref 000002templateCvpnURL
      Parameters:
        vpcID: !Ref vpc
        vpcName: !Ref 000101vpcName
        cvpnCIDR: !Ref 020201cvpnCIDR
        cvpnCertARN: !Ref 020202cvpnCertARN
        cvpnDnsIP: !Ref 020203cvpnDnsIP
        cvpnSgID: !GetAtt sgCvpn01.GroupId
        cvpnNwId1: !Ref nwPrivate1
        cvpnNwId2: !Ref nwPrivate2
        cvpnAuthCIDR: !Ref 000102vpcCIDR

# ------------------------------------------------------------#
#  AWS::CloudFormation::Stack - ec2
# ------------------------------------------------------------#

# Create EC2.

 stackEc2:
   Type: AWS::CloudFormation::Stack
   DependsOn:
     - eniAp01
     - eniDb01
   Properties:
     TemplateURL: !Ref 000003templateEc2URL
     Parameters:
       amiID: !FindInMap [ EC2, AMI, !Ref 030101AmiName ]
       instanceType: !Ref 030102InstanceType
       eniID1: !Ref eniAp01
       eniID2: !Ref eniDb01
       keyName: !Ref 030103KeyName
       vpcName: !Ref 000101vpcName
       serverName1: "ap01"
       serverName2: "db01"

以下、解説です。

Parameters:
  000001templateRouteURL:
  000002templateCvpnURL:
  000003templateEc2URL:

ここで子スタックの保管先 (S3 上の URL) を指定します。

  stackRoute:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Ref 000001templateRouteURL
     Parameters:

ここで子スタックを呼び出しています。「Parameters:」では、子スタックに渡す引数を指定しています。

子スタック (route)

ルートテーブル関連のオブジェクトを作成する子スタックです。IGW や NAT Gateway もあわせて作成しています。

ちなみに「AWS::EC2::Route」の Public デフォルトルート (0.0.0.0/0) で、DependsOn に「AWS::EC2::VPCGatewayAttachment」を指定しておくのは、地味に重要です。これを指定しておかないと、紐付け前の IGW を経路登録しようとして、エラーが出てしまいます (2 敗)。

AWSTemplateFormatVersion: 2010-09-09

# ------------------------------------------------------------#
#  Parameter
# ------------------------------------------------------------#

Parameters:

  vpcID:
    Type: AWS::EC2::VPC::Id
  vpcName:
    Type: String
  pub1SubnetID:
    Type: AWS::EC2::Subnet::Id
  priv1SubnetID:
    Type: AWS::EC2::Subnet::Id
  priv2SubnetID:
    Type: AWS::EC2::Subnet::Id

# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#

Resources:

# ------------------------------------------------------------#
#  AWS::EC2::InternetGateway
# ------------------------------------------------------------#

  igw:
    Type: "AWS::EC2::InternetGateway"
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "igw-${vpcName}"

# ------------------------------------------------------------#
#  AWS::EC2::VPCGatewayAttachment
# ------------------------------------------------------------#

  igwAttachGateway1:
    Type: "AWS::EC2::VPCGatewayAttachment"
    DependsOn:
      - igw
    Properties:
      VpcId: !Ref vpcID
      InternetGatewayId: !Ref igw

# ------------------------------------------------------------#
#  AWS::EC2::EIP
# ------------------------------------------------------------#

  eipNgw01:
    Type: "AWS::EC2::EIP"
    Properties:
      Domain: "vpc"
      Tags:
        - Key: Name
          Value: !Sub "eip-${vpcName}-ngw01"

# ------------------------------------------------------------#
#  AWS::EC2::NatGateway
# ------------------------------------------------------------#

  ngw:
    Type: "AWS::EC2::NatGateway"
    Properties:
      AllocationId: !GetAtt eipNgw01.AllocationId
      SubnetId: !Ref pub1SubnetID
      Tags:
        - Key: Name
          Value: !Sub "ngw-${vpcName}"

# ------------------------------------------------------------#
#  AWS::EC2::RouteTable
# ------------------------------------------------------------#

  rtPublic:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref vpcID
      Tags:
        - Key: Name
          Value: !Sub "rt-${vpcName}-public"

  rtPrivate:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref vpcID
      Tags:
        - Key: Name
          Value: !Sub "rt-${vpcName}-private"

# ------------------------------------------------------------#
#  AWS::EC2::Route
# ------------------------------------------------------------#

# Default : 0.0.0.0/0

  rtPublicDefault:
    Type: 'AWS::EC2::Route'
    DependsOn:
      - rtPublic
      - igwAttachGateway1
    Properties:
      RouteTableId: !Ref rtPublic
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref igw

  rtPrivateDefault:
    Type: 'AWS::EC2::Route'
    DependsOn:
      - rtPrivate
      - ngw
    Properties:
      RouteTableId: !Ref rtPrivate
      DestinationCidrBlock: "0.0.0.0/0"
      NatGatewayId: !Ref ngw

# ------------------------------------------------------------#
#  AWS::EC2::SubnetRouteTableAssociation
# ------------------------------------------------------------#

  rtPublicAssociation1:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    DependsOn:
      - rtPublic
    Properties:
      SubnetId: !Ref pub1SubnetID
      RouteTableId: !Ref rtPublic

  rtPrivateAssociation1:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    DependsOn:
      - rtPrivate
    Properties:
      SubnetId: !Ref priv1SubnetID
      RouteTableId: !Ref rtPrivate

  rtPrivateAssociation2:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    DependsOn:
      - rtPrivate
    Properties:
      SubnetId: !Ref priv2SubnetID
      RouteTableId: !Ref rtPrivate

子スタック (cvpn)

Client VPN Endpoint 関連のオブジェクトを作成する子スタックです。とりあえず、証明書認証のみの想定です。

Client VPN Endpoint は、上記で作成した Private NW x 2 に関連付けを行っています。また、認証設定で、Amazon DNS (VPC の CIDR + 2 のアドレス)、および、VPC の CIDR への通信を許可しています。

AWSTemplateFormatVersion: 2010-09-09

# ------------------------------------------------------------#
#  Parameter
# ------------------------------------------------------------#

Parameters:

  vpcID:
    Type: AWS::EC2::VPC::Id
  vpcName:
    Type: String
  cvpnCIDR:
    Type: String
  cvpnCertARN:
    Type: String
  cvpnDnsIP:
    Type: String
  cvpnSgID:
    Type: AWS::EC2::SecurityGroup::Id
  cvpnNwId1:
    Type: AWS::EC2::Subnet::Id
  cvpnNwId2:
    Type: AWS::EC2::Subnet::Id
  cvpnAuthCIDR:
    Type: String

# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#

Resources:

# ------------------------------------------------------------#
#  AWS::EC2::ClientVpnEndpoint
# ------------------------------------------------------------#

  cvpn:
    Type: AWS::EC2::ClientVpnEndpoint
    Properties:
      ClientCidrBlock: !Ref cvpnCIDR
      ServerCertificateArn: !Ref cvpnCertARN
      AuthenticationOptions:
      - Type: "certificate-authentication"
        MutualAuthentication:
          ClientRootCertificateChainArn: !Ref cvpnCertARN
      ConnectionLogOptions:
        Enabled: false
      DnsServers:
        - !Ref cvpnDnsIP
      TransportProtocol: "tcp"
      SplitTunnel: "true"
      VpcId: !Ref vpcID
      SecurityGroupIds:
        - !Ref cvpnSgID
      VpnPort: 443
      TagSpecifications:
        - ResourceType: "client-vpn-endpoint"
          Tags:
          - Key: "Name"
            Value: !Sub "cvpn-${vpcName}"

# ------------------------------------------------------------#
#  AWS::EC2::ClientVpnTargetNetworkAssociation
# ------------------------------------------------------------#

  cvpnAssociation1:
    Type: "AWS::EC2::ClientVpnTargetNetworkAssociation"
    DependsOn:
      - cvpn
    Properties:
      ClientVpnEndpointId: !Ref cvpn
      SubnetId: !Ref cvpnNwId1

  cvpnAssociation2:
    Type: "AWS::EC2::ClientVpnTargetNetworkAssociation"
    DependsOn:
      - cvpn
    Properties:
      ClientVpnEndpointId: !Ref cvpn
      SubnetId: !Ref cvpnNwId2

# ------------------------------------------------------------#
#  AWS::EC2::ClientVpnAuthorizationRule
# ------------------------------------------------------------#

  cvpnAuthRule1:
    Type: "AWS::EC2::ClientVpnAuthorizationRule"
    DependsOn:
      - cvpn
    Properties:
      ClientVpnEndpointId: !Ref cvpn
      AuthorizeAllGroups: true
      TargetNetworkCidr: !Join [ "/", [ !Ref cvpnDnsIP, "32" ] ]

  cvpnAuthRule2:
    Type: "AWS::EC2::ClientVpnAuthorizationRule"
    DependsOn:
      - cvpn
    Properties:
      ClientVpnEndpointId: !Ref cvpn
      AuthorizeAllGroups: true
      TargetNetworkCidr: !Ref cvpnAuthCIDR

子スタック (ec2)

EC2 関連のオブジェクトを作成する子スタックです。

AWSTemplateFormatVersion: 2010-09-09

# ------------------------------------------------------------#
#  Parameter
# ------------------------------------------------------------#

Parameters:

  amiID:
    Type: String
  instanceType:
    Type: String
  eniID1:
    Type: String
  eniID2:
    Type: String
  keyName:
    Type: String
  vpcName:
    Type: String
  serverName1:
    Type: String
  serverName2:
    Type: String

# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#

Resources:

# ------------------------------------------------------------#
#  AWS::EC2::Instance
# ------------------------------------------------------------#
  sv01:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref amiID
      InstanceType: !Ref instanceType
      NetworkInterfaces:
        - DeviceIndex: '0'
          NetworkInterfaceId: !Ref eniID1
      KeyName: !Ref keyName
      Tags:
        - Key: Name
          Value: !Sub "sv-${vpcName}-${serverName1}"

  sv02:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref amiID
      InstanceType: !Ref instanceType
      NetworkInterfaces:
        - DeviceIndex: '0'
          NetworkInterfaceId: !Ref eniID2
      KeyName: !Ref keyName
      Tags:
        - Key: Name
          Value: !Sub "sv-${vpcName}-${serverName2}"

実行方法

  1. 子スタックを S3 にアップロードする。アップロード URL は控えておくこと。
  2. 親スタックをアップロードして実行する。その際のパラメータで、子スタックの URL を指定する。
  3. だいたい 10 ~ 20 分ぐらいで環境ができあがる。

まとめ

以上、CloudFormation のスタックネスト機能をいろいろ考えながら使ってみる、でした。

コード分割の一番の利点は、やはり「細かくテストできる」というところでしょうか。小さな単位にコードを分割することで、その小さな単位毎に単体テストを実行できます。また、小さな単位に分割されるので、修正や保守もやりやすくなります。

「分割の考え方」に答えはないので、あーでもないこーでもないと試行錯誤し続けながら、自分なりの最適解を見つけていきたいと思います。

おまけで、Route53 、ELB、RDS も含めた場合の構成も考えてみました。いつか検証できる機会があれば、下記で実際に動くか検証してみたいと思います。
02_ELBなどを含めた版.png

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