Edited at

CloudFormationウォークスルーにYAMLで入門してみる

More than 1 year has passed since last update.

AWSリソースの構築を自動化できるCloud Formationについて学んで見ます.

まずは既存のVPC内にSG切ってインスタンス立てて...ぐらいができればいいんだけど...と思っていたのですが、

ウォークスルー: スタックの更新

がある程度まとまってたので、とりあえず走らせて見て、何が起きてるかを学んでくことにしようと思います.


下準備: JSON => YAMLへの変換

このウォークスルーではスタック(CloudFormationで構築されるひとかたまり)を作成したのち、それを更新していく…というチュートリアルですが、一番上のセクション「単純なアプリケーション」に下地となるテンプレートが記載されているのでそれをコピーして進める…

前にこれをJSONからYAMLへ変換します (JSONしんどいため&コメントつけられるようにするため)

AWS CloudFormation Template FlipというツールがAWSから公開されているので、pipでインストールします.

pip install cfn-flip

次に下地となる、「単純なアプリケーション」セクションの一番下にある、長ーいJSONをwalkthrough01.jsonなど適当な名前でローカルに保存して、下記のコマンドを実行します.

cfn-flip -c walkthrough01.json walkthrough01.yaml

するとYAMLへ変換されたファイルが生成されます.

この際、通常のJSON => YAMLの変換だけでなく、組み込み関数宣言部Fn::が短縮形!に変換されます. (3文字減るだけですが...ドキュメントをでは短縮形が併記されています)

また、-c, --cleanオプションをつけると、Fn::Join からFn::Subへと書き換えてくれます. 文字列を連結する際[]で括らなくても良くなるようで、見やすかったのでこちらで進めていきます.

(最初は関数って何してんのと思ったのですが、ユーザーの入力受け取ったり文字列を結合したりできる、といった感じなのでひとまずそのまま進めれば大丈夫です. )

生成されたテンプレートはこんな感じです(長いですねー...)


walkthrough01.yaml

AWSTemplateFormatVersion: '2010-09-09'

