terraform-bundle で Terraform の実行に必要なファイルをパッケージングする

## 2021-07-15: 追記

Terraform v1.0.0 で terraform-bundle の開発が終了となりました。
代替手段を以下の記事で紹介しているので、そちらを参照頂ければと思います。

https://qiita.com/ryysud/items/e6e0d1701dc27ceec171

## terraform-bundle とは

[terraform-bundle](https://github.com/hashicorp/terraform/tree/main/tools/terraform-bundle) は、特定バージョンの Terraform バイナリと Provider バイナリを含んだ ZIP ファイルである bundle archive を作成する CLI で、Terraform コミュニティによりメンテナンスされている公式のツールです。

通常、Terraform は初期化処理の際に Provider バイナリを HashiCorp が提供する [Terraform Registry](https://registry.terraform.io/) などの外部レジストリからインストールしますが、Terraform を実行するホストが組織や地域のファイアウォールにより外部レジストリにアクセスできない場合があります。

事前に Provider バイナリをインストールしておいたり、実行ホストのネットワークから到達可能なプライベートレジストリを利用することで上記の課題を解決できますが、terraform-bundle を実行するだけで Terraform 実行に必要なファイルをまとめてパッケージングして用意できるのが terraform-bundle を利用するメリットかと思います。[^1]

Provider のインストールに関する仕様については [Terraform における Provider インストール処理を理解する（2021年2月版）](https://qiita.com/ryysud/items/a38bcc856069801878e9) という記事でまとめていますので興味があれば参照してみてください。

## インストール方法

以下は、2021年2月24日時点での最新バージョン `v0.14.7` をインストールする例となります。

```bash
git clone --depth 1 -b v0.14.7 https://github.com/hashicorp/terraform.git
cd terraform
go install ./tools/terraform-bundle
```

terraform-bundle のバージョンは、bundle archive に含める予定の Terraform と互換性があるバージョンにすることが推奨されています。例として Terrarom v0.14.x を利用する予定であれば、terraform-bundle のバージョンも v0.14.x を利用するのが良いでしょう。

## 設定ファイル

terraform-bundle は、bundle archive に含める Terraform バイナリと Provider バイナリが定義された HCL 形式の設定ファイルを元に bundle archive を作成します。以下は設定ファイルの例となります。

```hcl
terraform {
  # Version of Terraform to include in the bundle. An exact version number
  # is required.
  version = "0.10.0"
}

# Define which provider plugins are to be included
providers {
  # Include the newest "aws" provider version in the 1.0 series.
  aws = {
    versions = ["~> 1.0"]
  }

  # Include both the newest 1.0 and 2.0 versions of the "google" provider.
  # Each item in these lists allows a distinct version to be added. If the
  # two expressions match different versions then _both_ are included in
  # the bundle archive.
  google = {
    versions = ["~> 1.0", "~> 2.0"]
  }

  # Include a custom plugin to the bundle. Will search for the plugin in the
  # plugins directory and package it with the bundle archive. Plugin must have
  # a name of the form: terraform-provider-*, and must be built with the operating
  # system and architecture that terraform enterprise is running, e.g. linux and amd64.
  customplugin = {
    versions = ["0.1"]
    source = "myorg/customplugin"
  }
}
```

設定ファイルは `terraform` ブロックと `providers` ブロックで構成されており、`terraform` ブロックでは Terraform バイナリのバージョンを定義して、`providers` ブロックでは Provider 名をキーにしてバージョンとソース [^2] を定義します。バージョンの定義方法は [Version Constraints](https://github.com/hashicorp/terraform/blob/v0.14.7/website/docs/language/expressions/version-constraints.html.md) に準拠しています。

bundle archive に特定の Provider バイナリを複数バージョン含めることも可能になっていて、その場合は Provider のバージョンに複数の値を定義する必要があります。複数バージョンを含めることで .tf ファイル毎に異なるバージョンの Provider を利用することができます。

## bundle archive の作成例

この例では以下の設定ファイルを利用します。

```hcl:terraform-bundle.hcl
# Terraform バイナリは v0.14.7 とする
terraform {
  version = "0.14.7"
}

providers {
  # registry.terraform.io/hashicorp/aws の v3.29.1 をインストールする
  # https://github.com/hashicorp/terraform-provider-aws
  aws = {
    versions = ["= 3.29.1"]
  }

  # registry.terraform.io/hashicorp/google の任意の3バージョンをインストールする
  # https://github.com/hashicorp/terraform-provider-google
  google = {
    versions = ["= 3.56.0", "= 3.57.0", "= 3.58.0"]
  }
}
```

設定ファイルを指定して terraform-bundle を実行します。

```bash
$ terraform-bundle --version
0.14.7

$ terraform-bundle package terraform-bundle.hcl
Fetching Terraform 0.14.7 core package...
Local plugin directory ".plugins" found; scanning for provider binaries.
No ".plugins" directory found, skipping local provider discovery.
- Finding hashicorp/aws versions matching "3.29.1"...
- Installing hashicorp/aws v3.29.1...
- Finding hashicorp/google versions matching "3.56.0"...
- Installing hashicorp/google v3.56.0...
- Finding hashicorp/google versions matching "3.57.0"...
- Installing hashicorp/google v3.57.0...
- Finding hashicorp/google versions matching "3.58.0"...
- Installing hashicorp/google v3.58.0...
Creating terraform_0.14.7-bundle2021022502_darwin_amd64.zip ...
All done!
```

bundle archive が作成されました。ファイル名は `terraform_<TERRAFORM_VERSION>-bundle<YYYYMMDDHH>_<OS>_<ARCH>` となり [^3] 、ファイル名から Terraform のバージョン、作成時間、利用を想定したホストを把握することができます。

```bash
$ ls -l terraform_0.14.7-bundle2021022502_darwin_amd64.zip
-rwxr-xr-x  1 ryumyosh  staff   128M Feb 25 11:46 terraform_0.14.7-bundle2021022502_darwin_amd64.zip
```

デフォルトだと bundle archive に対して terraform-bundle を実行したホストの OS と CPU アーキテクチャで実行可能な Terraform バイナリと Provider バイナリを含める仕様なので、bundle archive を利用する環境に応じて、以下のように `-os` と `-arch` オプションを付与してください。

```bash
$ terraform-bundle package -os=linux -arch=amd64 terraform-bundle.hcl
```

それでは作成された bundle archive を解凍して中身を確認していきます。

```bash
$ unzip terraform_0.14.7-bundle2021022502_darwin_amd64.zip
Archive:  terraform_0.14.7-bundle2021022502_darwin_amd64.zip
  inflating: plugins/registry.terraform.io/hashicorp/aws/3.29.1/darwin_amd64/terraform-provider-aws_v3.29.1_x5  
  inflating: plugins/registry.terraform.io/hashicorp/google/3.56.0/darwin_amd64/terraform-provider-google_v3.56.0_x5  
  inflating: plugins/registry.terraform.io/hashicorp/google/3.57.0/darwin_amd64/terraform-provider-google_v3.57.0_x5  
  inflating: plugins/registry.terraform.io/hashicorp/google/3.58.0/darwin_amd64/terraform-provider-google_v3.58.0_x5  
  inflating: terraform 
```

設定ファイルに定義したとおり、bundle archive に以下が含まれていることが確認できました。

- Terraform: v0.14.7
- registry.terraform.io/hashicorp/aws: v3.29.1
- registry.terraform.io/hashicorp/google: v3.56.0、v3.57.0、v3.58.0

```bash
$ ls -l
total 424512
drwxr-xr-x  3 ryumyosh  staff    96B Feb 25 11:47 plugins
-rwxr-xr-x  1 ryumyosh  staff    79M Feb 25 11:46 terraform
-rw-r--r--  1 ryumyosh  staff   502B Feb 25 11:47 terraform-bundle.hcl
-rwxr-xr-x  1 ryumyosh  staff   128M Feb 25 11:47 terraform_0.14.7-bundle2021022502_darwin_amd64.zip

$ ./terraform --version
Terraform v0.14.7

$ tree plugins
plugins
└── registry.terraform.io
    └── hashicorp
        ├── aws
        │   └── 3.29.1
        │       └── darwin_amd64
        │           └── terraform-provider-aws_v3.29.1_x5
        └── google
            ├── 3.56.0
            │   └── darwin_amd64
            │       └── terraform-provider-google_v3.56.0_x5
            ├── 3.57.0
            │   └── darwin_amd64
            │       └── terraform-provider-google_v3.57.0_x5
            └── 3.58.0
                └── darwin_amd64
                    └── terraform-provider-google_v3.58.0_x5
```

plugins ディレクトリ配下の構造が `<HOSTNAME>/<NAMESPACE>/<TYPE>/<VERSION>/<TARGET>` になっているので、何も手を加えずにそのまま `filesystem_mirror` 方式で bundle archive に含まれるファイルから Provider をインストールすることが可能になっています。

```bash
# 適当な .tf ファイルを用意する
$ cat main.tf
terraform {
  required_version = "= 0.14.7"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "= 3.29.1"
    }
    google = {
      source  = "hashicorp/google"
      version = "= 3.58.0"
    }
  }
}

# .terraformrc を用意せず -plugin-dir オプションを利用して
# filesystem_mirror 方式で Provider をインストールする
# Terraform バイナリは bundle archve に含まれているものを利用する
$ ./terraform init -plugin-dir plugins

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "3.29.1"...
- Finding hashicorp/google versions matching "3.58.0"...
- Installing hashicorp/aws v3.29.1...
- Installed hashicorp/aws v3.29.1 (unauthenticated)
- Installing hashicorp/google v3.58.0...
- Installed hashicorp/google v3.58.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.

# bundle archive に含まれるファイルから Provider をインストールできた
$ tree -a .
.
├── .terraform
│   ├── plugin_path
│   └── providers
│       └── registry.terraform.io
│           └── hashicorp
│               ├── aws
│               │   └── 3.29.1
│               │       └── darwin_amd64 -> /Users/ryumyosh/Desktop/terraform/plugins/registry.terraform.io/hashicorp/aws/3.29.1/darwin_amd64
│               └── google
│                   └── 3.58.0
│                       └── darwin_amd64 -> /Users/ryumyosh/Desktop/terraform/plugins/registry.terraform.io/hashicorp/google/3.58.0/darwin_amd64
├── .terraform.lock.hcl
├── main.tf
├── plugins
│   └── registry.terraform.io
│       └── hashicorp
│           ├── aws
│           │   └── 3.29.1
│           │       └── darwin_amd64
│           │           └── terraform-provider-aws_v3.29.1_x5
│           └── google
│               ├── 3.56.0
│               │   └── darwin_amd64
│               │       └── terraform-provider-google_v3.56.0_x5
│               ├── 3.57.0
│               │   └── darwin_amd64
│               │       └── terraform-provider-google_v3.57.0_x5
│               └── 3.58.0
│                   └── darwin_amd64
│                       └── terraform-provider-google_v3.58.0_x5
├── terraform
├── terraform-bundle.hcl
└── terraform_0.14.7-bundle2021022502_darwin_amd64.zip
```

## Custom Provider のサポートについて

terraform-bundle では公式レジストリから提供される Provider に加えて Custom Provider を bundle archive に含めることも可能になっています。Custom Provider を含める場合には、事前に `<HOSTNAME>/<NAMESPACE>/<TYPE>/<VERSION>/<OS>_<ARCH>` という命名のディレクトリ配下に `terraform-provider-<TYPE>` という命名のバイナリを配置して、terraform-bundle 実行時にそのディレクトリを指定する必要があります。

以下は `example.com/dev/myprovider` という Custom Provider を含む bundle archive を作成する例となります。

```bash
# OS と ARCH は MacOS での実行を想定したものにしています
$ mkdir -p custom-plugins/example.com/dev/myprovider/0.1.0/darwin_amd64

# 今回は例なので Custom Provider といて空ファイルを配置しています
$ touch custom-plugins/example.com/dev/myprovider/0.1.0/darwin_amd64/terraform-provider-myprovider

# Custom Provider を含める場合にソースを
# <HOSTNAME>/<NAMESPACE>/<TYPE> 形式で定義する必要があります
$ cat terraform-bundle.hcl
terraform {
  version = "0.14.7"
}

providers {
  myprovider = {
    versions = ["= 0.1.0"]
    source = "example.com/dev/myprovider"
  }
}

# 実行時に Custom Provider を配置したディレクトリを指定する
$ terraform-bundle package -plugin-dir custom-plugins terraform-bundle.hcl
Fetching Terraform 0.14.7 core package...
Local plugin directory "custom-plugins" found; scanning for provider binaries.
Found provider "example.com/dev/myprovider" in "custom-plugins". p
- Finding example.com/dev/myprovider versions matching "0.1.0"...
- Installing example.com/dev/myprovider v0.1.0...
Creating terraform_0.14.7-bundle2021022505_darwin_amd64.zip ...
All done!

# Custom Provider を含む bundle archive が作成できた
$ unzip -Z terraform_0.14.7-bundle2021022505_darwin_amd64.zip
Archive:  terraform_0.14.7-bundle2021022505_darwin_amd64.zip
Zip file size: 35045518 bytes, number of entries: 2
-rw-r--r--  2.0 unx        0 bX defN 21-Feb-25 14:14 plugins/example.com/dev/myprovider/0.1.0/darwin_amd64/terraform-provider-myprovider
-rwxr-xr-x  2.0 unx 83173328 bX defN 21-Feb-25 14:14 terraform
2 files, 83173328 bytes uncompressed, 35045092 bytes compressed:  57.9%
```

## 備考

`v0.14.7` では go.sum に不備があるため、手順通りにインストールを実行すると以下の Go Modules のチェックサムエラーが発生するので、エラーを回避するために一時的に go.sum を更新する必要があります。これは動作検証の目的でインストールするためのワークアラウンドですので、本番環境で利用する際にはこの問題が解消されたバージョンを利用してください。

```bash
# そのままだと Go Modules のチェックサムエラーが発生する
$ go install ./tools/terraform-bundle

verifying github.com/hashicorp/go-getter@v1.5.1: checksum mismatch
        downloaded: h1:LZ49OxqBBtdKJymlpX7oTyqGBQRg4xxQDyPW4hzoZqM=
        go.sum:     h1:lM9sM02nvEApQGFgkXxWbhfqtyN+AyhQmi+MaMdBDOI=

SECURITY ERROR
This download does NOT match an earlier download recorded in go.sum.
The bits may have been replaced on the origin server, or an attacker may
have intercepted the download attempt.

For more information, see 'go help module-auth'.

# 一時的に go.sum を更新する
$ sed -i '' -e 's/h1:lM9sM02nvEApQGFgkXxWbhfqtyN+AyhQmi+MaMdBDOI=/h1:LZ49OxqBBtdKJymlpX7oTyqGBQRg4xxQDyPW4hzoZqM=/' go.sum

# インストールする
$ go install ./tools/terraform-bundle

# 期待したバージョンがインストールできたことを確認する
$ terraform-bundle --version
0.14.7
```

[^1]: `terraform providers mirror` コマンドを利用した Provider のダウンロードや、公式サイトからの Terraform バイナリのダウンロードなどの作業を、`terraform-bundle` コマンド1発で完結できるイメージです。
[^2]: ソースに定義する値は [ソースアドレス](https://github.com/hashicorp/terraform/blob/v0.14.7/website/docs/language/providers/requirements.html.md#source-addresses) における `<HOSTNAME>/<NAMESPACE>` となります。
[^3]: ソースコードは https://github.com/hashicorp/terraform/blob/v0.14.7/tools/terraform-bundle/package.go#L289-L297 を参照してください。