CLOVER🍀

That was when it all began.

TerraformのProvider Plugin Cacheを試す

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

Terraformを使うと、terraform init時に使用するProviderをダウンロードしてくるのですが、これをそれぞれのディレクトリで行っていると
各ルートモジュール配下の.terraformディレクトリのサイズの合計がだんだん無視できなくなってきます。

それぞれでダウンロードしてしまいますからね。

これをどうにかする方法はないのかな?ということで、Provider Plugin Cacheというものを試してみることにしました。

CLI Configuration File (.terraformrc or terraform.rc) / Provider Installation / Provider Plugin Cache

Provider Plugin Cache

Provider Plugin Cacheに関するドキュメントはこちらです。

CLI Configuration File (.terraformrc or terraform.rc) / Provider Installation / Provider Plugin Cache

最初に冒頭で書いたことに似た課題感が書いてあって、デフォルトではterraform init時にそれぞれProviderをダウンロードします。
Providerは数百MBになることもあるため、低速なネットワークのユーザーには不便であり、このダウンロード結果を共有することで
ダウンロード回数を1回にできるという話になっています。

By default, terraform init downloads plugins into a subdirectory of the working directory so that each working directory is self-contained. As a consequence, if you have multiple configurations that use the same provider then a separate copy of its plugin will be downloaded for each configuration.

Given that provider plugins can be quite large (on the order of hundreds of megabytes), this default behavior can be inconvenient for those with slow or metered Internet connections. Therefore Terraform optionally allows the use of a local directory as a shared plugin cache, which then allows each distinct plugin binary to be downloaded only once.

使い方は$HOMEディレクトリに置いた.terraformrcファイル(Windowsでは%APPDATA%ディレクトリに置いたterraform.rcファイル)の
plugin_cache_dir、またはTF_PLUGIN_CACHE_DIR環境変数でプラグインをキャッシュするためのディレクトリを指定します。

これでProvider Plugin Cacheが有効になります。

なお、キャッシュ用のディレクトリは先に作成しておく必要があり、Terraform自身がこのディレクトリを作成することはないようです。

特性がもう少し書かれているので、そちらも書いておきましょう。

  • Provider Plugin Cacheが有効でも、terraform init時に適切なバージョンがキャッシュから使用可能か確認し、なければダウンロードする
  • キャッシュからProviderを利用する場合は、シンボリックリンクとして実現される
  • キャッシュ用のディレクトリは、他の目的(たとえばレジストリーのミラーなど)と共有しないこと
  • キャッシュされたProviderは、使用するProviderのバージョンが増えるとそれに合わせてキャッシュ上に増えていくことになるので、使用しないバージョンができた場合は手動で削除すること

では試してみましょう。

環境

今回の環境はこちら。

$ terraform version
Terraform v1.7.3
on linux_amd64

Terraformを使うのでProviderが必要になりますが、今回はAWS Providerを使うことにします。使う対象はLocalStackにしましょう。

$ python3 --version
Python 3.10.12


$ localstack --version
3.1.0

起動。

$ localstack start

確認用のTerraformモジュールを作成する

ひとまず、Terraformの構成ファイルを作らないと始まりません。Providerが共有されることを確認したいので、2つのルートモジュールを
作る必要があります。

今回はAmazon S3とAmazon SQSを構築するモジュールを作成することにします。

$ mkdir s3 sqs

内容は単純なものにしておきます。

Amazon S3バケット構築用。

s3/main.tf

terraform {
  required_version = "1.7.3"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.36.0"
    }
  }
}

provider "aws" {
  access_key                  = "mock_access_key"
  region                      = "us-east-1"
  s3_use_path_style           = true
  secret_key                  = "mock_secret_key"
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true

  endpoints {
    apigateway     = "http://localhost:4566"
    cloudformation = "http://localhost:4566"
    cloudwatch     = "http://localhost:4566"
    dynamodb       = "http://localhost:4566"
    es             = "http://localhost:4566"
    firehose       = "http://localhost:4566"
    iam            = "http://localhost:4566"
    kinesis        = "http://localhost:4566"
    lambda         = "http://localhost:4566"
    route53        = "http://localhost:4566"
    redshift       = "http://localhost:4566"
    s3             = "http://localhost:4566"
    secretsmanager = "http://localhost:4566"
    ses            = "http://localhost:4566"
    sns            = "http://localhost:4566"
    sqs            = "http://localhost:4566"
    ssm            = "http://localhost:4566"
    stepfunctions  = "http://localhost:4566"
    sts            = "http://localhost:4566"
  }
}

