この記事は terraform Advent Calendar 2025 6 日目の記事です。
Terraform × Azure なら AVM
みなさんは AVM (Azure Verified Modules) というものをご存知でしょうか?
従来、Terraform で Azure リソースを管理する場合は AzureRM か AzAPI を使用するのが通例でした。ただし、これらはそれぞれ利用に関して課題があります。詳細は、以前に私が書いたブログを参照いただければと思います。
そこで、AVM (Azure Verified Modules) です。AVM とは Microsoft が公式に提供する Infrastructure as Code (IaC) モジュールの標準化イニシアティブになります。Azure リソースの構築・管理を効率化し、一貫性・信頼性・ベストプラクティス準拠を実現することを目的にされています。よって、「とにかく早く、安全に、Azureのベストプラクティスに沿った構成を作りたい!」という場合は、AVM をまず使用することが推奨となります。
AVM は WAF 準拠
AVM は Azure Well-Architected Framework (WAF) に準拠しています。Azure Well-Architected Framework (WAF) とは、Azure クラウド上で高品質なシステムを設計・運用するためのベストプラクティスと設計指針をまとめたものです。クラウドアーキテクトや開発者が、Azure 上でアプリケーションやサービスを構築する際の品質を評価・改善するための指針として使われています。
つまり、AVM を使用すれば、必然的に Microsoft が推奨するベストプラクティスに沿ったサービス構築ができるということです。その点で Azure リソースを IaC で管理する際に使用を検討する価値があります。
他プロバイダーとの比較
AVM は、Microsoft のエンジニアによって開発・保守されています。Microsoft が公式としてサポートし、継続してメンテナンスされることが明記されています。その点でまず、利用についての安心が出ます。
AzureRM に関しては、メンテナーがコミュニティのため、Microsoft Azure に精通していないユーザーによるプロバイダーを使用するという運用面でのリスクがあります。新機能に対しての対応は比較的遅いですし、任意設定項目のデフォルト値が WAF に準拠しない、セキュリティ的に NG な内容だったりすることも多いです。そのため、WAF への準拠のために設定を行いたい際、任意設定項目なのに明示的に指定しないといけなかったり、細かい部分まで手が届かなかったりと、辛い思いをすることもあります。
AzAPI は Microsoft によってサポートがされているものではありますが、そもそもの作りに課題があります。
AzAPI は Azure Resource Manager (ARM) の REST API 上に構築されているレイヤーになるので、他に比べてコードの記述量が多くなり可読性の低下が懸念されます。また、ARM REST API のラッパー的なもののため、使用する API バージョン指定などが必要になることから、将来的な互換性リスクや API 仕様変更に弱く、型チェック面でも課題があります。
さらに、REST API に依存する関係で必然的に JSON 構造の記述が多くなることから、tf.state ファイルの差分検出部分に懸念があり、未変更なのに意図しない再作成が起きるなどのリスクもあります。
正直なところ、上述のような AzureRM と AzAPI それぞれの課題を解決したモジュールが AVM というのが私の認識です。
実際に AVM を使ってみる
以降では、実際に AVM でどのように Terraform のコード記述をするのかについて紹介します。
AzureRM や AzAPI と比較した際のポイントとしては、以下があります。
- resource 定義を作成する必要がない
- ベストプラクティスの構成に沿った設定値変更を自前で実装する必要がない
以下は、Azure Functions の Flex Consumption の環境を AVM で書いてみた例です。
#--- Resource Group ---#
module "resource_group" {
source = "Azure/avm-res-resources-resourcegroup/azurerm"
location = var.location_japan_east
name = var.resource_group_name
tags = var.common_tags
}
output "resource_group_name" {
value = module.resource_group.name
}
#--- API Management Service ---#
module "apim" {
source = "Azure/avm-res-apimanagement-service/azurerm"
# Required
location = var.location_japan_east
name = var.apim_name
publisher_email = var.apim_publisher_email
resource_group_name = module.resource_group.name
# Optional
sku_name = var.apim_sku_name
tags = var.common_tags
}
#--- Storage Account ---#
module "storage_account" {
source = "Azure/avm-res-storage-storageaccount/azurerm"
# Required
location = var.location_japan_east
resource_group_name = module.resource_group.name
name = var.storage_account_name
# Optional
account_replication_type = var.storage_account_account_replication_type
containers = var.storage_account_containers
network_rules = var.storage_account_network_rules
tags = var.common_tags
}
output "storage_account_razure_api_containers" {
value = module.storage_account_razure_api.containers
}
output "storage_account_razure_api_name" {
value = module.storage_account_razure_api.name
}
output "storage_account_razure_api_resource_id" {
value = module.storage_account_razure_api.resource_id
}
output "storage_account_razure_api_containers_app_package_endpoint" {
value = format("https://%s.blob.core.windows.net/%s",
module.storage_account.name,
var.storage_account_containers["app-package"].name
)
}
#--- App Service Plan ---#
module "service_plan" {
source = "Azure/avm-res-web-serverfarm/azurerm"
# Required
location = var.location_japan_east
name = var.service_plan_name
os_type = var.os_type_linux
resource_group_name = module.resource_group.name
# Optional
sku_name = var.service_plan_sku_name
tags = var.common_tags
}
output "service_plan_resource_id" {
value = module.service_plan.resource_id
}
#--- Azure Function App (Flex Consumption) ---#
locals {
app_settings = merge(
var.function_app_app_settings,
{
AzureWebJobsStorage__accountName = module.storage_account.name
AzureWebJobsStorage__blobServiceUri = "https://${module.storage_account.name}.blob.core.windows.net/"
AzureWebJobsStorage__queueServiceUri = "https://${module.storage_account.name}.queue.core.windows.net/"
AzureWebJobsStorage__tableServiceUri = "https://${module.storage_account.name}.table.core.windows.net/"
}
)
}
module "function_app" {
source = "Azure/avm-res-web-site/azurerm"
# Required
kind = var.function_app_kind
location = var.location_japan_east
name = var.function_app_name
os_type = var.os_type_linux
resource_group_name = module.resource_group.name
service_plan_resource_id = module.service_plan.resource_id
# Optional
app_settings = local.app_settings
application_insights = {
#--- Application Insights ---#
location = var.location_japan_east
name = var.aai_name
resource_group_name = module.resource_group.name
workspace_id = var.aai_workspace_id
application_type = var.aai_application_type
local_authentication_disabled = var.aai_local_authentication_disabled
sampling_percentage = var.aai_sampling_percentage
tags = var.common_tags
}
fc1_runtime_name = var.function_app_fc1_runtime_name
fc1_runtime_version = var.function_app_fc1_runtime_version
function_app_uses_fc1 = var.function_app_razure_api_uses_fc1
https_only = var.function_app_https_only
managed_identities = var.function_app_managed_identities
storage_account_name = module.storage_account.name
storage_authentication_type = var.function_app_storage_authentication_type
storage_container_endpoint = format("https://%s.blob.core.windows.net/%s",
module.storage_account.name,
var.storage_account_containers["app-package"].name
)
storage_uses_managed_identity = var.function_app_storage_uses_managed_identity
tags = var.common_tags
webdeploy_publish_basic_authentication_enabled = var.function_app_webdeploy_publish_basic_authentication_enabled
}
output "function_app_application_insights" {
value = module.function_app.application_insights
sensitive = true
}
output "function_app_system_assigned_managed_identity_principal_id" {
value = module.function_app.system_assigned_mi_principal_id
}
output "function_app_resource_id" {
value = module.function_app.resource_id
}
#--- Role Assignment ---#
# Storage Blob Data Contributor
module "role_assignment_storage_blob_data_contributor" {
source = "Azure/avm-res-authorization-roleassignment/azurerm"
# Optional
role_assignments_azure_resource_manager = {
storage_blob_data_contributor = {
role_definition_name = "Storage Blob Data Contributor"
principal_id = module.function_app.system_assigned_mi_principal_id
scope = module.storage_account.resource_id
principal_type = "ServicePrincipal"
}
}
}
# Website Contributor
module "role_assignment_website_contributor" {
source = "Azure/avm-res-authorization-roleassignment/azurerm"
# Optional
role_assignments_azure_resource_manager = {
website_contributor = {
role_definition_name = "Website Contributor"
principal_id = var.principal_deployment_id
scope = module.function_app.resource_id
principal_type = "ServicePrincipal"
}
}
}
例えば、Blob Storage の access_tier、account_tier や min_tls_version の値は記述していませんし、App Service Plan の worker_count や zone_balancing_enabled のような値も記述していません。明示的に定義しない場合、これらが全て AVM の推奨値で自動構成されます。
Blob Storage のコンテナー (var.storage_account_containers) に関しては、variables.tf にて型の定義を書き、terraform.tfvars にて Production/Development 環境別に作成するよう対応しています。
設計都合やシステム構成の都合により、AVM のデフォルト設定から変更したい場合は、従来同様変数設定もできます。
AVM で担保される推奨設定値から逸脱させるものに限って、いわゆるパラメータシート的な形で管理するようになるため、運用負荷もかなり軽減されます。
variable "storage_account_containers" {
type = map(object({
name = string
public_access_type = string
cors_rules = any
}))
description = "ストレージアカウント内に作成するコンテナ一覧"
default = {}
}
storage_account_containers = {
"hosts" = {
name = "azure-webjobs-hosts"
public_access_tier = "private"
cors_rules = []
}
"secrets" = {
name = "azure-webjobs-secrets"
public_access_tier = "private"
cors_rules = []
}
"app-package" = {
name = "app-package"
public_access_tier = "private"
cors_rules = []
}
}
Terraform × Azure を始めるならまずは AVM
正直な話、IaC を初めて学ぶ人にとって Terraform は学習コストが低いとは言えません。そういった点においても、Terraform × Azure の場合は、AVM から始めてみるのが非常におすすめです。
最近は Microsoft Foundry (旧 Azure AI Foundry) や Azure OpenAI をきっかけに Microsoft Azure へ入門する人もかなり増え、実務で IaC に取り組む人も増えたと思います。
アプリケーションコードだけでなく、ぜひこの機会に AVM でインフラの IaC にも取り組み、良い CI/CD ライフを過ごしていただければと思います。