AWS CloudFormation 実践 第1回 VPC編

今回から、何回かにわたってAWS CloudFormation の実践について書いていきます。

経緯

今年の夏に、SAA(AWS Solution Architect Assosiate)資格の期限が切れてしまうため、一念発起してSAP(同 Professional)資格取得を決めました。従来は Udemy での勉強をしていましたが、今回はチャネルを変えて「Cloud Tech」さんのサイトで勉強を始めました。

現在は動画中心で勉強をすすめているのですが、実践編の都度、環境を作っては崩してを繰り返しているので手間がかかり、必要に応じてCloudFormationで基盤構築することを試してみようと思います。

これまでも、IaC(Infrastructure as Code)の重要性は理解していましたが、実際に手を動かして構築したことはなかったので、良い機会かと。ただ、目的はあくまで試験合格なので脱線注意でいきたいな、とは思っています。

CloudFormationとIaCの基礎知識

CloudFormationはYaml(またはJSON)形式で書かれたテンプレートファイルに従って、AWSのインフラ環境を自動構築するのサービスです。AWSインフラ構築はAWSコンソールからのGUI操作でも可能ですが、テンプレートファイルにまとめておくことで、新規構築、変更、破棄が簡単に行えます。構築するセットのことを「スタック」と呼びます。

このように、コード(テンプレートファイル記述)を使用してインフラ環境を構築することを 「Infrastrucure As Code(IaC)」と呼び、再利用性や、保守性の高さから昨今のトレンドとなっています。IaCの実現は、AWS専用のCloudFormationの他、Terraformや、Ansibleといった汎用ツールも存在します。

CloudFormationの実行方法

CloudFormationには、次のような実行方法があります。

  • AWS CLIで、ローカル環境のテンプレートファイルをAWSに対して実行する。
  • AWSマネージメントコンソールで、CloudFormationを選択して、作成したテンプレートファイルをアップロードする。
  • Code Pipelineを使用して、Gitに登録したテンプレートファイルをデプロイする

今回は、1点目のAWS CLIを使用してデプロイを行う方法を採用します。

事前準備

AWS CLIを使用するには、AWSのアカウントを作成の上で、次の準備が必要となります。

  1. AWS CLIをローカル環境にインストールする。
  2. AWSコンソールで IAM サービスを開き、CLIを使用するアカウントとアクセスキーを作成する
  3. ローカル環境でコマンドプロンプトから aws configure をコマンドを実行し、リージョン・アクセスキーを設定する。

ここでは詳細は割愛します。↓の公式ページを参照ください。

docs.aws.amazon.com

Cloud Formation テンプレート基礎知識

テンプレートファイル(ここではYaml形式で説明します)のスケルトンは以下のとおりとなっています(AWS公式ページを引用)

AWSTemplateFormatVersion: "version date"

Description:
  String

Metadata:
  template metadata

Parameters:
  set of parameters

Rules:
  set of rules

Mappings:
  set of mappings

Conditions:
  set of conditions

Transform:
  set of transforms

Resources:
  set of resources

Outputs:
  set of outputs

各セクションの内容は次の表のとおりです。

