以下の内容はhttps://kazuhira-r.hatenablog.com/entry/2025/08/03/223507より取得しました。


TerraformのRemote StateをGitLabに保存して、複数のユーザーで操作する

これは、なにをしたくて書いたもの?

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

 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

アクセストークンはapiスコープのみを設定しています。

なお、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_USERNAMETF_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ステートを複数のユーザーで操作できました、と。

どうしてこれを気にしたのか?

気になったのは、トラブルシュートに関するこちらの記述を見かけたからです。

Troubleshooting the Terraform integration with GitLab / Troubleshooting Terraform state / Can’t lock Terraform state files in CI jobs for terraform apply with a previous job’s plan

こちらは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
}

GitLabのアクセストークンは環境変数で指定。

$ 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上に置くのも久しぶりにやったのでいろいろと復習になりました。




以上の内容はhttps://kazuhira-r.hatenablog.com/entry/2025/08/03/223507より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14