GitHub Actionsを月2,000分動かして学んだTerraform + TROCCO入門　前半：基礎から分かるTerraform

# はじめに

本記事は、TROCCO® Advent Calendar 2024の20日目の記事になります。

https://qiita.com/advent-calendar/2024/trocco

:::note warn

骨太すぎたのでこちらは前半です。後半は[「GitHub Actionsを月2,000分動かして学んだTerraform + TROCCO入門　後半：GitHub ActionsでのCI／CD」](https://qiita.com/SoySoySoyB/items/05e4d31d3e372790c775)となっています！

:::

昨年はデータエンジニアリング初心者から学んできたことをもとに、[「1年前の自分が読みたかった、データエンジニアリング入門」](https://qiita.com/SoySoySoyB/items/44fda40de0276a83957a)を執筆し、もうすぐ40,000PV近くになるほど参考にしていただきました。

今年もまた骨太な記事を書こうとテーマを考えていましたが、今回はTerraform初心者からの経験をもとに、サンプルリポジトリ付きのTerraform + TROCCO®の入門記事を執筆することにしました。Terraformの解説の方が遥かに分量が多いので、TROCCO®を知らない／興味のない方でも参考にしていただけると思います。

Terraformはなかなか勘所を掴むのが難しく、特にCI／CDまで絡むとなおさらで、直近1か月でGitHub Actionsの無料枠である2,000分／月を溶かし切ってしまいました。私が溶かした時間を、みなさんのために捧げたいと思います。

![GitHub Actions usage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492462/f4975110-7f85-649b-9f74-43aa935a5fdc.png)

サンプルリポジトリは以下で公開しています。

https://github.com/SoySoySoyB/terraform-trocco-sample

ちなみにTROCCO®としては、8月にTerraform Provider for TROCCOが公開され（参考：[「TROCCO®のTerraform Provider（β版）ができたので最速で触ってみる」](https://qiita.com/SoySoySoyB/items/c07ecf7bf199d5730bf2)）、これから対応リソースが増えていくタイミングになっています。


# こんな方におすすめ

- TROCCO®に関わらずTerraformが気になっているが、なんだかよくわかっていない方
- TROCCO®でTerraformという話が出てきて、どういうものか気になっている方
- 普段からTerraformを活用していて、TROCCO®での活用を検討している方

:::note warn

- 勢い余って書きすぎたため、Terraformを基礎から解説している初心者向けの記事と、CI／CDを含めた運用を取り上げた中級者向けの記事に分けています
- TROCCO®でのTerraform活用については、Advancedプランから利用できるTROCCO APIが前提になり、かつ現時点で対応しているリソースは一部になる（鋭意拡充中！）のにはご留意ください

:::

まだ経験が浅いので、記載内容が誤っている／もっとこうしたらいいということがあれば教えていただけると大変ありがたいです！！！


# Terraformとはなにか

まずは、Terraformの概要について説明していきます。既に分かっているという方は読み飛ばして or 後半へ進んでいただいて大丈夫です。


## Terraformで実現できること

TerraformはIaC（Infrastructure as Code）を実現するためのツールです。IaCは名前の通りインフラをコード管理することですが、Terraformを利用すると下記のようなことが実現できます。

- コード管理のメリット
    - 現状把握：コードで一覧化されていることで、現状を俯瞰的に理解しやすくなる
    - バージョン管理：Gitなどのバージョン管理ツールを利用することで、過去からの変更内容を確認できる
    - 経緯の確認：GitHubなどのコード管理ツールに検討内容やレビューコメントを記載することで、開発の経緯を把握できる
    - ミスの防止：レビュープロセスが入ることで、手運用によるミスを予防できる
- Terraformのメリット
    - 影響の事前確認：実際の環境にリソースの設定を適用する前に、作成／変更内容の確認ができる
    - 依存関係の解決：作成するリソース間の依存関係を自動で解決してくれる
    - リソース作成／変更の効率化：作成／変更の円滑な実現ができる
    - リソース削除の効率化：不要になったリソースを簡単に削除することができる
    - リソース定義の転用：既存の設定をコピーすることはもちろん、変数を上手く利用することで似たようなリソースを円滑に作成できる

端的に言うと、現状や過去からの経緯が分かりやすくなる上に、リソースの作成／変更／削除を簡単にできるようになります。

かつてのインフラはハードウェアを準備する必要があったり、作成や変更も手順書をもとに行われていたりしていました。

しかし、Terraformを利用すると、アプリケーション開発のような開発フローでインフラを利用できるようになり（IaCのこの効果でDevOpsの取り組みが生まれてきた）、ソフトウェアエンジニア／データエンジニアにとってもインフラを理解／活用しやすくなっています。

余談ですが、私がこの領域の話を初めて聞いたのが、なぜかライブで視聴していたForkwellさんの[Infra Study Meetup](https://www.youtube.com/playlist?list=PLa7KKuU8Ysnv3cJJUMCeG8fECnhGYRvBo)で、ここから派生して弊社と共催しているData Engineering Studyが始まって参加し、primeNumberに転職することになりました。

初回のテーマがまさにInfrastructure as Codeで、こういう世界があるのか～と思った当時から4年振りに学び直しています（笑）

https://www.youtube.com/watch?v=m1mm9VngA8g&list=PLa7KKuU8Ysnv3cJJUMCeG8fECnhGYRvBo&index=18

そしてこのときに2人目でLTをしているchaspyさんが、このあとで紹介する&TROCCO®でも活用しているtfactionの開発者suzuki-shunsukeさんをSpecial Thanksにあげていて、おお！！となるなど。


## Terraformの仕組み

次に、Terraformを利用するときの基本をさらっていきます。仕組みとしては以下の図のような形になっています。

![Terraform.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492462/180671bb-8033-79a9-2c23-f302a02c97c1.png)

詳細は次から解説していきます。


### 統合的なAPIのラッパーとしてのTerraform

Terraformは端的に言うと、「各種サービスのCRUD処理のAPIをラップして、1つのインターフェースにまとめたもの」と言えます。CRUD処理の意味と、Terraformでの位置づけは以下の通りです。

| 種類 | 内容 |
| --- | ---- |
| C | Create（作成）：Terraformで管理するリソースの作成 |
| R | Read（読み出し）：Terraformを通したリソースの現状の把握 |
| U | Update（更新）：Terraformで管理するリソース定義の更新 |
| D | Delete（削除）：Teraformで管理するリソースの削除 |

APIで処理を行うときには、Terraform本体からProviderを経由して、各種サービスにアクセスしています。TROCCO®におけるTROCCO®本体とコネクタ（Embulkプラグイン）みたいな関係ですね。

ProviderにはHashiCorp社が管理している公式プロバイダー、パートナー企業が管理しているパートナープロバイダー、サードパーティー企業が管理しているコミュニティプロバイダーがあり、Terraform Provider for TROCCOはコミュニティプロバイダーの1つになります。

いずれにせよProviderを介することで、TROCCO®でもGoogle CloudでもAWSでもAzureでもSnowflakeでも、Terraformという共通のインターフェースでリソース管理ができるようになります。


### 肝となるtfファイルとstateファイルの管理

Terraformの肝は、Terraform自体の設定と、こうしたいという定義を指定するための.tfのファイルと、Terraformで実行した結果どういう状態になったかを管理する.tfstateのファイルがどのように機能しているかにあります。まずはこれを見ていきましょう。


#### tfファイル

設定は、例えば以下のようなものになります。

```terraform:main.tf
# ここがTerraform本体とProviderの設定
terraform {
  # バージョンの指定は任意だが、動作を安定させるときには固定する
  required_version = "1.9.8"
  required_providers {
    # ここでリソースで利用するプロバイダーを指定する
    trocco = {
      source  = registry.terraform.io/trocco-io/trocco"
      version = "0.2.1"
    }
  }
}

# プロバイダー個別の設定を記載
provider "trocco" {
  region = "japan"
}

# リソースはresource "{リソース種別}" "{TerraformでのID}"と記載して、{}内に各種パラメータの値を設定する
resource "trocco_bigquery_datamart_definition" "minimum" {
  name                     = "example_minimum"
  is_runnable_concurrently = false
  bigquery_connection_id   = 1
  query                    = "SELECT * FROM tables"
  query_mode               = "insert"
  destination_dataset      = "dist_datasets"
  destination_table        = "dist_tables"
  write_disposition        = "append"
}
```

tfファイルの内容は同一ディレクトリであればファイル分割をしても処理上は統合されるので、以下のように分割していくこともあります。

| tfファイル | 概要 |
| --------- | ---- |
| provider.tf | プロバイダーの情報を記載する |
| versions.tf | Terraformのバージョンを記載する |
| resources.tf | リソースの定義を記載する、より細かく分割することもある |
| variables.tf | 変数を記載する |
| locals.tf | ローカル変数を記載する |
| outputs.tf | アウトプットを記載する、module分割する際にアウトプットしたものをインプットして連携させる |
| modules.tf | モジュールを記載する |
| data.tf | Terraform管理していないものの設定情報を取り込むための定義を記載する |


tfファイルの書き方については、Registryに各種ドキュメントが公開されているので、そちらを参考にしてください。例えば[TROCCO®では以下のキャプチャのようになっています](https://registry.terraform.io/providers/trocco-io/trocco/latest/docs)。リソースの拡充と並行して、ドキュメントもこれから充実させていきますね。

![Terraform Provider for TROCCO.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492462/58af630e-4e6b-99de-e76a-c1785e75c31e.png)


#### stateファイル

.tfstateのファイルはstateファイルと呼ばれ、前回実行した結果どういう状態になったかを管理しています。Terraformの変更差分は、このstateファイルとtfファイル／読み出しした実際の定義を比較して検知するようになっています。そこで、

- Terraformで管理するときのtfファイル／stateファイルは一元化しておかないと、現状が適切なのかが分からなくなる
    - そのリソースについて最後に実行された結果が現状になってしまう、もしくはリソースが複数生成されてしまう
- Terraformの外部で変更してしまうと、tfファイル／stateファイルとの差分が発生してしまう
    - そのままでは定義と現状にズレがあることになり、次に実行すると変更を戻そうとしてしまう

など、上手く扱えていないと予期せぬ事態が発生してしまう可能性があります。いかにSSOT（Single Source of Truth）を実現できるかが重要です。

また、新規作成のときはいいとして、既存のリソースはどうするのかという疑問が浮かぶでしょう。推奨は新しく立て直せのようですが（身も蓋もない）、importコマンド／importブロックで既存リソースの取り込みもできます。詳しい方法はここでは割愛しますが、ここでは既存のリソースをstateファイルに取り込む方法があるということだけ、覚えておいてください。

直近、既存リソースの管理について以下の記事が非常に良かったので、玄人向けですが（苦笑）紹介しておきます。

https://zenn.dev/soundtricker/articles/fa27c919ad3630#%E3%81%A9%E3%81%93%E3%81%8B%E3%82%89%E3%81%AF%E3%81%98%E3%82%81%E3%82%8B%E3%81%8B

そのほか、tfファイルでは定義することのできない値（例えばIDは、リソースを作成した時点で付与される）は、tfファイルには存在せず、stateファイルだけに存在するというような差分があります。これは後ほど確認してみてください。

センシティブなデータについては、リソース自体に定義されているか、自身で`sensitive = true`に指定することで、コンソールで表示しないように設定できます。ただしstateファイルには平文で入ってしまうので、取扱いには注意してください。

```console:コンソールでの表示例
  # trocco_connection.bigquery will be created
  + resource "trocco_connection" "bigquery" {
      + connection_type          = "bigquery"
      + description              = (known after apply)
      + id                       = (known after apply)
      + name                     = (known after apply)
      + project_id               = "{YOUR_PROJECT_ID}"
      + service_account_json_key = (sensitive value)
    }
```


# Terraformの基本コマンド：`init`、`plan`、`apply`と`destroy`

Terraformを利用するときの基本的なコマンドとして、`terraform init`、`terraform plan`、`terraform apply`と`terraform destroy`の4つのコマンドがあります。概要は以下の通りです。

| コマンド | 概要 |
| ------- | --- |
| `init` | Terraformを実行するための環境を設定する |
| `plan` | 定義と現状の差分をもとに、リソースへの影響を表示する |
| `apply` | 定義と現状の差分をもとに、リソースに反映する |
| `destroy` | Terraformで管理しているリソースを削除する |

以下で詳しく見てみましょう。なお、後ほどサンプルリポジトリとその説明があるので、この場では流し読みしていただければ大丈夫です。


## 初期化：`terraform init`

Terraformを実行するための環境を設定します。Terraformのバージョン確認、stateファイルの保管場所の設定、Providerのインストールとバージョン確認、moduleの読み込みなどを行っています。詳しくは後述します。

例えば以下のような形です。

```console:コンソールでの表示例
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/random versions matching "3.6.3"...
- Finding hashicorp/google versions matching "6.12.0"...
- Finding trocco-io/trocco versions matching "0.2.1"...
- Installing hashicorp/random v3.6.3...
- Installed hashicorp/random v3.6.3 (signed by HashiCorp)
- Installing hashicorp/google v6.12.0...
- Installed hashicorp/google v6.12.0 (signed by HashiCorp)
- Installing trocco-io/trocco v0.2.1...
- Installed trocco-io/trocco v0.2.1 (self-signed, key ID 4AD358D3E1334E66)
Partner and community providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://www.terraform.io/docs/cli/plugins/signing.html
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
```

## 影響の表示：`terraform plan`

tfファイルに記載したこの定義でこのリソースを作成したいという定義に対して、stateファイル／現状の実体との差分を確認して、リソースにどういった影響が与えられるのかを表示します。

このとき、表示される影響は、

- 新規作成（create）
- 変更（update）
- 削除（destroy）

があります。

例えば、作りたいものをtfファイルに記載して初回実行しようとすると、stateファイルには何もない状態なので、tfファイルに記載されたリソース定義をもとにリソースが新規作成されると表示されます。

一度リソースを作成したあとに、tfファイルの既存の設定を変更したり、削除したりすると、実態との差分で変更や削除が生じると表示されます。

新規作成の場合は、以下のような形になります。なお、これ以降{YOUR_PROJECT_ID}の部分は実行する環境に応じたプロジェクトIDが入ります。

```console:コンソールでの表示例
$ terraform plan
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:

（略）

  # trocco_bigquery_datamart_definition.dm_trocco_sample__sample will be created
  + resource "trocco_bigquery_datamart_definition" "dm_trocco_sample__sample" {
      + bigquery_connection_id   = (known after apply)
      + destination_dataset      = "dm_trocco_sample"
      + destination_table        = "dm_trocco_sample__sample"
      + id                       = (known after apply)
      + is_runnable_concurrently = true
      + labels                   = [
          + {
              + id   = (known after apply)
              + name = "Terraform Managed"
            },
        ]
      + name                     = "dm_trocco_sample__sample"
      + query                    = "select '転送設定を心待ちにしながら、Terraformでデータマートを作ってみよう' as sample_column"
      + query_mode               = "insert"
      + write_disposition        = "truncate"
    }

  # trocco_connection.bigquery will be created
  + resource "trocco_connection" "bigquery" {
      + connection_type          = "bigquery"
      + description              = (known after apply)
      + id                       = (known after apply)
      + name                     = (known after apply)
      + project_id               = "{YOUR_PROJECT_ID}"
      + service_account_json_key = (sensitive value)
    }

（略）

Plan: 22 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.
```


## 実行：`terraform apply`

`plan`ではあくまで実行したときにどのような結果が実現されるかというのを表示するだけであり、実際にリソースを管理するには`apply`を実行します。

```console:コンソールでの表示例
$ terraform apply
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:

（略）

  # trocco_bigquery_datamart_definition.dm_trocco_sample__sample will be created
  + resource "trocco_bigquery_datamart_definition" "dm_trocco_sample__sample" {
      + bigquery_connection_id   = (known after apply)
      + destination_dataset      = "dm_trocco_sample"
      + destination_table        = "dm_trocco_sample__sample"
      + id                       = (known after apply)
      + is_runnable_concurrently = true
      + labels                   = [
          + {
              + id   = (known after apply)
              + name = "Terraform Managed"
            },
        ]
      + name                     = "dm_trocco_sample__sample"
      + query                    = "select '転送設定を心待ちにしながら、Terraformでデータマートを作ってみよう' as sample_column"
      + query_mode               = "insert"
      + write_disposition        = "truncate"
    }

  # trocco_connection.bigquery will be created
  + resource "trocco_connection" "bigquery" {
      + connection_type          = "bigquery"
      + description              = (known after apply)
      + id                       = (known after apply)
      + name                     = (known after apply)
      + project_id               = {YOUR_PROJECT_ID}"
      + service_account_json_key = (sensitive value)
    }

（略）

Plan: 22 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.
```

実はこれだけでは`plan`と同じ結果が表示されるだけです。実行するには、`yes`と入力します。（もしくは、`terraform apply --auto-approve`でもできます）

```console:コンソールでの表示例
$ yes

（略）

trocco_connection.bigquery: Creating...
trocco_connection.bigquery: Creation complete after 0s [name={YOUR_PROJECT_ID}__trocco-sample@{YOUR_PROJECT_ID}.iam.gserviceaccount.com]
trocco_bigquery_datamart_definition.dm_trocco_sample__sample: Creating...
trocco_bigquery_datamart_definition.dm_trocco_sample__sample: Creation complete after 0s [name=dm_trocco_sample__sample]

Apply complete! Resources: 22 added, 0 changed, 0 destroyed.
```

上の処理では実は、サービスアカウントの作成→サービスアカウントキーの作成→BigQuery接続情報の作成→BigQueryデータマートの作成、という処理をしているのですが、依存関係通りに処理されています。便利ですね（後ほど見てください）。

なお、APIが有効化されていなかったり、リソースの作成／変更／削除権限がないと失敗することもあるので注意してください。


## 削除：`terraform destroy`

最後はリソースの削除です。本運用ではまず使わないでしょうが、技術検証のときはサクッとリソースをお掃除できるので便利です。applyと同じく消していい？と聞かれるので、`yes`と返答します（または`terraform destroy --auto-approve`を実行します）。

```console:コンソールでの表示例
$ terraform destroy

（略）

trocco_connection.bigquery: Refreshing state... [name={YOUR_PROJECT_ID}__trocco-sample@{YOUR_PROJECT_ID}.iam.gserviceaccount.com]
trocco_bigquery_datamart_definition.dm_trocco_sample__sample: Refreshing state... [name=dm_trocco_sample__sample]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

（略）

  # trocco_bigquery_datamart_definition.dm_trocco_sample__sample will be destroyed
  - resource "trocco_bigquery_datamart_definition" "dm_trocco_sample__sample" {
      - bigquery_connection_id   = 3469 -> null
      - destination_dataset      = "dm_trocco_sample" -> null
      - destination_table        = "dm_trocco_sample__sample" -> null
      - id                       = 55329 -> null
      - is_runnable_concurrently = true -> null
      - labels                   = [
          - {
              - id   = 4513 -> null
              - name = "Terraform Managed" -> null
            },
        ] -> null
      - name                     = "dm_trocco_sample__sample" -> null
      - query                    = "select '転送設定を心待ちにしながら、Terraformでデータマートを作ってみよう' as sample_column" -> null
      - query_mode               = "insert" -> null
      - write_disposition        = "truncate" -> null
    }

  # trocco_connection.bigquery will be destroyed
  - resource "trocco_connection" "bigquery" {
      - connection_type          = "bigquery" -> null
      - description              = "プロジェクト{YOUR_PROJECT_ID}に対してサービスアカウントのtrocco-sample@{YOUR_PROJECT_ID}.iam.gserviceaccount.comで接続するための接続情報" -> null
      - id                       = 3469 -> null
      - name                     = "{YOUR_PROJECT_ID}__trocco-sample@{YOUR_PROJECT_ID}.iam.gserviceaccount.com" -> null
      - project_id               = "{YOUR_PROJECT_ID}" -> null
      - service_account_json_key = (sensitive value) -> null
    }

（略）

Plan: 0 to add, 0 to change, 22 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

（略）

trocco_bigquery_datamart_definition.dm_trocco_sample__sample: Destruction complete after 1s

（略）

trocco_connection.bigquery: Destroying... [name={YOUR_PROJECT_ID}__trocco-sample@{YOUR_PROJECT_ID}.iam.gserviceaccount.com]

（略）

trocco_connection.bigquery: Destruction complete after 0s

（略）

Destroy complete! Resources: 22 destroyed.
```


# Terraformの言語と運用

基本コマンドをおさえたところで、Terraformの言語と運用について簡単に触れておきましょう。


## 言語

Terraformの言語はHCL（Hashicorp Configuration Language）という独自言語になっています。独自言語と聞いて難しく思うかもしれませんが、HCLはSQLと同様に宣言型言語なので、慣れるとさくっと書けるようになります。

宣言型言語とは、コードとしては実現したい結果を記載して、その処理方法はいい感じにしてくれるものです。

SQLの処理をオプティマイザがいい感じにしてくれるように、Terraformでは依存関係があるリソースに対して、処理の順番を記載することなく処理してくれるなど、いい感じに対処してくれます。

このとき、シンプルにリソース定義をするのは簡単なのですが、for_eachという形で複数の値をループ処理したり、modulesというものでモジュール分割したりと、実は奥が深いのはひとまず置いておきましょう。


## 運用

詳細は後述しますが、Terraformを実運用で利用する場合、適切なバックエンド（stateファイルの保存場所）やCI/CD（後半で解説します）を構築する必要が出てきます。その背景は以下の通りです。

- stateファイルをSSOTとして適切に運用するため
- Terraformのplan／applyを適切に管理するため
- セキュリティを堅牢に担保するため

1点目について、バックエンドを指定せずにTerraformを実行すると、stateファイルはローカルに保存されます。しかしこの状態ではSSOTを実現することができません。そこで、オブジェクトストレージに保存場所を変える必要があります。

オブジェクトストレージにstateファイルを保存するときは、機密情報の管理／運用ミスの予防のため、管理しているプロジェクト／アカウントを切り分けたり、削除禁止の設定をしたり、切り戻しできるようにバージョン管理を設定したりします。

また、stateファイル自体に機密情報が含まれるということもありますし、リソースの管理を委ねるということは、Terraformの実行環境に大きな権限を与えることになります。そこで、認可／認証を適切に設定し、権限を最小限に絞り、脆弱性に注意を払うなど、セキュリティへの細心の注意が必要になります。

Terraformは上手く利用すればで大変便利ですが、Terraform自体またはTerraformの実行環境自体について適切に理解し、統制することを心掛けましょう。

極端な話ではあると思いつつ、`apply`ではなくて`plan`でも機密情報を流出させられるという話があり、怖がりすぎる必要はないですがなに！？！？と思いました。

https://zenn.dev/shunsuke_suzuki/scraps/53d7f5e954f90d

ただし確かにこの言葉の通り、`Bucket full of secrets`ではあるんですよね。自組織のセキュリティポリシーを踏まえて、適切に設計／運用するようにしましょう。


# サンプルリポジトリ解説その1：基礎「ローカル実行」

では、ここからサンプルリポジトリを紹介していきます。[サンプルのリポジトリの`./sample1_basic/`](https://github.com/SoySoySoyB/terraform-trocco-sample/tree/main/sample1_basic)を参考にしながら、ローカルでTerraformを実行してみましょう。見ればわかる方は、流し見でもしてくれればという感じです。

:::note warn

私はWindows環境でコマンドプロンプトを利用しているので、環境が異なる場合は適宜読み替えてください。
また、TROCCO®の関連部分をコメントアウトすれば、Google Cloud環境のみで試すこともできます。

:::

関連するディレクトリは以下のようになっています。

```
.
└── sample1_basic
    ├── _google.tf
    ├── _trocco.tf
    ├── .trivyignore
    ├── locals.tf
    ├── provider.tf
    ├── README.md
    ├── tfaction.yaml
    └── train.csv
```

余談ですが、VSCodeでツリーを出すのには[Ascii Tree Generatorの拡張機能](https://marketplace.visualstudio.com/items?itemName=aprilandjan.ascii-tree-generator)が便利です。


## Google Cloudの準備をする

今回はGoogle Cloudのリソースの作成を同時に行っているので、まずGoogle Cloudへアクセスできる環境を構築します。ある程度大きい権限が必要になるので、難しい場合は検証用にプロジェクトを作成するか、権限のないリソースをコメントアウトするなどしてください。

なお、APIを利用するのに課金の設定が必要になるのですが、個人アカウントだと課金に設定できるプロジェクトがデフォルトでは5つまでなのでご注意ください。（地味にハマりました・・・）

https://dev.classmethod.jp/articles/gcp-billing-quota-increase/

ではまず、Google CloudのCLIであるgcloudを使えるようにします。[gcloud CLIをインストール](https://cloud.google.com/sdk/docs/install?hl=ja)し、以下のコマンドを実行してデフォルトのプロジェクトを指定します。

`gcloud auth application-default login`
`gcloud auth application-default set-quota-project {YOUR_DEFAULT_PROJECT_ID}`

Terraformの実行時には、このときに設定したプロジェクトを参照することになります。


## Terraformの準備をする

次に、[Terraformをダウンロード](https://developer.hashicorp.com/terraform/install)してPATHを通します。Terraformのバージョンは`./sample1_basic/provider.tf`の指定に合わせてください。

続いて、サンプルの設定を自分の環境で実行できる形に変更します。

- locals.tfにあるproject_id、sample_gcs_bucket_name_backend、sample_gcs_bucket_name_datasource、your_emailを変更する
- TROCCO®のGUIで`Terraform Managed`のラベルを作成しておく（現時点ではTerraformに対応していないため）
- provider.tfの設定を見てみる（特に変更は必要ないかと思います）
- `./sample1_basic/_google.tf`、`./sample1_basic/_trocco.tf`を見て、リソースが管理可能かを確認する

リソースを作成／変更／削除する権限があるか、既存のリソースが同一名称で存在していないか、そもそもAPIが有効されているかどうか（これは最悪エラーになってから有効化するでもよい）を確認してください。

なお、TROCCOのユーザーは特権管理者でないと削除ができないので、それで困る場合はユーザーの部分はコメントアウトしてください。


## Terraformを実行する

ちなみに、VSCodeでtfファイルを編集する際には、[HashiCorp Terraformの拡張機能](https://marketplace.visualstudio.com/items?itemName=HashiCorp.terraform)を入れておくと非常に便利です。

### 各種リソースを作成する

- `cd ./sample1_basic`: ワーキングディレクトリを変更する
- `terraform init`: Terraformを初期化する
- `set TROCCO_API_KEY={YOUR_TROCCO_API_KEY}`: APIキーを環境変数に設定する
- `terraform plan`: plan結果を見てみる
- `terraform apply` `yes`または`terraform apply --auto-approve`: リソースを作成する

これでリソースが作成できます。簡単ですね。applyが失敗してしまう場合は、ひとまず対象および依存関係のあるリソースをコメントアウトして試してみてもよいです。

ちなみにapplyの実行中には、`./sample1_basic/.terraform.tfstate.lock.info`というファイルが一時的に生成されています。このファイルがあることで、変更が競合しないようにロックをかけています。

applyが完了すると、ローカルに`./sample1_basic/terraform.tfstate`があると思うので、このファイルとtfファイルとの差分を確認してみてください。リソースの作成後にしかわからない値がstateファイルの方にはあることが確認できます。

このとき、`"type": "trocco_connection"`で検索すると、BigQuery接続情報の定義が取得できます。`https://trocco.io/connections/bigquery/{接続情報のID}`で接続情報を開いて、下部の「接続を確認」ボタンをクリックすると、接続確認ができます。

![接続確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492462/b6a50e50-37e4-194f-7715-3f1a30d7ad79.png)

これで、Terraformで生成したサービスアカウント、サービスアカウントのキーを使って、TROCCO®の接続情報を作成できたことが分かります。

同様に、`"type": "trocco_bigquery_datamart_definition"`で検索して`https://trocco.io/datamart/definitions/{データマート定義のID}`にアクセスすると、サービスアカウントにBigQueryのジョブユーザーとデータ編集者の権限を付与しているので、データマート定義も実行できるようになっています。

![データマート定義.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492462/3121fde2-f70d-a325-4050-56d11047e0da.png)

実行した場合は、後ほどデータセットをdestroyできなくなってしまうので、作ったテーブルは削除しておいてください。


### バックエンドをGCSに移す

では続いて、バックエンドをGCSに変更してみましょう。`./sample1_basic/provider.tf`のコメントアウトを外し、バケットを先ほど自身で作成したものに書き換えてください。

ここで`terraform plan`を実行してみると、バックエンド（stateファイルの保存先）の設定が変更されたので、再度`terraform init`せよと怒られてエラーになります。

```console:コンソールでの表示例
$ terraform plan
│ Error: Backend initialization required, please run "terraform init"
│
│ Reason: Initial configuration of the requested backend "gcs"
│
│ The "backend" is the interface that Terraform uses to store state,
│ perform operations, etc. If this message is showing up, it means that the
│ Terraform configuration you're using is using a custom configuration for
│ the Terraform backend.
│
│ Changes to backend configurations require reinitialization. This allows
│ Terraform to set up the new configuration, copy existing state, etc. Please run
│ "terraform init" with either the "-reconfigure" or "-migrate-state" flags to
│ use the current configuration.
│
│ If the change reason above is incorrect, please verify your configuration
│ hasn't changed and try again. At this point, no changes to your existing
│ configuration or state have been made.
```

現在のstateをそのまま移行したいので、`terraform init -migrate-state`を実行しましょう。
```console:コンソールでの表示例
$ terraform init -migrate-state
Initializing the backend...
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend to the
  newly configured "gcs" backend. No existing state was found in the newly
  configured "gcs" backend. Do you want to copy this state to the new "gcs"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value: yes


Successfully configured the backend "gcs"! Terraform will automatically
use this backend unless the backend configuration changes.
Initializing provider plugins...
- Reusing previous version of hashicorp/random from the dependency lock file
- Reusing previous version of hashicorp/google from the dependency lock file
- Reusing previous version of trocco-io/trocco from the dependency lock file
- Using previously-installed hashicorp/random v3.6.3
- Using previously-installed hashicorp/google v6.12.0
- Using previously-installed trocco-io/trocco v0.2.1

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
```

この状態でGoogle Cloud Storageの対応するバケットにアクセスすると、確かにstateファイルが移行されていることを確認することができます。


### stateの状態を色々いじってみる

では、stateを少しいじってみましょう。stateに関連するコマンドで主なものは以下の通りです。

| コマンド | 意味 |
| ------- | ---- |
| state list | stateファイルにあるリソースを一覧表示する |
| state mv | stateファイルにあるリソースのTerraformでのIDを変更する |
| state show {id} | stateファイルにある指定したIDのデータを表示する |
| state rm | stateファイルにあるリソースをstateファイルから除外する |
| import {TerraformのID} {リソースのID} | Terraform管理外のリソースをstateに取り込む |

`terraform state list`で現在stateに存在するリソースが一覧化されます。

```console:コンソールでの表示例
$ terraform state list
google_bigquery_dataset.dl_trocco_sample
google_bigquery_dataset.dm_trocco_sample
google_project_iam_member.bigquery_data_editor
google_project_iam_member.bigquery_job_user
google_service_account.trocco
google_service_account_key.trocco
google_storage_bucket.trocco_sample
google_storage_bucket.trocco_sample_datasource
google_storage_bucket_iam_member.google_service_account_trocco

（略）

trocco_bigquery_datamart_definition.dm_trocco_sample__sample
trocco_connection.bigquery

（略）
```

リソースをmv（move）で移行してみます。これはTerraform側で指定しているリソースIDの変更処理になります。

```console:コンソールでの表示例
$ terraform state mv trocco_bigquery_datamart_definition.dm_trocco_sample__sample trocco_bigquery_datamart_definition.dm_trocco_sample__sample2
Move "trocco_bigquery_datamart_definition.dm_trocco_sample__sample" to "trocco_bigquery_datamart_definition.dm_trocco_sample__sample2"
Successfully moved 1 object(s).
```

リソースIDが変更されたので、この状態で`terraform plan`すると元のIDのリソースがなくなり、新しいIDが増えて、create／destroyが表示されます。リソースのTerraformのID変更や、moduleを利用してリファクタリングをする際には、このようにstateを変更しながら、tfファイルの設定を合わせていきます。

```console:コンソールでの表示例
$ terraform plan

（略）

trocco_bigquery_datamart_definition.dm_trocco_sample__sample2: Refreshing state... [name=dm_trocco_sample__sample]

（略）

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
  - destroy

Terraform will perform the following actions:

  # trocco_bigquery_datamart_definition.dm_trocco_sample__sample will be created
  + resource "trocco_bigquery_datamart_definition" "dm_trocco_sample__sample" {
      + bigquery_connection_id   = 3471
      + destination_dataset      = "dm_trocco_sample"
      + destination_table        = "dm_trocco_sample__sample"
      + id                       = (known after apply)
      + is_runnable_concurrently = true
      + labels                   = [
          + {
              + id   = (known after apply)
              + name = "Terraform Managed"
            },
        ]
      + name                     = "dm_trocco_sample__sample"
      + query                    = "select '転送設定を心待ちにしながら、Terraformでデータマートを作っ てみよう' as sample_column"
      + query_mode               = "insert"
      + write_disposition        = "truncate"
    }

  # trocco_bigquery_datamart_definition.dm_trocco_sample__sample2 will be destroyed
  # (because trocco_bigquery_datamart_definition.dm_trocco_sample__sample2 is not in configuration)   
  - resource "trocco_bigquery_datamart_definition" "dm_trocco_sample__sample2" {
      - bigquery_connection_id   = 3471 -> null
      - destination_dataset      = "dm_trocco_sample" -> null
      - destination_table        = "dm_trocco_sample__sample" -> null
      - id                       = 55771 -> null
      - is_runnable_concurrently = true -> null
      - labels                   = [
          - {
              - id   = 4513 -> null
              - name = "Terraform Managed" -> null
            },
        ] -> null
      - name                     = "dm_trocco_sample__sample" -> null
      - query                    = "select '転送設定を心待ちにしながら、Terraformでデータマートを作っ てみよう' as sample_column" -> null
      - query_mode               = "insert" -> null
      - write_disposition        = "truncate" -> null
    }

Plan: 1 to add, 0 to change, 1 to destroy.
```

個別のstateの中身を確認するには、`terraform state show {リソースID}`を実行します。

```console
$ terraform state show trocco_bigquery_datamart_definition.dm_trocco_sample__sample2
# trocco_bigquery_datamart_definition.dm_trocco_sample__sample2:
resource "trocco_bigquery_datamart_definition" "dm_trocco_sample__sample2" {
    bigquery_connection_id   = 3471
    destination_dataset      = "dm_trocco_sample"
    destination_table        = "dm_trocco_sample__sample"
    id                       = 55771
    is_runnable_concurrently = true
    labels                   = [
        {
            id   = 4513
            name = "Terraform Managed"
        },
    ]
    name                     = "dm_trocco_sample__sample"
    query                    = "select '転送設定を心待ちにしながら、Terraformでデータマートを作ってみ よう' as sample_column"
    query_mode               = "insert"
    write_disposition        = "truncate"
}
```

ここのIDをメモしておいてください。では、一度stateから削除してしまいましょう。

```console:コンソールでの表示例
$ terraform state rm trocco_bigquery_datamart_definition.dm_trocco_sample__sample2
Removed trocco_bigquery_datamart_definition.dm_trocco_sample__sample2
Successfully removed 1 resource instance(s).
```

このままではなくなっただけなので、元に戻すために再度インポートします。

```console:コンソールでの表示例
$ terraform import trocco_bigquery_datamart_definition.dm_trocco_sample__sample {先ほどのID}
trocco_bigquery_datamart_definition.dm_trocco_sample__sample: Importing from ID "{先ほどのID}"...
trocco_bigquery_datamart_definition.dm_trocco_sample__sample: Import prepared!
  Prepared trocco_bigquery_datamart_definition for import
trocco_bigquery_datamart_definition.dm_trocco_sample__sample: Refreshing state...

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
```

この状態で`plan`をすると、差分がないはずです。

```console:コンソールでの表示例
$ terraform plan

（略）

trocco_connection.bigquery: Refreshing state... [name={YOUR_PROJECT_ID}__trocco-sample@{YOUR_PROJECT_ID}.iam.gserviceaccount.com]
trocco_bigquery_datamart_definition.dm_trocco_sample__sample: Refreshing state... [name=dm_trocco_sample__sample]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
```

運用時にはこうしたコマンドを手元で実行するのは避けるべきですが、仕組みを理解するには試してみる（そして失敗するｗ）ことが大切です。


### 作ったリソースをお掃除する

では最後に、作ったリソースを削除します。その前に、`./sample1_basic/provider.tf`のバックエンドの設定をコメントアウトして、再度`terraform init -migrate-state`でバックエンドをローカルに戻してください。そうしないと、バックエンドに設定しているバケットごとstateファイルを消してしまうので・・・（経験者は語る）

```console:コンソールでの表示例
$ terraform destroy

（略）

trocco_connection.bigquery: Refreshing state... [name={YOUR_PROJECT_ID}__trocco-sample@{YOUR_PROJECT_ID}.iam.gserviceaccount.com]
trocco_bigquery_datamart_definition.dm_trocco_sample__sample: Refreshing state... [name=dm_trocco_sample__sample]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

（略）

  # trocco_bigquery_datamart_definition.sample will be destroyed
  - resource "trocco_bigquery_datamart_definition" "dm_trocco_sample__sample" {
      - bigquery_connection_id   = 3471 -> null
      - destination_dataset      = "dm_trocco_sample" -> null
      - destination_table        = "dm_trocco_sample__sample" -> null
      - id                       = 55771 -> null
      - is_runnable_concurrently = true -> null
      - labels                   = [
          - {
              - id   = 4513 -> null
              - name = "Terraform Managed" -> null
            },
        ] -> null
      - name                     = "dm_trocco_sample__sample" -> null
      - query                    = "select '転送設定を心待ちにしながら、Terraformでデータマートを作っ てみよう' as sample_column" -> null
      - query_mode               = "insert" -> null
      - write_disposition        = "truncate" -> null
    }

  # trocco_connection.bigquery will be destroyed
  - resource "trocco_connection" "bigquery" {
      - connection_type          = "bigquery" -> null
      - description              = "プロジェクト{YOUR_PROJECT_ID}に対してサービスアカウントのtrocco-sample@{YOUR_PROJECT_ID}.iam.gserviceaccount.comで接続するための接続情報" -> null
      - id                       = 3472 -> null
      - name                     = "{YOUR_PROJECT_ID}__trocco-sample@{YOUR_PROJECT_ID}.iam.gserviceaccount.com" -> null
      - project_id               = "{YOUR_PROJECT_ID}" -> null
      - service_account_json_key = (sensitive value) -> null
    }

（略）

Plan: 0 to add, 0 to change, 22 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

（略）

trocco_bigquery_datamart_definition.dm_trocco_sample__sample: Destroying... [name=dm_trocco_sample__sample]

（略）

trocco_bigquery_datamart_definition.dm_trocco_sample__sample: Destruction complete after 0s
trocco_connection.bigquery: Destroying... [name={YOUR_PROJECT_ID}__trocco-sample@{YOUR_PROJECT_ID}.iam.gserviceaccount.com]
（略）

trocco_connection.bigquery: Still destroying... [name={YOUR_PROJECT_ID}__trocco-sample@{YOUR_PROJECT_ID}.iam.gserviceaccount.com, 10s elapsed]

（略）

trocco_connection.bigquery: Still destroying... [name={YOUR_PROJECT_ID}__trocco-sample@{YOUR_PROJECT_ID}.iam.gserviceaccount.com, 20s elapsed]
trocco_connection.bigquery: Still destroying... [name={YOUR_PROJECT_ID}__trocco-sample@{YOUR_PROJECT_ID}.iam.gserviceaccount.com, 30s elapsed]
trocco_connection.bigquery: Still destroying... [name={YOUR_PROJECT_ID}__trocco-sample@{YOUR_PROJECT_ID}.iam.gserviceaccount.com, 40s elapsed]
trocco_connection.bigquery: Still destroying... [name={YOUR_PROJECT_ID}__trocco-sample@{YOUR_PROJECT_ID}.iam.gserviceaccount.com, 50s elapsed]
trocco_connection.bigquery: Still destroying... [name={YOUR_PROJECT_ID}__trocco-sample@{YOUR_PROJECT_ID}.iam.gserviceaccount.com, 1m0s elapsed]
trocco_connection.bigquery: Destruction complete after 1m1s

（略）

Destroy complete! Resources: 22 destroyed.
```


# おわりに

ここまでがTerraformの基礎編でした！まだ難しいなという方は、Software Designの記事を転載しているCaddiのシリーズ記事が非常に参考になるので、よければ合わせて読んでみてください。

https://caddi.tech/archives/4294

後半はCI／CDを含めたTerraformの運用について考えていきます。今回より技術レベルが急に上がるので大変そうですが（他人事）、後半もぜひ参考にしてみてください！