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のアカウントを作成の上で、次の準備が必要となります。
- AWS CLIをローカル環境にインストールする。
- AWSコンソールで IAM サービスを開き、CLIを使用するアカウントとアクセスキーを作成する
- ローカル環境でコマンドプロンプトから
aws configure
をコマンドを実行し、リージョン・アクセスキーを設定する。
ここでは詳細は割愛します。↓の公式ページを参照ください。
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
には、リソースに応じた各種の設定内容を記述します。ここではVPCのIPアドレス(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 cftVpc
はVpcId
パラメータに、前述の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/0
と GatewayId: !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画面でスタックを調べます。
「イベント」タブを選択し、画面上部にある「根本原因を検出」ボタンを押すと、直接的なエラー原因にジャンプしますので、内容を確認して対応を行っていきます。
ハマったところ
今回の作業中、実際にエラーになった内容を例に、解決方法を示します。
記述ミス
エラー内容
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
- 正 :
設定内容の誤り
エラー内容
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)
原因:
SubnetRouteTableAssociation
のRouteTableId
属性に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 が正解
設定誤り(処理が終わらない)
エラー内容
Resource handler returned message: "Exceeded attempts to wait" (RequestToken: 6aff00a4-3536-a3d6-ec63-54d73f6ac3ac, HandlerErrorCode: NotStabilized) <日本語訳> リソース ハンドラーがメッセージを返しました: "待機の試行回数を超えました" (RequestToken: 6aff00a4-3536-a3d6-ec63-54d73f6ac3ac、HandlerErrorCode: NotStabilized)
原因:タイムアウトした → NATを指すRouteタグにおいて、NATをNatGatewayIdではなく、GatewayIdに指定していた。(以下のサイト参考)
CloudFormationでルートテーブルを構築する際にGatewayIdとNATGatewayIdを間違えると、スタック作成が終わらないことがある
まとめ
実際に操作してみたところ、思った以上に簡単に実装できることが分かりました。特にVisual Studio Codeのスニペットのおかげで、ゼロから調べる必要がないのは助かります。
今回作成した内容は以下のGitHubにもアップしておきます。
次回は作成したVPCにEC2を構築します。EC2リソースの構築のほか、セキュリティグループの設定内容や、EC2のログイン鍵の管理などをどのように行うか、といった疑問も出ると思うので、そのあたりの解消方法を学びたいと思います。