こんにちは、インフラ部の id:sue445 です。
Terraformなにもわからないけどディレクトリ構成の実例を晒して人類に貢献したい - エムスリーテックブログ や Terraformのディレクトリ構成の模索 - Adwaysエンジニアブログ を読んで影響されたのでピクシブのTerraform運用事例を紹介しようと思います。
Terraformの採用理由
Terraformと同じようなプロビジョニングツールとしてAWSであればCloudFormation*1、GCPであればCloud Deployment Manager*2 がそれぞれクラウドベンダから提供されています。
しかし、
- ピクシブではAWSとGCPを両方利用しているため同一ツールでメンテできた方が嬉しい
- Terraformはサードパーティ製だけど活発にバージョンアップされていて信用できる
という理由でTerraformを採用しています。
GitLabでのリポジトリ構成
TerraformのリポジトリはGitLabで管理しています。
GitLabではグループが何階層でも作れるため、「infra/(AWSアカウント名|GCPプロジェクト名)/Terraformのリポジトリ」のような構成で運用しています。
- pixivグループ:全エンジニアがMaintainer権限(GitHubのAdminに相当)を持っているグループ。(通常はここにリポジトリを作る)
- infraグループ:インフラ部メンバーのみがMaintainer権限を持っていて、他のエンジニアはDeveloper権限(GitHubのWriteに相当)を持っているグループ
このようにグループ毎に権限を分けている理由は下記です
- GitLabの仕様上、Maintainer権限を持っているとリポジトリに設定された機微情報が普通に見れてしまうため
- 特にTerraformはその性質上強い権限のアクセスキーやサービスアカウントを持っているため悪用されるのを防ぎたい
- Terraform系のリポジトリは全てmasterブランチで
terraform apply
、他のブランチでterraform plan
を実行するようなCIを構築しているが(後述)、不用意にmasterブランチが更新されると下手したら本番環境が破滅するためインフラ部メンバーのみがMergeRequestをマージできるようにしたい
また、AWSのアカウント単位・GCPのプロジェクト単位でさらにサブグループを作り、その下にTerraformのリポジトリを置くようにしています。
最初はこのようなサブグループは作らずにフラットな構成で運用していたのですが、AWSのアカウント(GCPのプロジェクト)ではTerraform以外にも用途毎にリポジトリが必要になることが多いのでサブグループを作るようにしました。
例えばGitLab CI用のAWSアカウントを管理するサブグループではTerraform以外にも下記のようなリポジトリがあります。
Terraformのファイル構成
例えば以前insideで紹介した rendertron で使っているTerraformだと下記のようなファイル構成になっています。
├── README.md ├── .envrc ├── .gitignore ├── .gitlab-ci.yml ├── .terraform-version ├── account.tf ├── backend.tf ├── cloud_audit_logging.tf ├── modules │ └── rendertron_cluster │ ├── cloud_load_balancing.tf │ ├── cloud_nat.tf │ ├── gke.tf │ ├── inputs.tf │ └── vpc.tf ├── rendertron_prestage.tf ├── rendertron_production.tf ├── service_account-datadog.tf ├── service_account-gcr_pusher.tf ├── service_account-gke_deploy.tf └── variables.tf
エムスリーさんとは対称的にmoduleはほとんど作っていません。
僕は下記のような理由でTerraformのmoduleを多用すべきではないと思っているためです。*3
- 一度module化するとmoduleの構成に合わないようなリファクタリングが必要になった時に
terraform state mv
が必要になってとたんにつらくなるため - Terraformのmoduleを細分化しすぎると
variable
とoutput
が大量に必要になって書きづらくなる
個人的にTerraformのmoduleは 再利用できる複数のリソースの単位(プログラミング言語でいうところの関数みたいなもの) で作るのがしっくりきています。
moduleがうまく使えたと思っている事例
1つ目は環境毎に全く同じリソース群(GCPだとVPC, GCLB, Cloud SQLなど)をセットで作る必要がある場合です。
開発環境と本番環境でVPC含めて完全に別にしたいというのはよくあります。
下記は rendertron_production.tf
ですが、スペックやnode数など本番環境と開発環境で変わりうるものは全てmoduleに variable
として渡すことによりいい感じに設定を共通化できたと思っています。
module "rendertron_production" { source = "./modules/rendertron_cluster" environment = "production" region = local.region gke_node_version = "1.16.8-gke.15" gke_location = local.region gke_zones = local.gce_tokyo_n2_available_zones # NOTE: # * rendertronの場合n1-standardの1/4のメモリ(1 vCPU辺り960MB)が一番バランスがいい # * customの場合メモリは256MBの倍数で指定する必要がある # https://cloud.google.com/dataproc/docs/concepts/compute/custom-machine-types # GKE gke_primary_node_machine_type = "n2-custom-8-7680" # 8 vCPU, 7.5GB RAM gke_primary_node_disk_size_gb = 128 gke_primary_node_count = 1 gke_autoscale_node_machine_type = "n2-custom-32-30720" # 32 vCPU, 30GB RAM gke_autoscale_node_disk_size_gb = 128 gke_autoscale_node_min_count = 0 gke_autoscale_node_max_count = 7 gke_autoscale_node_preemptible = true }
2つ目はサービス固有でない汎用的な機能です。
Terraformは別のリポジトリからもmoduleを読み込むことができるため、他のTerraformでも使いそうになる汎用性のある機能を予めmoduleで作っておいて、実際に他のTerraformリポジトリで使いたくなったタイミングでmodule専用のリポジトリに抽出することはよくやっています。
GitLab CIでTerraformをいい感じにCIする
ピクシブにはAWSとGCP合わせて計10個以上のTerraformリポジトリがあります。
AWSアカウントやGCPプロジェクト作成の度にCIの初期設定したり複数リポジトリで設定修正をするのは大変なので、冒頭のGitLabのスクショにも出てきた pixiv/ci-templates/terraform-template
でCIのテンプレートファイルを一元管理し、各Terraformのリポジトリからincludeするようにしています。
テンプレートの使い方
各Terraformのリポジトリでは下記のような必要最低限の .gitlab-ci.yml
を書いていい感じにCIのワークフローを実現しています。
include: - project: "pixiv/ci-templates/terraform-template" file: "/template.yml" variables: TERRAFORM_VERSION: "0.12.16" TFLINT_VERSION: "0.9.2" TFNOTIFY_VERSION: "0.7.0"
Terraformはバージョン間の差異が激しいので利用側でバージョンを指定し、Terraformのバージョンアップ時は TERRAFORM_VERSION
のみ書き換える運用をしています。
ピクシブで実際に使っているテンプレートファイル
テンプレートファイルの中身と解説です。
一部項目を削っていますが下記が実際に使ってるTerraformのCIで使ってるテンプレートファイルです。
variables: TERRAFORM_VERSION: "" TFLINT_VERSION: "" TFNOTIFY_VERSION: "0.4.0" .terraform: image: name: hashicorp/terraform:${TERRAFORM_VERSION} entrypoint: [""] stages: - test - deploy cache: key: "${CI_JOB_NAME}" untracked: true paths: - .terraform/plugins/ .setup_terraform: &setup_terraform # moduleのキャッシュが悪さして存在するはずのrefが見つからないことがあるのでinit前に削除する - rm -rf .terraform/modules/ - terraform init -input=false .setup_tfnotify: &setup_tfnotify # Setup tfnotify - | if [[ ! -f tfnotify_${TFNOTIFY_VERSION}_linux_amd64.tar.gz ]]; then set +e wget https://github.com/mercari/tfnotify/releases/download/v${TFNOTIFY_VERSION}/tfnotify_linux_amd64.tar.gz -O tfnotify_${TFNOTIFY_VERSION}_linux_amd64.tar.gz; ret=$? set -e # v0.6.0以降のファイル名でマッチしなければそれ以前のファイル名にフォールバックする if [[ $ret -ne 0 ]]; then wget https://github.com/mercari/tfnotify/releases/download/v${TFNOTIFY_VERSION}/tfnotify_${TFNOTIFY_VERSION}_linux_amd64.tar.gz fi fi - tar zxf tfnotify_${TFNOTIFY_VERSION}_linux_amd64.tar.gz # tfnotifyはyamlのフィールドに自由に変数を入れられないのでシェルの変数からyamlを動的生成する - | cat > tfnotify.yml << YAML --- ci: gitlabci notifier: gitlab: token: $TFNOTIFY_GITLAB_TOKEN base_url: https://gitlab.example.com/api/v4 repository: owner: $CI_PROJECT_NAMESPACE name: $CI_PROJECT_NAME terraform: fmt: template: | {{ .Title }} {{ .Message }} {{ .Result }} {{ .Body }} plan: template: | {{ .Title }} <sup>[CI link]( {{ .Link }} )</sup> {{ .Message }} {{if .Result}} <pre><code> {{ .Result }} </pre></code> {{end}} <details><summary>Details (Click me)</summary> <pre><code> {{ .Body }} </pre></code></details> apply: template: | {{ .Title }} {{ .Message }} {{if .Result}} <pre><code> {{ .Result }} </pre></code> {{end}} <details><summary>Details (Click me)</summary> <pre><code> {{ .Body }} </pre></code></details> YAML tflint: extends: .terraform stage: test script: - *setup_terraform # tflintのDockerイメージを使うとterraform initできなくてmoduleが読めないためバイナリをDLする - cd /tmp - wget https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/tflint_linux_amd64.zip - unzip tflint_linux_amd64.zip - ./tflint only: variables: - $TFLINT_VERSION != "" tags: - docker terraform-fmt: extends: .terraform stage: test script: - *setup_terraform - | set +e terraform fmt -recursive -check ret=$? set -e if [ $ret -ne 0 ]; then echo '[ERROR] terraform fmt -recursive をするか下記の修正を行ってpushを行ってください' terraform fmt -recursive git --no-pager diff fi exit $ret tags: - docker terraform-plan: extends: .terraform stage: test script: - *setup_terraform - *setup_tfnotify - terraform plan -input=false | ./tfnotify plan except: - master resource_group: terraform-tfstate terraform-apply: extends: .terraform stage: deploy script: - *setup_terraform - *setup_tfnotify - terraform plan -input=false | ./tfnotify plan - terraform apply -input=false -auto-approve | ./tfnotify apply only: - master resource_group: terraform-tfstate terraform-apply-manual: extends: .terraform stage: deploy script: - *setup_terraform - *setup_tfnotify - terraform plan -input=false | ./tfnotify plan - terraform apply -input=false -auto-approve | ./tfnotify apply except: - master when: manual resource_group: terraform-tfstate
実はGitLabだとTerraformの汎用的なCIのテンプレートが提供されているのですが*4、このテンプレートが作られる前に既に自分でテンプレートを作ったためGitLabのテンプレートを使っていません。
このテンプレートでできること
masterブランチ以外
terraform plan
,terraform fmt
, https://github.com/terraform-linters/tflint を自動実行terraform apply
を手動実行できるようになる- MergeRequest上で試行錯誤したい場合によく使っています
masterブランチ
terraform apply
を自動実行
このテンプレートファイルのポイント
- https://github.com/mercari/tfnotify を使い
terraform plan
やterraform apply
の結果をMergeRequestにコメントで出せるようにしている- GitLabにコメントつけるために必要なGitLabのAccess TokenはGitLab Runnerの設定に入れることにより、Terraformの各リポジトリで機微情報の設定を不要にしている
- tfnotifyに自由に変数を入れることができないため、
.gitlab-ci.yml
の中でtfnotify用のYAMLファイルを動的生成する というかなり黒魔術なハックを行っている - DynamoDBやGCSで
terraform.tfstate
の排他ロックを行っているとterraform plan
やterraform apply
を同時に1つだけしか実行したくないということがよくあるが、GitLab CIだとresource_group
を使うことでこのように「通常はCIのqueueが空いていれば可能な限り並列実行してほしいが特定のジョブだけは1つずつ実行してほしい」という同時実行数の制御ができるようになるterraform.tfstate
を安全に管理するには前述の排他制御が必須なのだが、同時実行数の制御ができないとトピックブランチでforce pushしたような場合に後からpushした方のビルドが排他ロックのエラーで失敗してちょっとイラッとする- 余談ですがこのようなジョブの同時実行数の制御は現時点ではTravis CI, CircleCI, Wercker, GitHub Actionsには実装されておらず、GitLab CI独自の機能になります。(CIマニア特有の早口)
- さらに余談ですが
resource_group
登場前はlimit = 1
のRunnerを作るしかなかったので割と面倒でした... *5
最後に
簡単ではありますがピクシブのTerraformの運用を紹介させていただきました。
Terraformは各会社の文化によって運用が変わってくると思うので全部真似できないかもしれないですが、もし運用方法が定まっていなくて迷っているなら参考になれば幸いです。