2017年 ある程度の規模で運用するAWS CloudFormationの勘所

## 概要

インフラエンジニアとしてAWS基盤の構築・運用に携わって早1年が経ちました。
今回は自分がCloudFormationを運用する中で培ってきたノウハウや勘所をご紹介したいと思います。

なお、これがCloudFormationのベストプラクティスだとかそんなことを言うつもりはなく、
あくまで自分がこう考えてきたぞというものなので、ご参考程度にお願いします。
いろんな考え方があると思いますので、ぜひ~~マサカリ~~コメントお待ちしてます。

## どの程度の規模で運用してきたか?

サービスとしてはビッグデータ分析プラットフォームのようなものを構築しておりますが、
AWSの規模感としては大体こんな感じです。

* AWSアカウント：**2**つ
  * 1つは開発環境・内部結合環境用
  * 1つはステージング環境、本番環境用
* 環境数：**5**面
  * 開発環境
  * 内部結合環境
  * ステージング環境1
  * ステージング環境2
  * 本番環境
* 利用しているAWSサービス：約**15**サービス
  * Amazon VPC
  * Amazon EC2
  * Amazon RDS
  * Amazon ElastiCache
  * Amazon S3
  * Amazon DynamoDB
  * Amazon CloudWatch
  * Amazon SNS
  * Amazon Cognito
  * Amazon Route53
  * AWS Lambda
  * AWS IAM
  * Amazon Kinesis
  * AWS WAF
  * AWS CloudTrail
* 基盤担当者：2～3名
* CloudFormation テンプレートステップ数：約**80K**Step

## CloudFormationの適用範囲

さてCloudFormationを利用するにあたって、どのAWSリソースをCloudFormationで管理すべきでしょうか？
個人的には**「可能な限り全て」**を推奨しています。
**「可能な限り全て」**というのは、**「EC2のキーペア登録などCloudFormationでは管理できないもの、新規サービス等でCloudFormationが対応していないものを除き、CloudFormationで構築可能なAWSリソースの全て」**という意味です。

AWSの利用サービスが多いほど全てに対応するのは大変に思えるかもしれません。
しかし管理方法（AWS CLI, CloudFormation, 管理コンソールなど）がバラバラになるよりかは遥かに混乱せずミスも防げます。
特にCloudFormationで作成したリソースをCloudFormation以外で更新・削除してしまうと整合性が取れなくなり、最悪CloudFormationの運用ができなくなってしまうので、そのような事故を避けるという意味でも**原則CloudFormationに統一**することをお勧めします。

1つの技術要素に統一しておけばキャッチアップコストも低くなるでしょう。

## テンプレートフォーマット

テンプレートのフォーマットはJSONとYAMLが選択できますが、これは可読性の観点から**「YAML一択」**です。
もともとJSONのみのサポートでコメントが書けない等の問題がありましたが、2016年9月のアップデート[^1]でYAMLがサポートされるようになりました。

既にJSONフォーマットのテンプレートを利用している場合でも、CloudFormationデザイナーを利用してコンバート可能なので積極的にYAMLフォーマットを利用しましょう。

## ディレクトリ/ファイル構成

ある程度の規模のAWSリソースを管理することが想定される場合、事前にディレクトリ構成やファイル構成をしっかり考えておかないと管理が非常につらくなってきます。

特にファイル構成（1テンプレートファイルに何のAWSリソースを含めるか）は、1度スタックを作成してしまうと後から容易に変更ができないため重要です。

### ディレクトリ構成

適切なファイルの構成を考えるためには、適切なディレクトリ構成を考える必要があります。
開発者や運用者がテンプレートファイルを管理しやすい構成が望ましいでしょう。  
筆者のチームでは「**AWS契約単位**」、「**環境単位**」、「**システム or サブシステム単位**」に**ディレクトリを分割する**ことを推奨 [^2]しています。