Description: 'AWS CloudFormation Sample Template: Sample template that can be used
to test EC2 updates. **WARNING** This template creates an Amazon Ec2 Instance. You
will be billed for the AWS resources used if you create a stack from this template.'
Parameters:
InstanceType:
Description: WebServer EC2 instance type
Type: String
Default: m1.small
AllowedValues:
- t1.micro
- t2.micro
- t2.small
- t2.medium
- m1.small
- m1.medium
- m1.large
- m1.xlarge
- m2.xlarge
- m2.2xlarge
- m2.4xlarge
- m3.medium
- m3.large
- m3.xlarge
- m3.2xlarge
- c1.medium
- c1.xlarge
- c3.large
- c3.xlarge
- c3.2xlarge
- c3.4xlarge
- c3.8xlarge
- g2.2xlarge
- r3.large
- r3.xlarge
- r3.2xlarge
- r3.4xlarge
- r3.8xlarge
- i2.xlarge
- i2.2xlarge
- i2.4xlarge
- i2.8xlarge
- hi1.4xlarge
- hs1.8xlarge
- cr1.8xlarge
- cc2.8xlarge
- cg1.4xlarge
ConstraintDescription: must be a valid EC2 instance type.
Mappings:
AWSInstanceType2Arch:
t1.micro:
Arch: PV64
t2.micro:
Arch: HVM64
t2.small:
Arch: HVM64
t2.medium:
Arch: HVM64
m1.small:
Arch: PV64
m1.medium:
Arch: PV64
m1.large:
Arch: PV64
m1.xlarge:
Arch: PV64
m2.xlarge:
Arch: PV64
m2.2xlarge:
Arch: PV64
m2.4xlarge:
Arch: PV64
m3.medium:
Arch: HVM64
m3.large:
Arch: HVM64
m3.xlarge:
Arch: HVM64
m3.2xlarge:
Arch: HVM64
c1.medium:
Arch: PV64
c1.xlarge:
Arch: PV64
c3.large:
Arch: HVM64
c3.xlarge:
Arch: HVM64
c3.2xlarge:
Arch: HVM64
c3.4xlarge:
Arch: HVM64
c3.8xlarge:
Arch: HVM64
g2.2xlarge:
Arch: HVMG2
r3.large:
Arch: HVM64
r3.xlarge:
Arch: HVM64
r3.2xlarge:
Arch: HVM64
r3.4xlarge:
Arch: HVM64
r3.8xlarge:
Arch: HVM64
i2.xlarge:
Arch: HVM64
i2.2xlarge:
Arch: HVM64
i2.4xlarge:
Arch: HVM64
i2.8xlarge:
Arch: HVM64
hi1.4xlarge:
Arch: HVM64
hs1.8xlarge:
Arch: HVM64
cr1.8xlarge:
Arch: HVM64
cc2.8xlarge:
Arch: HVM64
AWSRegionArch2AMI:
us-east-1:
PV64: ami-50842d38
HVM64: ami-08842d60
HVMG2: ami-3a329952
us-west-2:
PV64: ami-af86c69f
HVM64: ami-8786c6b7
HVMG2: ami-47296a77
us-west-1:
PV64: ami-c7a8a182
HVM64: ami-cfa8a18a
HVMG2: ami-331b1376
eu-west-1:
PV64: ami-aa8f28dd
HVM64: ami-748e2903
HVMG2: ami-00913777
ap-southeast-1:
PV64: ami-20e1c572
HVM64: ami-d6e1c584
HVMG2: ami-fabe9aa8
ap-northeast-1:
PV64: ami-21072820
HVM64: ami-35072834
HVMG2: ami-5dd1ff5c
ap-southeast-2:
PV64: ami-8b4724b1
HVM64: ami-fd4724c7
HVMG2: ami-e98ae9d3
sa-east-1:
PV64: ami-9d6cc680
HVM64: ami-956cc688
HVMG2: NOT_SUPPORTED
cn-north-1:
PV64: ami-a857c591
HVM64: ami-ac57c595
HVMG2: NOT_SUPPORTED
eu-central-1:
PV64: ami-a03503bd
HVM64: ami-b43503a9
HVMG2: ami-b03503ad
Resources:
WebServerInstance:
Type: AWS::EC2::Instance
Metadata:
Comment: Install a simple PHP application
AWS::CloudFormation::Init:
config:
packages:
yum:
httpd: []
php: []
files:
/var/www/html/index.php:
content: '<?php

echo ''<h1>AWS CloudFormation sample PHP application</h1>'';

?>

'
mode: '000644'
owner: apache
group: apache
/etc/cfn/cfn-hup.conf:
content: !Sub '[main]

stack=${AWS::StackId}

region=${AWS::Region}

'
mode: '000400'
owner: root
group: root
/etc/cfn/hooks.d/cfn-auto-reloader.conf:
content: !Sub '[cfn-auto-reloader-hook]

triggers=post.update

path=Resources.WebServerInstance.Metadata.AWS::CloudFormation::Init

action=/opt/aws/bin/cfn-init -s ${AWS::StackId} -r WebServerInstance --region ${AWS::Region}

runas=root

'
services:
sysvinit:
httpd:
enabled: 'true'
ensureRunning: 'true'
cfn-hup:
enabled: 'true'
ensureRunning: 'true'
files:
- /etc/cfn/cfn-hup.conf
- /etc/cfn/hooks.d/cfn-auto-reloader.conf
Properties:
ImageId: !FindInMap [AWSRegionArch2AMI, !Ref 'AWS::Region', !FindInMap [AWSInstanceType2Arch,
!Ref 'InstanceType', Arch]]
InstanceType: !Ref 'InstanceType'
SecurityGroups:
- !Ref 'WebServerSecurityGroup'
UserData: !Base64
Fn::Sub: '#!/bin/bash -xe

