Terraformで構築したシステムの自動テスト

# 概要
本記事では、Terraformで構築したシステムの設定値を効率的にテストする方法について説明します。方法は以下の通りです。

1. 構築するシステムの設定値を外部ファイルに記録
1. Terraformは1のファイルを読み込み設定値を入力するよう設定
1. テストツールも1のファイルの値を想定される値としてテストする

以上より、構築の入力値をテストの入力値とすることで、テストの入力値とTerraformで構築するための入力値の二重管理を防ぐことができる。

本記事では、TerraformでGCPのシステムを構築しテストするパイプラインを構築し、CICDのデプロイパイプラインの構築しテストを実行実行する方法を説明します。
また、本記事のテストツールはInSpecのOSSツールCINCを使い、テストを実施する方法について説明します。

# 背景
Terraformで構築したシステムは、設定値のテストをおこなう必要がないように考えられる。しかし、Terraformは `for_each` や `count` などを使うと、設定のon/offが可能となります。
この機能を使うと、開発環境、ステージング環境、本番環境の各環境でリソースのon/offが可能となります。そのため、ステージング環境で、テストを実施し出荷判定などが通った後、本番環境の値をonにして本番環境へとリリースするなどリリースを簡便にすることができます。

また、Terraform モジュール機能を使うことで、システムを構築するロジックを隠蔽したり、他のシステムで構築したコードを流用することが可能になります。

このようにTerraformコードを複雑にすると、運用管理が簡便になりますが、意図した設定がなされなくなることもあります。
例えば、on/off機能であれば、本番リリース時にonにし忘れリリースが漏れることがあります。また、このとき、Terraformはエラーとならないため、リリース漏れは目視で確認する必要があります。そのため、Terraformで構築したインフラも別途テストの必要があると考えられます。

しかし、Terraformで設定する値と、テストコードのあるべき値を別途管理すると、管理コストが2倍となります。
また、テストコード側の値の更新忘れにより、意味をなさないテストがおこなわれることもあります。
そこで、Terraformで構築するための入力情報をテストの入力情報とすることで管理コストを下げることができ、値の反映忘れも起りません。

本記事では、Terraformに入力する設定値をInSpecの入力値とする方法を示します。

# 設計
TerraformとInSpecの両方で同じ入力値をもたせる方法として、設定値を外部ファイルで管理しコード内で読み込ませることで実現します。
ここでは、TerraformおよびInSpecの両方で、外部ファイルで管理されている値を読み込ませる方法を示します。
また、最後に外部ファイル、Terraformコード、InSpecコードを管理するディレクトリ構成の案を示します。

## Terraformの設計
Terraformで外部の設定値ファイルを読み込み設定する方法を示します。設定方法は、以下の通りです。

1. リソースに設定する値を直接入力せず、変数から入力するようにする。
1. ローカル変数への代入を外部ファイルからおこなう

入力値をテスト項目とするため、直接入力するのではなく、外部へ入力します。 `variable`を使うとリリース時に変更の可能性があるので、環境変数を読み込むなどの特殊な事情がない限り、`local`変数から値を渡します。
また、`local`変数への値の代入は、InSpecと共有させる必要があるため、外部ファイルから読み込ませるようにします。
Terraformの[関数](https://www.terraform.io/docs/language/functions/index.html)には、`yaml`,`json`,`csv`をデコードする関数があります。
また、ファイルパスからファイルを読み取る`file`関数がある。これらを組み合わせることで、外部ファイルで管理している設定値ファイルをTerraformの設定ファイルとして読み込ませることができる。

上記の内容をGCPのVPCネットワークの作成する方法示します。
外部の設定ファイルのフォーマットは、yaml形式とします。


VPCネットワークの設定ファイルを`sample.yaml`と定義し、中身を次のようにします。
```
name: sample
```

次に上記のsample.yamlのファイルを読み込み、vpcネットワークを作成するTerraformコードは以下のようになります。

```
locals {
  vpc_network = yamldecode(file("./sample.yaml"))
}

resource google_compute_network main {
  name = local.vpc_network.name
}
```

## InSpecの設計
InSpecはRubyコードであるため、外部ファイルの形式に沿ったライブラリをインポートし、ファイルを読み込むことで、外部ファイルの設定値を読み込むことができます。

以下では、先のTerraformの設計で示した`sample.yaml`を読み込みテストする方法は以下の通りです。

```
require `yaml`
require `json`
network = YAML.load_file("./sample.yaml")

describe "sample" do
  actual_networks = JSON.parse(command("gcloud compute network list --format json").stdout).map { |vpc| [vpc["name"], vpc]}.to_h
  
  it "vpcネットワーク #{network["name"]}が存在する。" do
    expect(actual_networks[network["name"]]["name"]).to eq network["name"]
  end
end
```
## ディレクトリ設計
コードとして管理すべき項目は、下記の3種類が存在します。

- Terraformコード
- InSpecのテストコード
- 設定値ファイル

また、これとは別にCICD用の設定ファイルが存在します。
以上を踏まえて、ディレクトリ構成を提案すると以下のようにすると管理が簡便なものとなります。
ここでは、CIツールとしてGCPのCloud Buildを用いるものとします。

```
.
├── README.md
├── cloudbuild  #Cloud Buildのビルド構成ファイル
├── config      #設定値ファイル
├── src         #Terraformコード
└── test        #InSpecコード
```

# 実装例
最後に実装例として、GCP上でVPCネットワークを作成し、作成したVPCネットワーク上にGCEを作成するサンプルを示します。
構成図は以下のようなものとします。また、Terraformコードでは、GCEの作成をenabledで制御することができ、trueとすると作成され、falseとすると削除されます。falseとして作成したときに、Terraformで作成されず、InSpecのテストで作成されていないことを検知されることを示します。また、実行はCloud Buildで実施させます。
サンプルコードは、[当該リポジトリ](https://github.com/AtsushiKitano/article-sample-codes/tree/master/infra_unittesting)を参照ください。

サンプルコードをCloud Buildを実行した結果は、以下のようになります。結果からも分かるようにGCEが作成されていないため、GCEのテストに失敗しエラーとなっています。

![スクリーンショット 2021-12-12 22.43.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40665/a07fd80f-2c68-5356-a98d-8dad39d41553.png)


# まとめ
Terraformコードで構築したインフラをテストするにあたり、同一の入力情報からテストする方法について示しました。
Terraformをsimpleに活用する上では、テストの必要性はないですが、複雑な機能を付与すると意図しないことがありえるため、
ミスをなくすためにもテストは必要となります。当該方法を適用することで、簡便にテストを実施することができます。
ただ、インプットコードを適用するようにコードを修正する必要があるため、それぞれの自由度が下ります。
また、テストコードも複雑となりえます。そのときはテストコードも必要に応じてテストする必要があります。