CloudFormationで地雷を踏まないために気を付けたいポイント

こんにちは、ちはらです。
今回は、私がCloudFormationを触っていた中で出会った気を付けたいポイントについてまとめています。
初歩的な内容から、少し複雑な内容まで記載していきたいと思います。
（JAWS-UG東京 ランチタイムLT会 #19でお話した内容を含みます。）

### CloudFormationとは
先ずCloudFormation（以下Cfn）について簡単に説明します。
Cfnとは、AWSリソースのモデル化及びセットアップに役立つサービスです。
使用する全てのAWSリソースを記述するテンプレートを作成することで、Cfnがリソースのプロビジョニングや設定を実行してくれます。
AWSリソースを個別の作成・設計してそれぞれの依存関係を考慮する必要がありません。
特徴としては大きく3つあります。

- インフラストラクチャの管理を簡略化
- インフラストラクチャをすばやく複製
- インフラストラクチャの制御や変更の追跡が簡単

### Cfnの実行環境
今回私がCfnで作業を行っていたのは、以下のようなマルチアカウント・マルチリージョンの環境です。

管理アカウントでStackSetsを実行し、メンバーアカウント上でMain_templateから複数のSub_templateを呼び出すという構成をとっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3867728/41145625-09cc-7306-e5cd-c2c773f087dc.png)


# 気を付けたいポイント一覧
冒頭で記載した通り、初歩的な内容から少し複雑な内容まで以下の内容についてまとめていきます。
1. 環境に既にリソースは作成されていないか
1. SSMパラメータの値は変更されていないか
1. テンプレートの文字コードは統一されているか
1. ドリフトの検出は正しくできているか
1. ロールバックの正しい挙動について

## 環境に既にリソースは作成されていないか
Cfnを触ったことがある人は必ず遭遇したことがあるであろうエラーです。
以下の画像のように、環境に既に「rol-A」というIAMロールが存在しているとします。
その状態でrol-Aを作成するCfnを実行すると、**「HandlerErrorCode : AlreadyExists」** というエラーが発生します。
これは**既に環境に同じ名前のリソースが存在する**というエラーです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3867728/73dd93f7-bac8-c3b6-6f42-6b92d4b23cfa.png)



私は検証中にこのエラーを連発していました。
というのも、リソースにDeletionPolicyを付与して作成をしていたので、再度Cfnを実行する時にスタックの削除だけでなくリソースの削除も都度行う必要があったのですが、どうしてもスタックを削除するだけで満足してしまい...。
自分の中では意外と頭から抜けてしまうポイントでした。

## SSMパラメータの値は変更されていないか

リソースの名称を命名規則に則って付与したい場合などにSSMパラメータに値を保存し、テンプレートの中でSSMパラメータを呼び出すことができます。（{{resolve:ssm:/XXXXX}} 部分）

```ruby:Template
  Trail:
    Type: AWS::CloudTrail::Trail
    Properties: 
      TrailName: !Sub 'cloudtrail-{{resolve:ssm:/XXXXX}}

    ~~~~~

```


構築した時と変更を行う時でSSMパラメータの値が異なると、SSMパラメータを使用している部分は新しいパラメータを使用した形で更新されます。
この時、更新は正常に完了しているが気が付かない間に設定が変わっている、又はリソースが再作成されているという事態が発生してしまうので、注意が必要です。

## テンプレートの文字コードは統一されているか
これは特に、インポートをする時に影響があるので注意が必要なポイントです。


**インポート**とは、既存のAWSリソースをスタックに組み込むことです。
使用するテンプレートはインポートするリソースに関する記述以外は変更箇所がない必要があります。

そのため重要になってくるのが文字コードです。
ここでは例として、以下の操作を用いて説明をします。

①Before_templateを使用して構築
②After_templateを使用して「rol-B」のインポートを実施


Before_templateとAfter_templateで記述の差があるのは「rol-B」に関する記述の部分だけなので、一見このテンプレートで正常にインポートが完了しそうに思えます。
しかし、このテンプレートではインポートに失敗してしまいます。
その原因が文字コードです。
この場合、Before_templateでは「**UTF-8**」、After_templateでは「**SJIS**」の文字コードを使用しているため、#でコメントアウトしている部分（#バージニア北リージョンで実施）が異なっており、インポートが正常に実行できないのです。

```ruby:Before_template（UTF-8）
#バージニア北リージョンで実施
Resources:
  AdminRole:
    Type: AWS::IAM::Role
    Properties: 
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRoleWithSAML"
            Principal:
        RoleName: rol-A
```

```ruby:After_template（SJIS）
＃バージニア北リージョンで実施
Resources:
  AdminRole:
    Type: AWS::IAM::Role
    Properties: 
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRoleWithSAML"
            Principal:
        RoleName: rol-A
Resources:
  AdminRole:
    Type: AWS::IAM::Role
    Properties: 
    ~~~~
        RoleName: rol-B

```

知っていればすぐに解決するエラーですが、逆に知らないと時間が解けていくエラーです。

## ドリフトの検出は正しくできているか

前述した通り、今回は管理アカウントでStackSetsを実行しメンバーアカウントにデプロイを行うという方法をとっていました。
このような場合、メンバーアカウントにあるSub_templateと実環境に差異があったとしても、管理アカウントでドリフトの検出を行うと「**IN_SYNC**」になってしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3867728/d16c2b8e-03c2-fb5b-0911-935d30cca008.png)


というのも、管理アカウントに存在するテンプレートはMain_templateのため、Main_templateの中身しかドリフトの検出で見ておらず、実際に差分が発生しているSub_templateの中までは見ていないため「IN_SYNC」になるのです。（メンバーアカウントにあるMain_templateのスタックでも同様のことが発生します。）

今回のようにマルチアカウント構成をとる場合は、どのスタックでドリフトの検出を行う必要があるのかを考える必要があります。

## ロールバックの正しい挙動について
ロールバックとはどのような動きをするのか、勘違いしている方もいらっしゃるかもしれません。私も最近まで勘違いしていました。

以下の図のように、Cfnでの更新を行ったとします。

①Cfnで環境Ver1を構築
②Cfnで環境Ver2に更新
③GUI/CLIでVer2aに更新
④CfnでVer3に更新をしようとしたところ、エラーが発生
⑤ロールバックの実行

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3867728/c9758dc2-4b9c-92b0-7456-4febdf5789a9.png)

⑤でロールバックが発生した時、Ver3の更新がなくなり、Ver2aの状態に戻ると思われている方もいると思います。（図＜誤＞）
しかし、実際は更新される前のテンプレート（今回の場合Ver2のテンプレート）で再度更新を行うのがロールバックの正しい動きです。（図＜正＞）
そのため、今回のようにGUIやCLIで設定を変更していた場合、その設定は削除されてしまいます。

Cfnで構築・更新を行っているものは手で変更をしないというのが前提ではありますが、どうしても手で変更を加える必要がある場合、ロールバックの時の正しい挙動についても意識しておく必要があると感じました。

# さいごに
約4か月間、毎日Cfnを触っていた中で出会った注意すべきポイントについてまとめてきました。
私がまだ知らないポイントも沢山あると思うので、是非コメントなどをいただけると嬉しいです。