kumogata2 を使用して、 AWS CloudFormation スタックを適用前に差分確認をする

この記事は [Akatsuki Advent Calendar 2020](https://adventar.org/calendars/5174) 4日目の記事です。
前回の記事は [tomotaka-yamasaki](https://github.com/tomotaka-yamasaki) さんの [開発エンジニアとQAの心理の違いとは何か？](https://hackerslab.aktsk.jp/2020/12/03/173822) でした。

## はじめに

私の所属するチームでは日々、安全な運用を継続しつつ運用業務改善を行っています。
今日はその中で AWS CloudFormation(以下Cfn) において、 kumogata2 を使った運用をご紹介します。

## AWS CloudFormation とは
詳しくは AWS の[公式ドキュメント](https://aws.amazon.com/jp/cloudformation/)をご確認ください。

Infrastructure as Code(以下IaC) を使用した、クラウドプロビジョニングサービスです。
スタックという単位で管理されており、スタックごと下記のようなyaml形式のテンプレートを作成して、 AWS リソースをコードで管理することができます。
1つのスタックで、VPC、AutoScaling、EC2の構成など複数サービスの依存関係を含めて表現することができます。

```ruby:test.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: Test Instance
Resources:
  TestInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-abcdexxxxxx
      InstanceType: t3.micro
      Tags:
      - Key: Name
        Value: test
      BlockDeviceMappings:
      - DeviceName: "/dev/xvda"
        Ebs:
          VolumeType: gp2
          VolumeSize: '32'
```
例）Cfn で EC2 インスタンスを作成する

### メリット
- 環境を複製できる
- インフラの変更をレビューすることができる
- ログを残して変更経緯を追うことができる
- 手動によるオペレーションミス・漏れがなくなる

何年も運用していくと検証・開発環境への要望も変わってくることもありますが、そういった要望にも柔軟に対応することができます。
また、作成・削除を簡単に行えるため、テンポラリな環境を作成することにも役立ちます。

### デメリット

強いてあげるとしたら

- 変更に手間が増える

がありますが Cfn 管理していても一時的な変更であれば手動で変更して確認できたら元に戻せば良いので、あまりデメリットとは言えないかもしれません

## kumogata2 とは

Cfn のスタックを作成することができるツールです。
特徴としては

- Rubyでかける
- Cfn スタックの作成・更新・削除を CLI で完結させられる
- dry-run で依存関係を含めて更新箇所を確認することができる

でしょうか。
> dry-run で依存関係を含めて更新箇所を確認することができる

依存関係がわかることによって、

「インスタンスのボリュームだけを変更するつもりが、インスタンスごと入れ替わってしまった」

といったことを事前に回避できます。

また、私のプロジェクトでは[kumogata2-plugin-ruby](https://github.com/kumogata/kumogata2-plugin-ruby)も合わせて使っています。

## 使い方
```ruby:test.rb
TestInstance do
  Type "AWS::EC2::Instance"
  Properties do
    ImageId 'ami-abcdexxxxxx'
    InstanceType 't3.micro'
    Tags [
      _{
       Key "Name"
       Value "test"
      }
    ]
    BlockDeviceMappings [
    _{
      DeviceName "/dev/xvda"
      Ebs do
        VolumeType "gp2"
        VolumeSize '32'
      end
      }
    ]
  end
end
```
### 新規作成
このように書くことで最初に紹介した `test.yml` の形式に kumogata2 が変換してスタックを作成してくれます。

```
$ kumogata2 create test.rb test
```
を実行することによって、 Cfn 上に `test` というスタックが作成され、インスタンスが立ち上がります。

### 更新

また、スタックの`VolumeSize`を 32 → 64に変更を行いたい際は
dry-runを実行することによって、更新する際に変更される差分のみ確認できます。

```
$ kumogata2 dry-run test.rb test
[
  {
    "Action": "Modify",
    "LogicalResourceId": "TestInstance",
    "PhysicalResourceId": "ami-abcdexxxxxx",
    "ResourceType": "AWS::EC2::Instance",
    "Details": [
      {
        "attribute": "Properties",
        "name": "BlockDeviceMappings"
      }
    ]
  }
]
```

## 困っていたこと

`BlockDeviceMappings` に変更が入るのみ

ということまでは確認できていたのですが、このままでは本当に 32 → 64 という変更が行われているのかがわかりません。

プロジェクトでは複数人が Cfn の管理を行っているので、適用時に`BlockDeviceMappings`には`VolumeSize`以外のパラメータもあるので、不要なものまで適用されてしまうかもしれません。

```
BlockDeviceMappings:
- DeviceName: "/dev/xvda"
  Ebs:
    VolumeType: gp2
    VolumeSize: '32'
```

そのため私のプロジェクトでは、

```
$ kumogata2 convert test.rb
```
でローカルで更新したいテンプレートフォーマットを作成したものと、
Cfn上のテンプレートを取得して、

変更差分が`VolumeSize`のみであることを確認してから適用していました。

### diffのコマンドがほしい
さすがに毎回確認をするのが大変だったので、diffコマンドを探しました。
[Kumogata2 Usage](https://github.com/kumogata/kumogata2#usage)にコマンド自体はあったのですが、そのまま適用するとパースでエラーになってしまったり、必要以上に機能があったのでプロジェクトに合わせて変更を加えました。

今回の要件としては、スタックの更新する際の差分が確認したい。

だったので
https://github.com/kumogata/kumogata2/blob/master/lib/kumogata2/client.rb
にオーバーライドする形で以下を追記しました。
また、Lambda や CloudWatch で日本語を使っていたのですが、 CloudFormation API がマルチバイトに対応していなかったため、diffを取る前に API の結果と同じになるようにコンバートしています。

```ruby:kumogata2_patch.rb
def export_template(stack_name)
  stack = get_resource.stack(stack_name)
  stack.stack_status
  stack.client.get_template(stack_name: stack_name).template_body
end

def diff(path_or_url, stack_name)
  remote_stack = export_template(stack_name)
  local_stack = convert(path_or_url)
  # 現状CloudFormation APIがマルチバイト文字を?に変換してしまうため、同様の変換をかけてからdiffをとる
  File.write("/tmp/remote_stack.txt", remote_stack.gsub(/[^\x00-\x7f]/, '?'))
  File.write("/tmp/local_stack.txt", local_stack.gsub(/[^\x00-\x7f]/, '?'))
  system("diff -u /tmp/remote_stack.txt /tmp/local_stack.txt")
end
```

### 結果

```
$ kumogata2 diff test.rb test
       - DeviceName: "/dev/xvda"
         Ebs:
           VolumeType: gp2
-          VolumeSize: '32'
+          VolumeSize: '64'
```
これで`VolumeSize`のみに差分があることを確認してから適用できるようになりました。
## さいごに
今回差分を出せるようにしたことによって

- 差分を見る手間が減った
- 誰が差分確認しても同じ確認をできるようになった
  - 自動化/コマンド化されていない手動の運用は作業者によって手順が異なる可能性があります。
  - 差分を確認したつもりが手順を間違えていたため、きちんとした差分確認ができず不要な対応を適用してしまうことがなくなります。

が達成できました。
少人数で運用を回しつつ新しい挑戦をするにはこういった小さな改善が必要不可欠なので今後も続けていきたいと思います！

## 参考
- [AWS CloudFormation](https://aws.amazon.com/jp/cloudformation/)
- [kumogata2](https://github.com/kumogata/kumogata2)
- [kumogata2-plugin-ruby](https://github.com/kumogata/kumogata2-plugin-ruby)