セクション 設定内容 必須
AWSTemplateFormatVersion テンプレートが準拠している AWS CloudFormation テンプレートバージョン。テンプレート形式バージョンは API または WSDL バージョンと同じではありません。テンプレート形式バージョンは API および WSDL バージョンとは関係なく変更できます。 ×
Description テンプレートを説明するテキスト文字列です。このセクションは、必ずテンプレートの Format Version セクションの後に記述する必要があります。 ×
Metadata テンプレートに関する追加情報を提供するオブジェクトです。 ×
Parameters 実行時 (スタックを作成または更新するとき) にテンプレートに渡す値です。テンプレートの Resources および Outputs セクションからのパラメータを参照できます。 ×
Rules スタックの作成またはスタックの更新時に、テンプレートに渡されたパラメータまたはパラメータの組み合わせを検証します。 ×
Mappings キーと関連する値のマッピングで、条件パラメータ値の指定に使用でき、ルックアップテーブルに似ています。Resources セクションと Outputs セクションで Fn::FindInMap 組み込み関数を使用することで、キーと対応する値を一致させることができます。 ×
Conditions スタックの作成中または更新中に、特定のリソースが作成されるかどうか、または特定のリソースプロパティに値が割り当てられるかどうかを制御する条件です。例えば、スタックが実稼働用であるかテスト環境用であるかに依存するリソースを、条件付きで作成できます。 ×
Transform サーバーレスアプリケーション (Lambda ベースアプリケーションとも呼ばれます) の場合は、使用する AWS Serverless Application Model (AWS SAM) のバージョンを指定します。変換を指定する場合は、AWS SAM 構文を使用して、テンプレート内のリソースを宣言できます。このモデルでは、使用できる構文と、その処理方法を定義します。 ×
Resources Amazon Elastic Compute Cloud インスタンスAmazon Simple Storage Service バケットなど、スタックリソースとそのプロパティを指定します。テンプレートの Resources と Outputs セクションのリソースを参照できます。
Outputs スタックのプロパティを確認すると返される値について説明します。たとえば、S3 バケット名の出力を宣言してから、aws cloudformation describe-stacks AWS CLI コマンドを呼び出して名前を表示することができます。 ×

表に示す通り「必須」なのは、Resoucesだけであり、これだけあればひとまず実行できます。

テンプレートファイル作成

ローカルに新しいYamlファイルを作成して、最低限の記述を記載します。今回は、cf-tutorial.yaml という名前でファイルを作成し、以下のとおり記述しました。

AWSTemplateFormatVersion: 2010-09-09

Resources: 

なお記述にあたっては、VsCodeを使用しています。Extentionsの、Cloud Formationを使用すると、リソース名から最低限のプロパティを自動出力してくれるので簡単に記述できます。

ただ、このExtentionは更新が止まっているせいか、すべてのリソースを出力してくれるわけではないようです。今回のケースでもNAT Gatewayは自分で執筆しました。

今回構築するAWSインフラ

一般的なEC2構成をCloudFormationで構築することを目指していきます。下図を参照。

  • リージョン内に、Subnetを二つ持つVPCを構築する
  • Subnetの一つはPublicとする。Internet Gatewayを介して外部からアクセスできるようにする。
  • もう一つのSubnetはPrivateとする。外部からの接続は禁止し、Public Subnetからしかアクセスできないこと。ただし、Private Subnet側から外部へはアクセスできること(NAT Gatewayを経由)

今回は上記のうち、EC2以外のネットルート(VPC、Subnet、IGW、NAT)までとし、EC2は次回に送ります。

VPC

最初にVPCを構成します。ここで、テンプレートファイルに対する基礎的な記述方法を学んでおきます。

AWSTemplateFormatVersion: 2010-09-09

Resources: 
# VPC
  cftVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/21
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: cf-tutorial-vpc
  • 冒頭「cftVpc」はテンプレート内のリソースIdで、任意につけられます。冒頭小文字のキャメルケースで記載するのが一般的なようで、名称は英数字のみ可能です(ハイフン不可)
  • Typeには、作成するリソースを指定します。設定できる内容は公式ドキュメントから選択します。
    AWS リソースおよびプロパティタイプのリファレンス
  • Propertiesには、リソースに応じた各種の設定内容を記述します。ここではVPCIPアドレス(CIDR)と、DNSサポートの有無、タグを設定しています。
  • 余談ですが、Tagsに設定するキー名は大文字小文字の区分けがされます。例えばNameタグをnameとしてしまうと、AWSマネジメントコンソールには何も表示されませんのでご注意ください。

このように、リソースに応じて設定するプロパティが異なるため、それぞれの設定を理解していく必要があります。

AWS CLIからCloudFormationでデプロイする

