これは、なにをしたくて書いたもの?
GitLabにはTerraformのステートを管理する機能があります。
こちらは以前にも使ったことがあるのですが、複数人で操作した時にどういう挙動になるんだっけ?というのを見ていなかったので
試してみることにしました。
結論を言うと、余計な心配だった気がしますが。
GitLabでTerraformのステートを管理する
機能としてはこちらですね。
GitLab-managed Terraform/OpenTofu state | GitLab Docs
GitLabプロジェクト、ユーザーと対応するアクセストークンがあれば利用できます。アクセストークンに必要なスコープはapiです。
the token must have the API scope
こちらは以前にも使ったことがあります。
GitLabをTerraformのStateの保存先として使う - CLOVER🍀
お題
今回は、以下のようにTerraformのステートはGitLabで管理、Terraformで操作するリソースはMySQLにします。
flowchart LR
A["GitLab"]
B["MySQL"]
C["mysql1/Terraform"] -- State --> A
D["mysql2/Terraform"] -- State --> A
C -- Resource --> B
D -- Resource --> B
Terraformを操作する箱が2つありますが、ここにGitLabのユーザーをそれぞれ割り当てます。つまり2人で操作することを
エミュレーションします。
GitLabリソースもTerraformで操作可能ですが、ごちゃごちゃになりそうな気がしたので…。
環境
今回の環境はこちら。
GitLab。
$ sudo gitlab-rake gitlab:env:info System information System: Ubuntu 24.04 Current User: git Using RVM: no Ruby Version: 3.2.5 Gem Version: 3.6.9 Bundler Version:2.6.5 Rake Version: 13.0.6 Redis Version: 7.2.9 Sidekiq Version:7.3.9 Go Version: unknown GitLab information Version: 18.2.1 Revision: baccadafcda Directory: /opt/gitlab/embedded/service/gitlab-rails DB Adapter: PostgreSQL DB Version: 16.8 URL: http://192.168.0.6 HTTP Clone URL: http://192.168.0.6/some-group/some-project.git SSH Clone URL: git@192.168.0.6:some-group/some-project.git Using LDAP: no Using Omniauth: yes Omniauth Providers: GitLab Shell Version: 14.43.0 Repository storages: - default: unix:/var/opt/gitlab/gitaly/gitaly.socket GitLab Shell path: /opt/gitlab/embedded/service/gitlab-shell Gitaly - default Address: unix:/var/opt/gitlab/gitaly/gitaly.socket - default Version: 18.2.1 - default Git Version: 2.50.1.gl1
Terraform。
$ terraform version Terraform v1.12.2 on linux_amd64
MySQL localhost:33060+ ssl SQL > select version(); +-----------+ | version() | +-----------+ | 8.4.6 | +-----------+ 1 row in set (0.0008 sec)
準備
ひとまずGitLabプロジェクトが必要なのですが、そちらは作成済みとしました。プロジェクトのidは19とします。
ユーザーとアクセストークンは以下の2つを使います。
- sample-user1/glpat--9d3GTXe4cHrzBRThVhe
- sample-user2/glpat-nssudNsDdxYNkyKjUwW7
なお、GitLabのリソースは結局Terraformで作っているので、そちらは最後に載せることにします。
sample-user1でTerraformを使う
最初はsample-user1でTerraformを使いましょう。
Terraformの構成ファイルとしては、こんなのを用意。
terraform.tf
terraform { required_version = "1.12.2" required_providers { mysql = { source = "petoju/mysql" version = "3.0.81" } } backend "http" { address = "http://192.168.0.6/api/v4/projects/19/terraform/state/mysql" lock_address = "http://192.168.0.6/api/v4/projects/19/terraform/state/mysql/lock" unlock_address = "http://192.168.0.6/api/v4/projects/19/terraform/state/mysql/lock" lock_method = "POST" unlock_method = "DELETE" retry_wait_min = 5 } }
main.tf
provider "mysql" { endpoint = "172.17.0.2:3306" username = "root" password = "password" } resource "mysql_database" "example" { name = "example" }
MySQLのリソースとしては、データベースを作成しているだけです。
GitLab上のステートの情報は、認証情報以外はそのまま書くことにしました。
backend "http" { address = "http://192.168.0.6/api/v4/projects/19/terraform/state/mysql" lock_address = "http://192.168.0.6/api/v4/projects/19/terraform/state/mysql/lock" unlock_address = "http://192.168.0.6/api/v4/projects/19/terraform/state/mysql/lock" lock_method = "POST" unlock_method = "DELETE" retry_wait_min = 5 }
terraform init。この時に認証情報を指定します。
$ terraform init -backend-config='username=sample-user1' -backend-config='password=glpat--9d3GTXe4cHrzBRThVhe'
環境変数TF_HTTP_USERNAME、TF_HTTP_PASSWORDで指定してもいいかもですね。
Backend Type: http | Terraform | HashiCorp Developer
ローカルにはこんなファイルができます。
.terraform/terraform.tfstate
{ "version": 3, "terraform_version": "1.12.2", "backend": { "type": "http", "config": { "address": "http://192.168.0.6/api/v4/projects/19/terraform/state/mysql", "client_ca_certificate_pem": null, "client_certificate_pem": null, "client_private_key_pem": null, "lock_address": "http://192.168.0.6/api/v4/projects/19/terraform/state/mysql/lock", "lock_method": "POST", "password": "glpat--9d3GTXe4cHrzBRThVhe", "retry_max": null, "retry_wait_max": null, "retry_wait_min": 5, "skip_cert_verification": null, "unlock_address": "http://192.168.0.6/api/v4/projects/19/terraform/state/mysql/lock", "unlock_method": "DELETE", "update_method": null, "username": "sample-user1" }, "hash": 1888410828 } }
この時点では、まだGitLab上にTerraformのステートはありません。
applyします。
$ terraform apply
データベースが作成され、ステートもできました。
$ curl -H 'Private-Token: glpat--9d3GTXe4cHrzBRThVhe' http://192.168.0.6/api/v4/projects/19/terraform/state/mysql
{
"version": 4,
"terraform_version": "1.12.2",
"serial": 1,
"lineage": "edf1661b-66e2-d054-8572-1585d4b36e00",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "mysql_database",
"name": "example",
"provider": "provider[\"registry.terraform.io/petoju/mysql\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"default_character_set": "utf8mb4",
"default_collation": "utf8mb4_general_ci",
"id": "example",
"name": "example"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
}
],
"check_results": null
}
このファイルにどんな情報が入るのか覚えていなかったのですが、この内容を見た時点で特に今回のお題は困らなさそうだなと
思いました。
sample-user2でTerraformを使う
次はsample-user2でTerraformを使います。sample-user1の続きをするわけです。
Terraformの構成ファイルはこんな感じにしました。
terraform.tf
terraform { required_version = "1.12.2" required_providers { mysql = { source = "petoju/mysql" version = "3.0.81" } } backend "http" { address = "http://192.168.0.6/api/v4/projects/19/terraform/state/mysql" lock_address = "http://192.168.0.6/api/v4/projects/19/terraform/state/mysql/lock" unlock_address = "http://192.168.0.6/api/v4/projects/19/terraform/state/mysql/lock" lock_method = "POST" unlock_method = "DELETE" retry_wait_min = 5 } }
main.tf
provider "mysql" { endpoint = "172.17.0.2:3306" username = "root" password = "password" } resource "mysql_database" "example" { name = "example" } resource "mysql_user" "app" { user = "app" host = "%" plaintext_password = "password" }
sample-user1の時と比べて、ユーザーを追加しています。
terraform init。こちらはsample-user2のアクセストークンを使います。
$ terraform init -backend-config='username=sample-user2' -backend-config='password=glpat-nssudNsDdxYNkyKjUwW7'
無事に終了。
ローカルのファイルはこんな感じです。
.terraform/terraform.tfstate
{ "version": 3, "terraform_version": "1.12.2", "backend": { "type": "http", "config": { "address": "http://192.168.0.6/api/v4/projects/19/terraform/state/mysql", "client_ca_certificate_pem": null, "client_certificate_pem": null, "client_private_key_pem": null, "lock_address": "http://192.168.0.6/api/v4/projects/19/terraform/state/mysql/lock", "lock_method": "POST", "password": "glpat-nssudNsDdxYNkyKjUwW7", "retry_max": null, "retry_wait_max": null, "retry_wait_min": 5, "skip_cert_verification": null, "unlock_address": "http://192.168.0.6/api/v4/projects/19/terraform/state/mysql/lock", "unlock_method": "DELETE", "update_method": null, "username": "sample-user2" }, "hash": 1888410828 } }
terraform planを実行してみましょう。
$ terraform plan
mysql_database.example: Refreshing state... [id=example]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# mysql_user.app will be created
+ resource "mysql_user" "app" {
+ discard_old_password = false
+ host = "%"
+ id = (known after apply)
+ plaintext_password = (sensitive value)
+ tls_option = "NONE"
+ user = "app"
}
Plan: 1 to add, 0 to change, 0 to destroy.
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
sample-user1の操作からの差分になっていますね。
apply。
$ terraform apply
ステートを確認してみましょう。
$ curl -H 'Private-Token: glpat-nssudNsDdxYNkyKjUwW7' http://192.168.0.6/api/v4/projects/19/terraform/state/mysql
{
"version": 4,
"terraform_version": "1.12.2",
"serial": 2,
"lineage": "edf1661b-66e2-d054-8572-1585d4b36e00",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "mysql_database",
"name": "example",
"provider": "provider[\"registry.terraform.io/petoju/mysql\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"default_character_set": "utf8mb4",
"default_collation": "utf8mb4_general_ci",
"id": "example",
"name": "example"
},
"sensitive_attributes": [],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
},
{
"mode": "managed",
"type": "mysql_user",
"name": "app",
"provider": "provider[\"registry.terraform.io/petoju/mysql\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"aad_identity": [],
"auth_plugin": null,
"auth_string_hashed": null,
"auth_string_hex": null,
"discard_old_password": false,
"host": "%",
"id": "app@%",
"password": null,
"plaintext_password": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
"retain_old_password": null,
"tls_option": "NONE",
"user": "app"
},
"sensitive_attributes": [
[
{
"type": "get_attr",
"value": "auth_string_hashed"
}
],
[
{
"type": "get_attr",
"value": "auth_string_hex"
}
],
[
{
"type": "get_attr",
"value": "password"
}
],
[
{
"type": "get_attr",
"value": "plaintext_password"
}
]
],
"identity_schema_version": 0,
"private": "bnVsbA=="
}
]
}
],
"check_results": null
}
よさそうですね。
というわけで、GitLab上のTerraformステートを複数のユーザーで操作できました、と。
どうしてこれを気にしたのか?
気になったのは、トラブルシュートに関するこちらの記述を見かけたからです。
こちらはGitLab CI/CD上でTerraformを使う($CI_JOB_TOKENを使う)時に、terraform planを行うジョブと
その結果を使うジョブが別々になる場合は$CI_JOB_TOKENがジョブを跨げなくてエラーとなるという話です。
これを見て、そういえば複数ユーザーで操作するとどうなるんだろう?と思ったのですが、人に割り当てられたアクセストークンを
使う場合はあまり気にする話ではなかったようです。
オマケ: GitLabリソースをTerraformで作成する
最後はGitLabのリソースを作成したTerraformコードを載せておきます。
terraform.tf
terraform { required_version = "1.12.2" required_providers { gitlab = { source = "gitlabhq/gitlab" version = "18.2.0" } } }
main.tf
variable "root_access_token" { type = string ephemeral = true } provider "gitlab" { token = var.root_access_token base_url = "http://192.168.0.6/" } resource "gitlab_group" "sample_group" { name = "sample group" path = "sample-group" } resource "gitlab_project" "sample_app" { name = "sample-app" namespace_id = gitlab_group.sample_group.id default_branch = "main" visibility_level = "private" auto_devops_enabled = false only_allow_merge_if_pipeline_succeeds = true only_allow_merge_if_all_discussions_are_resolved = true } resource "gitlab_branch_protection" "main_branch" { project = gitlab_project.sample_app.id branch = "main" allow_force_push = false merge_access_level = "maintainer" push_access_level = "no one" unprotect_access_level = "maintainer" } resource "gitlab_group_membership" "sample_user1" { group_id = gitlab_group.sample_group.id user_id = gitlab_user.sample_user1.id access_level = "owner" } resource "gitlab_user" "sample_user1" { name = "sample-user1" username = "sample-user1" password = "P@ssw0rd" email = "sample-user1@example.com" } resource "gitlab_personal_access_token" "sample_user1" { user_id = gitlab_user.sample_user1.id name = "access token" expires_at = "2025-09-02" scopes = ["api"] } resource "gitlab_group_membership" "sample_user2" { group_id = gitlab_group.sample_group.id user_id = gitlab_user.sample_user2.id access_level = "owner" } resource "gitlab_user" "sample_user2" { name = "sample-user2" username = "sample-user2" password = "P@ssw0rd" email = "sample-user2@example.com" } resource "gitlab_personal_access_token" "sample_user2" { user_id = gitlab_user.sample_user2.id name = "access token" expires_at = "2025-09-02" scopes = ["api"] } output "project_id" { value = gitlab_project.sample_app.id } output "sample_user1_pat" { value = gitlab_personal_access_token.sample_user1.token sensitive = true } output "sample_user2_pat" { value = gitlab_personal_access_token.sample_user2.token sensitive = true }
$ export TF_VAR_root_access_token=...
リソース作成。
$ terraform init $ terraform apply
アクセストークンはterraform output全体では見れませんが、個別に指定すると確認できます。
$ terraform output sample_user1_pat "glpat--9d3GTXe4cHrzBRThVhe" $ terraform output sample_user2_pat "glpat-EyTiBikzAbHd-BdxsHny"
おわりに
TerraformのRemote StateをGitLabに保存して、複数のユーザーで操作してみました。
これをやろうとしたきっかけはトラブルシュートのガイドでしたが、それ自体はあまり心配することではなかったですね。
とはいえ、TerraformのステートをGitLab上に置くのも久しぶりにやったのでいろいろと復習になりました。