背景
権限管理を含め、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などこういったツールを適材適所で使って攻めにも工数を使っていきやすいようにしていきましょう。