CloudFormation入門 – ハマリポイント・注意点
こんにちは。インフラ担当のいせです。
弊社が運用しているサービスのなかのいくつかはAWS上で動いています。
当初はWebコンソールでリソースの作成や設定変更を行っていたのですが、だんだん規模が大きくなるなかで手作業での設定には限界を感じ、CloudFormation(以下CFN)を利用することにしました。
今回はCFNの社内向け勉強会の資料をブログとしてまとめてみます。
CFNとは
- CFNはyamlなどでAWSの設定内容を記述したテンプレートを元に、リソース構築や設定変更を行ってくれるサービスです。
ざっくりした使い方
新規作成
- テンプレートを作成します。(テンプレートの詳しい話は後述)
- Webコンソールにログインして、CFNの管理画面を開きます。
- 「スタックの作成」をクリックします。
- テンプレートをアップロードして、「次へ」をクリックします。
- スタックの名前とテンプレートに応じてパラメータを入力して「次へ」をクリックします。
- スタックオプションの設定をする画面が表示されますが、とりあえずはいじらなくて問題ないので、再び「次へ」をクリックします。
- 確認画面が表示されるので、内容に問題がなければ「スタックの作成」をクリックします。
設定変更
- 新規作成時に使用したテンプレートをもとに、変更したい内容を反映したテンプレートを作成します。
- Webコンソールにログインして、CFNの管理画面を開きます。
- 変更したいスタックを選択し、「更新する」をクリックします。
- テンプレートをアップロードして、「次へ」をクリックします。
- 必要に応じてパラメータを修正し、「次へ」をクリックします。
- スタックオプションの設定をする画面が表示されますが、とりあえずはいじらなくて問題ないので、再び「次へ」をクリックします。
- 確認画面が表示されるので、内容に問題がなければ「実行」をクリックします。
スタックについて
- テンプレートをyamlで作成したら、Webコンソールなどで上記のようにテンプレートをアップロードしてリソースを作成することができます。
- ひとつのテンプレートから作成されたリソースをまとめてスタックといいます。
- スタックの中にテンプレートに定義されたリソースが所属しているようなイメージです。
- 通常は1つのテンプレートから1つのスタックが生まれます。
- 可読性のためにテンプレートは複数のファイルに分割するがスタックは一つにだけ、というようなこともできます。
- 弊社の場合はリソースのライフサイクルやリソースを作成するのに必要な権限ごとにテンプレートとスタックを分割しています。
- 例えば以下のようにスタックを分割します。
- IAMのスタック
- DBのスタック
- アプリ用のスタック
- IAMのスタックはCFNの実行に高権限が必要になるため、DBのスタックは他のリソースの設定変更時に間違ってDBの設定が変わらないようにするため、などの理由でスタックを分割しています。
- スタックの分割にはデメリットもあります。
- 同じスタックの中であればCFNが依存関係を解決して、同時並行でリソース構築を実行できるものは並行して構築されるため時間が短縮されるが、スタックを分割するとそれがやりにくくなる。
- スタックをまたいで
!Ref
などで値を参照するのが大変(後述)。
テンプレートの書き方
まず最初にやるべきこと
- お使いのテキストエディタにCFNの構文チェックプラグインを追加してください。
- 詳しい話は「エディタ名 CloudFormation 構文チェック」などのキーワードで検索してください。
- 罠がたくさんあるのでこれがないとかなり辛いです。
- 私はvimmerなので、ALE + cnf-python-lint というツールを使っています。
テンプレートの例: VPNの中にSubnetを一つ作る
AWSTemplateFormatVersion: '2010-09-09'
Resources:
vpc: # 1.
Type: AWS::EC2::VPC # 2.
Properties: # 3.
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
InstanceTenancy: default
Tags:
- Key: Name
Value: vpc
subnetPublicA: # 1.
Type: AWS::EC2::Subnet # 2.
Properties: # 3.
CidrBlock: 10.0.0.0/24
VpcId: !Ref vpc
AvailabilityZone: ap-northeast-1a
Tags:
- Key: Name
Value: subnetPublicA
AWSTemplateFormatVersion
- 決まり文句です。
- いまのところ(2020年9月時点)、他のバージョンはありません。
Resourcesセクション
- AWS上に作成するリソースを定義します。
- リソースの定義でよく設定する属性は以下の4種類です。
- リソースの名前
- Type
- Properties
- Condition(必須ではない)
1. リーソスの名前
- Resouces直下に、まず「リソースの名前」を指定します。
- 上記の例だと
vpc
やsubnetPublicA
などです。 - 使用可能な文字は半角英数字のみで記号不可。
- 「リソースの名前」はARNやWebコンソール上で表示される名前とはまったく関係ない、CFN内でのみ使用される名前です。
- Webコンソールで表示される名前は実はKeyが
Name
というタグになっているケースが多いです(全てではない、IAMなど)、
- Webコンソールで表示される名前は実はKeyが
- 「リソースの名前」と組み込み関数を使用すると、テンプレートの中で別のリソースのIDなどのパラメータを動的に設定することができます。
- 例えば、上記の例の中では18行目に
!Ref vpc
と記載されていますが、CFN実行時にこの部分はvpc
のVPC IDに置換されます。 - VPCであれば
!Ref
でVPC IDに置換されますが、リソースによってはARNに置換されたり、名前に置換されたりと挙動がバラバラで統一されていません。 !Ref
で何に置換されるかはリファレンスの「戻り値」の欄に記載されています。
- 例えば、上記の例の中では18行目に
!GetAtt
という組み込み関数を使用すると、リソースが持っているさまざまなパラメータに置換されるように構成することができます。- 例えばEC2インスタンスのIPが自動割当になっているときは
!GetAtt ec2.PrivateIp
で実際に割り当てられたIPに置換することができます。 !GetAtt
でどんなパラメータに置換できるかは、!Ref
と同じようにリファレンスの「戻り値」欄に記載されています。
- 例えばEC2インスタンスのIPが自動割当になっているときは
!GetAtt
や!Ref
を使用したときにどんな値に置換できるかきれいにまとめられている表を見つけました。作者の方、ありがとうございます!
2. Type
- リソースの種類を指定します。
3. Properties
- リソースの設定値です。
- リファレンスをよくみて型を間違えないように気をつけてください。
- 数値なのか? 文字列なのか?(ELBで302リダイレクトを設定するときの
302
は文字列扱いなので'
でくくる) - 配列を取るのか? 辞書型をとるのか? 単体の値をとるのか?(値を複数指定できないのに配列として渡さなければいけない場合がある)
- エディタに構文チェックのプラグインを入れておかないと辛いです。
- 数値なのか? 文字列なのか?(ELBで302リダイレクトを設定するときの
- 値として他のリソースへの参照を指定する場合はリファレンスをよく見て、IDなのかARNなのか名前なのか確認してください。
- ただしリファレンスを見ても曖昧な表記でよくわからない場合があります。
- そういうときは
!Ref
で取得できるものを指定すべきであるケースが多いです。 - また、ARNでもIDでもどっちでもいいよみたいなケースもあります。
AWSはややこしいことにリソースを特定する方法が主に3つあります
- リソースの種別ごとに固有のID
- 例えばVPC ID(
vpc-xxxxxxxxxxxxxxxxxx
)やEC2インスタンスID(i-xxxxxxxx
)などが該当します。 - リソースを作成すると自動的に採番されるIDです。
- 例えばVPC ID(
- 名前
- ユーザーが命名できるIDで、重複するとエラーになるようになっているためアカウント内orリージョン内でリソースを一意に識別できます。
- EC2インスタンスなどに設定できる名前は実はただのタグで重複チェックされず、したがってリソースを識別するためには使用できません。
- ただしタグで検索した結果、サービスごとに固有のIDやARNを取得することはできるので、間接的にIDとして使用する場合もあります。
- IAM Roleの名前(例:
AWSServiceRoleForRDS
)などがこれに該当します。
- ARN
- AWSのどのサービスでも共通のフォーマットでリソースを特定することができるIDです。
- フォーマットは大抵の場合、
arn:aws:サービス名:リージョン:アカウントID:リソースの種類/リソースの種別ごとに固有のID or 名前
となっています。 - IAM Roleであれば
arn:aws:iam::123456789012:role/AWSServiceRoleForRDS
。 - VPCであれば
arn:aws:ec2:ap-northeast-1:123456789012:vpc/vpc-xxxxxxxxxxxxxxxxxx
。 - EC2インスタンスなら
arn:aws:ec2:ap-northeast-1:123456789012:instance/i-xxxxxxxx
。
4. Condition
- 例えばステージング環境にだけに必要で、本番環境では作りたくないリソースを定義するときなどに使用します。
- 場合分けをする必要がない場合はこの属性を定義する必要はありません。
- 条件はConditionsセクションに定義します。
- 例
Conditions:
isStaging: !Equals [!Ref "AWS::AccountId", 123456789012]
Resources:
vpcStagingOnly:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
InstanceTenancy: default
Condtion: isStaging
Parametersセクション
- CFN実行時に変更したい値を定義するためのセクションです。
- Webコンソールであれば以下のようにCFN実行時に値を変更することができます。
- Webコンソールであれば以下のようにCFN実行時に値を変更することができます。
- Paramterを定義する際によく設定する属性は以下の4種類です。
- パラメータの名前
- Type
- Default(必須ではない)
- AllowedPattern(必須ではない)
- 例
Parameters:
envName: # 1.
Type: String # 2.
ipVpc: # 1.
Type: String # 2.
Default: 10.0.0.0/16 # 3.
AllowedPattern: "[./0-9]+" # 4.
Resources:
vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref ipVpc
EnableDnsHostnames: true
EnableDnsSupport: true
InstanceTenancy: default
Tags:
- Key: Name
Value: !Sub "${envName}-vpc"
1. パラメータの名前
- リソースのプロパティとして参照するときに使用する名前を定義します。
- 半角英数字のみで、記号は使用できません。
- 例の14行目では
!Ref ipVpc
のようにしてVPCに設定するIPを参照しています。 - 例の最終行では
!Sub "${envName}-vpc"
のようにVPCの名前のプレフィックスを可変にしています。- 弊社では同じアカウント内で複数の環境を構築できるようにするためや、CFNで作成したリソースかどうかすぐに見分けがつくようにするために、すべてのリソースで名前にプレフィックスをつけるようにしています。
envName
はCFN実行時にstaging
やprodution
などの文字列入力するという運用をしています。
2. Type
- パラメータの型を指定します。
- StringやNumberなどがあります。
3. Default
- デフォルト値を指定します。
- CFN実行時に値を変更しなければこのデフォルト値が使用されます。
4. AllowedPattern
- Typeが
String
の場合に許可する文字列を正規表現で指定します。 - Typeが
Number
のときはAllowedValues
という属性で許可する数値を配列で指定することができます。
Mappingsセクション
- 例えばステージング環境と本番環境でリソースに設定する値を変えたい場合などに使用します。
- 以下の例では、
envName
の値によってVPCとSubnetに設定されるIPが変化します。
Parameters:
envName:
Type: String
Mappings:
ip:
staging:
vpc: 10.10.0.0/16
subnet: 10.10.1.0/24
production:
vpc: 192.168.0.0/16
subnet: 192.168.1.0/24
Resources:
vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !FindInMap [ip, !Ref envName, vpc]
EnableDnsHostnames: true
EnableDnsSupport: true
InstanceTenancy: default
subnetPublicA:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: !FindInMap [ip, !Ref envName, subnet]
VpcId: !Ref vpc
AvailabilityZone: ap-northeast-1a
- ちなみに
CidrBlock: !FindInMap [ip, !Ref envName, vpc]
は以下と同値です
CidrBlock:
Fn::FindInMap:
- ip
- !Ref envName
- vpc
スタックをまたいで値を参照するには
- 弊社のサービスでは複数のスタックからサービスが構成されているので、スタックをまたいで値を参照したい場面がでてきます。
- 例えばIAMロールをEC2インスタンスに割り当てたいときなど
- そのようなときはまず、Outputsセクションで値をエクスポートして、その値を別のスタックから
Fn::ImportValue
で参照します。 - Outputsセクションで指定するのは主に以下の3つです。
- Outputの名前
- Value
- Export > Name
- 例
# IAMのテンプレート
Outputs:
outputEc2ProfileEc2AppServer: # 1.
Value: !Ref ec2ProfileEc2AppServer # 2.
Export:
Name: exportEc2ProfileEc2AppServer # 3.
Resources:
roleEc2AppServer:
Type: AWS::IAM::Role
Properties:
RoleName: roleEc2AppServer
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
Path: /
ec2ProfileEc2AppServer:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: ec2ProfileEc2AppServer
Path: /
Roles:
- !Ref roleEc2AppServer
# EC2のテンプレート
Resources:
ec2AppServer:
Type: AWS::EC2::Instance
Properties:
LaunchTemplate:
LaunchTemplateId: !Ref ec2TemplateAppServer
Version: !GetAtt ec2TemplateAppServer.LatestVersionNumber
IamInstanceProfile:
Name: !ImportValue exportEc2ProfileEc2AppServer
1. Outputの名前
- ここでつけた名前を他で参照することはないので適当に命名します。
- 使用可能な文字は英数字のみで記号不可です。
2. Value
- エクスポートしたい値を定義します。
3. Export > Name
- 別のスタックから値を参照するときに指定する名前を設定します。
- 上記の例の最終行のように、
!ImportValue
でこの名前を指定するとValueで指定した値を他のスタックから参照できるようになります。
Outputの注意点
- 以下のような問題が考えられるので、DB用などの構成変更しづらい重要なスタックは、極力スタックをまたぐ参照が発生しないように気をつけて作っています。
1. Outputの使用は極力避けるべき
- なぜかというと、他のスタックから参照された状態では、参照元のスタックで値を変更することができなくなってしまうためです。
- たとえば、上記の例において
ec2ProfileEc2AppServer
のInstanceProfileName
を変更すると、!Ref ec2ProfileEc2AppServer
の値が変わってしまいますが、この値が他のスタックから参照されているため設定変更が失敗してしまいます。 - したがってこのような設定変更をするには、変更対象のOutputを参照しているスタックを一度削除する必要がでてきてしまいます。
- たとえば、上記の例において
- AWSのリソースの中には設定変更時に既存のリソースが削除され、設定変更後のパラメータで新たにリソースが作り直されるものがあります。
- このような場合に
!Ref
などでリソースのIDを参照していると、設定変更時にIDが変わってしまうので、他のスタックからの参照が問題になってしまいます。 - 設定変更時に単純に設定が変更されるだけなのか、既存のリソースを一度削除して新しく作り直されるのかどうかはリファレンスの"Update requires"を参照してください。
- このような場合に
- スタックを分割しない場合は設定変更時に依存関係を自動的に調査して再設定が必要なパラメータは自動的に更新してくれるのでこのような問題は発生しません。
2. Outputの参照は一方向にすべき
- 弊社のサービスではスタックに番号が振ってあり、小さい方から大きい方へのみ参照するようにしています。
- 反対方向に参照を貼ると最悪の場合、相互参照状態になってしまい、一度スタックを削除して作り直すことすらできなくなる可能性が考えられます。
CFNで設定変更するときの確認ポイント
- CFNで設定変更しようとすると、どこがどのように変わるのか、実行前に教えてくれるのでよく確認してください。
- 変更内容の表示方法
- 上記「ざっくりした使い方」の「設定変更」の手順の7番目で「実行」ではなく「変更セットの表示」をクリックします。
- 「JSONの変更」タブをクリックし、JSONの内容を確認します(これが一番変更内容が細かく載っていると思う)。
- 問題なければ「実行」をクリックします。
- 「JSONの変更」の例
[
{
"resourceChange": {
"logicalResourceId": "elbListenerAppHttps", # 変更対象
"action": "Modify", # 何をしようとしているか
"physicalResourceId": "arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:listener/app/elb/123456789012/xxxxxxxxxxxx",
"resourceType": "AWS::ElasticLoadBalancingV2::Listener",
"replacement": "False", # これがtrueだとリソースが一度削除されて作り直されるのでサービスが停止する可能性あり
"moduleInfo": null,
"details": [ # 変更の内容
{
"target": {
"name": "Certificates",
"requiresRecreation": "Never",
"attribute": "Properties"
},
"causingEntity": null,
"evaluation": "Static",
"changeSource": "DirectModification"
}
],
"scope": [
"Properties"
]
},
"type": "Resource"
}
]
"replacement": "False"
であればリソースが一度削除されて作り直されるといったことはないので、サービスが中断することはなさそうですが、実はreplasementがFalse
な場合でも一時的にサービス停止する場合もあるのでリファレンスを要チェックです。
まとめ
今回はCloudFormationの基礎とハマリポイント・注意点について見ていきました。
意外なところで落とし穴があったりして難しそうだなと思われたかもしれませんが、今となっては、だからといって手動でひとつひとつ設定していくわけにも行きません。
弊社のAWS環境は小規模な方だと思いますが、それでもCFNのテンプレートは数千行に及んでいました。
結局銀の弾丸はないということなのかなと感じています。