yum install -y aws-cfn-bootstrap

# Install the files and packages from the metadata

/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource
WebServerInstance --region ${AWS::Region}

# Start up the cfn-hup daemon to listen for changes to the Web Server metadata

/opt/aws/bin/cfn-hup || error_exit ''Failed to start cfn-hup''

# Signal the status from cfn-init

/opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource
WebServerInstance --region ${AWS::Region}

'
CreationPolicy:
ResourceSignal:
Timeout: PT5M
WebServerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Enable HTTP access via port 80
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: '80'
ToPort: '80'
CidrIp: 0.0.0.0/0
Outputs:
WebsiteURL:
Description: Application URL
Value: !Sub 'http://${WebServerInstance.PublicDnsName}'



起動

ともあれ、生成されたテンプレートを元に「初期スタックの作成」を参考にしてCloudFormationをマネジメントコンソールから起動して見ます.

するとデフォルトVPC内にインスタンスが立ち上がっていることが確認できるかと思います.

CloudFormationコンソールの[Output]タブに表示されたアドレスへアクセスすると、AWS CloudFormation sample PHP applicationというメッセージが表示されているかと思います.


テンプレートを読む

一旦起動し終わったところで、どんな構造になっているか読んでいきたいと思います.

長いようでやってることはシンプルなテンプレートなので、そんに項目数は多くないかと思います.

YAMLの最上位には


  • AWSTemplateFormatVersion

  • Description

  • Parameters

  • Mappings

  • Resources

  • Outputs

が並んでいました. それぞれ、

項目
内容

AWSTemplateFormatVersion
テンプレートフォーマットのバージョン、基本固定?

Description
テンプレートの説明文

Parameters
CloudFormation実行時にユーザーが与えるパラメーター

Mappings
与えらた条件に応じて変更される値の定義

Resources
起動されるAWSリソースの設定

Outputs
起動完了後に出力されるメッセージ

といった感じです. Version, Description, Outputは簡単そうなので脇に置いて、残りについて見ていきたいと思います.


Parameters

実行するユーザーが与えるパラメータです.

テンプレートでは下記の通り.

Parameters:

InstanceType: # 項目名
Description: WebServer EC2 instance type # 説明
Type: String # 入力される値の型
Default: m1.small # デフォルト値
AllowedValues: # 受け付ける値
- t1.micro
# ...
# 中略
# ...
ConstraintDescription: must be ... # 入力値違反時のエラーメッセージ

これが反映されるとCloudFormation起動画面ではこんな感じになります.

Screen Shot 2017-09-07 at 11.42.15.png

Prameters 以下にセレクトボックスができているのがわかります.

AllowedValueの他にも、AllowedPatternで正規表現で入力値を規定することができます(SSHアクセスを許可するIPアドレスレンジを入力させたいときなど)


Mappings

CloudFormationが実行される条件に応じて、この次のResourcesを変更したいときに利用するための値のマッピングです.

Mappings:

AWSInstanceType2Arch:
t1.micro:
Arch: PV64
# ...
# 中略
# ...
AWSRegionArch2AMI:
us-east-1:
PV64: ami-50842d38
HVM64: ami-08842d60
HVMG2: ami-3a329952
# ...
# 後略
# ...

今回はParameterでユーザーから与えられたインスタンスタイプに応じて仮想化の方式を決定したり、実行されたリージョン(+インスタンスタイプ)に応じて選択するAMIを決定しています.

(ParameterとMappingがこのサンプルテンプレートを長くしちゃってる原因、ですね...)


Resources

CloudFormationの本体部分になる、起動されるリソースの設定です.

Resources:

WebServerInstance:
# ...
# 中略
# ...
WebServerSecurityGroup:
# ...
# 後略
# ...

と今回はEC2インスタンスとセキュリティグループの2つになります.

ここではEC2の設定について、読んでいこうと思います.

(VPCとサブネットは指定しないとデフォルトが選択される模様)


EC2

