Blogical

AWS/Salesforceを中心に様々な情報を配信していきます(/・ω・)/

AWS CloudFormationを用いたリソース構築

こんにちは、ロジカル・アーツの村下です。

AWSで環境構築する際、それらが過去に構築した環境と同じ、似た構成だった場合、もしくは複数の環境を構築する場合、同じ作業を行うのは面倒だと感じたことはありませんか?

こういう時、CloudFormationでテンプレートを作成しておけば、簡単に環境構築をすることができます。

今回は、そんなCloudFormationについて解説していきたいと思います。

CloudFormationとは

CloudFormationとは、AWSリソースを自動作成・管理するサービスです。

予め、「どのリソースをどのような設定で作るか」をテンプレートにJSONYAML形式で記述して作成します。 作成したテンプレートを用いることで、AWSリソースを自動作成してくれます。

作成したテンプレートをそのまま使ったり、少しだけ編集することで、同じ・似た環境を複数構築することができます。 結果、作業時間の短縮や労力の削減に繋がります。

シナリオ

今回は、VPCと、その中にパブリック・プライベートサブネットを構築し、それぞれのサブネットの中にEC2インスタンスを構築するテンプレートを作成してみたいと思います。

また、VPC・サブネットにゲートウェイやルートテーブルの設置にルーティング、EC2用のセキュリティグループの作成も同時に行います。

CloudFormationの基本

テンプレートを作成する前に、CloudFormationの基本情報について解説します。

テンプレート構造

以下が、テンプレートの基本的な構成の例です。今回はYAML形式で作成しています。

AWSTemplateFormatVersion: 2010-09-09 #テンプレートのバージョン
Resources: #構築するリソースの宣言
  VPC: #論理名
    Type: AWS::EC2::VPC #リソースタイプ
    Properties: #リソースのパラメータを設定
      CidrBlock: 10.0.0.0/16

・AWSTemplateFormatVersion: テンプレートのバージョンを指定します。2023年4月時点では 2010-09-09 のみ指定できます。

・Resources: 構築するリソースを宣言します。Resourcesセクションは記述必須なため、書き忘れに注意して下さい。

・論理名: 論理名を指定します。こちらは同じテンプレート内で一意である必要があり、テンプレート内で参照する際に使用します。

・Type: 構築するリソースタイプ。例だとVPCなので、AWS::EC2::VPC を指定しています。(例えば、EC2インスタンスの場合だと、AWS::EC2::Instance を指定します。)

・Properties: リソースのパラメータを設定を記述します。どのようなパラメータがあるか、どれが必須なのかはリソースによって異なります。

リソースタイプやパラメータなどの記述方法、その他詳しい解説などは以下のAWS公式ドキュメントを参照してみて下さい。

docs.aws.amazon.com

docs.aws.amazon.com

組み込み関数

同じテンプレート内の異なるリソースの情報を参照したい場合、組み込み関数を使用することができます。

  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.0.0/24
      VpcId: !Ref VPC

上記は組み込み関数 Ref の使用例です。

サブネットのパラメータにはVPCのIDが必要です。 この際、組み込み関数 Ref を使用して、VPCを指定することで、指定したVPCのIDを参照できます。

「!Ref 論理名」 または 「Ref: 論理名」のように使用します。 例の場合、TestVPC のIDを参照したいため、サブネットのパラメータ VpcId に「!Ref TestVPC」を指定しています。

  NATGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NATGatewayEIP.AllocationId

上記は組み込み関数 Fn::GetAtt の使用例です。

NATゲートウェイに AllocationId でEIPのIDを取得する際に使用しています。

「Fn::GetAtt: [論理名, 属性名]」 または 「!GetAtt 論理名.属性名」のように使用します。 例の場合、NATGatewayEIP という論理名のリソースが持つ、AllocationId を取得しています。

他にも組み込み関数はありますが、ここでは記事内で使用するものについて解説しました。 その他の組み込み関数の解説などは以下のAWS公式ドキュメントを参照してみて下さい。

docs.aws.amazon.com

