AWS CloudFormationについて
AWS の CloudFormation は、インフラに必要なリソース(VPC や EC2 など)をテンプレートに定義して実行するだけで作成や削除をまとめてやってくれる、AWS専用の Infrastructure As Code 環境です。
オンプレミスのインフラ構築では機器購入や工事手配の前に綿密な設計や検証が不可欠ですが、AWS と CloudFormation を使えば、まず作ってみて評価しながら何度でもスクラップアンドビルドを繰り返すことができます。ウォーターフォールからアジャイルへの進化ですね。
テンプレートとスタック
1つのテンプレートで定義されたリソースの集合は「スタック」という単位で管理され、削除の操作をするとスタック内のリソースがまとめて削除されます。だから手作業での削除もれによる予定外の請求なんかもなくなります。
またテンプレートを修正してスタックを更新すれば、自動的に差分だけを追加/更新/削除してくれます。
テンプレートの例
CloudFormation のテンプレートは、以下のような YAML(またはJSON)で記述されたテキストファイルです。
AWSTemplateFormatVersion: '2010-09-09'
# 作成するリソースを定義するセクション
Resources:
MainVpc:
Type: 'AWS::EC2::VPC' # VPCを作成する
Properties:
CidrBlock: 172.16.0.0/16
これは CIDR が172.16.0.0/16
の VPC を1つだけ定義したスタックの例です。
スタックの分割
しかし自動とはいえ、毎回全部を削除したくない場合もあります。その場合はスタックを適度に分割することができます。
スタックを分割する理由
例えば Elastic IP(固定IPアドレス)は再作成するとアドレスが変わってしまいますし、サーバにアップロードしたファイルやログが毎回消えてしまいます。
またスタックを分割しないと、1つのテンプレートが際限なく巨大化してしまうという問題もあります。
ただ分割しただけでは困ること
以下はスタックに含まれるリソース間に依存関係がある例です。
Resources:
# 作成するVPCの定義
MainVpc: # VPC作成処理の戻り値として、変数'MainVpc'にVPCのIDが格納される
Type: 'AWS::EC2::VPC'
Properties:
CidrBlock: 172.16.0.0/16
# VPCの中に作成するサブネットの定義
MainSubnet:
Type: 'AWS::EC2::Subnet'
Properties:
CidrBlock: 172.16.1.0/24
VpcId: !Ref MainVpc # 上で作成したVPCのIDを設定する
サブネットを作成するためには親となる VCP を指定する必要があるので、この例ではMainVpc
という変数に格納された VPC の ID を、サブネットのVpcId
プロパティに設定しています。
このように、同じスタック中ならRef
という組み込み関数を使うことで変数に格納された値を参照することができます。
このスタックをそのまま VPC とサブネットに分割すると…
Resources:
MainVpc: # VPC作成処理の戻り値として、変数'MainVpc'にVPCのIDが格納される
Type: 'AWS::EC2::VPC'
Properties:
CidrBlock: 172.16.0.0/16
Resources:
MainSubnet:
Type: 'AWS::EC2::Subnet'
Properties:
CidrBlock: 172.16.1.0/24
VpcId: !Ref MainVpc # エラー!別のスタックの変数を参照できない
サブネットのスタック内ではVPC のMainVpc
変数を参照することができないために、サブネットの親となる VPC を指定することができません。
スタック間で値を受け渡す方法
固定値であれば両方のParameters:
セクションで同じ値を定義してもよいのですが、MainVpc
の値は VPC を作成する時に動的に割り当てられる ID なのでこの方法は使えません。
この場合は VPC のスタックを作成する時に ID の値をエクスポートしておけば、サブネットのスタックを作成する時に値をインポートすることができるようになります。
と言っても値を記憶するために記憶領域の用意や保存などの処理を自分でする必要はなく、エクスポートした変数の名前と値を CloudFormation が自動的に記憶してくれるようになっています。
Resources:
MainVpc: # VPC作成処理の戻り値として、変数'MainVpc'にVPCのIDが格納される
Type: 'AWS::EC2::VPC'
Properties:
CidrBlock: 172.16.0.0/16
# エクスポートするためのセクション
Outputs:
MainVpc:
Value: !Ref MainVpc # 作成されたVPCのIDを
Export:
Name: main-vpc-id # この名前でエクスポートする
Resources:
MainSubnet:
Type: 'AWS::EC2::Subnet'
Properties:
CidrBlock: 172.16.1.0/24
VpcId: {'Fn::ImportValue': 'main-vpc-id'} # 同じ名前でVPCのIDをインポートする
エクスポートした値をインポートするために組み込み関数のImportValue
を使用します。
もうちょっと現実的な例
少しだけ複雑な例として、以下の表のようにスタックを分割する場合を考えます。「インポートする値」に記載のあるスタックは、その値をエクスポートするスタックに依存することになります。
スタック | エクスポートする値 | インポートする値 |
---|---|---|
Elastic IP | (A) EIPのAllocationId
|
|
VPC とインターネットゲートウェイ | (B) VPCのID (C) インターネットゲートウェイのID |
|
公開用サブネットとルートテーブル | (D) サブネットのID | (B) VPCのID (C) インターネットゲートウェイのID |
公開用 EC2 インスタンスとセキュリティグループ | (A) EIPのAllocationId (B) VPCのID (D) サブネットのID |
なお、EC2 インスタンスへのアクセスに必要なキーペアはあらかじめコンソールで作成しておきます。
CloudFormation コンソールでスタックを作成する
以下はこれらの値を受け渡すスタックの実装例です。
各スタックのテンプレートを YAML ファイルに保存し、AWS CloudFormation のコンソールでファイルをアップロードすることでスタックを作成することができます。
Elastic IP
作成した Elastic IP からAllocationId
というプロパティを取得するためにGetAtt
という組み込み関数を使用します。
AWSTemplateFormatVersion: '2010-09-09'
Resources:
# Elastic IPを作成する
MainEip:
Type: "AWS::EC2::EIP"
Properties:
Domain: vpc
Outputs:
MainEipAllocationId:
# 組み込み関数GetAttを使用してAllocationIdプロパティを取得しエクスポートする
Value: !GetAtt MainEip.AllocationId
Export:
Name: 'main-eip-allocationid'
スタックの作成に成功すると、コンソールの「出力」タブでエクスポートされた値を確認することができます。
VPC とインターネットゲートウェイ
VPC とインターネットゲートウェイを作成して両者を関連付けてから、両方のIDをエクスポートします。
AWSTemplateFormatVersion: '2010-09-09'
Resources:
# VPCを作成する
MainVpc:
Type: 'AWS::EC2::VPC'
Properties:
CidrBlock: 172.16.0.0/16
Tags:
- Key: 'Name'
Value: 'main-vpc'
# インターネットゲートウェイを作成する
MainInetGateway:
Type: 'AWS::EC2::InternetGateway'
Properties:
Tags:
- Key: 'Name'
Value: 'main-igw'
# 作成したVPCとインターネットゲートウェイを関連付ける
MainVpcGatewayAttachment:
Type: 'AWS::EC2::VPCGatewayAttachment'
Properties:
InternetGatewayId: !Ref MainInetGateway
VpcId: !Ref MainVpc
Outputs:
# 作成したVPCのIDをエクスポートする
MainVpc:
Value: !Ref MainVpc
Export:
Name: 'main-vpc-id'
# 作成したインターネットゲートウェイのIDをエクスポートする
MainInetGateway:
Value: !Ref MainInetGateway
Export:
Name: 'main-igw-id'
作成した VPC とインターネットゲートウェイの ID がエクスポートされました。
- AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::VPC
- AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::InternetGateway
- AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::VPCGatewayAttachment
公開用サブネットとルートテーブル
サブネット自体は、CIDR とインポートした VPC ID を設定するだけで作成することができます。EC2 インスタンスのスタックで必要になるので、サブネットの ID をエクスポートします。
ルートテーブルは少し複雑で、以下のような構成になります。
- ルートテーブル本体を、インポートした VPC ID を設定して作成する
- ルートテーブルのデフォルトルート(インターネットへの出口)を作成し、インポートしたインターネットゲートウェイ ID をターゲットに設定する
- サブネットとルートテーブルを関連付ける
AWSTemplateFormatVersion: '2010-09-09'
Resources:
# サブネットを作成する
PublicSubnet:
Type: 'AWS::EC2::Subnet'
Properties:
CidrBlock: 172.16.1.0/24
# このサブネット内のEC2起動時にパブリックIPアドレスを割り当てる
MapPublicIpOnLaunch: true
VpcId: {'Fn::ImportValue': 'main-vpc-id'} # VPCのIDをインポート
Tags:
- Key: 'Name'
Value: 'public-subnet'
# ルートテーブルを作成する
PublicRouteTable:
Type: 'AWS::EC2::RouteTable'
Properties:
VpcId: {'Fn::ImportValue': 'main-vpc-id'} # VPCのIDをインポート
Tags:
- Key: 'Name'
Value: 'public-rtb'
# ルートテーブルのデフォルトルート(インターネットへの出口)を作成する
PublicDefaultRoute:
Type: 'AWS::EC2::Route'
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
# インターネットゲートウェイのIDをインポート
GatewayId: {'Fn::ImportValue': 'main-igw-id'}
# ルートテーブルとサブネットを関連付ける
PublicSubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet
RouteTableId: !Ref PublicRouteTable
Outputs:
# 作成したサブネットのIDをエクスポートする
PublicSubnet:
Value: !Ref PublicSubnet
Export:
Name: !Sub 'public-subnet-id'
作成したサブネットとルートテーブルが関連付けられ、デフォルトの送信先0.0.0.0/0
のターゲットとしてインポートしたインターネットゲートウェイのIDが設定されました。
- AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::Subnet
- AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::RouteTable
- AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::Route
- AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::SubnetRouteTableAssociation
公開用 EC2 インスタンスとセキュリティグループ
公開用サーバーとして固定IPアドレスを持ち、インターネットからアクセス可能な EC2 インスタンスを作成するために、最初に作成した EIP(固定IPアドレス)を関連付けます。
また HTTP/HTTPS 用の受信ルールを持つセキュリティグループ(ファイヤーウォール)を作成して関連付けます。
AWSTemplateFormatVersion: '2010-09-09'
# パラメータを定義するセクション
Parameters:
# EC2のキーペアの名前を定義し、スタック作成時にコンソールで指定する
Ec2KeyName:
Type: 'AWS::EC2::KeyPair::KeyName'
Resources:
# 公開用EC2インスタンスを作成する
PublicEc2Instance:
Type: 'AWS::EC2::Instance'
Properties:
ImageId: ami-00d101850e971728d
InstanceType: 't2.micro'
KeyName: !Ref Ec2KeyName
# ストレージを作成する
BlockDeviceMappings:
- DeviceName: '/dev/xvda'
Ebs:
VolumeType: 'gp2'
VolumeSize: 8
# ネットワークインタフェースとして、下で作成するENIを設定する
NetworkInterfaces:
- NetworkInterfaceId: !Ref PublicEni
DeviceIndex: '0'
Tags:
- Key: 'Name'
Value: 'public-ec2-instance'
# 公開用Elastic Network Interface (ENI) を作成する
PublicEni:
Type: 'AWS::EC2::NetworkInterface'
Properties:
# 下で作成するセキュリティグループ(ファイヤーウォール)を設定する
GroupSet:
- !Ref PublicSecurityGroup
PrivateIpAddress: 172.16.1.4
# インポートした公開用サブネットのIDを設定する
SubnetId: {'Fn::ImportValue': 'public-subnet-id'}
Tags:
- Key: 'Name'
Value: 'public-eni'
- Key: 'Interface'
Value: 'eth0'
# 作成したENIにインポートしたEIP(固定IPアドレス)を関連付ける
PublicEIPAssociation1:
Type: AWS::EC2::EIPAssociation
Properties:
AllocationId: {'Fn::ImportValue': 'main-eip-allocationid'}
NetworkInterfaceId: !Ref PublicEni
# セキュリティグループを作成する
PublicSecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupName: public-sg
GroupDescription: "SecurityGroup for Public EC2 instances"
# インバウンドのルールを作成する
SecurityGroupIngress:
# インターネットからのHTTP通信を受信する
- IpProtocol: tcp
FromPort: '80'
ToPort: '80'
CidrIp: 0.0.0.0/0
# インターネットからのHTTPS通信を受信する
- IpProtocol: tcp
FromPort: '443'
ToPort: '443'
CidrIp: 0.0.0.0/0
Tags:
- Key: 'Name'
Value: 'public-sg'
VpcId: {'Fn::ImportValue': 'main-vpc-id'} # VPCのIDをインポートして設定する
CloudFormation のコンソールでスタックを作成する時に、作成済みのキーペアをパラメータとして指定します。
スタックの作成が完了すると、EC2 インスタンスと Elastic IP の関連付け、およびインターネットからの HTTP/HTTPS トラフィックを許可するセキュリティグループが作成されました。
- AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::Instance
- AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::NetworkInterface
- AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::EIPAssociation
- AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::SecurityGroup
あとがき
AWS 自体の複雑な仕組みと CloudFormation の理解を同時に進めるのは大変ではありますが、良質な情報がとにかく豊富であることは大変ありがたいです。
参考: ユーザーガイド: AWS CloudFormation とは
時にはリソースどうしの複雑な関係を整理して理解するために、クラス図を作ったりしました。
AWSを始めたらVPCとEC2の複雑さに悩んだのでクラス図にしてみた
さらに少しずつでも実際に構築して動きを体感することで理解を進めるために、CloudFormation を使ったスクラップアンドビルドが大変有効だと感じたので、今回の記事を作成しました。