ここが一番重たい...気がします.

  WebServerInstance:          # 項目名

Type: AWS::EC2::Instance # リソースタイプ
Metadata: # 追加コメントの記載や起動スクリプトの定義や
# ...
# 中略
# ...
Properties: # EC2自身の起動パラメータ
# ...
# 中略
# ...
CreationPolicy: # CloudFormation側とのやりとりの設定
# ...
# 後略
# ...

と大きく分けて4つ、要素があります.

Typeはそのままであり、CreationPolicyはこのテンプレートではシンプルなので、ここではMetadataとPropatiesの2つについて見ていきます.


Metadata

    Metadata:

Comment: Install a simple PHP application
AWS::CloudFormation::Init: # 起動スクリプトの設定
config:
packages: # 利用するパッケージ、今回はyumでapacheとphpをインストール
yum:
httpd: []
php: []
files: # 設置する設定ファイル類
/var/www/html/index.php: # デモ用phpファイル
content: # 略
mode: '000644'
owner: apache
group: apache
/etc/cfn/cfn-hup.conf: # Stackを通じてEC2のアップデートをするための設定
content: !Sub '[main] # !Subで文字列の結合

stack=${AWS::StackId} # Stack IDの取得

region=${AWS::Region} # リージョンの取得

'
mode: '000400'
owner: root
group: root
/etc/cfn/hooks.d/cfn-auto-reloader.conf: # 同じくアップデートのための設定
content: # 略
services:
sysvinit:
httpd:
enabled: 'true'
ensureRunning: 'true'
cfn-hup:
enabled: 'true'
ensureRunning: 'true'
files:
- /etc/cfn/cfn-hup.conf
- /etc/cfn/hooks.d/cfn-auto-reloader.conf

ドキュメントにあるように、実装の詳細をコメントとして含めたり、AWS::CloudFormation::Initを利用した起動スクリプトの設定ができる箇所になります。

AWS::CloudFormation::Initではyumなどによるインストール、設定ファイルの配置、起動するサービスの選択ができます. 今回はApacheと、CloudFormationスタックを通してEC2の方に変更を反映するためのエージェント(cfn-hup)の設定をしている形になります. ウォークスルーを進めると早速index.phpファイルを変更しスタックをアップデートするのですが、その際CloudFormation側からインスタンス内のindex.phpファイルを触りに行くのにこれを通じて行う、ようです.

参考: cfn-hup

また!Subという組み込み関数を用いて文字列の連結を行なっています.


Properties

EC2本体の設定値やスクリプトの実行を行ないます.

    Properties:

ImageId: !FindInMap [AWSRegionArch2AMI, !Ref 'AWS::Region', !FindInMap [AWSInstanceType2Arch,
!Ref 'InstanceType', Arch]] # AMIのID取得、Mappingsで設定した値を利用
InstanceType: !Ref 'InstanceType'
SecurityGroups:
- !Ref 'WebServerSecurityGroup'
UserData: !Base64 # スクリプト実行
Fn::Sub: '#!/bin/bash -xe

yum install -y aws-cfn-bootstrap

# Metadataからのファイルとパッケージのインストール

/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource
WebServerInstance --region ${AWS::Region}

# cfn-hup daemon の起動とStackの変更のリッスン開始

/opt/aws/bin/cfn-hup || error_exit ''Failed to start cfn-hup''

# インスタンスの状態をCloudFormation側へ通知

/opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource
WebServerInstance --region ${AWS::Region}

'

!RefでParameterで指定された値を受け取り、!FindInMapでMappingで設定した値を取得し、EC2の設定パラメータを指定しています. (こうすることでどのリージョンでも起動可能)

また、UserDataにCloudFormation起動時に実行するスクリプトを渡しています. Metadataで記載したファイルやパッケージもここで読み込まれ、インストールが実行されます.

