Terraform における Provider インストール処理を理解する（2021年2月版）

## はじめに

Terraform ではプラグインベースのアーキテクチャが採用されており、利用者はプラグインを利用することで自由に Terraform の機能拡張を行うことができます。プラグインとしては AWS などのインフラプロバイダーのリソースを制御する Provider と、作成されたリソースをセットアップする Provisioner の2種類がサポートされています。

Provisioner [^1] は Terraform バイナリに含まれているのですが、Provider は含まれていないため [^2] [^3]、Terraform を初期化する際に HashiCorp が提供する [Terraform Registry](https://registry.terraform.io/) などの外部レジストリから必要に応じてインストールする仕様になっています。

具体的な処理フローとしては `terraform init` が実行されると、まず Terraform は作業ディレクトリにある設定ファイルを読み込んでリソース制御に必要なインストール対象の Provider を判定します。次にインストール済みの Provider を確認して、不足しているものがあれば外部レジストリからダウンロードして実行環境にインストールする処理が行われ、最後に `terraform init` が再び実行された際に同じ Provider を使用するように、バージョンやハッシュ値を `.terraform.lock.hcl` というロックファイルに書き込んで一連の処理が完了となります。[^4]

通常は上記のフローになりますが、Terraform を実行するホストが組織や地域のファイアウォールにより外部レジストリにアクセスできない場合は、事前に Provider をインストールしておいたり、実行ホストのネットワークから到達可能なプライベートレジストリを利用する必要があります。

このような仕様を踏まえて、今回は Terraform における Provider インストール処理の仕様を紹介していきます。なお、こちらの記事は 2021年2月24日 時点での最新バージョン `v0.14.7` での仕様を元にまとめたものとなります。

## インストールする Provider の判定方法

インストールする Provider は .tf ファイルの `required_providers` ブロックで定義された Provider の [ソースアドレス](https://github.com/hashicorp/terraform/blob/v0.14.7/website/docs/language/providers/requirements.html.md#source-addresses) とバージョンから判定されます。`required_providers` ブロックが定義されていない場合は .tf ファイル内の定義から判定します。

以下のような設定の場合であれば、registry.terraform.io というレジストリの hashicorp という Namespace の aws という Provider のバージョン 3.28.0 をインストールする必要があると判定されるイメージです。ソースアドレスは公式レジストリの場合に限り `hashicorp/aws` のようにレジストリ名を省略することができます。

```hcl
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "= 3.28.0"
    }
  }
}
```

上記はロックファイルが存在しない場合の仕様となっており、既に Terraform の初期化が完了していてロックファイルが存在している場合は、`required_providers` ブロックに加えてロックファイルに記録された情報も考慮され、インストールする Provider が判別されます。詳細は https://github.com/hashicorp/terraform/blob/v0.14.7/website/docs/language/dependency-lock.html.md#dependency-installation-behavior を参照してください。

## Provider のインストール方法

Provider のインストール方法は、以下のように CLI の [設定ファイル](https://github.com/hashicorp/terraform/blob/v0.14.7/website/docs/cli/config/config-file.html.md) 内の `provider_installation` ブロックで指定できます。インストール方法を複数定義して、その中で include と exclude というパラメーターを指定することで、Provider 毎にインストール方法を個別に指定できます。

```hcl
provider_installation {
  filesystem_mirror {
    path    = "/usr/share/terraform/providers"
    include = ["example.com/*/*"]
  }
  direct {
    exclude = ["example.com/*/*"]
  }
}
```

CLI の設定ファイルについても簡単に紹介します。設定ファイルのパスは Terraform を実行する OS に依存しており、Windows であれば実行ユーザーの `%APPDATA%` ディレクトリに配置された `terraform.rc` というファイルを、それ以外の OS であれば実行ユーザーのホームディレクトリに配置された `.terraformrc` というファイルを設定ファイルとして読み込む仕様になっています。環境変数 `TF_CLI_CONFIG_FILE` で任意のパスを指定することも可能です。

それでは `provider_installation` ブロックでサポートされている Provider のインストール方法を紹介していきます。

## direct

Terraform におけるデフォルトの Provider インストール方法です。レジストリに Provider 情報を要求して、レジストリが示した場所からネットワーク経由で Provider をインストールする方法になります。以下は `example.com` というレジストリの Provider をネットワーク経由でインストールする設定例です。

```hcl
provider_installation {
  direct {
    include = ["example.com/*/*"]
  }
}
```

## filesystem_mirror

ローカルのファイルシステムに配置されたファイルから Provider をインストールする方法になります。こちらは `terraform init` の `-plugin-dir` オプションで代用することも可能になっています。以下は `example.com` というレジストリの Provider を `/usr/share/terraform/providers` からインストールする設定例です。

```hcl
provider_installation {
  filesystem_mirror {
    path    = "/usr/share/terraform/providers"
    include = ["example.com/*/*"]
  }
}
```

設定例からわかるように、パラメーターで Provider バイナリを検索するディレクトリを指定しているのですが [^5] 、事前にバイナリをどのような形式で配置するかによって、指定されたパス配下のディレクトリ構造をどのように用意しておくかが異なります。

`terraform providers mirror` コマンドを利用して外部レジストリから取得した ZIP 圧縮したバイナリを配置しておく場合は `<HOSTNAME>/<NAMESPACE>/<TYPE>/terraform-provider-<TYPE>_<VERSION>_<TARGET>.zip` というディレクトリ構造を、ZIP ファイルを解凍してバイナリ本体を配置しておく場合は `<HOSTNAME>/<NAMESPACE>/<TYPE>/<VERSION>/<TARGET>` というディレクトリ構造を用意しておく必要があります。TARGET というのは darwin_amd64、linux_arm、windows_amd64 など OS と CPU アーキテクチャを連結させた文字列を指しています。

|配置するファイル|ディレクトリ構造|
|:-:|:-:|
|ZIP ファイル|\<HOSTNAME>/\<NAMESPACE>/\<TYPE>/terraform-provider-\<TYPE>\_\<VERSION>_\<TARGET>.zip|
|バイナリ本体|\<HOSTNAME>/\<NAMESPACE>/\<TYPE>/\<VERSION>/\<TARGET>|

ZIP 圧縮したバイナリが配置されている場合は、Provider のインストール時に ZIP ファイルを解凍した結果がワーキングディレクトリ直下の `.terraform` ディレクトリに出力される仕様で、バイナリ本体が配置されている場合は `.terraform` ディレクトリから設定されたディレクトリにシンボリックリンクが作成される仕様になっていますので、Teraform の運用方法によって最適な方を採用すれば良いかと思います。

説明だけだとわかりづらいので、MacOS での具体的なインストール例を以下に記載します。

### ZIP 圧縮した Provider バイナリを配置した際のインストール例

```bash
$ cat main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "= 3.28.0"
    }
  }
}

# ZIP 圧縮した Provider バイナリをダウンロードする
$ terraform providers mirror ./plugins
- Mirroring hashicorp/aws...
  - Selected v3.28.0 to meet constraints 3.28.0
  - Downloading package for darwin_amd64...
  - Package authenticated: signed by HashiCorp

# デフォルトで仕様に準拠したディレクトリ構造が作成される
# <HOSTNAME>/<NAMESPACE>/<TYPE>/terraform-provider-<TYPE>_<VERSION>_<TARGET>.zip
$ tree plugins
plugins
└── registry.terraform.io
    └── hashicorp
        └── aws
            ├── 3.28.0.json
            ├── index.json
            └── terraform-provider-aws_3.28.0_darwin_amd64.zip

# .terraformrc を用意せず -plugin-dir オプションを利用して
# filesystem_mirror 方式で Provider をインストールする
$ terraform init -plugin-dir plugins

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "3.28.0"...
- Installing hashicorp/aws v3.28.0...
- Installed hashicorp/aws v3.28.0 (unauthenticated)

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.

# 利用した Provider のバージョンやハッシュ値がロックファイルに保存される
$ cat .terraform.lock.hcl
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/aws" {
  version     = "3.28.0"
  constraints = "3.28.0"
  hashes = [
    "h1:0cCqlVoOAj4YOi61kVpqoxu1bdAmB67z6uZf+lsHJOw=",
  ]
}

# ZIP ファイルを解凍した結果がワーキングディレクトリ直下の .terraform ディレクトリに出力される
$ tree .terraform
.terraform
├── plugin_path
└── providers
    └── registry.terraform.io
        └── hashicorp
            └── aws
                └── 3.28.0
                    └── darwin_amd64
                        └── terraform-provider-aws_v3.28.0_x5

# 次の例のために不要なファイルを削除する
$ rm -rf .terraform .terraform.lock.hcl
```

### Provider バイナリ本体を配置した際のインストール例

1つ前の例で用意した `main.tf` と `plugins/registry.terraform.io/hashicorp/aws/terraform-provider-aws_3.28.0_darwin_amd64.zip` をこの例でも使用します。

```bash
# <HOSTNAME>/<NAMESPACE>/<TYPE>/<VERSION>/<TARGET> に準拠したディレクトリを作成する
$ mkdir -p ./unpacked-plugins/registry.terraform.io/hashicorp/aws/3.28.0/darwin_amd64

# ZIP ファイルを解凍して作成したディレクトリ配下にバイナリ本体を配置する
$ unzip plugins/registry.terraform.io/hashicorp/aws/terraform-provider-aws_3.28.0_darwin_amd64.zip \
  -d ./unpacked-plugins/registry.terraform.io/hashicorp/aws/3.28.0/darwin_amd64
Archive:  plugins/registry.terraform.io/hashicorp/aws/terraform-provider-aws_3.28.0_darwin_amd64.zip
  inflating: ./unpacked-plugins/registry.terraform.io/hashicorp/aws/3.28.0/darwin_amd64/terraform-provider-aws_v3.28.0_x5

# バイナリ本体が配置されたことを確認する
$ tree unpacked-plugins
unpacked-plugins
└── registry.terraform.io
    └── hashicorp
        └── aws
            └── 3.28.0
                └── darwin_amd64
                    └── terraform-provider-aws_v3.28.0_x5

# .terraformrc を用意せず -plugin-dir オプションを利用して
# filesystem_mirror 方式で Provider をインストールする
$ terraform init -plugin-dir unpacked-plugins

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "3.28.0"...
- Installing hashicorp/aws v3.28.0...
- Installed hashicorp/aws v3.28.0 (unauthenticated)

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.

# 利用した Provider のバージョンやハッシュ値がロックファイルに保存される
$ cat .terraform.lock.hcl
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/aws" {
  version     = "3.28.0"
  constraints = "3.28.0"
  hashes = [
    "h1:0cCqlVoOAj4YOi61kVpqoxu1bdAmB67z6uZf+lsHJOw=",
  ]
}

# .terraform ディレクトリから設定されたディレクトリにシンボリックリンクが作成される
$ tree .terraform
.terraform
├── plugin_path
└── providers
    └── registry.terraform.io
        └── hashicorp
            └── aws
                └── 3.28.0
                    └── darwin_amd64 -> /Users/ryumyosh/Desktop/terraform/unpacked-plugins/registry.terraform.io/hashicorp/aws/3.28.0/darwin_amd64
```

## network_mirror

Terraform を実行するホストのネットワークで到達可能なプライベートレジストリから Provider をインストールする方法になります。以下は `example.com` というレジストリの Provider を `https://terraform.example.com/providers` というプライベートレジストリからインストールする設定例です。

```hcl
provider_installation {
  network_mirror {
    url     = "https://terraform.example.com/providers"
    include = ["example.com/*/*"]
  }
}
```

設定例からわかるように、パラメーターでプライベートレジストリの URL を指定しているのですが、プライベートレジストリでは [Provider Network Mirror Protocol](https://github.com/hashicorp/terraform/blob/v0.14.7/website/docs/internals/provider-network-mirror-protocol.html.md) を実装する必要があるので注意してください。

## Provider のインストール方法が指定されていない場合の仕様

CLI の設定ファイル内の `provider_installation` ブロックや `terraform init` のオプションなどで Provider のインストール方法が指定されていない場合は、`filesystem_mirror` ブロックと `direct` ブロックで構成された暗黙的な設定が使用されます。

暗黙的設定の `filesystem_mirror` ブロックで設定される Provider バイナリを検索するディレクトリは Terraform を実行する OS に依存しており以下のような仕様になっています。

### Windows

- %APPDATA%/terraform.d/plugins
- %APPDATA%/HashiCorp/Terraform/plugins


### MacOS

- $HOME/.terraform.d/plugins/
- $HOME/Library/Application\ Support/io.terraform/plugins
- /Library/Application\ Support/io.terraform/plugins

### Linux [^6]

- $PWD/terraform.d/plugins
- $HOME/.terraform.d/plugins
- $HOME/.local/share/terraform/plugins
- /usr/local/share/terraform/plugins
- /usr/share/terraform/plugins

`filesystem_mirror` ブロックでローカルからインストールできなかった Provider は `direct` ブロックにより外部レジストリからインストールされる仕様となります。

## Provider のキャッシュについて

Terraform は、初期化した際にワーキングディレクトリ直下に `.terraform/providers` ディレクトリを作成して、そのディレクトリ配下に Provider をインストールするのがデフォルトの挙動になっているのですが、この仕様だと direct 方式や network_mirror 方式を利用している場合、Terraform の初期化で Provider バイナリを都度ダウンロードすることになり、無駄に時間を要してしまう場合があります。

これを解決するために Terraform では Provider リソースをキャッシュする機能がサポートされており、以下のように CLI の設定ファイル内に `plugin_cache_dir` でキャッシュするディレクトリを指定することでキャッシュ機能を有効にできます。設定をしなくても環境変数 `TF_PLUGIN_CACHE_DIR` でディレクトリを指定して機能を有効にすることもできます。

```hcl
plugin_cache_dir = "$HOME/.terraform.d/plugin-cache"
```

キャッシュ機能が有効になっている場合、CLI の設定ファイルで指定されたインストール方法が実行される前にキャッシュディレクトリをチェックして、キャッシュされていれば以前にダウンロードした Provider バイナリがインストールで再利用される仕様 [^7] になっています。キャッシュディレクトリを `filesystem_mirror` のディレクトリとして使用することは推奨されていないので注意してください。

単一ホストで同じ Provider を使用する .tf ファイルが複数ある場合、ローカルに共有キャッシュを持つことで初期化処理で無駄な Provider バイナリのダウンロード処理をスキップすることができるので、非常に有効な機能かと思います。

## まとめ

ここまで説明した仕様を元に Provider インストール処理のフローを整理しました。Terraform のソースコードを深く読み込んでいるわけではないので細かな部分で誤りはあると思いますが、概要を把握したい方の参考になれば幸いです。

### ステップ1: インストールする Provider の判定

.tf ファイルに `required_providers` ブロックが定義されていれば、そこからインストールする Provider を判定する。定義されていない場合は .tf ファイル内の各設定から判定する。`.terraform.lock.hcl` が存在している場合には、ロックファイルに記録された情報も考慮して判定する。

### ステップ2: インストール済み Provider の確認

ワーキングディレクトリ直下の `.terraform/providers` ディレクトリを参照して、インストール済みの Provider を確認する。全ての Provider がインストール済みの場合には処理はここで完了となり、未インストールのものがあれば次のステップが開始される。

### ステップ3: Provider のインストール方法の決定

CLI の設定ファイル内の `provider_installation` ブロックや `terraform init` のオプションなどで Provider のインストール方法が指定されている場合は、そこからインストール方法を決定する。インストール方法が指定されていない場合は `filesystem_mirror` ブロックと `direct` ブロックで構成された暗黙的な設定を使用する。

### ステップ4: キャッシュされている Provider リソースの確認

キャッシュ機能が有効になっている場合、Provider リソースがキャッシュされていたら、そのキャッシュを使用して Provider をインストールする。キャッシュ機能が有効になっていない場合はこのステップはスキップされる。

### ステップ5: Provider のインストール

指定されたインストール方法に従って Provider をインストールして、インストールした Provider の情報をロックファイルに書き込んでインストール処理が完了となる。

## さいごに

今回は Terraform における Provider インストール処理の仕様を紹介しました。調査を開始した当初は Terraform の仕様を正しく理解できていない部分があり、yak shaving で時間が溶けてしまいましたが、結果的に Terraform の理解を深めることができてとても良かったです。この記事がどなたかの参考になれば幸いです。

# 参考資料

- https://www.terraform.io/docs/extend/index.html
- https://www.terraform.io/docs/cli/config/config-file.html#provider-installation
  - https://github.com/hashicorp/terraform/blob/v0.14.7/website/docs/cli/config/config-file.html.md#provider-installation

[^1]: https://www.terraform.io/docs/language/resources/provisioners/index.html 配下の Generic Provisioners や Vendor Provisioners から公式でサポートされている Provisioner を参照できます。ソースコードは https://github.com/hashicorp/terraform/tree/v0.14.7/builtin/provisioners を参照してください。
[^2]: Provider は Terraform バイナリに含まれていないと記載していますが、厳密には `terraform_remote_state` データソースという Provider のみが Terraform バイナリに含まれています。詳細は https://github.com/hashicorp/terraform/blob/v0.14.7/website/docs/language/providers/requirements.html.md#built-in-providers を参照してください。
[^3]: Provider バイナリは別プロセスとして起動して、RPC インターフェースを介して Terraform バイナリと通信します。
[^4]: 今回は Provider のインストール処理にフォーカスしているため、バックエンドの初期化など他の処理の説明は割愛しています。
[^5]: 検索するディレクトリを複数指定したい場合には `filesystem_mirror` ブロックを複数定義する必要があります。
[^6]: `docker run -it --rm --name tf --entrypoint sh hashicorp/terraform:0.14.7` で Terraform コンテナを起動して、適当な .tf ファイルを用意した後で `TF_LOG=DEBUG terraform init` を実行するとデバッグログから Linux 環境で検索されるディレクトリを確認できます。
[^7]: キャッシュされた Provider バイナリは Terraform によって自動に削除されず、時間経過と共に未使用のバージョンが溜まっていくことが予想されるので、定期的に手動で削除する必要があることに注意してください。