CodeCommit + CodeBuildでAWS CloudFormation Guardを試してみた

## はじめに
[UL Systems Advent Calendar 2020](https://qiita.com/advent-calendar/2020/ulsystems) の18日目です。

CloudFormationテンプレートでEC2を作成した際、うっかりEBSの暗号化を忘れてしまい作り直しになった経験はあるのではないでしょうか。
暗号化以外にもプロジェクトルールで決まっている設定が漏れていた。なんてこともあるのではないでしょうか。
レビューでこれらの漏れを防ぐことも重要ですが、できれば自動でチェックする仕組みが欲しいですよね？

そんな時に使えるコマンドラインツールに、[AWS CloudFormation Guard](https://github.com/aws-cloudformation/cloudformation-guard)があります。
[AWSによる紹介ブログはこちら](https://aws.amazon.com/jp/about-aws/whats-new/2020/10/aws-cloudformation-guard-an-open-source-cli-for-infrastructure-compliance-is-now-generally-available/)

どんなものなのか、実際に確かめてみました。

## 実行環境構築
ローカルマシンで実行するだけでは面白くないため、CodeCommit + CodeBuildで実行できる環境にしました。
ソースプロバイダにCodeCommitを、buildspec.ymlに以下の内容を指定していしたCodeBuildプロジェクトを作成。

```buildspec.yml
version: 0.2

phases:
  install:
    commands:
       - wget https://github.com/aws-cloudformation/cloudformation-guard/releases/download/1.0.0/cfn-guard-linux-1.0.0.tar.gz
       - tar -xvf cfn-guard-linux-1.0.0.tar.gz
  build:
    commands:
       - ./cfn-guard-linux/cfn-guard --version
       - ./cfn-guard-linux/cfn-guard check -t ebs_volume_template.yml -r ebs_volume_template.ruleset
```

[How it works](https://github.com/aws-cloudformation/cloudformation-guard#how-it-works)にあるサンプルコードではjsonですが、yamlに置き換えたebs_volume_template.ymlとebs_volume_template.rulesetをCodeCommitにpush。
ビルドを実行し、3件のエラーが発生することを確認。

![result1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533280/6083ac71-8352-4f54-b6b3-3b9329ee7488.png)
 

## AWS::EC2::Instanceで試してみる
### CFnテンプレートの作成
サンプルではEBSだが、実運用ではEC2 Instanceで作成することが多い。同様のチェックをEC2 Instanceでも試してみる。
適当なEC2を作成するCFnテンプレートを作成。

```ec2_template.yml
Resources:
  NewInstance1:
    Type: AWS::EC2::Instance
    Properties:
      DisableApiTermination: true
      SubnetId: subnet-XXXXXXXXX
      ImageId: ami-00f045aed21a55240
      InstanceInitiatedShutdownBehavior: stop
      InstanceType: t2.micro
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            VolumeSize: 500
            VolumeType: gp2
            Encrypted: false
        - DeviceName: /dev/sda2
          Ebs:
            VolumeSize: 50
            VolumeType: gp2
            Encrypted: false
        - DeviceName: /dev/sda3
          Ebs:
            VolumeSize: 10
            VolumeType: gp2
      KeyName: sample-key
      Monitoring: false
      SecurityGroupIds:
        - sg-XXXXXXXXXXXXXXXXX
```

### ルールセットの作成
1からルールセットを記載するのは面倒なので、[Rulegen](https://github.com/aws-cloudformation/cloudformation-guard/blob/master/cfn-guard/README.md#rulegen)コマンドでの自動生成を試してみた。

```bash
> cfn-guard rulegen sample-project/ec2_template.yml
AWS::EC2::Instance BlockDeviceMappings == [{"DeviceName":"/dev/sda1","Ebs":{"Encrypted":false,"VolumeSize":500,"VolumeType":"gp2"}},{"DeviceName":"/dev/sda2","Ebs":{"Encrypted":false,"VolumeSize":50,"VolumeType":"gp2"}},{"DeviceName":"/dev/sda3","Ebs":{"VolumeSize":10,"VolumeType":"gp2"}}]
AWS::EC2::Instance DisableApiTermination == true
AWS::EC2::Instance ImageId == ami-00f045aed21a55240
AWS::EC2::Instance InstanceInitiatedShutdownBehavior == stop
AWS::EC2::Instance InstanceType == t2.micro
AWS::EC2::Instance KeyName == sample-key
AWS::EC2::Instance Monitoring == false
AWS::EC2::Instance SecurityGroupIds == ["sg-XXXXXXXXXXXXXXXXX"]
AWS::EC2::Instance SubnetId == subnet-XXXXXXXXX
```

ImageIdなど単純な比較となる項目は自動生成でも問題ないが、BlockDeviceMappingsなどの多階層の項目の値をチェックには使えそうにないですね。
リスト、かつ他階層の項目のため、ワイルドカードを利用し、以下のようなルールセットを作成

```ec2_template.ruleset
let encryption_flag = true

AWS::EC2::Instance BlockDeviceMappings.*.Ebs.Encrypted == %encryption_flag
AWS::EC2::Instance BlockDeviceMappings.*.Ebs.VolumeSize <= 100
```

### 実行結果の確認

チェック対象のテンプレート、ルールセットが作成できたため、CodeCommitのリポジトリへpush。
チェック対象のテンプレートが変更になったため、buildspec.ymlの一部を修正。

```buildspec.yml
version: 0.2

phases:
  install:
    commands:
       - wget https://github.com/aws-cloudformation/cloudformation-guard/releases/download/1.0.0/cfn-guard-linux-1.0.0.tar.gz
       - tar -xvf cfn-guard-linux-1.0.0.tar.gz
  build:
    commands:
       - ./cfn-guard-linux/cfn-guard --version
       - ./cfn-guard-linux/cfn-guard check -t ec2_template.yml -r ec2_template.ruleset
```

実行したところ、暗号化チェックでエラーが発生していない。
（failed because [Encrypted]～のメッセージがなく、失敗ケースが1件になっている。）
![result2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533280/8c343b50-f51c-e9bf-493f-06f24af6679f.png)

### 原因調査、対応
[ドキュメント](https://github.com/aws-cloudformation/cloudformation-guard/blob/master/cfn-guard/README.md#writing-rules)をもう一度確認してみたところ、[Wildcard Semantics](https://github.com/aws-cloudformation/cloudformation-guard/blob/master/cfn-guard/README.md#wildcard-semantics)に比較時の注意点書いてますね。
先ほどの記載ではいづれかがtrueならば結果OKとなってしまうため、全てfalseではないという条件へ修正。

```ec2_template.ruleset
AWS::EC2::Instance BlockDeviceMappings.*.Ebs.Encrypted != false
AWS::EC2::Instance BlockDeviceMappings.*.Ebs.VolumeSize <= 100
```

実行し、Encryptedのエラーが発生することを確認。

![result3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533280/288e25c5-90af-4fa6-e578-a463c01189ba.png)

### Encryptedが指定されていないデバイスもエラーにしたい
「DeviceName: /dev/sda3」は「Encrypted」が指定されていないが、先ほどの結果ではOKと見なされている。
暗号化されていないディスクが作成されてしまうため、未指定も結果NGとなることが望ましい。
項目がない場合結果NGとなるよう[--strict-checks](https://github.com/aws-cloudformation/cloudformation-guard/blob/master/cfn-guard/README.md#strict-checks)オプションを指定するようbuildspec.ymlを修正。

```buildspec.yml
version: 0.2

phases:
  install:
    commands:
       - wget https://github.com/aws-cloudformation/cloudformation-guard/releases/download/1.0.0/cfn-guard-linux-1.0.0.tar.gz
       - tar -xvf cfn-guard-linux-1.0.0.tar.gz
  build:
    commands:
       - ./cfn-guard-linux/cfn-guard --version
       - ./cfn-guard-linux/cfn-guard check -t ec2_template.yml -r ec2_template.ruleset --strict-checks
```

ビルド実行を行い、望む結果が得られたことを確認
![result4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533280/1f392a7e-6af3-fdc1-ce7f-979b8f6c8510.png)



## まとめ
実際に使ってみないとわからないことが多いので、とりあえず試すことは重要ですね。

また、現状ではサンプルでは1テンプレート、1rulesetになってますが、実運用を考慮した際、テンプレートごとにrulesetを作成することは現実的ではありません。

どの単位でCFnテンプレートを作成するのかにもよるが、Strict Checks用のrulesetとそれ以外のrulesetの2つで運用するのがベターなのかな？
ここら辺は運用しながら自分なりの答えを見つけたいと思います。

最後に今回作成したテンプレート、ルールセット、buildspec.ymlは以下となります。

```ec2_template.yml
Resources:
  NewInstance1:
    Type: AWS::EC2::Instance
    Properties:
      DisableApiTermination: true
      SubnetId: subnet-XXXXXXXXX
      ImageId: ami-00f045aed21a55240
      InstanceInitiatedShutdownBehavior: stop
      InstanceType: t2.micro
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            VolumeSize: 500
            VolumeType: gp2
            Encrypted: false
        - DeviceName: /dev/sda2
          Ebs:
            VolumeSize: 50
            VolumeType: gp2
            Encrypted: false
        - DeviceName: /dev/sda3
          Ebs:
            VolumeSize: 10
            VolumeType: gp2
      KeyName: sample-key
      Monitoring: false
      SecurityGroupIds:
        - sg-XXXXXXXXXXXXXXXXX
```

```ec2_template.ruleset
AWS::EC2::Instance BlockDeviceMappings.*.Ebs.Encrypted != false
AWS::EC2::Instance BlockDeviceMappings.*.Ebs.VolumeSize <= 100
```

```buildspec.yml
version: 0.2

phases:
  install:
    commands:
       - wget https://github.com/aws-cloudformation/cloudformation-guard/releases/download/1.0.0/cfn-guard-linux-1.0.0.tar.gz
       - tar -xvf cfn-guard-linux-1.0.0.tar.gz
  build:
    commands:
       - ./cfn-guard-linux/cfn-guard --version
       - ./cfn-guard-linux/cfn-guard check -t ec2_template.yml -r ec2_template.ruleset --strict-checks
```