(UserData側に全てインストール項目記載しても良さそうですが、後からスタックを通しての変更ができない、テンプレートの見通しが悪くなる等々デメリットがあるのだと思います.


(追記)改行など整えたもの

自動変換の際、改行を表現するのに1行あける、ということがされていましたが、YAMLでは|が置かれた後の属性は改行を評価する、ということになっているそうです.

これを用いるともう少しスッキリ書けます

リソースのプロパティの追加のSSHによるアクセス追加の部分を加えてあります.

AWSTemplateFormatVersion: '2010-09-09'

Description: 'AWS CloudFormation Sample Template: Sample template that can be used
to test EC2 updates. **WARNING** This template creates an Amazon Ec2 Instance. You
will be billed for the AWS resources used if you create a stack from this template.'
Parameters:
InstanceType:
Description: WebServer EC2 instance type
Type: String
Default: m1.small
AllowedValues:
- t1.micro
- t2.micro
- t2.small
- t2.medium
- m1.small
- m1.medium
- m1.large
- m1.xlarge
- m2.xlarge
- m2.2xlarge
- m2.4xlarge
- m3.medium
- m3.large
- m3.xlarge
- m3.2xlarge
- c1.medium
- c1.xlarge
- c3.large
- c3.xlarge
- c3.2xlarge
- c3.4xlarge
- c3.8xlarge
- g2.2xlarge
- r3.large
- r3.xlarge
- r3.2xlarge
- r3.4xlarge
- r3.8xlarge
- i2.xlarge
- i2.2xlarge
- i2.4xlarge
- i2.8xlarge
- hi1.4xlarge
- hs1.8xlarge
- cr1.8xlarge
- cc2.8xlarge
- cg1.4xlarge
ConstraintDescription: must be a valid EC2 instance type.
KeyName:
Description: Name of an existing Amazon EC2 key pair for SSH access
Type: AWS::EC2::KeyPair::KeyName
SSHLocation:
Description: The IP address range that can be used to SSH to the EC2 instances
Type: String
MinLength: 9
MaxLength: 18
Default: 192.168.0.1/32
AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})
ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x.
Mappings:
AWSInstanceType2Arch:
t1.micro:
Arch: PV64
t2.micro:
Arch: HVM64
t2.small:
Arch: HVM64
t2.medium:
Arch: HVM64
m1.small:
Arch: PV64
m1.medium:
Arch: PV64
m1.large:
Arch: PV64
m1.xlarge:
Arch: PV64
m2.xlarge:
Arch: PV64
m2.2xlarge:
Arch: PV64
m2.4xlarge:
Arch: PV64
m3.medium:
Arch: HVM64
m3.large:
Arch: HVM64
m3.xlarge:
Arch: HVM64
m3.2xlarge:
Arch: HVM64
c1.medium:
Arch: PV64
c1.xlarge:
Arch: PV64
c3.large:
Arch: HVM64
c3.xlarge:
Arch: HVM64
c3.2xlarge:
Arch: HVM64
c3.4xlarge:
Arch: HVM64
c3.8xlarge:
Arch: HVM64
g2.2xlarge:
Arch: HVMG2
r3.large:
Arch: HVM64
r3.xlarge:
Arch: HVM64
r3.2xlarge:
Arch: HVM64
r3.4xlarge:
Arch: HVM64
r3.8xlarge:
Arch: HVM64
i2.xlarge:
Arch: HVM64
i2.2xlarge:
Arch: HVM64
i2.4xlarge:
Arch: HVM64
i2.8xlarge:
Arch: HVM64
hi1.4xlarge:
Arch: HVM64
hs1.8xlarge:
Arch: HVM64
cr1.8xlarge:
Arch: HVM64
cc2.8xlarge:
Arch: HVM64
AWSRegionArch2AMI:
us-east-1:
PV64: ami-50842d38
HVM64: ami-08842d60
HVMG2: ami-3a329952
us-west-2:
PV64: ami-af86c69f
HVM64: ami-8786c6b7
HVMG2: ami-47296a77
us-west-1:
PV64: ami-c7a8a182
HVM64: ami-cfa8a18a
HVMG2: ami-331b1376
eu-west-1:
PV64: ami-aa8f28dd
HVM64: ami-748e2903
HVMG2: ami-00913777
ap-southeast-1:
PV64: ami-20e1c572
HVM64: ami-d6e1c584
HVMG2: ami-fabe9aa8
ap-northeast-1:
PV64: ami-21072820
HVM64: ami-35072834
HVMG2: ami-5dd1ff5c
ap-southeast-2:
PV64: ami-8b4724b1
HVM64: ami-fd4724c7
HVMG2: ami-e98ae9d3
sa-east-1:
PV64: ami-9d6cc680
HVM64: ami-956cc688
HVMG2: NOT_SUPPORTED
cn-north-1:
PV64: ami-a857c591
HVM64: ami-ac57c595
HVMG2: NOT_SUPPORTED
eu-central-1:
PV64: ami-a03503bd
HVM64: ami-b43503a9
HVMG2: ami-b03503ad
Resources:
WebServerInstance:
Type: AWS::EC2::Instance
Metadata:
Comment: Install a simple PHP application
AWS::CloudFormation::Init:
config:
packages:
yum:
httpd: []
php: []
files:
/var/www/html/index.php:
content: |
<?php
echo '<h1>AWS CloudFormation sample PHP application</h1>';
echo 'Updated virsions via UpdateStack';
?>
mode: '000644'
owner: apache
group: apache
/etc/cfn/cfn-hup.conf:
content: !Sub |
[main]
stack=${AWS::StackId}
region=${AWS::Region}
mode: '000400'
owner: root
group: root
/etc/cfn/hooks.d/cfn-auto-reloader.conf:
content: !Sub |
[cfn-auto-reloader-hook]
triggers=post.update
path=Resources.WebServerInstance.Metadata.AWS::CloudFormation::Init
action=/opt/aws/bin/cfn-init -s ${AWS::StackId} -r WebServerInstance --region ${AWS::Region}
runas=root
services:
sysvinit:
httpd:
enabled: 'true'
ensureRunning: 'true'
cfn-hup:
enabled: 'true'
ensureRunning: 'true'
files:
- /etc/cfn/cfn-hup.conf
- /etc/cfn/hooks.d/cfn-auto-reloader.conf
Properties:
ImageId: !FindInMap [AWSRegionArch2AMI, !Ref 'AWS::Region', !FindInMap [AWSInstanceType2Arch,
!Ref 'InstanceType', Arch]]
InstanceType: !Ref 'InstanceType'
SecurityGroups:
- !Ref 'WebServerSecurityGroup'
KeyName: !Ref 'KeyName'
UserData: !Base64
Fn::Sub: |
#!/bin/bash -xe
yum install -y aws-cfn-bootstrap
# Install the files and packages from the metadata
/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource WebServerInstance --region ${AWS::Region}
# Start up the cfn-hup daemon to listen for changes to the Web Server metadata
/opt/aws/bin/cfn-hup || error_exit ''Failed to start cfn-hup''
# Signal the status from cfn-init
/opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource WebServerInstance --region ${AWS::Region}
CreationPolicy:
ResourceSignal:
Timeout: PT5M
WebServerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Enable HTTP access via port 80
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: '80'
ToPort: '80'
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: '22'
ToPort: '22'
CidrIp: !Ref 'SSHLocation'
Outputs:
WebsiteURL:
Description: Application URL
Value: !Sub 'http://${WebServerInstance.PublicDnsName}'

phpのところで、''で括られていた箇所を'に直していますが、これもYAMLの規定で、文字列中でシングルクォーテーションのエスケープには2つ連ねる、というのを変換のスクリプトが利用していたからでした.


なかなか手が出せなかったCloudFormationだったのですが、ウォークスルーの一番最初の部分について、ドキュメント読み読み進めて見ました.


ウォークスルーでいきなりこの長さはちょっとしんどいですね...