resource "aws_s3_bucket" "this" {
  bucket = "my-bucket"
}

output "bucket" {
  value = aws_s3_bucket.this.bucket
}

Amazon SQS構築用。

sqs/main.tf

terraform {
  required_version = "1.7.3"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.36.0"
    }
  }
}

provider "aws" {
  access_key                  = "mock_access_key"
  region                      = "us-east-1"
  s3_use_path_style           = true
  secret_key                  = "mock_secret_key"
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true

  endpoints {
    apigateway     = "http://localhost:4566"
    cloudformation = "http://localhost:4566"
    cloudwatch     = "http://localhost:4566"
    dynamodb       = "http://localhost:4566"
    es             = "http://localhost:4566"
    firehose       = "http://localhost:4566"
    iam            = "http://localhost:4566"
    kinesis        = "http://localhost:4566"
    lambda         = "http://localhost:4566"
    route53        = "http://localhost:4566"
    redshift       = "http://localhost:4566"
    s3             = "http://localhost:4566"
    secretsmanager = "http://localhost:4566"
    ses            = "http://localhost:4566"
    sns            = "http://localhost:4566"
    sqs            = "http://localhost:4566"
    ssm            = "http://localhost:4566"
    stepfunctions  = "http://localhost:4566"
    sts            = "http://localhost:4566"
  }
}

resource "aws_sqs_queue" "this" {
  name = "my-queue"
}

output "queue_name" {
  value = aws_sqs_queue.this.name
}

output "queue_url" {
  value = aws_sqs_queue.this.url
}

それぞれterraform initからterraform applyまでを行い、リソースが構築できることを確認します。

## S3
$ cd s3
$ terraform init
$ terraform apply


## SQS
$ cd ../sqs
$ terraform init
$ terraform apply

結果は省略します。

terraform init時にはそれぞれ以下のログが出力され、Providerがダウンロードされます。

Initializing provider plugins...
- Finding hashicorp/aws versions matching "5.36.0"...
- Installing hashicorp/aws v5.36.0...
- Installed hashicorp/aws v5.36.0 (signed by HashiCorp)

サイズを確認してみましょう。

$ cd ..
$ du -sh s3/.terraform
393M    s3/.terraform
$ du -sh sqs/.terraform
393M    sqs/.terraform

400MB弱ですね。AWS Providerでもそこそこのサイズがあることになります。

これが積み重なるとちょっと辛いですよね、ということでProvider Plugin Cacheを試してみましょう。

構築したリソースは、この後は要らないので削除しておきます。

$ cd s3
$ terraform destroy
$ $ cd ../sqs
$ terraform destroy

Provider Plugin Cacheを試す

では、Provider Plugin Cacheを試してみます。

先に、各.terraformディレクトリは削除しておきましょう。

$ rm -rf s3/.terraform sqs/.terraform

キャッシュ先のディレクトリは先に作成する必要があるという話でしたので、作っておきます。

$ mkdir -p $HOME/.terraform.d/plugin-cache

まずは環境変数から試してみましょう。

$ export TF_PLUGIN_CACHE_DIR=$HOME/.terraform.d/plugin-cache
## S3
$ cd s3
$ terraform init

terraform init時に少し表示が変わりました。

## 1回目(S3)
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Installing hashicorp/aws v5.36.0...
- Installed hashicorp/aws v5.36.0 (signed by HashiCorp)


## 2回目(SQS)
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using hashicorp/aws v5.36.0 from the shared cache directory

2回目は、すでにキャッシュにあるものを使っているように見えます。

一見すると.terraformディレクトリがあり、中身もあるように見えます。

$ ll
合計 24
drwxrwxr-x 3 xxxxx xxxxx 4096  2月 11 18:31 ./
drwxrwxr-x 5 xxxxx xxxxx 4096  2月 11 18:18 ../
drwxr-xr-x 3 xxxxx xxxxx 4096  2月 11 18:31 .terraform/
-rw-r--r-- 1 xxxxx xxxxx 1406  2月 11 18:23 .terraform.lock.hcl
-rw-rw-r-- 1 xxxxx xxxxx 1449  2月 11 18:20 main.tf
-rw-rw-r-- 1 xxxxx xxxxx 2652  2月 11 18:23 terraform.tfstate


