これは、なにをしたくて書いたもの?
GitLab 13.0で、TerraformのStateのバックエンドとして利用できるようになったというのを見かけまして。
ポイントは、このあたりですね。
- Multiple named state files per project
- Locking
- Object storage
- Encryption at rest
こちらを試してみようかな、と。
Terraform State BackendとしてのTerraform
TerraformのRemote Stateに関しては、以前にこちらで書きました。
TerraformでRemote Stateを参照して、Data Sourceとして扱う - CLOVER🍀
通常はローカルファイルシステムに保存されるTerraformのStateを、別のリモートのストアに保存できるようにすることですね。
State: Remote Storage - Terraform by HashiCorp
Stateをリモート管理することで、Stateをチーム内で共有したり、ロックを使用した排他制御を行うことでTerraformを同時に
実行して事故になることを避ける、といったことができるようになります。
どこまでできるかは、実際に利用するバックエンド次第ですが。
GitLabのドキュメントにおける、Terraformに関するページはこちらです。
Infrastructure as code with Terraform and GitLab | GitLab
S3などを用意しなくても、Stateをリモート管理できると謳われており、転送中や保存中のStateの暗号化、ロックをサポート
することがポイントとして書かれています。
- Supporting encryption of the state file both in transit and at rest.
- Locking and unlocking state.
- Remote Terraform plan and apply execution.
あとは、CIで使う時の方法が書かれていたり。
CIに組み込む際には、合わせてGitLabで定義されている環境変数を見ることになりそうですね。
Predefined environment variables reference | GitLab
TerraformのStateを管理する機能は、デフォルトで有効になっていて、その設定などはこちらに書かれています。
Terraform state administration (alpha) | GitLab
「/etc/gitlab/gitlab.rb」で設定するようです。
また、GitLabをTerraform State Backendとして使う場合は、HTTPとなり、RESTでのアクセスとなります。
Backend Type: http - Terraform by HashiCorp
と、説明はこのくらいにして、実際に使っていってみましょう。
TerraformのStateをGitLabで管理し、ProviderはMySQLを使って遊んでみることにします。
環境
今回の環境まわりです。
$ terraform version Terraform v0.12.26 + provider.mysql v1.9.0
GitLabは192.168.0.3のサーバーで動作しているものとします。バージョンは、こちら。
$ sudo gitlab-rake gitlab:env:info System information System: Ubuntu 18.04 Current User: git Using RVM: no Ruby Version: 2.6.6p146 Gem Version: 2.7.10 Bundler Version:1.17.3 Rake Version: 12.3.3 Redis Version: 5.0.9 Git Version: 2.26.2 Sidekiq Version:5.2.7 Go Version: unknown GitLab information Version: 13.0.6 Revision: 5aa982e01ea Directory: /opt/gitlab/embedded/service/gitlab-rails DB Adapter: PostgreSQL DB Version: 11.7 URL: http://192.168.0.3 HTTP Clone URL: http://192.168.0.3/some-group/some-project.git SSH Clone URL: git@192.168.0.3:some-group/some-project.git Using LDAP: no Using Omniauth: yes Omniauth Providers: GitLab Shell Version: 13.2.0 Repository storage paths: - default: /var/opt/gitlab/git-data/repositories GitLab Shell path: /opt/gitlab/embedded/service/gitlab-shell Git: /opt/gitlab/embedded/bin/git
また、MySQLは8.0で、172.17.0.2で動作しているものとします。
シンプルに使ってみる
まずは、こちらを見ながらシンプルに使っていってみましょう。
Infrastructure as code with Terraform and GitLab / Get started using local development
ドキュメントを見ると、Terraformの構成ファイルへ書くのはこれくらいで
terraform {
backend "http" {
}
}
残りは「terraform init」時に指定、という雰囲気で書かれていますが、ちょっと面倒だなぁと思ったので…。
$ terraform init \ -backend-config="address=https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/terraform/state/<YOUR-PROJECT-NAME>" \ -backend-config="lock_address=https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/terraform/state/<YOUR-PROJECT-NAME>/lock" \ -backend-config="unlock_address=https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/terraform/state/<YOUR-PROJECT-NAME>/lock" \ -backend-config="username=<YOUR-USERNAME>" \ -backend-config="password=<YOUR-ACCESS-TOKEN>" \ -backend-config="lock_method=POST" \ -backend-config="unlock_method=DELETE" \ -backend-config="retry_wait_min=5"
また、可変の値としてGitLabのプロジェクトのID、プロジェクト名、ユーザー名およびアクセストークンが必要だということに
なっています。gitlab.comを使わない場合(今回もですが)は、アクセス先も変わります。
プロジェクトIDとプロジェクト名は、今回はそれぞれ2、sample-projectとします。
今回は、このくらいは定義の方に書くことにしました。
terraform { required_version = "0.12.26" backend "http" { address = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project" lock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/lock" unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/lock" lock_method = "POST" unlock_method = "DELETE" retry_wait_max = 5 } } provider "mysql" { endpoint = "172.17.0.3:3306" username = "root" password = "password" version = "1.9.0" }
アクセス先はHTTPにしているので、転送中は思いきり平文ですね。
ユーザー名とパスワードだけは、terraform init時に指定することにします。
$ terraform init \ -backend-config="username=[your-username]" \ -backend-config="password=[your-access-token]"
チーム内で使うにしても、他の項目はメンバー単位に変わらないでしょう…。
パスワードは、GitLabのアクセストークンです。持っていなければ、「User Settings」→「Access Tokens」から発行できます。
アクセストークンのスコープとしては、「api」があれば大丈夫な気がします。
で、全体でこんな感じで定義してみました。MySQLデータベースを作成する、リソース定義です。
main.tf
terraform { required_version = "0.12.26" backend "http" { address = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project" lock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/lock" unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/lock" lock_method = "POST" unlock_method = "DELETE" retry_wait_max = 5 } } provider "mysql" { endpoint = "172.17.0.2:3306" username = "root" password = "password" version = "1.9.0" } resource "mysql_database" "database" { name = "my_database" default_character_set = "utf8mb4" default_collation = "utf8mb4_ja_0900_as_cs_ks" }
applyで、問題なくデータベースの作成ができます。
$ terraform apply
この時、アクセストークンがあればStateの保存内容をcurlなどで見ることができます。
$ curl -H 'Authorization: Bearer [your-access-token]' http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project { "version": 4, "terraform_version": "0.12.26", "serial": 7, "lineage": "", "outputs": {}, "resources": [ { "mode": "managed", "type": "mysql_database", "name": "database", "provider": "provider.mysql", "instances": [ { "schema_version": 0, "attributes": { "default_character_set": "utf8mb4", "default_collation": "utf8mb4_ja_0900_as_cs_ks", "id": "my_database", "name": "my_database" }, "private": "bnVsbA==" } ] } ] }
また、「terraform init」の時点で、ローカルにはバックエンドの情報を保存したこんなファイルができます。
.terraform/terraform.tfstate
{ "version": 3, "serial": 1, "lineage": "fe17a8cb-a6c4-be4c-2b3c-2883f103a836", "backend": { "type": "http", "config": { "address": "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project", "lock_address": "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/lock", "lock_method": "POST", "password": "[your-access-token]", "retry_max": null, "retry_wait_max": 5, "retry_wait_min": null, "skip_cert_verification": null, "unlock_address": "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/lock", "unlock_method": "DELETE", "update_method": null, "username": "[your-username]" }, "hash": 1929719276 }, "modules": [ { "path": [ "root" ], "outputs": {}, "resources": {}, "depends_on": [] } ] }
Remote State+Data Sourceとして使う
続いて、GitLabをTerraformのRemote Stateとして使いつつも、Data Sourceとして参照にも使用してみましょう。
データベースの作成と、ユーザーの作成をそれぞれ別のリソース定義で行い、Data Sourceの参照で紐付けます。
まずは、データベース側のリソース定義。
main.tf
terraform { required_version = "0.12.26" backend "http" { address = "http://192.168.0.3/api/v4/projects/2/terraform/state/database" lock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/database/lock" unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/database/lock" lock_method = "POST" unlock_method = "DELETE" retry_wait_max = 5 } } provider "mysql" { endpoint = "172.17.0.2:3306" username = "root" password = "password" version = "1.9.0" } resource "mysql_database" "database" { name = "my_database" default_character_set = "utf8mb4" default_collation = "utf8mb4_ja_0900_as_cs_ks" } output "database_name" { value = mysql_database.database.name }
ドキュメントではプロジェクト名がBackendのアクセス先になっていましたが、このあたりの値は任意で良さそうなので、
変更しました。「Multiple named state files per project」というのが、リリースブログでもポイントとして書かれていましたしね。
今回は「database」としています。
terraform { required_version = "0.12.26" backend "http" { address = "http://192.168.0.3/api/v4/projects/2/terraform/state/database" lock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/database/lock" unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/database/lock" lock_method = "POST" unlock_method = "DELETE" retry_wait_max = 5 } }
このリソース定義の結果を次で使うので、Outputも定義。データベース名だけですが。
output "database_name" {
value = mysql_database.database.name
}
で、apply。
$ terraform apply
続いて、ユーザーを作成するリソース定義を行いましょう。データベースを作成した時のリソース定義とは、別のディレクトリで
行ってください。
全体はこんな感じです。
main.tf
terraform { required_version = "0.12.26" backend "http" { address = "http://192.168.0.3/api/v4/projects/2/terraform/state/users" lock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/users/lock" unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/users/lock" lock_method = "POST" unlock_method = "DELETE" retry_wait_max = 5 } } provider "mysql" { endpoint = "172.17.0.2:3306" username = "root" password = "password" version = "1.9.0" } variable "gitlab_username" {} variable "gitlab_password" {} data "terraform_remote_state" "database" { backend = "http" config = { address = "http://192.168.0.3/api/v4/projects/2/terraform/state/database" lock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/database/lock" unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/database/lock" lock_method = "POST" unlock_method = "DELETE" username = var.gitlab_username password = var.gitlab_password retry_wait_max = 5 } } resource "mysql_user" "admin_user" { user = "adminuser" plaintext_password = "password" host = "%" } resource "mysql_grant" "admin_user" { user = "adminuser" host = "%" database = data.terraform_remote_state.database.outputs.database_name privileges = ["ALL"] depends_on = [mysql_user.admin_user] } resource "mysql_user" "application_user" { user = "appuser" plaintext_password = "password" host = "%" } resource "mysql_grant" "application_user" { user = "appuser" host = "%" database = data.terraform_remote_state.database.outputs.database_name privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"] depends_on = [mysql_user.application_user] }
Backendのパスは、先ほどのものとは変更し、用途に合わせて「users」としました。
terraform { required_version = "0.12.26" backend "http" { address = "http://192.168.0.3/api/v4/projects/2/terraform/state/users" lock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/users/lock" unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/users/lock" lock_method = "POST" unlock_method = "DELETE" retry_wait_max = 5 } }
また、先ほどのデータベース定義の結果を参照したいので、Data Sourceの定義を行います。
こんな感じで。参照先は、「database」のパスになります。
variable "gitlab_username" {} variable "gitlab_password" {} data "terraform_remote_state" "database" { backend = "http" config = { address = "http://192.168.0.3/api/v4/projects/2/terraform/state/database" lock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/database/lock" unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/database/lock" lock_method = "POST" unlock_method = "DELETE" username = var.gitlab_username password = var.gitlab_password retry_wait_max = 5 } }
ここではユーザー名とパスワードを指定せざるをえないので、変数に切り出しました。
値は、環境変数で与えた方がいいのかな?と思います。
$ export TF_VAR_gitlab_username=[your-username] $ export TF_VAR_gitlab_password=[your-access-token]
そして、Remote StateをData Sourceとして参照して、リソース定義を行います。
resource "mysql_grant" "application_user" { user = "appuser" host = "%" database = data.terraform_remote_state.database.outputs.database_name privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"] depends_on = [mysql_user.application_user] }
apply。
$ terraform apply
こんな感じで、確認完了です。
GitLab上でのStateの保存先について
GitLab側に保存されるTerraformのStateですが、保存先のパスなどはこちらのドキュメントに記載のある設定箇所で調整します。
Terraform state administration (alpha) | GitLab
設定ファイルは、「/etc/gitlab/gitlab.rb」です。
デフォルトの保存先は、「/var/opt/gitlab/gitlab-rails/shared/terraform_state」ディレクトリなので、こちらを確認してみます。
$ sudo ls -l /var/opt/gitlab/gitlab-rails/shared/terraform_state total 8 drwxr-xr-x 2 git git 4096 Jun 13 11:01 2 drwxr-xr-x 4 git git 4096 Jun 13 09:22 tmp
プロジェクトID(今回は2)のディレクトリがありますね。
中身を見ると、3つStateと思しきファイルがあります。3つあるのは、今回3回実行したからですね。
$ sudo ls -l /var/opt/gitlab/gitlab-rails/shared/terraform_state/2 total 16 -rw-r--r-- 1 git git 150 Jun 13 10:53 16daa9081b1106c5c970411eaaec7e25.tfstate -rw-r--r-- 1 git git 695 Jun 13 11:01 840999dc0e463bed8d360b4fdaac939f.tfstate -rw-r--r-- 1 git git 4451 Jun 13 11:01 f0763fc6cc0691fcd13731b408ea0488.tfstate
ということを考えると、「/api/v4/projects/<YOUR-PROJECT-ID>/terraform/state/<YOUR-PROJECT-NAME>」といった指定のパスとの
マッピングがちょっと気になるところです。
中身を見ると、読めないようになっています。暗号化されているようですね。
$ sudo cat /var/opt/gitlab/gitlab-rails/shared/terraform_state/2/16daa9081b1106c5c970411eaaec7e25.tfstate \ �Ջ��nUˤ1��Z�z3�����Cܵ�&c��&�IJ�v��M�)������u�2��<Nk$�t$��:�r2�?�&�]X�ou\��1���苮����=���UX�#���u�`�b�+�$���Ԟ�����D(��[��Ux��
というわけで、ちょっと気になることとして、アクセス先にサブディレクトリのようなものを追加したらどうなるのかな?と
思ったのですが
terraform { required_version = "0.12.26" backend "http" { address = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/foo" lock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/foo/lock" unlock_address = "http://192.168.0.3/api/v4/projects/2/terraform/state/sample-project/foo/lock" lock_method = "POST" unlock_method = "DELETE" retry_wait_max = 5 } }
こうすると、404になるようです。
$ terraform apply Error: Error locking state: Error acquiring the state lock: Unexpected HTTP response code 404 Terraform acquires a state lock to protect the state from being written by multiple users at the same time. Please resolve the issue above and try again. For most commands, you can disable locking with the "-lock=false" flag, but this is not recommended.
というわけで、「/api/v4/projects/<YOUR-PROJECT-ID>/terraform/state/<YOUR-PROJECT-NAME>」のパスルールで
コントロールしましょう、と。