```text:ディレクトリ構成例
cloudformation
├─ aws-000000000000              # AWSアカウントID[000000000000]のリソースのテンプレートを格納
│  ├─ common                     # 環境共通的なリソースのテンプレートを格納（IAM設定, CloudTrail設定など）
│  │  ├─ iam.template
│  │  ├─ cloudtrail.template
│  │  ├─ …
│  │
│  ├─ production                 # 本番環境のリソースのテンプレートを格納
│  │  ├─ common                  # 本番環境のシステム/サブシステム共通的なリソースのテンプレートを格納　　
│  │  │  ├─ network.template
│  │  │  ├─ s3.template
│  │  │  ├─ dns.template
│  │  │  ├─ …
│  │  │
│  │  ├─ systemA                 # 本番環境のAシステム/サブシステムのリソースの/テンプレートを格納
│  │  │  ├─ composite.template
│  │  │  ├─ …
│  │  │
│  │  ├─ systemB                 # 本番環境のBシステム/サブシステムのリソースの/テンプレートを格納
│  │  │  ├─ …
│  │  │
│  │  ├─ …
│  │
│  ├─ staging1                   # ステージング1環境のテンプレートを格納
│  │  ├─ …
│  │
│  └─ staging2                   # ステージング2環境のテンプレートを格納
│     ├─ …
│
└─ aws-111111111111              # AWSアカウントID[111111111111]のリソースのテンプレートを格納
   ├─ …

```

### ファイル構成

ディレクトリ構成が決まるとファイルの構成が概ね見えてきます。
上記のディレクトリ構成に基づくと、1つのテンプレートファイルに複数環境のAWSリソースが存在したり、複数システムのAWSリソースが存在したりするということはあり得ません。

仮に1テンプレートファイルに本番とステージング環境のAWSリソースが混在する場合を考えてみましょう。
ステージング環境のAWSリソースを更新する際は、必然的に本番環境のAWSリソースを含むスタックを更新することになります。仮に本番環境のAWSリソースに変更を加えていないとしても、精神衛生上よろしいものではありませんね。
事故を未然に防ぐという意味でも、最低限「**環境単位**」、「**システム or サブシステム単位**」に**ディレクトリを分割する**ことは有効です。

ではディレクトリ内のファイル単位についてはどう考えるべきでしょうか。
ここで考慮すべきは**AWSリソース間の依存度**と**AWS管理者の単位**です。

#### AWSリソース間の依存度

* 互いに依存度の**高い**AWSリソースは**同一**テンプレートで管理すべきです。  
別テンプレートで管理してしまうと、AWSリソース間の依存関係を**人が**意識してスタックの作成・更新・削除を行わなければなりません。これはAWSに熟練した人ならまだしも、通常は容易なことではありません。  
　
* 互いに依存度の**低い**AWSリソースは**別**テンプレートで管理すべきです。  
そうすることでスタックの作成・更新・削除時の影響を極小化することができます。（他のリソースをうっかり更新して事故を起こす可能性がなくなります。）

#### AWS管理者の単位

* 例えばアカウント（IAM）管理者、データベース（RDS）管理者といったようにAWSのリソースに対して管理者が分かれている場合は、権限制御の観点から1テンプレートファイルに含めるリソースを判断したほうがよいでしょう。


筆者の経験上、上記のディレクトリ構成に基づくのであれば、ディレクトリ配下のAWSリソースは1テンプレートファイルにまとめてしまったほうが運用しやすいです。
実際`system`系のディレクトリ配下は各種AWSリソース（ALB, EC2, RDS, SecurityGroup, IAMRole, InstanceProfile etc..）を`composite.template`にひとまとめにしており、分割しているのは`common`ディレクトリに含まれるIAM（ユーザ・グループ）、CloudTrailなど明確に他のAWSリソースとの結合度が低いもののみとなっております。


## テンプレートの共通化

ここまで読んでいただいた方は「テンプレートの共通化をしないのか？」と思われるかもしれません。

公式ドキュメントの[AWS CloudFormationのベストプラクティス](http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/best-practices.html)でも紹介されていますが、テンプレートファイルはパラメータを利用することによって共通化することが可能です。
同じコンポーネントを宣言する共通パターンを共通テンプレートとして再利用することで、ダブルメンテを防ぐことができます。

しかしながら、筆者はテンプレートの**共通化は極力しないほうがむしろメンテナンス性は高い**と考えています。  

### 1. 可読性が低い  
共通化をしようとすればするほど、各種リソースの設定値をパラメータ化して、外部から値を受け取るようになります。極端な例ですがEC2インスタンスを作成するテンプレートを共通化すると下記のようになります。
パラメータを利用しているため、具体的に何の値が設定されているかは管理コンソールや呼び出し元の親テンプレートを参照しないとわかりません。