$ find .terraform
.terraform
.terraform/providers
.terraform/providers/registry.terraform.io
.terraform/providers/registry.terraform.io/hashicorp
.terraform/providers/registry.terraform.io/hashicorp/aws
.terraform/providers/registry.terraform.io/hashicorp/aws/5.36.0
.terraform/providers/registry.terraform.io/hashicorp/aws/5.36.0/linux_amd64

ですが、このファイルはシンボリックリンクになっています。

$ tree .terraform
.terraform
└── providers
    └── registry.terraform.io
        └── hashicorp
            └── aws
                └── 5.36.0
                    └── linux_amd64 -> $HOME/.terraform.d/plugin-cache/registry.terraform.io/hashicorp/aws/5.36.0/linux_amd64

6 directories, 0 files

.terraformディレクトリのサイズが劇的に減りましたね。

$ du -sh .terraform
28K     .terraform

ファイルは、先ほど作成したキャッシュ用のディレクトリにダウンロードされています。

$ tree $HOME/.terraform.d/plugin-cache
$HOME/.terraform.d/plugin-cache
└── registry.terraform.io
    └── hashicorp
        └── aws
            └── 5.36.0
                └── linux_amd64
                    └── terraform-provider-aws_v5.36.0_x5

5 directories, 1 file

Amazon SQS側でも確認。

$ cd ../sqs
$ terraform init

結果は同じですね。

$ tree .terraform
.terraform
└── providers
    └── registry.terraform.io
        └── hashicorp
            └── aws
                └── 5.36.0
                    └── linux_amd64 -> $HOME/.terraform.d/plugin-cache/registry.terraform.io/hashicorp/aws/5.36.0/linux_amd64

6 directories, 0 files


$ du -sh .terraform
28K     .terraform

これでProviderを各ディレクトリでダウンロードせずに済むようになりました。

.terraformrcファイルでも試してみましょう。

1度ディレクトリを削除。

$ cd ..
$ rm -rf s3/.terraform sqs/.terraform

環境変数TF_PLUGIN_CACHE_DIRを削除。

$ unset TF_PLUGIN_CACHE_DIR

ドキュメントのとおり、以下のファイルを作成。

$HOME/.terraformrc

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

確認。

## S3
$ cd s3
$ terraform init


## SQS
$ cd ../sqs
$ terraform init

terraform init時の様子は先ほどと同じですが、すでにダウンロード済みなのでこんな表示になります。

Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using hashicorp/aws v5.36.0 from the shared cache directory

.terraformディレクトリ内がシンボリックリンクになっているのも同じですね。

$ tree .terraform
.terraform
└── providers
    └── registry.terraform.io
        └── hashicorp
            └── aws
                └── 5.36.0
                    └── linux_amd64 -> $HOME/.terraform.d/plugin-cache/registry.terraform.io/hashicorp/aws/5.36.0/linux_amd64

6 directories, 0 files

良さそうです。

もう少し試してみる

ここまででやりたいことは確認できましたが、もう少し踏み込んでみましょう。

使用するProviderのバージョンを変更する

今回は2つのルートモジュールで同じバージョンのAWS Providerを使用しましたが、バージョンが揃っていなくても大丈夫かどうか
確認しておきます。

Amazon SQSの方は、AWS Provider 5.35.0を使うことにしました。

terraform {
  required_version = "1.7.3"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.35.0"
    }
  }
}

Amazon S3の方は5.36.0です。

terraform {
  required_version = "1.7.3"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.36.0"
    }
  }
}

1度.terraformは削除。

$ cd ..
$ rm -rf s3/.terraform sqs/.terraform

確認します。

## S3
$ cd s3
$ terraform init


## SQS
$ cd ../sqs
$ terraform init -upgrade

Amazon SQSの方は、.terraform.lock.hclを残したままだったので-upgradeオプションを付けています…。

terraform init時のProviderのダウンロードの様子です。Amazon SQSの方は5.35.0なので、新しくダウンロードされています。

## S3
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using hashicorp/aws v5.36.0 from the shared cache directory


## SQS
Initializing provider plugins...
- Finding hashicorp/aws versions matching "5.35.0"...
- Installing hashicorp/aws v5.35.0...
- Installed hashicorp/aws v5.35.0 (signed by HashiCorp)

キャッシュディレクトリ上は、こうなりました。