パラメータ

環境によって変化する箇所がある場合、Parameters セクションを使用すると便利です。

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  VPCCidrBlock: #論理名
    Type: String #パラメータのタイプ
    Default: 10.0.0.0/16 #デフォルトの値を設定
Resources:
  # VPC
  VPC:
    Type: AWS::EC2::VPC 
    Properties: 
      CidrBlock: !Ref VPCCidrBlock #組み込み関数 Ref で参照する

例えば、VPC の CidrBlock が開発環境・テスト環境で変化する場合、 Parameters セクション以下に、VPCCidrBlock というパラメータを宣言します。 そして、そのパラメータのタイプと、デフォルトの値を設定します。

値を設定したら、Resouces セクション ,VPC の CidrBlock で、組み込み関数 Ref を使い、VPCCidrBlock を参照します。 パラメータタイプは様々ありますが、String 型が自由入力で汎用的なためそちらを使用します。

Parameters の詳しい解説や、その他のパラメータタイプは、以下のAWS公式ドキュメントを参照してみて下さい。

docs.aws.amazon.com

テンプレートを用いてリソースを構築する際、Parameters セクションで宣言したパラメータの値を入力することができます。 また、デフォルトを設定すると、値が入力欄に予め入力されています。

ここの例では VPC の CidrBlock が環境によって変化する想定で書きましたが、 今回の記事で作成するテンプレートでは、EC2インスタンスのイメージIDとインスタンスタイプ、セキュリティグループの名前が変化する想定で作成していきます。

どのリソースのどのパラメータが環境によって変化するかを想定しておくと書きやすいと思います。

タグ

AWSのリソースはタグを付与することで、識別や分類に役立てることができます。 テンプレート上でも、タグを付与することができます。

      Tags:
        - Key: Name
          Value: VPC

このように、Key でキー名、Value でタグの値を指定できます。

テンプレート作成

前置きが長くなりましたが、テンプレート作成をしていきます。

なお、VPCやサブネット、EC2等のAWSリソースの知識はすでに持っている前提で解説していきます。

VPC

まずはVPCを構築するテンプレートの作成をします。

  # VPC
  VPC:
    Type: AWS::EC2::VPC 
    Properties: 
      CidrBlock: 10.0.0.0/16
      Tags:
        - Key: Name
          Value: VPC

以上の内容を、テンプレートの構成に記述してある、AWSTemplateFormatVersion と Resources の下に記述します。

CidrBlock でインターネット範囲を指定しています。

サブネット

次にVPC内に、パブリック・プライベートサブネットを構築するテンプレートの作成をします。

  # PublicSubnet
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.0.0/24
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: PublicSubnet
  # PrivateSubnet
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.1.0/24
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: PrivateSubnet

VPC同様、CidrBlock で範囲を指定します。

VpcId でVPCのIDを指定します。 組み込み関数で説明したように、「!Ref VPC」で VPC のIDを参照できます。

ゲートウェイ

続いて、ゲートウェイを構築するテンプレートを作成します。

まずは、インターネットゲートウェイ(以下、IGW)を作成し、VPCに設置する内容を記述します。

  # InternetGateway
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags: 
      - Key: Name
        Value: InternetGateway

IGWは作成だけでなく、別途設置の設定を記述する必要があります。

  # InternetGatewayAttach
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

VpcId で設置先のVPC、InternetGatewayId で設置するIGWを指定します。 例では、すでに作成したリソースを、組み込み関数 Ref を使用して参照しています。

次に、パブリックサブネット内に設置するNATゲートウェイ(以下、NGW)を構築するテンプレートを作成します。

NGWはパブリックサブネットに設置する場合、EIPのIDが必要なため、EIPを作成します。

  # NATGatewayEIP
  NATGatewayEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

EIPを用意できたら、NGWを設定します。

  # NATGateway
  NATGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NATGatewayEIP.AllocationId
      SubnetId: !Ref PublicSubnet
      Tags:
      - Key: Name
        Value: NATGateway