```yaml
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  ImageId:
    Description: EC2 ImageId
    Type: String
    Default: ""
  InstanceType:
    Description: EC2 InstanceType
    Type: String
    Default: ""
  AvailabilityZone:
    Description: EC2 AvailabilityZone
    Type: String
    Default: ""
  InstanceInitiatedShutdownBehavior:
    Description: EC2 InstanceInitiatedShutdownBehavior
    Type: String
    Default: ""
  DeviceName:
    Description: EC2 DeviceName
    Type: String
    Default: ""
  VolumeType:
    Description: EC2 VolumeType
    Type: String
    Default: ""
  VolumeSize:
    Description: EC2 VolumeSize
    Type: String
    Default: ""

Resources:
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref ImageId
      InstanceType: !Ref InstanceType
      AvailabilityZone: !Ref AvailabilityZone
      InstanceInitiatedShutdownBehavior: !Ref InstanceInitiatedShutdownBehavior
      BlockDeviceMappings:
        - DeviceName: !Ref DeviceName
          Ebs:
            VolumeType: !Ref VolumeType
            VolumeSize: !Ref VolumeSize
      # 省略
```

共通化をしない場合、下記のようにパラメータを利用せず設定値をベタ書きする形になります。
似たような記述を繰り返し書くことになりますが、実際のリソースとテンプレートの定義が1対1で定義されており、設定値が一目でわかります。

```yaml
AWSTemplateFormatVersion: 2010-09-09
Resources:
  EC2Instance001:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-da9e2cbc
      InstanceType: t2.micro
      AvailabilityZone: ap-northeast-1a
      InstanceInitiatedShutdownBehavior: stop
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: 100
            VolumeSize: gp2
      # 省略

  EC2Instance002:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-da9e2cbc
      InstanceType: t2.large
      AvailabilityZone: ap-northeast-1c
      InstanceInitiatedShutdownBehavior: stop
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: 200
            VolumeSize: gp2
      # 省略
```

### 2. 修正に伴う影響範囲が大きくなる

共通化できると考えていたとしても後から「この環境だけ、もしくはこのリソースだけ個別に変更を加えたい」というようなことは往々にして発生します。
共通テンプレートを修正する場合は影響を受けるリソースを常に意識しなければなりません。
最悪、共通化したテンプレートの中でConditionによる条件分岐を行うなど、負の遺産を生み出しかねません。

### 3. 変更プレビューにてネストされたスタックの変更分が参照できない

これは親テンプレートを用意してその中で共通テンプレートを利用する場合に発生する問題となります。
管理コンソールからCloudFormationを実行する場合、実行前に「変更のプレビュー」として、差分を確認できますが、親スタック（テンプレート）の更新を行う場合、子スタック（テンプレート）の詳細な変更分は見ることができません。