$ tree $HOME/.terraform.d/plugin-cache
$HOME/.terraform.d/plugin-cache
└── registry.terraform.io
    └── hashicorp
        └── aws
            ├── 5.35.0
            │   └── linux_amd64
            │       └── terraform-provider-aws_v5.35.0_x5
            └── 5.36.0
                └── linux_amd64
                    └── terraform-provider-aws_v5.36.0_x5

7 directories, 2 files

ちゃんとそれぞれのバージョンを使ってくれそうです。

モジュールはどうなるのか?

特にドキュメントには書かれていませんが、モジュールについてはどうなるのでしょうか?

こちらも試してみましょう。

お題はterraform-aws-modules/s3-bucketにします。

terraform-aws-modules/s3-bucket

モジュール用のディレクトリを作成。

$ mkdir s3_1 s3_2

Provider Plugin Cacheの設定は、$HOME/.terraformrcファイルで行っているものとします。

$HOME/.terraformrc

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

今回は割と定義はなんでもいいので、versionを固定した以外はモジュールのサンプルをそのまま使いました。

つまり、こんな感じです。

s3_1/main.tf

terraform {
  required_version = "1.7.3"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.36.0"
    }
  }
}

provider "aws" {
  access_key                  = "mock_access_key"
  region                      = "us-east-1"
  s3_use_path_style           = true
  secret_key                  = "mock_secret_key"
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true

  endpoints {
    apigateway     = "http://localhost:4566"
    cloudformation = "http://localhost:4566"
    cloudwatch     = "http://localhost:4566"
    dynamodb       = "http://localhost:4566"
    es             = "http://localhost:4566"
    firehose       = "http://localhost:4566"
    iam            = "http://localhost:4566"
    kinesis        = "http://localhost:4566"
    lambda         = "http://localhost:4566"
    route53        = "http://localhost:4566"
    redshift       = "http://localhost:4566"
    s3             = "http://localhost:4566"
    secretsmanager = "http://localhost:4566"
    ses            = "http://localhost:4566"
    sns            = "http://localhost:4566"
    sqs            = "http://localhost:4566"
    ssm            = "http://localhost:4566"
    stepfunctions  = "http://localhost:4566"
    sts            = "http://localhost:4566"
  }
}

module "s3_bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "4.1.0"

  bucket = "my-s3-bucket-1"
  acl    = "private"

  control_object_ownership = true
  object_ownership         = "ObjectWriter"

  versioning = {
    enabled = true
  }
}

s3_2/main.tf

terraform {
  required_version = "1.7.3"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.36.0"
    }
  }
}

provider "aws" {
  access_key                  = "mock_access_key"
  region                      = "us-east-1"
  s3_use_path_style           = true
  secret_key                  = "mock_secret_key"
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true

  endpoints {
    apigateway     = "http://localhost:4566"
    cloudformation = "http://localhost:4566"
    cloudwatch     = "http://localhost:4566"
    dynamodb       = "http://localhost:4566"
    es             = "http://localhost:4566"
    firehose       = "http://localhost:4566"
    iam            = "http://localhost:4566"
    kinesis        = "http://localhost:4566"
    lambda         = "http://localhost:4566"
    route53        = "http://localhost:4566"
    redshift       = "http://localhost:4566"
    s3             = "http://localhost:4566"
    secretsmanager = "http://localhost:4566"
    ses            = "http://localhost:4566"
    sns            = "http://localhost:4566"
    sqs            = "http://localhost:4566"
    ssm            = "http://localhost:4566"
    stepfunctions  = "http://localhost:4566"
    sts            = "http://localhost:4566"
  }
}

module "s3_bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "4.1.0"

  bucket = "my-s3-bucket-2"
  acl    = "private"

  control_object_ownership = true
  object_ownership         = "ObjectWriter"

  versioning = {
    enabled = true
  }
}

ほぼ同じ内容ですが、申し訳程度にバケット名だけ変えてあります…。

では、各モジュール内でterraform initしていってみます。

## ひとつ目
$ cd s3_1
$ terraform init


## 2つ目
$ cd ../s3_2
$ terraform init

terraform init時のモジュールおよびProviderの初期化ログはこうなりました。

Initializing modules...
Downloading registry.terraform.io/terraform-aws-modules/s3-bucket/aws 4.1.0 for s3_bucket...
- s3_bucket in .terraform/modules/s3_bucket

