私的CloudFormationベストプラクティス


CloudFormationとは

CloudFormation(以下CFn)は、テキストファイルからAWSのリソースを作成することができるInfrastructure as Code(以下IaC)のサービスです。

この記事で話すこと・話さないこと

話すこと

  • CFnを用いたAWSリソース作成のベストプラクティス

話さないこと

  • CFnに関するサービスの細かい仕様や用語の説明
  • 他のIaCを実現するサービスとの比較

ベストプラクティス

Multi-AZ構成のVPC上にALBとWebサーバを構築するサンプル
https://github.com/hicka04/cfn-best-practice

ポイント

①YAML形式で記述

  • CFnで使える記述方式
    • YAML
    • JSON
  • YAMLで記述するメリット
    • コメントが書ける
    • JSONで記述する際の読みづらさ( [] {} によって文字が増える)の問題がない

②ライフサイクルと所有権を意識してStackを分割

参考 : AWS公式のベストプラクティス

ライフサイクル

  • 変更するタイミングが同一のリソースが1つのStackにまとまっていること
    • 複数人で開発する際に同一のテンプレートを編集する可能性を減らせる
      コンフリクトやデグレを防げる
  • 削除するタイミングが同一のリソースが1つのStackにまとまっていること
    • そのStackを削除するだけで不要になったリソースをすべて削除できる
      オペミスを防げる

所有権

  • 簡単に言うと、「誰がそのリソースの変更を担当するのか?」
    • アプリケーションとDBで担当者/部署が別れている場合はその単位でStackの分割をすべき

③1テンプレートで複数環境のStackを作成できるようにする

  • テンプレートには環境による差異は記述せず、パラメータで注入できるようにする
  • パラメータを注入してStackを作成して初めて環境ごとの意味のあるリソースとなるようにする

筆者は、環境ごとにテンプレートを作成してしまい環境共通の改修をするのが面倒な状態にしてしまったことがあります…。

メリット

  • 環境ごとにテンプレートファイルが分かれていない
    • 共通の変更を加えたい場合にミスが生まれない
    • 工数削減

デメリット

  • 環境ごとの差分がパラメータだけで表現できないときに追加で対応が必要(リソースの数が違う場合など)

解決方法

  • 環境ごとの差分を表現する用のテンプレートを追加
  • Conditionをもとに、作成するリソースを分岐

今回のサンプルコードでは、1つ目の方法を用いている。
理由としては、2つ目の方法では以下のようなデメリットがあるためである。

  • 条件の設定をミスしてしまうと本番影響が出てしまう
  • テンプレート内に、条件によって作成されるリソースと作成されないリソースが混在して読みづらくなる

④クロススタック参照を用いてStack同士の依存関係を作る

参考 : AWS公式のベストプラクティス

  • 値のハードコードを避ける
    • テンプレートが再利用できる
  • 強い依存関係を作れる
    • 他のStackに依存されているリソースがある場合、そのリソースを変更したり削除したりできなくなる
    • 誤った変更や削除をしようとすると、変更や削除に失敗し、ロールバック等の安全な行動をとってくれる

依存される側

  • Export名は ${AWS::StackName}-${ResourceName}
    • Stack名をExport名に含めることで、1テンプレートで複数環境のStackを作る場合でも一意のExport名になる
network/network.yml
Outputs:
  Vpc:
    Value: !Ref Vpc
    Export:
      Name: !Sub ${AWS::StackName}-Vpc  # 👈

依存する側

  • 依存したいリソースを作成しているStack名をパラメータで受け取る
    • Metadata で依存しているStackの一覧を整理するとよりわかりやすい
  • ${AWS::StackName}-${ResourceName}ImportValue する
web/web.yml
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  NetworkStack:  # 👈
    Type: String

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Dependent Stacks
        Parameters:
          - NetworkStack  # 👈依存しているStackの一覧をここで整理

Resources:
  SgAlb:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId:
        Fn::ImportValue:
          !Sub ${NetworkStack}-Vpc  # 👈

Stackのネストはしない

Stackのネストとは

  • CFnのテンプレートを共通化する手法
  • CFnのテンプレート内でStackを作るという記述をすることで、Stackに親子関係を作る

なぜネストさせないか

  • AWS公式のベストプラクティスではネストでテンプレートを使い回すことを推奨している
  • だがネストさせる場合、子のStackの中で作られているリソースの詳細な変更セットが見れないデメリットがある
    • 子のStackでリソースが削除される危険な変更をしても、変更セットとしては「子のStackのどこかが変わってるみたいだな〜」しかわからない
    • S3やDynamoDBといったデータストアのリソースが削除されるとデータも消えてしまい、CFnだけでは元の状態に戻すことができなくなる
  • ネストでも詳細な変更セットが見れるようになれば、ネストを採用するのもアリだと筆者は思っている

⑥命名

Stack名

stack-${ServiceName}-${SystemName}-${Purpose}-${Env}

具体例 : stack-blog-web-prod

  • すべて小文字
  • ハイフン(-)つなぎ
  • ${Purpose} はないこともある

リソース名

クラスメソッドさんが公開しているAWSリソースの命名規則がとても参考になる

${SystemName}${Env} はパラメータで受け取る
${SystemName} のような固定値は DefaultAllowedPattern を用いると完全に固定可能

Parameters:
  Env:
    Type: String
    AllowedValues:
      - prod
      - stg
      - dev
  SystemName:
    Type: String
    Default: web
    AllowedPattern: ^web$
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Metadata
        Parameters:
          - Env
          - SystemName

論理ID名

${ResourceTypeName}${Purpose}

具体例 : SgAlb
ALB用のSecurityGroup

  • アッパーキャメルケース
  • 作成したいリソースの種類を最初に持ってくると分かりやすくなる
    • 最初の数文字見ただけで何を作るのか伝えられる
    • テンプレート内ではリソースの種類ごとにまとまって記述するとさらに分かりやすくなる

⑦タグ付け

Stack作成時にタグ付けすると、そのStack内で作成したすべてのリソースに同じタグをつけることができる
以下のタグをつけると詳細な絞り込みできる(と、AWSリソースの命名規則でおまけとして紹介されてました)

  • SystemName
  • Env

最後に

本記事でご紹介したCFnのベストプラクティスは、チームメンバーや作成するシステムに応じて変化する可能性があります。状況に合わせて変更していただければと思います。
みなさんがCFnでIaCをする際の1つの指針になれば幸いです。