背景
権限管理を含め、BigQueryのデータセットの管理をTerraformで行なっている人は多いと思います。Terraformでデータセットを管理する際、descriptionやlabelsなどのメタデータはデータマネジメント(データ品質やセキュリティ)でも重要です。
description- データセットやテーブルの棚卸しをする際、
descriptionが入っていないと用途の把握などに時間がかかってしまいます
- データセットやテーブルの棚卸しをする際、
labels- labelsは様々なメタデータをkey/value形式で持たせることができます。代表的な事例は以下にまとまっています
- 特に
ownerやcreated_byは重要です- 利用者視点ではデータのことで分からないことがあれば誰に聞けばいいかの情報になりますし、管理者視点であれば問題があった場合の問い合わせ先にもなります
こういった重要なメタデータはなるべく入力したいですが、慈善活動だと中々入力されないことが多いと思います(レビューで毎回指摘するおじさん役はやりたくない)。必要なメタデータが必ず入力される、validationされるということを保証するため、TerraformのModuleを使うアプローチを書きました。
ただ、TerraformのModuleの使い方は組織によってポリシーが色々あると思うので、Moduleを使うことなく素のgoogle_bigquery_datasetに対して制約を付けたい、というケースはあると思います。
ConftestによるTerraformのポリシーテスト
私は全然使ったことがなかったですが、Terraformを含む構造化された設定ファイル(KubernetesやDockerfileとかも対象にできるらしい)のポリシーテストをできるConftestというのがあるそうです。
例: ConftestでBigQueryのデータセットのlabelにownerが設定されていることをテストする
具体例を見たほうが雰囲気を掴めると思うので、実際に見ていきましょう。細かい文法は自分も全然まだ把握しきれていないですが、labelsやlabels.ownerがnullでないlabels.ownerが空文字でない、などを制約としてTerraformに対してポリシーテストを行なうことができます。
package gcp.missing_label_owner_google_bigquery_dataset
target_resources = ["google_bigquery_dataset"]
missing(r) {
not r.change.after.labels
}
missing(r) {
r.change.after.labels == null
}
missing(r) {
not r.change.after.labels.owner
}
missing(r) {
r.change.after.labels.owner == null
}
missing(r) {
r.change.after.labels.owner == ""
}
deny_missing_label_owner_google_bigquery_dataset[{"msg": reason}] {
resource := input.resource_changes[_]
resource_type := resource.type
resource_address := resource.address
resource_actions := resource.change.actions[_]
target_resources[_] == resource_type
resource_actions != "no-op"
resource_actions != "delete"
missing(resource)
reason := sprintf("`%v`: `owner` field in `labels` is missing", [resource_address])
}
実際のリソースに対してポリシーテストしてもいいですし、小さい事例を入力として手元でテストすることもできます。例えば以下のようなテストコードを書くことができます。
package gcp.missing_or_wrong_label_managed_by_terraform_google_bigquery_dataset
test_deny_missing_or_wrong_label_managed_by_terraform_google_bigquery_dataset__label_managed_by_is_undefined {
reason := "`google_bigquery_dataset.test_dataset`: `managed_by` field in `labels` is missing or not terraform"
input := {"resource_changes": [{
"address": "google_bigquery_dataset.test_dataset",
"mode": "managed",
"type": "google_bigquery_dataset",
"name": "test_dataset",
"provider_name": "registry.terraform.io/hashicorp/google",
"change": {
"actions": ["create"],
"before": null,
"after": {
"access": [{
"dataset": [],
"domain": "",
"group_by_email": "",
"iam_member": "",
"role": "READER",
"routine": [],
"special_group": "",
"user_by_email": "me@example.com",
"view": [],
}],
"creation_time": 1234567890,
"dataset_id": "test_dataset",
"default_collation": "",
"default_encryption_configuration": [],
"default_partition_expiration_ms": 0,
"default_table_expiration_ms": 0,
"delete_contents_on_destroy": false,
"description": "",
"effective_labels": {"owner": "developer"},
"etag": "abcdefg",
"friendly_name": "",
"id": "projects/test-project/datasets/test_dataset",
"is_case_insensitive": false,
"labels": {"owner": "developer"},
"last_modified_time": 1234567890,
"location": "US",
"max_time_travel_hours": "",
"project": "test-project",
"self_link": "https://bigquery.googleapis.com/bigquery/v2/projects/test-project/datasets/test_dataset",
"storage_billing_model": "PHYSICAL",
"terraform_labels": {"owner": "developer"},
"timeouts": null,
},
},
}]}
deny_missing_or_wrong_label_managed_by_terraform_google_bigquery_dataset[{"msg": reason}] with input as input
}
test_deny_missing_or_wrong_label_managed_by_terraform_google_bigquery_dataset__label_managed_by_is_not_terraform {
reason := "`google_bigquery_dataset.test_dataset`: `managed_by` field in `labels` is missing or not terraform"
input := {"resource_changes": [{
"address": "google_bigquery_dataset.test_dataset",
"mode": "managed",
"type": "google_bigquery_dataset",
"name": "test_dataset",
"provider_name": "registry.terraform.io/hashicorp/google",
"change": {
"actions": ["create"],
"before": null,
"after": {
"access": [{
"dataset": [],
"domain": "",
"group_by_email": "",
"iam_member": "",
"role": "READER",
"routine": [],
"special_group": "",
"user_by_email": "me@example.com",
"view": [],
}],
"creation_time": 1234567890,
"dataset_id": "test_dataset",
"default_collation": "",
"default_encryption_configuration": [],
"default_partition_expiration_ms": 0,
"default_table_expiration_ms": 0,
"delete_contents_on_destroy": false,
"description": "",
"effective_labels": {"owner": "developer", "labels": "NOT_terraform"},
"etag": "abcdefg",
"friendly_name": "",
"id": "projects/test-project/datasets/test_dataset",
"is_case_insensitive": false,
"labels": {"owner": "developer", "labels": "NOT_terraform"},
"last_modified_time": 1234567890,
"location": "US",
"max_time_travel_hours": "",
"project": "test-project",
"self_link": "https://bigquery.googleapis.com/bigquery/v2/projects/test-project/datasets/test_dataset",
"storage_billing_model": "PHYSICAL",
"terraform_labels": {"owner": "developer", "labels": "NOT_terraform"},
"timeouts": null,
},
},
}]}
deny_missing_or_wrong_label_managed_by_terraform_google_bigquery_dataset[{"msg": reason}] with input as input
}
手元でテストを実行する際はconftest verify --policy . --report failsという形で実行します。--report failsを付けないと落ちたテスト名しか出てこないので、何で落ちたか全然分からなくて困りました。
実際の業務への取り込み方
labelsが付いていないデータセットが多い状態でこれまで紹介したようなconftestを書くと、CIが落ちまくって大変なことになってしまいます。
- 気合で最初は
labelsを埋めてしまう - それ以降のデータセットの追加はconftestでポリシーテストが常にされる
という進め方をする形になるかと思います(最後に頼れるの筋肉だけ...)。対象となるプロジェクトを絞る、など小さいスコープから始めるのも手ですね。また、descriptionはなかなか埋めるのが難しかったりするので、ひとまず埋めやすいlabels.ownerあたりから外堀を埋めていって「descriptionも当然書くよね...?」という雰囲気に持っていくのがいいかなぁと考えています。
データマネジメント / データガバナンスがきちんとされている状態を保っていくのも楽ではないですが、conftestなどこういったツールを適材適所で使って攻めにも工数を使っていきやすいようにしていきましょう。