![Changes.png](https://qiita-image-store.s3.amazonaws.com/0/42275/b6546fe5-6fb6-f2f4-f6d3-649d722d8959.png)


仮にテンプレートファイルの差分を事前に別の方法で確認していたとしても、これは精神衛生上非常によくありませんし、思わぬ事故を引き起こすかもしれません。
　
　
以上の理由から、テンプレートファイルはプログラマブルに共通化することによって返って複雑度が増してしまうと考えています。
筆者のチームでは、原則**共通化禁止**、**ネストスタック禁止**という形で可能な限りテンプレートをわかりやすくシンプルにしてきました。
単純、故に冗長な部分もありますが、裏を返せば簡単であり、知識の少ない運用者でもキャッチアップが容易でミスも最小化できます。

## コーディング規約

コーディング規約というほど大それたものではありませんが、テンプレートを作成するにあたっていくつか決めておいたほうがいいことがあります。

* **AWSリソースのキー名**  
下記でいう`EC2InstanceProductionSystemA0001`に相当する部分となります。  
筆者のチームでは「AWSリソース名」+「環境名」+「システム名」+「連番」としています。

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  EC2InstanceProductionSystemA0001:
    Type: AWS::EC2::Instance
    # 以下省略
```

* **AWSリソースに付与するタグ**  
筆者のチームでは「環境」、「システム名」、「一意の名称」は最低限必須としています。  
　
* **使用しないプロパティの記載要否**  
使用しないプロパティの記載方法については次の3パターンが考えられます。  
筆者のチームでは明示的に使用しない意図が分かるよう「3. 記載した上でAWS::NoValueを参照する」方針としています。
    1. 使用しないプロパティは記載しない
    2. 記載した上でコメントアウトする
    3. 記載した上でAWS::NoValueを参照する

## 開発フロー/CI

テンプレートファイルはGitなどのバージョン管理システムを利用して管理することが望ましいでしょう。
筆者のチームではGitlabを利用して下記のフローで開発を進めています。

![プレゼンテーション1.png](https://qiita-image-store.s3.amazonaws.com/0/42275/4746bda5-9765-5d94-845c-06487e518cee.png)

aws-cliの`aws cloudformation validate-template`コマンドを実行することでフォーマットの検証を行うことで、実際にスタックを作成する前に、タイポ等の単純なミスを発見することができます。
より細かいチェックを行ってくれる[cfn-lint](https://github.com/martysweet/cfn-lint)というツールがあるみたいですが、筆者は未検証です。

ある程度の規模までは、このフローで問題なく運用できるはずです。
しかし規模が大きくなればなるほど次のような課題がでてきます。

* テンプレートファイルが複数に分かれているため、環境横断的に各種リソースの設定値を見たり、横串で修正をしたりするのがつらい。
* YAMLを書くのがそもそもつらい。
* コーディング規約違反のチェックなどレビューもつらい。

そこで筆者のチームではCloudFormationの設定値をExcel, RDBで管理し、YAML自動生成するような仕組みを導入しています。
![プレゼンテーション2.png](https://qiita-image-store.s3.amazonaws.com/0/42275/118ed687-4895-9a54-7eeb-5cfc0f36b72b.png)

これにより、開発者はExcelだけをメンテナンスすればよくなりました。
同じAWSリソースは1シートに全て定義しているため、環境横断的にリソースの設定値を参照・修正することも容易です。

ただしこのような仕組みを作るのはそれなりに時間がかかりますし、汎用的に作ろうとするとある程度高度な設計も必要になってきます。ご紹介した方法は決して推奨するようなものではなくただの一例になりますが、何かしらのメンテナンスコストを下げるような仕組みがあると幸せになれると思います。

## リリース

ここでのCloudFormationのリリースとはスタックを作成・更新・削除することを指します。
AWS CLIを利用してJOB等で実行させるなど色々な方法が考えられますが、スタックの操作については**「管理コンソールからの実行」が一番良い**と筆者は考えています。
理由としては「変更のプレビュー」により、AWSリソースの変更点が「視覚的」に確認できるためです。

![console-changeset-details.png](https://qiita-image-store.s3.amazonaws.com/0/42275/cbca56f3-ade0-3c3f-7d8f-47303d8275fb.png)


スタックの更新を行う場合には必ず[変更セットの作成](http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets-create.html)から更新を行うようにしましょう。

## CloudFormationを腐らせてはいけない

[公式ドキュメント](http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/best-practices.html)にも書かれていますが、AWS CloudFormationで作成したリソースをCloudFormation以外の方法で変更しては**絶対に**いけません。

> スタックを起動した後、AWS CloudFormation コンソール、API、または AWS CLI を使用して、スタック内のリソースを更新します。スタックのリソースを AWS CloudFormation 以外の方法で変更しないでください。 変更するとスタックのテンプレートとスタックリソースの現在の状態の間で不一致が起こり、スタックの更新または削除でエラーが発生する場合があります。詳細については、「ウォークスルー: スタックの更新」を参照してください。

CloudFormationはあくまでCloudFormationの世界でAWSリソースを管理しており、管理コンソールから行った変更をいい感じに取り込んではくれません。最悪の場合、二度とCloudFormationが実行できなくなる可能性があります。

特にこの問題は、CloudFormationに詳しくない運用者に引継ぎを行う場合などに発生します。
本当の緊急事態を除き、原則AWSの管理コンソールはRead Onlyにしておくなど、権限制御を行いましょう。
（人を信じてはいけません。）

## おわりに

まとまりなくつらつらと書いてしまいましたが、いかがでしたでしょうか。
CloudFormationを実際の現場でどう運用するのか考える際に、この記事が少しでも参考になれば幸いです。



[^1]: https://aws.amazon.com/jp/blogs/aws/aws-cloudformation-update-yaml-cross-stack-references-simplified-substitution/
[^2]: マルチリージョンでサービスを提供する場合はAWS契約単位、リージョン単位、環境単位、システム or サブシステム単位に分割したほうがいいでしょう。