はじめに
この記事では、AWSさんが提供しているAWS CloudFormationのベストプラクティスを解読し、「ベストプラクティスを実現する構成」を考えてみるということを目的としております。
そのため、CloudFormationの用語についての説明は省かせて頂いております。
また、あくまで自己流で考えているものなので、AWS CloudFormationのベストプラクティスの理解が間違っている可能性もあります。その際は、ご指摘頂けると幸いです。
より良いものにするための、ご意見・アドバイスは絶賛お待ちしております!
よろしくお願いいたします
=== 2020.06 追記 ===
CDKを使いましょう。
CDKについての記事はまた別途書こうと思います。
背景
AWSさんが提供しているAWS CloudFormationのベストプラクティスには概念的なことしか書かれておらず、実際にCloudFormationベストプラクティスを体現している構成は(私が調べた限りでは)提供されておりませんでした。
そこで、AWS CloudFormationのベストプラクティスを体現する構成を、自分なりに考えてみよう!というのがきっかけです。
考慮していないもの (2018/12/16)
StackSetsについて
実はStackSetsに関してのベストプラクティスは、別にドキュメントが用意されています。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/stacksets-bestpractices.html
今取り扱っているサービスでは、StackSetsを使う予定がなかったため、一旦StackSetsの管理に関しては考慮しておりません。
今後、StackSetsが必要になった場合には、随時更新していこうと思います。
My CloudFormationベストプラクティス v0.1.0
2018年02月16日時点での、ベストプラクティス構成です。
各ディレクトリと、各要素についてはこちらで説明しております。
Stacks
┣ <Service>
┃ ┣ <Layer>
┃ ┃ ┗ master.yaml
┃ ┗ master.yaml
Templates
┗ <Template>.yaml
AWS CloudFormationのベストプラクティスの解説
AWS CloudFormationのベストプラクティスの中で、テンプレートの構成を考える際に実際に参考にした項目について、私の認識を説明いたします。
ここで触れない項目に関しても、重要な事は書かれているので、是非一読することをおすすめします。
ライフサイクルと所有権によるスタックの整理
この項目では、Stackを分ける判断基準について書かれています。
(この項目が構成を考える上で一番参考になりました。)
この項目でてくる「ライフサイクル」という概念と、「所有権」という概念について、それぞれ解説します。
ライフサイクル
この項目で出てくる「ライフサイクル」という言葉は、AWSリソースのライフサイクルを指しています。
簡単に説明すると、あるAWSリソースのライフサイクルが同じなのであれば、同じStackにまとめてましょうという感じです。
このAWSリソースのライフサイクルを考える上でのポイントは、「いつリソースが生まれて、いつ死ぬのか」を見極めることです。
例えば、プライベートAPIサーバのSecurityGroupを考えてみます。
仮に、このAPIサーバを利用しているサーバが、オンプレからAWSに移行することが決定し、移行期間中はオンプレ上の旧サーバからのアクセスとAWS上の新サーバからのアクセスを許可する必要があるとします。
この場合、旧サーバからのアクセスを許可するSecurityGroupと、新サーバからのアクセスを許可するSecurityGroupのライフサイクルは異なります。
なぜならば、旧サーバからのアクセスを許可するSecurityGroupが死ぬタイミングと、新サーバからのアクセスを許可するSecurityGroupが死ぬタイミングと異なるためです。
この場合であれば、以下のようにして2つの__Stack__が作られるようにSecurityGroupを作成します。
# security_groups/api.yaml
Resources:
PrivateApiSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: API server security group.
SecurityGroupIngress:
CidrIp: <新サーバのCIDR>
FromPort: 443
ToPort: 443
IpProtocol: tcp
Description: From servers on AWS.
VpcId: !Ref VpcId
# security_groups/api-old.yaml
Resources:
OnpremisesApiSecurityGroup:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupDescription: API server security group for old servers.
SecurityGroupIngress:
CidrIp: <旧サーバのCIDR>
FromPort: 443
ToPort: 443
IpProtocol: tcp
Description: From servers on On-premise.
VpcId: !Ref VpcId
こうしておくことで、移行期間が終わった後に、 OnpremiseApiSecurityGroup
が書かれているStackを削除するだけで旧サーバのアクセスを遮断することだできます。
こうしておくことのメリットは、テンプレートを変更することなく、Stackを削除するという操作だけで、作業を完了することができるところです。
所有権
この項目における「所有権」は、AWSリソースの所有権についてです。
簡単に説明すると、誰が、または、どのチームが、AWSリソースを管理すべきかという観点で、Stackをまとめましょうという感じです。
悪い例を考えてみます。
例えば、チームAとチームBが利用しているEC2インスタンスが以下のような形で同じStackに記述されていたとします。
Resources:
TeamAEc2Instance:
Type: AWS::EC2::Instance
Properties:
...
TeamBEc2Instance:
Type: AWS::EC2::Instance
Properties:
...
仮に、TeamBのサービスが必要なくなり、クローズすることになったとします。
そうなると、テンプレートの記述を書き換える必要があります。
しかし、テンプレートの記述を書き換える場合、TeamAとTeamBに承認を取らなくてはなりません(倫理的に)。
そうなると、テンプレートの変更コストが上がってしまいます。
また、変更を加える際にも関係ない箇所を変更してしまうなどの、ヒューマンエラーの可能性が上がってしまいます。
今回の例の場合でも、Stackを分けておくことで、TeamBの確認が取れた後に、TeamBのInstanceが記述されているStackを削除するだけで、作業を完了することができます。
# application/teamA/instance.yaml
Resources:
TeamAInstance:
Type: AWS::EC2::Instance
Properties:
...
# application/teamB/instance.yaml
Resources:
TeamBInstance:
Type: AWS::EC2::Instance
Properties:
...
ただ、一つ注意点がございます。
それは、組織構造はいずれ変わるので、それに依存した形でStackを分けるのは危険だということです。
そのため、現状の組織構造に完璧に合わせてStackを分けるのではなく、あるAWSリソースの所有者は異なるべきという視点からStackを分けることをおすすめします。
クロススタック参照を使用して共有リソースをエクスポートします
この項目では、別StackのリソースをあるStackで利用する際には、ハードコードするのではなく、クロススタック参照を利用しましょうということが書かれています。
ここに書かれている内容は当たり前ですが、バカにしてはいけません。
ハードコードをした場合、リソースの変更が検知できず、予期せぬエラーに繋がる可能性があります。
特に、インフラ側の予期せぬエラーは影響が大きいことが多いため、注意が必要です。
ただ、クロススタック参照を使うと、別Stackと強い依存関係が生まれてしまいます。
というのも、別Stackから参照されている値は変更することができないからです。
そのため、クロススタック参照で参照すべきものは極力変更が加わらないか、変更する場合には全く別のリソースになるものを選択することをおすすめします。
(ex. リソースID, ARN, etc...)
悪い例は、VPCのNameタグをクロススタック参照するなどです。
VPCのNameタグはあくまでタグなので、変更してもコンソール上の見た目が変わるだけです。
こういうものをクロススタック参照で別リソースと依存させてしまうと、気軽に変えられるものも、変更できなくなってしまいます。
ネストされたスタックを使用して共通テンプレートパターンを再利用する
これもかなり重要な項目です。
ここで、大事なのは、「Template」と「Stack」は別の概念だということです。
これは、私だけかもしれないですが、「Template」と「Stack」は、ごっちゃ混ぜになりやすいものだと思います。
私の中では「Template」というのは、ある所から呼び出されて、利用されることを前提としたCloudFormationテンプレートという認識です。
そのため、「Template」はParametersを注入されない限り、AWSリソースを作るために必要な情報が足りないため、単体でStackを作ることはできません。
それに対して「Stack」は、「Template」に固有のParametersを注入し、AWSリソースを作成するための情報が全てが明確になっている状態のCloudFormationテンプレートです。
「Stack」は、AWSリソースを作るために必要な情報を全てもっているので、単体でStackを作成することができます。
この2つの概念を分けて考えることで、CloudFormationテンプレートの行数が膨大になることを防ぎ、どこに何を書けば良いのかわからない状態を防ぐことができます。
具体的には、AWS::CloudFormation::Stack
というタイプのAWSリソースをCloudFormationテンプレートで宣言して、「Template」にParametersを注入することで、「Stack」を作成します。
# Templates/network/VPC.yaml
Parameters:
VpcCidrBlock:
Type: String
Description: IP address class used for VPC.
AllowedValue:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
Resources:
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidrBlock
...
# Stacks/playground/network/master.yaml
Resources:
Vpc:
Type: AWS::CloudFormation::Stack
Properties:
Parameters:
VpcCidrBlock: 10.0.0.0/8
TemplateURL: 's3://icfn-best-practice/Templates/network/VPC.yaml'
このようにしておくことで、共通パターンがあるCloudFormationテンプレートと、Stackを生成するCloudFormationテンプレートは別の箇所で管理することができます。
共通パターンを切り出すことで、無駄なコピペも減らし、膨大なCloudFormationが出来上がるのを防ぐことができます。
テンプレートを再利用して複数の環境にスタックを複製する
実は、この項目には、少しだけ納得できていない部分があります。
「Template」を再利用するというのは、前節で書いたようにCloudFormationテンプレートが膨大になることを防いでくれるため、とても良いものだと思います。
ただ、複数の環境を作る時にテンプレートを使い回しにするのは如何なものかなぁと思っています。
というもの、本番環境とテスト環境で、結構異なる部分があったからです。
例えば、Security Groupであれば、本番環境はPublicで公開しているサービスでも、テスト環境では、社内アクセスに限定したいということはよくあると思います。
また、Webサービスの機能テストをしたい場合に、本番環境と同じスペックの環境を用意するのも、リソースの無駄使いになってしまう可能性が高いため、低いスペックのインスタンスにしたり、冗長化をしないということもあると思います。
こういう差分を同じテンプレートに記述すると、Conditions
やParameters
を使った複雑な制御が必要になってきたり、ある環境では使わないリソースの記述が書かれてしまう可能性があります。
そうなると、管理するには煩雑なテンプレートが出来上がってしまうのではないか...と思ってしまい、腹落ちが悪い感じになっています。
ただ、AWSさんがベストプラクティスとして提唱しているくらいなので、何かしら私の考えの及ばない部分があるのかなぁと思います(Lambdaとかは使いまわしたほうが楽そうだし)。
My CloudFormationベストプラクティスの構成
私が考えるCloudFormationベストプラクティスのディレクトリ構成は以下になります。
Stacks
┣ <Layer>
┃ ┣ <Service>
┃ ┃ ┗ master.yaml
┃ ┗ master.yaml
Templates
┗ <Template>.yaml
各要素とポイント、この構成に至った経緯について説明しようと思います。
Stacks
Stacks
ディレクトリ以下には、実際にStackを作るために必要な情報を全て記述します。
ライフサイクルと所有権によるスタックの整理で触れたように、「ライフサイクル」と「所有権」によってStackを分けます。
この分けたStackの依存解決も、このStacks
ディレクトリ以下で行います。
ここでは依存解決を、master.yaml
で行うものとしています。
また、ネストされたスタックを使用して共通テンプレートパターンを再利用するで触れたように、「Template」と「Stack」という概念はディレクトリレベルで分けています。
このStacks
ディレクトリに書かれるCloudFormationテンプレートには、AWSリソースを作るために必要な情報を全て記述します。
もし、CloudFormationを使用せずに作ってしまったAWSリソースと依存関係を持ちたい場合には、最悪Stacks
以下のどこかでハードコーディングします。
(ベストはCloudFormationで作ったものと置き換えること。)
<Layer>
この<Layer>
についても、AWS CloudFormationのベストプラクティスのライフサイクルと所有権によるスタックの整理で多層アーキテクチャを使用してStackを整理する詳細なガイダンスとして、紹介されています。
多層アーキテクチャーは、スタックを積み上げて構築する複数の水平の層に整理します。各層はその直下の層に依存します。各層には 1 つ以上のスタックを持つことができますが、各層のスタックは類似したライフサイクルと所有権を持つ AWS リソースを持つ必要があります。
<Layer>
については、各層がその直下の層にのみ依存するというルールを基づいて作成します。
適切なレイヤーの切り方については、多層アーキテクチャの考えが引用できると思いますので、詳しい説明は別途お調べください...。
master.yaml
<Service>
以下の各ディレクトリには必ずmaster.yaml
というCloudFormationテンプレートを設置します。
このCloudFormationテンプレートでは、各層の依存解決を担当します。
<Layer>
のmaster.yaml
であれば、<Layer>
以下にあるmaster.yaml
以外のCloudFormationテンプレートファイルを読み込みます。
<Service>
のmaster.yaml
であれば、各<Layer>
のmaster.yaml
を読み込み、下の層で出力された値を、上の層に注入することで、依存解決を行います。
# Stacks/playground/master.yaml
Resources:
Layer2:
Type: AWS::CloudFormation::Stack
Properties:
Parameters:
TemplateBucketName: !Ref TemplateBucketName
VpcId: !GetAtt Layer1.Outputs.VpcId
TemplateURL: !Sub 's3://${TemplateBucketName}/Stacks/playground/Layer2/master.yaml'
Layer1:
Type: AWS::CloudFormation::Stack
Properties:
Parameters:
TemplateBucketName: !Ref TemplateBucketName
TemplateURL: !Sub 's3://${TemplateBucketName}/Stacks/playground/Layer1/master.yaml'
# Stacks/playground/Layer1/master.yaml
Resources:
Vpc:
Type: AWS::CloudFormation::Stack
Properties:
Parameters:
VpcCidrBlock: 10.0.0.0/8
TemplateURL: !Sub 's3://${TemplateBucketName}/Stacks/playground/Layer1/VPC.yaml'
Outputs:
VpcId:
Value: !GetAtt Vpc.Outputs.VpcId
<Service>
この<Service>
については、説明が難しいですが、あえて、明文化するのであれば、「ある機能郡を構成するAWSリソース郡をまとめたもの」という認識です。
ある処理単位だったり、ドメイン単位だったり、実現したいものによって変わったり、人によっても変わってくる部分なのかなぁと思います。
このサービスという言葉自体は、AWS CloudFormationのベストプラクティスのライフサイクルと所有権によるスタックの整理でもSOAを使用してStackを整理する詳細なガイダンスとして紹介されています。
サービス指向アーキテクチャーを使用すると、業務上の大きな問題を処理しやすい大きさに整理できます。これらのパートはそれぞれ、明確に定義された目的があり、機能の自己充足単位を表します。これらのサービスを、それぞれ独自のライフサイクルと所有者があるスタックにマッピングできます。これらのサービス (スタック) をすべて 1 つに繋いで、相互に通信するようにできます。
適切なサービスの分け方については、SOAの考え方が引用できると思いますので、詳しい説明は別途お調べください...。
<Template>.yaml
ある共通のテンプレートパターンをまとめたCloudFormationテンプレート。
おわりに
AWS CloudFormationのベストプラクティスを紐解いて、実際の形に落とし込む時に考えたことをまとめてみました。
CloudFormationテンプレートをベストプラクティスに則った形にすることで、所有者が明確になり、少ない操作で、安全にAWSリソースを管理することができます。
AWSを触れるエンジニアが不足しているチームでも、所有者を明確にしておけば、スタックの管理を所有者に任せることができるかもしれません。
また本記事で、今まで不透明だったCloudFormationテンプレート管理を、ある程度実践で使える形まで落とし込むことができたと思います。
これが、CloudFormationでリソース管理をするのが億劫になった方や、同じ悩みを抱える方の少しでも助けになれば幸いです。
ただ、まだ完成には至ってはいないと思うので、継続して改善していこうと思います。