組み込み関数 GetAtt を使用して、作成したEIPのIDを取得します。 また、SubnetId で設置するサブネットを指定します。

ルートテーブルとルーティング

ゲートウェイのテンプレート作成が完了したら、ルートテーブルの作成とルーティング設定を行うテンプレートを作成します。 こちらは大きく分けて、3つの段階があります。

1.ルートテーブルの作成

2.作成したルートテーブルの中身を設定(Cidrブロック と通信の行き先であるゲートウェイを設定)

3.ルートテーブルをサブネットに関連付ける

まずは、パブリックルートテーブルを作成します。

  # PublicRouteTable
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: PublicRoteTable

VpcId でルートテーブルを設置するVPCを指定します。

次に、ルートテーブルの中身を設定します。

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

RouteTableId で設定する対象のルートテーブル、DestinationCidrBlock でCidrブロック、GatewayId で通信先のゲートウェイをそれぞれ指定します。

最後に、ルートテーブルをサブネットに関連付けます。

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

SubnetId と RouteTableId で関連付けるサブネットとルートテーブルを設定します。

同様に、プライベートルートテーブルの作成・設定のテンプレートを作成します。

  # PrivateRouteTable
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: PrivateRoteTable
  # PrivateRoute
  NATGatewayRoute:
    Type: AWS::EC2::Route
    Properties: 
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NATGateway
  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateRouteTable

一度に書きましたが、内容は概ねパブリックと同じなので詳しい解説は省略します。 注意点としては、パブリックのIGWとは違い、NGWを指定しています。

EC2

今度は、EC2インスタンスの構築するテンプレートを作成します。 パブリック、プライベートサブネットにそれぞれインスタンスが作成されるように記述します。

ここで、Parameters の項で書いた通り、 EC2インスタンスのイメージIDのインスタンスタイプは環境によって変化する想定でテンプレートを作成します。

まずは、変化するパラメータを Parameters セクションで宣言していきます。

Parameters:
  # PublicInstance
  PublicInstanceImageId:
    Type: String
    Default: ami-079a2a9ac6ed876fc
  PublicInstanceType:
    Type: String
    Default: t2.micro
  # PrivateInstance
  PrivateInstanceImageId:
    Type: String
    Default: ami-079a2a9ac6ed876fc
  PrivateInstanceType:
    Type: String
    Default: t2.micro

注意点としては、Parameters は Resources 以下ではなく外側に書くことです。

ImageId は今回は無料枠のモノを使用します。

宣言ができたら、次は Resources を設定していきます。

  # PublicInstance
  PublicInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref PublicInstanceImageId
      InstanceType: !Ref PublicInstanceType
      SubnetId: !Ref PublicSubnet
      SecurityGroupIds:
        - !Ref PublicInstanceSecurityGroup
  # PrivateInstance
  PrivateInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref PrivateInstanceImageId
      InstanceType: !Ref PrivateInstanceType
      SubnetId: !Ref PrivateSubnet
      SecurityGroupIds:
        - !Ref PrivateInstanceSecurityGroup

Parameters で宣言したパラメータを 組み込み関数 Ref を使用して指定します。

SubnetId でそれぞれのサブネット、SecurityGroupIds でそれぞれのセキュリティグループを指定します。

パブリックインスタンスにはEIPを付与したいので、別途EIPを用意します。

  # PublicInstancEIP
  PublicInstancEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      InstanceId: !Ref PublicInstance

InstanceId でEIPを付与するインスタンスを指定します。

セキュリティグループ

最後に、それぞれのインスタンスに設定するセキュリティグループ(以下、SG)のテンプレートを作成します。

EC2インスタンス同様に、セキュリティグループの名前が環境ごとに変化する想定で作成するので、 変化するパラメータを Parameters セクションで宣言します。

  # SecurityGroupName
  PublicInstanceSecurityGroupName:
    Type: String
    Default: Test-SG-Instance-Public
  PrivateInstanceSecurityGroupName:
    Type: String
    Default: Test-SG-Instance-Private

