こんにちは。READYFOR 株式会社で SRE として働いている ジェダイ・パンくず🚀 と申します。
突然ですが、皆さんの現場ではインフラのテストの仕組みは導入済みですか?我々はまだ出来ていません(笑)。
READYFOR では IaaS として AWS を、IaC として Terraform を導入しているのですがコードのテスト・コードによってデプロイされた状態のテストを何かしらの仕組みを使って実践したいなぁと考え、今調査している最中です。
今回は Terraform のテストを書くツール Terratest について調べた内容を皆さんに共有したいと思っています。また AWS の状態をテストする仕組みとしてよく知られている Awspec との比較も行っていきたいと思っています。
必要になるモノ
事前に必要になるソフトウェアの一覧です。
- Golang (requires version >= 1.13)
- Ruby (required by Awspec)
- Awspec
今回ターゲットにする AWS インフラと Terraform コード
任意の VPC, サブネット上に ECS クラスタ・サービス・タスクを作る Terraform コードを前提にテストを実施していきたいと思います。VPC やサブネットは事前に作成したモノを利用 (terraform の data や variable を用いる) します。
今回 AWS インフラをデプロイするために用意した main.tf と output.tf です。(ID などはマスクさせて頂きました) Terratest のテストを行う際に事前に AWS インフラがデプロイされている必要は無いためここでは terraform apply
しません。
main.tf
terraform { required_version = ">= 0.12" } # ------------------------------------------------ # variables の設定 # ------------------------------------------------ variable "vpc_id" { default = "****" } variable "cluster_name" { description = "example" type = string default = "example" } variable "service_name" { description = "example" type = string default = "example" } # ------------------------------------------------ # 既存 Resource の指定 # ------------------------------------------------ data "aws_vpc" "example" { id = var.vpc_id } data "aws_subnet_ids" "all" { vpc_id = data.aws_vpc.example.id filter { name = "tag:Name" values = ["rf-sandbox-***-private0-sb", " rf-sandbox-***--private0-sb"] } } # ------------------------------------------------ # 新規 Resource の作成 # ------------------------------------------------ resource "aws_ecs_cluster" "example" { name = var.cluster_name } resource "aws_ecs_service" "example" { name = var.service_name cluster = aws_ecs_cluster.example.arn task_definition = aws_ecs_task_definition.example.arn desired_count = 0 launch_type = "FARGATE" network_configuration { subnets = data.aws_subnet_ids.all.ids } } resource "aws_ecs_task_definition" "example" { family = "terratest" network_mode = "awsvpc" cpu = 256 memory = 512 requires_compatibilities = ["FARGATE"] execution_role_arn = aws_iam_role.execution.arn container_definitions = <<-JSON [ { "image": "terraterst-example", "name": "terratest", "networkMode": "awsvpc" } ] JSON } resource "aws_iam_role" "execution" { name = "${var.cluster_name}-ecs-execution" assume_role_policy = data.aws_iam_policy_document.assume-execution.json } resource "aws_iam_role_policy_attachment" "execution" { role = aws_iam_role.execution.id policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" } data "aws_iam_policy_document" "assume-execution" { statement { effect = "Allow" actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["ecs-tasks.amazonaws.com"] } } }
output.tf
output "task_definition" {
value = aws_ecs_task_definition.example.arn
}
Terratest のテストを書いてみる
テストを書く
次に Terratest のテストを書いていきます。
package test import ( "fmt" "testing" "github.com/gruntwork-io/terratest/modules/aws" "github.com/gruntwork-io/terratest/modules/terraform" awsSDK "github.com/aws/aws-sdk-go/aws" "github.com/stretchr/testify/assert" ) func TestTerraformAwsEcs(t *testing.T) { t.Parallel() expectedClusterName := fmt.Sprintf("example") expectedServiceName := fmt.Sprintf("example") // テストで起動するリージョンを複数指定し、どのリージョンでも起動することを確認する // awsRegion := aws.GetRandomStableRegion(t, []string{"us-east-1", "ap-northeast-1"}, nil) awsRegion := "ap-northeast-1" terraformOptions := &terraform.Options{ TerraformDir: "../", Vars: map[string]interface{}{ "cluster_name": expectedClusterName, "service_name": expectedServiceName, }, EnvVars: map[string]string{ "AWS_DEFAULT_REGION": awsRegion, }, } defer terraform.Destroy(t, terraformOptions) if _, err := terraform.InitE(t, terraformOptions); err != nil { fmt.Printf("Terraform Init Error.") } if _, err := terraform.ApplyE(t, terraformOptions); err != nil { fmt.Printf("Terraform Apply Error.") } taskDefinition := terraform.Output(t, terraformOptions, "task_definition") // 各テスト (次項 "各テストの説明" を参照のこと) // (1) クラスタ周りのテスト // Reference: https://docs.aws.amazon.com/ja_jp/sdk-for-go/v1/api/service.ecs.Cluster.html cluster := aws.GetEcsCluster(t, awsRegion, expectedClusterName) assert.Equal(t, int64(1), awsSDK.Int64Value(cluster.ActiveServicesCount)) assert.Equal(t, expectedClusterName, awsSDK.StringValue(cluster.ClusterName)) assert.Equal(t, int64(0), awsSDK.Int64Value(cluster.PendingTasksCount)) // (2) ECS サービス周りのテスト // Reference: https://docs.aws.amazon.com/ja_jp/sdk-for-go/v1/api/service.ecs.Service.html service := aws.GetEcsService(t, awsRegion, expectedClusterName, expectedServiceName) assert.Equal(t, int64(0), awsSDK.Int64Value(service.DesiredCount)) assert.Equal(t, "FARGATE", awsSDK.StringValue(service.LaunchType)) assert.Equal(t, expectedServiceName, awsSDK.StringValue(service.ServiceName)) assert.Equal(t, "LATEST", awsSDK.StringValue(service.PlatformVersion)) // (3) ECS Task 周りのテスト // Reference: https://docs.aws.amazon.com/ja_jp/sdk-for-go/v1/api/service.ecs.TaskDefinition.html task := aws.GetEcsTaskDefinition(t, awsRegion, taskDefinition) assert.Equal(t, "256", awsSDK.StringValue(task.Cpu)) assert.Equal(t, "512", awsSDK.StringValue(task.Memory)) assert.Equal(t, "awsvpc", awsSDK.StringValue(task.NetworkMode)) }
コードの説明をしていきます。
- import 行で 'terratest' と 'testify/assert' をロードしています
- expectedClusterName, expectedServiceName で期待するクラスタ名・サービス名を記しています
- awsRegion でテストの際に AWS インフラをデプロイするリージョンを指定しています
- defer でテストが終わった後の処理として Destroy することを明記しています
- ECS クラスタ・サービス・タスク毎にテストが記されています (次項)
各テストの説明
assert 文で Terraform によってデプロイされた AWS インフラの状態でテストしています。ここではそれぞれのテストの意味を説明していきます。
(1) ECS クラスタ
テストされるモノ | 期待値 |
---|---|
アクティブな ECS サービス数 | 1 |
ECS クラスタ名 | expectedClusterName |
ペンディングタスク数 | 0 |
(2) ECS サービス
テストされるモノ | 期待値 |
---|---|
ECS DesiredCount | 0 |
ECS ローンチタイプ | 'FARGATE' |
ECS サービス名 | expectedServiceName |
(3) ECS タスク
テストされるモノ | 期待値 |
---|---|
タスク定義で記されている CPU ユニット数 | 256 |
タスク定義で記されているメモリ量 | 512 |
タスク定義で記されているネットワークモード | 'awsvpc' |
テストの記述方法
テストの1行をピックアップしてテストの書き方について説明します。
assert.Equal(t, int64(1), awsSDK.Int64Value(cluster.ActiveServicesCount))
整数値: 1 と awsSDK.Int64Value(cluster.ActiveServicesCount) が同一であることをテストしている行ですが、cluster.ActiveServicesCount はどの様に知れば良いでしょう?答えとしては 'aws-sdk-go' のドキュメント (下記: クラスタ・サービス・タスク) を確認しつつテストを書いていく作業が必要になります。(この時にエディタの補完機能を使っていると容易に導き出せます)
テストの実行
上記で書いたテストを test ディレクトリに配置します。ディレクトリ構造は下記のようになります。
. ├── main.tf ├── output.tf └── test └── terraform-aws-ecs_test.go
では go コマンドを使ってテストを実行します。
cd test go mod init test go test
結果、全てのテストが終了すると結果の末尾に下記のようなログが出力されます。この時に terraform apply
, テスト実行, terraform destroy
が処理されるので、テスト完了までしばらく待つ必要があります。
PASS ok test 80.068s
失敗するケースとして誤ったテストを書きテストを実行すると下記のようなログが得られます。
--- FAIL: TestTerraformAwsEcs (91.04s) terraform-aws-ecs_test.go:60: Error Trace: terraform-aws-ecs_test.go:60 Error: Not equal: expected: "511" actual : "512" Diff: --- Expected +++ Actual @@ -1 +1 @@ -511 +512 Test: TestTerraformAwsEcs FAIL exit status 1 FAIL test 91.344s
上記の場合、ECS タスクに定義されたメモリ量が期待したものと異なっていたとエラー出力されています。
Awspec のテストを書いてみる
Awspec は国内のエンジニアの方が開発した AWS 状態をテストするツールです。海外の技術ブログやコードの中にも最近は登場するようになりました。今回 AWS インフラをデプロイするために用いた Terraform のコードに沿って Awspec テストを書いてみました。
require 'spec_helper' describe ecs_cluster('example') do it { should exist } it { should be_active } end describe ecs_service('example'), cluster: 'example' do it { should exist } it { should be_active } its(:cluster) { should eq 'example' } its(:service_name) { should eq 'example' } its(:service_arn) { should eq 'arn:aws:ecs:ap-northeast-1:********:service/example' } its(:cluster_arn) { should eq 'arn:aws:ecs:ap-northeast-1:********:cluster/example' } its(:desired_count) { should eq 0 } its(:pending_count) { should eq 0 } its(:running_count) { should eq 0 } end describe ecs_task_definition('terratest') do it { should exist } it { should be_active } its(:family) { should eq 'terratest' } end
※ awspec の Resource Type 'ecs_service' のドキュメントによると上記のような記述は書けませんが、, cluster: 'exmaple'
を追記してテストを実施することが実際には出来ました。今回も main.tf でデフォルトではないクラスタを作成しているので上記のように記述しています。尚、awspec のドキュメントに対して下記の通り PR をしておきました。
Terratest, Awspec のテストを比較・考察
両者ともに Terraform のデプロイを行った結果としての AWS インフラの状態をテストしているツールだということがわかりました。Awspec に関しては Resource Types の開発が追いつかないとそもそもテストが書けませんが、Terratest は AWS 公式のライブラリ 'aws-sdk-go' に沿ってテストを書くのでその心配は必要なさそうです。また Terratest は Terraform によるデプロイとテスト、その後のインフラの削除まで自動で行ってくれるツールですが Awspec は Terraform でデプロイした結果をテストするツールということで、その辺りの違いもあるようです。また Terratest は Terraform コードの output 文で得られた値もテストに利用できます。
一方で Awspec は Test-Kitchen (https://github.com/test-kitchen/test-kitchen) と Test-Kitchen の Terraform Driver である Kitchen-Terraform (https://github.com/newcontext-oss/kitchen-terraform) と組合せて、下記の URL にあるようなことも出来るようです。
Terraform デプロイし tfstate の状態と同一になっているかテストする例
これは 'terraform-aws-modules/terraform-aws-eks' という EKS の Terraform Module なのですがその中のファイル 'test_eks.rb' で下記のように記述されていて
require 'awspec' # rubocop:disable LineLength state_file = 'terraform.tfstate.d/kitchen-terraform-default-aws/terraform.tfstate' tf_state = JSON.parse(File.open(state_file).read) region = tf_state['modules'][0]['outputs']['region']['value'] ENV['AWS_REGION'] = region
これは Terraform デプロイした結果として得られる tfstate ファイルの通りに AWS インフラがデプロイされているかどうかを Awspec でテストしているように見えます。(まだ手元で動かしたわけではないので間違っていたら指摘頂けると助かります)
また、このテストを実行している .kitchen.yml
ファイルは下記のようになっています。
--- driver: name: "terraform" root_module_directory: "examples/basic" provisioner: name: "terraform" platforms: - name: "aws" verifier: name: "awspec" suites: - name: "default" verifier: name: "awspec" patterns: - "test/integration/default/test_eks.rb"
まとめ
Golang でテストを書くのか Rspec DSL でテストを書くのか、それによって選定してもいいですし、CI/CD のパイプラインの中でテストをどう自動実行するかを考慮して選定してもいいと思います。READYFOR では CI/CD に CodeBuild, CodePipeline を使っているので、その中でどう処理出来るのか、これから検討していきたいと思っています。