CloudFormationをうまく行うコツは、小さい単位でデプロイをおこなうことだそうです。一気に実行しようとすると、エラー時に原因を切り分けるのに苦労したり、トライ&エラーに時間がかかったりするためです。

実際、今回のような小さなものでもすべて記載してデプロイするのに、1回5分程度かかるため、細かなNGが重なると効率が悪かったりします。

デプロイは次のコマンドで実行します。

> aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial
  • --template-file オプションの後ろには、テンプレートファイルを指定します。
  • --stack-name オプションの後ろには、スタックの名称(任意)を指定します。

最低限、これだけあれば実行可能ですが、詳細なオプションとして指定できるものは、こちらの公式ドキュメントを参照ください。

実行が正常終了すると、以下のように結果が表示されます。Successfully~と出ていれば成功です(エラー時は An error occurred~のように出ます)

> aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - cf-tutorial

AWSマネージメントコンソールでCloudFormationを見ると、当該スタック(cf-tutorial)に対して「CREATE_COMPLETE」が表示されていることが分かります。

同じく、AWSマネージメントコンソールでVPCを参照すると、上記のテンプレートファイルで指定したVPCが作成されていました。

以上で確認は終了です。以降で継続してリソースの追加をしていきます。

補足. デプロイがエラーになったら、リトライ前にDELETE STACKすること

CloudFormationのデプロイが失敗した後、再度、修正したテンプレートファイルでdeployをしようとすると、以下のようなエラーとなります。

> aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial

An error occurred (ValidationError) when calling the CreateChangeSet operation: Stack:arn:aws:cloudformation:ap-northeast-1:922813957008:stack/cf-tutorial/579b4ab0-d0bf-11ee-809a-0a4a4aa379bb is in ROLLBACK_COMPLETE state and can not be updated.

リトライする前に、以下のコマンドでスタックを削除するようにしてください。

> aws cloudformation delete-stack --stack-name cf-tutorial

Internet Gateway

次にInternet Gatewayを記述します。

# Internet Gateway
  cftIgw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: cf-tutorial-igw

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref cftVpc    # !Ref関数でVPCリソース「cftVpc」を指定する
      InternetGatewayId: !Ref cftIgw    

正確には前半のcftIgwがInternetGatewayで、後半のAttachGateway(VPCGatewayAttachment)はVPCへのアタッチを示す別のリソースです。InternetGatewayのプロパティはほとんどなく、VPCGatewayAttachmentでVPCとInternetGatewayのIdを指定してリンクさせます。

  • !Ref のように「!」から始まる記述は、CloudFormationの組込み関数です。!Ref は指定したパラメータの値を返却します。VpcId: !Ref cftVpcVpcId パラメータに、前述のVPCを設定する、という意味です。

Subnet(Public)

次はPublicのサブネットの定義となります。Subnetに適用するルートテーブルも併せて定義します。

# Subnet (public)  
  cftPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: true      # パブリックIPv4アドレスの自動割り当てを有効
      VpcId: !Ref cftVpc
      CidrBlock: 10.0.1.0/24
      Tags:
        - Key: Name
          Value: cf-tutorial-public-subnet
  
  cftPublicRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
        - Key: Name
          Value: cf-tutorial-public-rtb
  
  cftPublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPublicRtb
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref cftIgw

  cftPublicRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPublicSubnet
      RouteTableId: !Ref cftPublicRtb  

ここで重要なのは、3つ目にあるルートリソース(cftPublicRoute)です。DestinationCidrBlock: 0.0.0.0/0GatewayId: !Ref cftIgw が、マネージメントコンソールにおける、次のルートの設定に該当します。

上記で定義したルートをルートテーブル(cftPublicRtb)に紐づけしたら、最後にAWS::EC2::SubnetRouteTableAssociationリソース(cftPublicRtAssoc)で、サブネットに関連付けします。

  • SubnetリソースにあるMapPublicIpOnLaunch: trueは、 AWS マネジメントコンソールの「パブリックIPv4アドレスの自動割り当てを有効にする」(true=はい)に相当しています。