こちらをEC2インスタンスの変化するパラメータの宣言に続けて記述します。

パラメータの宣言後、まずは、パブリックインスタンスのSGから設定します。

  # PublicInstancSG
  PublicInstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: http and ssh
      GroupName: !Ref PublicInstanceSecurityGroupName
      VpcId: !Ref VPC
      SecurityGroupIngress:
        -
          IpProtocol: tcp
          FromPort : 80
          ToPort : 80
          CidrIp: 0.0.0.0/0
        -
          IpProtocol: tcp
          FromPort : 22
          ToPort : 22
          CidrIp: 0.0.0.0/0
      SecurityGroupEgress:
        - 
          IpProtocol: -1
          FromPort : -1
          ToPort : -1
          CidrIp: 0.0.0.0/0

GroupName を 組み込み関数 !Ref でPublicInstanceSecurityGroupName を参照し 、 VpcId を指定します。

SecurityGroupIngress でインバウンド、SecurityGroupEgress でアウトバウンドルールをそれぞれ設定できます。

インバウンドでは今回、HTTP と SSH からの全ての通信を許可します。

IpProtocol でプロトコル名、FromPort でポート範囲の開始、ToPort でポート範囲の終了、CidrIp でアドレス範囲をそれぞれ指定します。

プロトコルは両方とも tcp を指定、HTTP のポート番号 80 と SSH のポート番号 22 をそれぞれポートの範囲に指定します。

アウトバウンドでは、すべての通信を許可します。 それぞれの値を -1 にすることで、全てを指定できます。

続いて、プライベートサブネットのSGを設定します。

  # PrivateInstancSG
  PrivateInstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: ssh
      GroupName: !Ref PrivateInstanceSecurityGroupName
      VpcId: !Ref VPC
      SecurityGroupIngress:
        -
          IpProtocol: tcp
          FromPort : 22
          ToPort : 22
          SourceSecurityGroupId: !Ref PublicInstanceSecurityGroup
      SecurityGroupEgress:
        - 
          IpProtocol: -1
          FromPort : -1
          ToPort : -1
          CidrIp: 0.0.0.0/0

インバウンドでは SSH のみを許可します。

CidrIp の代わりに SourceSecurityGroupId を指定することで、指定したSGに属するリソースからの接続が許可されます。

アウトバウンドはパブリックインスタンス用SGと同様の設定をします。

SG の設定まで完了すれば、今回のテンプレート作成は一通り完了です。

完成テンプレート

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  # PublicInstance
  PublicInstanceImageId:
    Type: String
    Default: ami-079a2a9ac6ed876fc
  PublicInstanceType:
    Type: String
    Default: t2.micro
  # PrivateInstance
  PrivateInstanceImageId:
    Type: String
    Default: ami-079a2a9ac6ed876fc
  PrivateInstanceType:
    Type: String
    Default: t2.micro
  # SecurityGroupName
  PublicInstanceSecurityGroupName:
    Type: String
    Default: Test-SG-Instance-Public
  PrivateInstanceSecurityGroupName:
    Type: String
    Default: Test-SG-Instance-Private

