こんにちは、今年の4月に新卒で入社したデータエンジニアの中村です。
この記事はEnigmo Advent Calendar 2023 の11日目の記事です。
入社してからは、社内のデータを利用者がより使いやすくなるように、データ基盤の整備・データ連携を進めております。
最近弊社のデータ基盤をTerraformによる管理に移行するタスクに取り組んでいるので、どのようなディレクトリ構成を採用したか説明していこうと思います。
データ基盤をこれからTerraform化しようとしている方に有益な情報となれば幸いです。
目次
背景
弊社のデータ基盤はGCP上にあり、BigQueryをはじめとした様々なサービスを活用してデータを収集しています。
GCPのサービスを利用する際に、これまではコンソール画面からボタンをポチポチしてリソースを作成していたのですが、それだと以下のような課題があります。
作業ミスによって誤ったリソースが作成されてしまう。
GUIでの設定だと再現性が低くなってしまう。
リソースの管理が大変。(このリソース消しちゃって大丈夫だっけ?いつ誰が作ったんだ??みたいな状態が起こる。)
同じような設定のリソースを繰り返し作成するのに効率が悪い。
これらの課題を解決するため、IaCサービスであるTerraformを活用し、GCP上のサービスをコードベースでの管理に移行することに決めました。
ディレクトリ構成
結論から行くと、色々試行錯誤した末に決定した弊社データ基盤のディレクトリ構成のベストプラクティスはこちらになります。
terraform ├── root (ルートモジュール) │ ├── bigquery │ │ ├── environments │ │ │ ├── development │ │ │ │ ├── terraform.tfbackend (開発環境のstateファイルのパス) │ │ │ │ ├── terraform.tfvars (開発環境の変数) │ │ │ │ └── dataset │ │ │ │ ├── <dataset>.yaml (yamlでデータセット毎に管理) │ │ │ │ ・・・ │ │ │ └── production │ │ │ ├── terraform.tfbackend (本番環境のstateファイルのパス) │ │ │ ├── terraform.tfvars (本番環境の変数) │ │ │ └── dataset │ │ │ ├── <dataset>.yaml │ │ │ ・・・ │ │ ├── main.tf │ │ ├── variables.tf │ │ └── versions.tf │ ├── datastream │ ├── <service_name> │ ・・・ └── modules(子モジュール) ├── bigquery ├── datastream ├── cloud_monitoring ├── secret_manager ├── <service_name> ・・・
構成の説明
各ディレクトリの役割
今回作成した構成は、大別するとrootディレクトリとmodulesディレクトリに分かれており、各役割は以下の通りです。
- root: modulesディレクトリで定義したモジュールを呼び出す
- modules: 再利用性の高いモジュールを配置
datastreamの例だと、以下のように、modulesで定義した複数のモジュールをrootから呼び出すことでリソースを定義しています。
module "secret_manager" { source = "../../../modules/secret_manager" ・・・ } module "monitoring" { source = "../../../modules/monitoring" ・・・ } module "data_stream" { source = "../../../modules/datastream/postgresql" ・・・ }
stateファイルの分割粒度
この構成では、stateファイルはリソース毎に分割しています。
stateファイルの粒度が大きすぎると、その分リリース毎の影響範囲が広くなってしまいます。
現状そこまで管理対象が多くないので、1環境1stateファイルのようなモノリシックな構成でも良かったですが、今後スケールすることを想定して、管理が煩雑にならないようにリソース粒度で分割しました。
工夫したポイント
ここまで聞くと、意外とスタンダードな構成だと感じた方も多いのではないでしょうか。
しかし、いくつか工夫した点があるのでそちらも説明していきたいと思います。
1. 環境差分の切り出し
リソース毎に分割する構成の場合、よくあるのが以下の構成です。
├── root │ ├── bigquery │ │ ├── development │ │ │ ├── main.tf │ │ │ ・・・ │ │ └── production │ │ ├── main.tf │ │ ・・・ │ ├── <service> │ │ ├── development │ │ └── production
この構成だと、「developmentをコピペしてproductionを作成して、差分だけ修正して・・」と二度手間になってしまいます。
その無駄な作業を省くために、環境毎で中身の変わらないファイルは共有する構成にしました。
├── root │ ├── bigquery │ │ ├── environments │ │ │ ├── development │ │ │ └── production │ │ ├── main.tf │ │ ├── variables.tf │ │ └── versions.tf
この構成のメリットは、環境毎の設定値差分だけenvironmentsディレクトリに記載すればその他共通のファイルを繰り返し作成する必要がないので、コーディング量が減らせるところです。
また、開発環境だけあるリソースを作成したいといった場合は、tfvarsとモジュール側でうまく吸収することで環境差分にも対応できます。
Workspacesの利用も考えましたが、公式では開発・本番環境毎に分けるような利用方法は非推奨だったので、今回は利用を見送りました。
2. BigQueryのリソースはyamlで管理
データ基盤の中でも中核をなすBigQueryの管理についてですが、一つのtfvarsで管理したいとなるとファイルが膨大になってしまします。
そこで、データセット毎にyamlファイルを作成することで見通しをよくしました。
本来tfvarsで全てのデータセットを管理すると数千行は当たり前に行くところを、以下のようにデータセット毎に設定ファイルを分割することで管理が非常に楽になりました。
dataset_id: "hoge" description: "hogehoge" location: "US" access_roles_group_by_email: "hoge@hoge.co.jp": ["READER"] access_roles_user_by_email: "hogehoge@hogehoge.co.jp": ["READER"]
Terraformでは、yamldecode関数が用意されているので、main.tf側で以下のように記述することでTerraformのmapとして扱うことができます。
locals { yaml_files = fileset("./environments/${var.env}/dataset", "*.yaml") datasets = { for file in local.yaml_files : element(split(".", basename(file)), 0) => yamldecode(file("./environments/${var.env}/dataset/${file}")) } }
今後の展望
まだTerraform化できていないサービスがあるので、引き続き完全Terraform化を目指していきたいと思います。
また、リリース作業は現在手動で行なっているので、CI/CD環境も整えて作業効率を高めていきたいところです。
最後に
データ基盤のTerraformディレクトリ構成に関する記事はネット上を探しても事例が少なく、ゼロから作るのは大変苦労しました。少しでも参考になった部分があれば幸いです。
最後までご覧いただきありがとうございました!
明日の記事の担当は、UIUXデザイナーの和田さんです。お楽しみに!
株式会社エニグモ すべての求人一覧