Terraform の required_providers はモジュールごとに定義しようねというお話

## はじめに

この記事は NE Advent Calendar 2022 13日目の記事です。

アプリケーションエンジニアとして社会に出て3年目。
最近となっては主力プロダクトで利用している PHP だけではなく、Go だったり Python だったり、はたまた Terraform といったインフラの構築には必須の知識に触れる機会が増えてきました。

今回は Terraform 初学者がつまずいた required_provider にまつわるお話をします。

## required_provider とは

今から書かれる Terraform にはこんな依存性がありますよ〜を定義するのものです。
以下の例ですと Microsoft Azure を利用するために Hashicorp 社が提供する `azurerm` パッケージを利用することを示しています。

```
terraform {
  required_version = "1.2.9"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.24.0"
    }
  }
}
```

もちろん Azure 以外にも AWS や GCP などを利用するためのパッケージがあり、必要になったタイミングで `required_providers` に追記することで利用可能になります。

`terraform init` とターミナル上で実行することでこれらの依存関係に従ってパッケージのインストールをしてくれます。

## やろうとしたこと

さて、あるアプリケーションを開発するにあたって Azure でのインフラ構築も求められる場面がありました。

ざっくりな要件としては Docker イメージをベースとしたアプリを Azure にデプロイしたい、みたいなところです。 `Azure Container Apps` などコンテナ周りのサービスを利用して実現しようと考えていました。

要件もなんとなく整理でき、以下のようなディレクトリ構成で進めることになりました。

```
.(一部省略)
├── common
│   ├── provider.tf
│   └── variables.tf
├── development
├── modules
│   ├── container_apps
│   │   ├── input.tf
│   │   ├── main.tf
│   │   └── output.tf
│   └── database
│       ├── input.tf
│       ├── main.tf
│       └── output.tf
├── production
└── staging
    └── resources
        ├── main.tf
        ├── provider.tf
        └── variables.tf
```

環境ごとに同様なインフラを構築したくコードの再利用化が求められたため、環境ごとの tf ファイルを置く層と、再利用されるリソースは modules 内にそれぞれ定義するものとしました。

さて書くぞ！と気合を入れ、とりあえず `providers.tf` に `required_provider` を書いて modules に `Azure Container Apps` 用のリソースをざっくり定義し、どんなもんになるかなとと一度 `terraform init` をしてみます。

すると何やらエラーが。

```
Initializing provider plugins...
- Reusing previous version of hashicorp/azurerm from the dependency lock file
- Finding latest version of hashicorp/azapi...
- Reusing previous version of azure/azapi from the dependency lock file
- Using previously-installed hashicorp/azurerm v3.24.0
- Using previously-installed azure/azapi v0.6.0
╷
│ Error: Failed to query available provider packages
│
│ Could not retrieve the list of available versions for provider hashicorp/azapi: provider registry registry.terraform.io does not have a provider named
│ registry.terraform.io/hashicorp/azapi
│
│ Did you intend to use azure/azapi? If so, you must specify that source address in each module which requires that provider. To see which modules are currently depending on
│ hashicorp/azapi, run the following command:
│     terraform providers
|
```

> Could not retrieve the list of available versions for provider hashicorp/azapi: provider registry registry.terraform.io does not have a provider named

`registry.terraform.io/hashicorp/azapi` がありませんとのこと。

なんだお前は、そんなもの使ってないぞ。

`resources/staging/providers.tf` を確認してみてもそんなパッケージを利用するとは一言も書いてありません。
```resources/staging/providers.tf
terraform {
  required_version = "1.2.9"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.24.0"
    }
    azapi = {
      source  = "Azure/azapi"
      version = "~> 0.6.0"
    }
  }

  backend "azurerm" {}
}

provider "azurerm" {}
provider "azapi" {}

```

使おうとしているパッケージは
- `registry.terraform.io/hashicorp/azurerm`
- `registry.terraform.io/Azure/azapi`

の2点です。
（`azapi` は HashiCorp が対応していないリソースを構築するためのパッケージです。）

何故か Terraform が自動で `registry.terraform.io/hashicorp/azapi` という闇のパッケージ（？）をインストールしようとしてしまうのです。
もちろんそんなパッケージは存在しないのでエラーとなってしまうわけです。

## 解決方法

https://developer.hashicorp.com/terraform/language/modules/develop/providers
> Note: Only provider configurations are inherited by child modules, not provider source or version requirements. Each module must declare its own provider requirements. This is especially important for non-HashiCorp providers.

公式ドキュメントを漁っていると上記の注意書きを見つけました。
要約すると、子のモジュールにインストールするパッケージの情報は継承されないので、各モジュールごとで定義してね、**HashiCorp 製ではないものでは特に重要です。**
とこのことです。

確かに Azapi は Microsoft 社提供のものであり、 HashiCorp 製のパッケージではありません。

https://stackoverflow.com/questions/73293980/terraform-failed-to-query-available-provider-packages-azapi

↑ 同じような問題の投稿を stackoverflow でも見つけました


したがって、module 郡にも `provider.tf` を追加し、最終的に以下のような構成となりました。

```
.（一部省略）
├── modules
│   ├── container_apps
│   │   ├── input.tf
│   │   ├── main.tf
│   │   ├── provider.tf
│   │   └── output.tf
│   └── database
│       ├── input.tf
│       ├── main.tf
│       └── output.tf
└── staging
    └── resources
        ├── main.tf
        ├── provider.tf
        └── variables.tf
```

``` staging/resource/provider.tf
terraform {
  required_version = "1.2.9"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.24.0"
    }
  }

  backend "azurerm" {}
}

provider "azurerm" {}

```

``` modules/container_apps/provider.tf
terraform {
  required_version = "1.2.9"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.24.0"
    }
    azapi = {
      source  = "Azure/azapi"
      version = "~> 0.6.0"
    }
  }

  backend "azurerm" {}
}

provider "azurerm" {}
provider "azapi" {}

```

## 終わりに

初めて触ってみる言語や仕組みは単純なものでもつまずきやすいので、
しっかりと公式ドキュメントを確認することや、頼りになる先輩エンジニアがいたらすぐに相談するのが吉です。