Subnet(Private)

Publicと同様の手順で、Privateサブネットも記述します。

# Subnet (private)  
  cftPrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: false    # パブリックIPv4アドレスの自動割り当てを無効
      VpcId: !Ref cftVpc
      CidrBlock: 10.0.2.0/24
      Tags:
        - Key: Name
          Value: cf-tutorial-private-subnet
  
  cftPrivateRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
        - Key: Name
          Value: cf-tutorial-private-rtb
  
  cftPrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPrivateRtb
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref cftNatgw    # ここでNAT Gatewayを指定する

  cftPrivateRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPrivateSubnet
      RouteTableId: !Ref cftPrivateRtb  

構成自体はPublicとほぼ同じですが、Privateサブネットの通信先はInternet Gatewayではなく、NAT Gatewayとなります。

そこでルートリソース cftPrivateRoute には NatGatewayId プロパティとしてNAT Gatewayリソース!Ref cftNatgw を設定します(この cftNatgw リソースはまだ記述していません。この後登場します)

NAT Gateway

最後に、NAT Gatewayを記述します。NAT Gatewayは外部に通信するためのサービスですが、その実態はパブリックな仮想IPアドレスなので、AWSのElasticIPでパブリックIPを生成して割り当てします。

# NAT Gateway  
  cftNatgw:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt cftNatgwEip.AllocationId   # 下のElasticIPを割当てする
      SubnetId: !Ref cftPublicSubnet     # NATGateway自体はPublic Subnetに配置する
      Tags:
        - Key: Name
          Value: cf-tutorial-natgw

  cftNatgwEip:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

前後しますが、Privateサブネットからの送信先に、上記で作成したNAT Gatewayを設定することで、Privateサブネットからの通信を外部に振り向けることができるようになります。

テンプレートファイル全文

ここまでの記述内容をまとめると次のようになります。

AWSTemplateFormatVersion: 2010-09-09

Resources: 
# VPC
  cftVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/21
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: cf-tutorial-vpc
  
# Internet Gateway
  cftIgw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: cf-tutorial-igw
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref cftVpc
      InternetGatewayId: !Ref cftIgw

# Subnet (public)  
  cftPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: true
      VpcId: !Ref cftVpc
      CidrBlock: 10.0.1.0/24
      Tags:
        - Key: Name
          Value: cf-tutorial-public-subnet
  
  cftPublicRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
        - Key: Name
          Value: cf-tutorial-public-rtb
  
  cftPublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPublicRtb
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref cftIgw

  cftPublicRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPublicSubnet
      RouteTableId: !Ref cftPublicRtb  

# Subnet (private)  
  cftPrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: false
      VpcId: !Ref cftVpc
      CidrBlock: 10.0.2.0/24
      Tags:
        - Key: Name
          Value: cf-tutorial-private-subnet
  
  cftPrivateRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
        - Key: Name
          Value: cf-tutorial-private-rtb
  
  cftPrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPrivateRtb
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref cftNatgw

  cftPrivateRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPrivateSubnet
      RouteTableId: !Ref cftPrivateRtb  

# NAT Gateway  
  cftNatgw:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt cftNatgwEip.AllocationId
      SubnetId: !Ref cftPublicSubnet
      Tags:
        - Key: Name
          Value: cf-tutorial-natgw

  cftNatgwEip:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

デプロイを実施

上記のテンプレートファイル(cf.yaml)を、AWS環境に対してデプロイします。

デプロイは次のコマンドで実行します。

> aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - cf-tutorial

CloudFormationが正常終了すれば、作業終了です。マネージメントコンソールから、各リソースを開いて、内容がテンプレートファイルのとおり作られているかチェックしてみましょう。

エラー時の調査方法

Deploy後、エラーが帰った場合は、CloudFormation画面でスタックを調べます。