Initializing provider plugins...
- Finding hashicorp/aws versions matching ">= 5.27.0, 5.36.0"...
- Installing hashicorp/aws v5.36.0...
- Installed hashicorp/aws v5.36.0 (signed by HashiCorp)

サイズを見てみます。

$ cd ..
$ du -sh s3_1/.terraform s3_2/.terraform
1.1M    s3_1/.terraform
1.1M    s3_2/.terraform

少ないですが、先ほどのProviderのみをキャシュした時よりは多いです。

どうなっているかというと、モジュールは各.terraformディレクトリ内にダウンロードされるようです。

$ tree s3_1/.terraform
s3_1/.terraform
├── modules
│   ├── modules.json
│   └── s3_bucket
│       ├── CHANGELOG.md
│       ├── LICENSE
│       ├── README.md
│       ├── UPGRADE-3.0.md
│       ├── examples
│       │   ├── complete
│       │   │   ├── README.md
│       │   │   ├── main.tf
│       │   │   ├── outputs.tf
│       │   │   ├── variables.tf
│       │   │   └── versions.tf
│       │   ├── complete-legacy
│       │   │   ├── README.md
│       │   │   ├── main.tf
│       │   │   ├── outputs.tf
│       │   │   ├── variables.tf
│       │   │   └── versions.tf
│       │   ├── notification
│       │   │   ├── README.md
│       │   │   ├── main.tf
│       │   │   ├── outputs.tf
│       │   │   ├── variables.tf
│       │   │   └── versions.tf
│       │   ├── object
│       │   │   ├── README.md
│       │   │   ├── main.tf
│       │   │   ├── outputs.tf
│       │   │   ├── variables.tf
│       │   │   └── versions.tf
│       │   ├── s3-analytics
│       │   │   ├── README.md
│       │   │   ├── main.tf
│       │   │   ├── outputs.tf
│       │   │   ├── variables.tf
│       │   │   └── versions.tf
│       │   ├── s3-inventory
│       │   │   ├── README.md
│       │   │   ├── main.tf
│       │   │   ├── outputs.tf
│       │   │   ├── variables.tf
│       │   │   └── versions.tf
│       │   └── s3-replication
│       │       ├── README.md
│       │       ├── iam.tf
│       │       ├── main.tf
│       │       ├── outputs.tf
│       │       ├── variables.tf
│       │       └── versions.tf
│       ├── main.tf
│       ├── modules
│       │   ├── notification
│       │   │   ├── README.md
│       │   │   ├── main.tf
│       │   │   ├── outputs.tf
│       │   │   ├── variables.tf
│       │   │   └── versions.tf
│       │   └── object
│       │       ├── README.md
│       │       ├── main.tf
│       │       ├── outputs.tf
│       │       ├── variables.tf
│       │       └── versions.tf
│       ├── outputs.tf
│       ├── variables.tf
│       ├── versions.tf
│       └── wrappers
│           ├── README.md
│           ├── main.tf
│           ├── notification
│           │   ├── README.md
│           │   ├── main.tf
│           │   ├── outputs.tf
│           │   ├── variables.tf
│           │   └── versions.tf
│           ├── object
│           │   ├── README.md
│           │   ├── main.tf
│           │   ├── outputs.tf
│           │   ├── variables.tf
│           │   └── versions.tf
│           ├── outputs.tf
│           ├── variables.tf
│           └── versions.tf
└── providers
    └── registry.terraform.io
        └── hashicorp
            └── aws
                └── 5.36.0
                    └── linux_amd64 -> $HOME/.terraform.d/plugin-cache/registry.terraform.io/hashicorp/aws/5.36.0/linux_amd64

22 directories, 70 files

モジュールはキャッシュ対象にならないということですね。

まあ、あくまでProviderのキャッシュのようなのでこの点は仕方ないかなと…。

issueもありましたが、やっぱりできないみたいですね。

Feature Request: Module cache dir à la plugins · Issue #16268 · hashicorp/terraform · GitHub

おわりに

TerraformのProvider Plugin Cacheを試してみました。

Providerはそこそこのサイズになるので、各ディレクトリでダウンロードするのではなくこうやってキャッシュできると便利ですよね。

というか、気づかない間に各ディレクトリ内に.terraformディレクトリが散らばっている状態だと合計ですごいサイズになって
困ったりしていたので、ちゃんとキャッシュしましょうという感じですね。

覚えておきましょう。