Resources:
  # VPC
  VPC:
    Type: AWS::EC2::VPC 
    Properties: 
      CidrBlock: 10.0.0.0/16
      Tags:
        - Key: Name
          Value: VPC
  
  # PublicSubnet
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.0.0/24
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: PublicSubnet

  # PrivateSubnet
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.1.0/24
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: PrivateSubnet

  # InternetGateway
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags: 
      - Key: Name
        Value: InternetGateway

  # InternetGatewayAttach
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  # NATGatewayEIP
  NATGatewayEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

  # NATGateway
  NATGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NATGatewayEIP.AllocationId
      SubnetId: !Ref PublicSubnet
      Tags:
      - Key: Name
        Value: NATGateway

  # PublicRouteTable
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: PublicRoteTable
  
  # PublicRoute
  InternetGatewayRoute:
    Type: AWS::EC2::Route
    Properties: 
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

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

  # PrivateRouteTable
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: PrivateRoteTable

  # PrivateRoute
  NATGatewayRoute:
    Type: AWS::EC2::Route
    Properties: 
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NATGateway
  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateRouteTable

  # PublicInstance
  PublicInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref PublicInstanceImageId
      InstanceType: !Ref PublicInstanceType
      SubnetId: !Ref PublicSubnet
      SecurityGroupIds:
        - !Ref PublicInstanceSecurityGroup

  # PublicInstancEIP
  PublicInstancEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      InstanceId: !Ref PublicInstance

  # PrivateInstance
  PrivateInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref PrivateInstanceImageId
      InstanceType: !Ref PrivateInstanceType
      SubnetId: !Ref PrivateSubnet
      SecurityGroupIds:
        - !Ref PrivateInstanceSecurityGroup
  
  # PublicInstancSG
  PublicInstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: http and ssh
      GroupName: !Ref PublicInstanceSecurityGroupName
      VpcId: !Ref VPC
      SecurityGroupIngress:
        -
          IpProtocol: tcp
          FromPort : 80
          ToPort : 80
          CidrIp: 0.0.0.0/0
        -
          IpProtocol: tcp
          FromPort : 22
          ToPort : 22
          CidrIp: 0.0.0.0/0
      SecurityGroupEgress:
        - 
          IpProtocol: -1
          FromPort : -1
          ToPort : -1
          CidrIp: 0.0.0.0/0
  
  # PrivateInstancSG
  PrivateInstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: ssh
      GroupName: !Ref PrivateInstanceSecurityGroupName
      VpcId: !Ref VPC
      SecurityGroupIngress:
        -
          IpProtocol: tcp
          FromPort : 22
          ToPort : 22
          SourceSecurityGroupId: !Ref PublicInstanceSecurityGroup
      SecurityGroupEgress:
        - 
          IpProtocol: -1
          FromPort : -1
          ToPort : -1
          CidrIp: 0.0.0.0/0

以上は完成したテンプレートの全体です。参考にしてみて下さい。

任意の場所にファイルを保存してください。 今回はファイル名を「cloudformation_Murasita」としてあります。

実行

では、完成したテンプレートを使用して、構築を行いましょう。

まず、AWSにログインし、CloudFormationのコンソールを開きます。 そして、「スタックの作成」を選択します。

前提条件はテンプレートの準備完了を選択し、テンプレートファイルのアップロードで先程保存したファイルを指定して次へ、をクリックします。

スタックの名前を付けます。

Parameters で宣言したパラメータの設定を行えます。 ここで環境によって値を指定できますが、今回はデフォルトの値を使用するので、 特に入力せずに次へ、をクリックします。

スタックオプションの設定、レビューは特に入力するものがないので次へ、をクリックします。

スタックが作成出来たら、テンプレートに従ったリソースが順次作られていきます。 しばらくすると、全てのリソースの構築が完了しました。

テンプレートに不備があった場合は、スタックを作成するときにエラーが出たり、リソースの構築に失敗します。 エラー文が出るので、それに従って修正してください。(インデントがズレている、権限がない、などでもエラーが出るので確認してみて下さい。)

削除

今回構築したリソースの削除は忘れずに行いましょう。

そのままにしておくと、料金が発生してしまう場合があるので、構築したリソースを使用しない場合は削除しましょう。 リソースの種類や設定、構成によっては、多額の料金が掛かってきます。

スタックを選択して、右上の削除を選択することで、構築したリソースが削除されます。

以下の画像のような表記が出ると、全てのリソースの削除が完了しました。

最後に

CloudFormation のテンプレート作成は、初めこそ時間が掛ると思いますが、 1度作ってしまえばそれを使いまわしたり、用途に合わせて編集することで簡単にリソース構築ができるようになります。

参考

組み込み関数リファレンス - AWS CloudFormation

AWS リソースおよびプロパティタイプのリファレンス - AWS CloudFormation

組み込み関数リファレンス - AWS CloudFormation

パラメータ - AWS CloudFormation