「イベント」タブを選択し、画面上部にある「根本原因を検出」ボタンを押すと、直接的なエラー原因にジャンプしますので、内容を確認して対応を行っていきます。

ハマったところ

今回の作業中、実際にエラーになった内容を例に、解決方法を示します。

  1. 記述ミス

    • エラー内容

        Resource handler returned message: "Value (ap-northeast-1) for parameter 
        availabilityZone is invalid. Subnets can currently only be created in the following
        availability zones: ap-northeast-1a, ap-northeast-1c, ap-northeast-1d. 
        (Service: Ec2, Status Code: 400, Request ID: 6edca7c9-57c2-44d5-9b8e-6c2788239eb9)"
        (RequestToken: 88105dee-6a98-d76e-c820-5d3eb0d15dcc, HandlerErrorCode: InvalidRequest)
      
        <日本語訳>
        リソース ハンドラーからメッセージが返されました: "パラメーター availabilityZone の値
        (ap-northeast-1) が無効です。サブネットは現在、ap-northeast-1a、ap-northeast-1c、
         ap-northeast-1d のアベイラビリティーゾーンでのみ作成できます。
        (サービス:ec2、ステータスコード:400、リクエストID:6edca7c9-57c2-44d5-9b8e-6c2788239eb9)」
        (RequestToken:88105dee-6a98-d76e-c820-5d3eb0d15dcc、HandlerErrorCode:InvalidRequest)
      
    • 原因:サブネットのプロパティにあるAZの指定を間違えた(--;)

      • 正 : AvailabilityZone: ap-northeast-1a
      • 誤 : AvailabilityZone: ap-northeast-1
  2. 設定内容の誤り

    • エラー内容

        Resource handler returned message: "The routeTable ID 'rtb-0cd66f4d6d462c986|0.0.0.0/0'
        does not exist (Service: Ec2, Status Code: 400, Request ID: 693a27c9-0a5e-47a3-8227-f44493db069d)"
        (RequestToken: 191fd476-0e73-194b-3a1a-fcc398d6af26, HandlerErrorCode: NotFound)
      
        <日本語訳>
        リソース ハンドラーがメッセージを返しました: "routeTable ID 'rtb-0cd66f4d6d462c986|0.0.0.0/0' が
        存在しません (サービス: ec2、状態コード: 400、要求 ID: 693a27c9-0a5e-47a3-8227-f44493db069d)" (RequestToken: 191fd476-0e73-194b-3a1a-fcc398d6af26、
        HandlerErrorCode: NotFound)
      
    • 原因:SubnetRouteTableAssociationRouteTableId 属性にRouteのIdを指定していた

        cftPublicRtb:
          Type: AWS::EC2::RouteTable
          Properties:
            VpcId: !Ref cftVpc
            Tags:
              - Key: name
               Value: cf-tutorial-public-rtb
      
        cftPublicRoute:
          Type: AWS::EC2::Route
          Properties:
            RouteTableId: !Ref cftPublicRtb
            DestinationCidrBlock: 0.0.0.0/0
            GatewayId: !Ref cftIgw
      
        cftPublicRtAssoc:
          Type: AWS::EC2::SubnetRouteTableAssociation
          Properties:
            SubnetId: !Ref cftPublicSubnet
            RouteTableId: !Ref cftPublicRoute   # ← cftPublicRtb が正解
      
  3. 設定誤り(処理が終わらない)

まとめ

実際に操作してみたところ、思った以上に簡単に実装できることが分かりました。特にVisual Studio Codeスニペットのおかげで、ゼロから調べる必要がないのは助かります。

今回作成した内容は以下のGitHubにもアップしておきます。

github.com

次回は作成したVPCにEC2を構築します。EC2リソースの構築のほか、セキュリティグループの設定内容や、EC2のログイン鍵の管理などをどのように行うか、といった疑問も出ると思うので、そのあたりの解消方法を学びたいと思います。