Terraformで既存のリソースを使ってデプロイする方法

2024年1月14日

今回の作業では、既存のリソースを基にして、新たなリソースをTerraformを使ってデプロイしていきます。

今回のソースはこちらで作成しました。

https://github.com/xiaotiantakumi/azure-terraform-templates/tree/azure-insights-to-functions

実際の運用でありそうな、リソースを既にあるApplication Insightを利用して新規作成するFunctionsのログを収集します。

既存のリソースは以下です。

  • リソースグループ
  • アプリケーションインサイト
  • tfstateを保持するstorage

新規に作成されるリソースは以下です。

  • Azure Function AppのLinuxバージョンを作成
  • Azure App Serviceプランを作成
  • Azure Storageアカウントを作成

tfstateの設定をAzure Storageに保持するように設定する

これまではtfstateファイルをローカルに出力していましたが、今後はAzure Storageのblobコンテナーに保存するよう変更します。

やり方については、下記に記載されています。

以下のように既にあるストレージアカウントにtfstate用のアカウントを作成しました。

私の設定は次のようになっています。

# Azure Provider source and version being used
terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.57.0"
    }
    azapi = {
      source  = "azure/azapi"
      version = "~> 1.6.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~>3.0"
    }
    tls = {
      source  = "hashicorp/tls"
      version = "~>4.0"
    }
  }
  backend "azurerm" {
      resource_group_name  = "tfstate"
      storage_account_name = "saxiaotianprincipal"
      container_name       = "tfstate"
      key                  = "terraform.tfstate"
  }
}

provider "azurerm" {
  features {}
}

provider "azapi" {
}

ストレージアカウントへ接続するための設定

Terraformの状態をAzure Storageに格納するためには、以下のスクリプトを実行する必要があります。このスクリプトは、必要なリソースグループ、ストレージアカウント、およびblobコンテナを作成し、その後、ストレージアカウントのアクセスキーを環境変数に設定します。

init.sh

#!/bin/bash

RESOURCE_GROUP_NAME=rg-sa-sample
STORAGE_ACCOUNT_NAME=saxiaotianprincipal
CONTAINER_NAME=tfstate

# Create resource group
# az group create --name $RESOURCE_GROUP_NAME --location eastus

# Create storage account
# az storage account create --resource-group $RESOURCE_GROUP_NAME --name $STORAGE_ACCOUNT_NAME --sku Standard_LRS --encryption-services blob

# Create blob container
# az storage container create --name $CONTAINER_NAME --account-name $STORAGE_ACCOUNT_NAME

# Retrieve and export the storage account key
ACCOUNT_KEY=$(az storage account keys list --resource-group $RESOURCE_GROUP_NAME --account-name $STORAGE_ACCOUNT_NAME --query '[0].value' -o tsv)
export ARM_ACCESS_KEY=$ACCOUNT_KEY
echo ARM_ACCESS_KEY=$ACCOUNT_KEY

terraform initする前に流してください。 source init.sh

設定にもかかわらずTerraformがストレージアカウントにアクセスできない場合、ストレージアカウントのネットワーク設定を確認することが重要です。特に、以下の設定が影響を与える可能性があります。

  1. パブリック ネットワーク アクセス: この設定を「すべてのネットワークから有効」にすると、どのネットワークからでもストレージアカウントへのアクセスが可能になります。セキュリティ上の理由から、制限が必要な場合は「選択した仮想ネットワークとIPアドレスから有効」を選択してください。完全に無効にすると、Terraformからのアクセスもできなくなるので注意が必要です。
  2. アクセス制御: ストレージアカウントに対するアクセス権を持つユーザーやサービスを確認し、Terraformが実行される環境からのアクセスが許可されている

ことを確認してください。アクセス権が不足していると、Terraformはストレージアカウントにアクセスできないため、適切な権限が与えられているかを検証することが重要です。

追加のチェックポイント

  • ファイアウォールとネットワークルールの設定: ストレージアカウントにアクセスするためのファイアウォール設定とネットワークルールが正しく設定されているかを確認してください。不適切な設定は、Terraformからのアクセスを妨げる可能性があります。
  • 接続のテスト: ストレージアカウントへの接続をテストすることで、ネットワーク問題が原因でないことを確認できます。これは、Terraformの問題とネットワークの問題を区別するのに役立ちます。

Application InsightsをPortalから作成

既存リソースを利用すると述べましたが、実はまだApplication Insightsを作成していなかったので、これを新たに作成します。

現状のリソースの状態

下記のように、既存リソースがあります。

既存リソースを利用するにはdataを利用する

今回、main.tfは以下のようになります。

locals {
  tags = {
    product     = var.prefix
    environment = var.environment
  }
}


data "azurerm_resource_group" "rg" {
  name = var.resource_group_name
}

data "azurerm_application_insights" "app_insights" {
  name                = var.app_insights_name
  resource_group_name = var.resource_group_name
}

module "functions" {
  source              = "./modules/functions"
  location            = var.default_region
  resource_group_name = var.resource_group_name
  tags                = local.tags
  application_insights_connection_string = data.azurerm_application_insights.app_insights.connection_string
  application_insights_key               = data.azurerm_application_insights.app_insights.instrumentation_key
}

Terraformのdataブロックは、既存のインフラストラクチャリソースに関する情報を参照するために使用されます。これにより、既に作成されているリソースのデータを読み込み、その情報を他のTerraform構成の部分で使用することができます。dataブロックは、新しいリソースを作成するのではなく、既存のリソースから情報を取得するために使用される点が重要です。

このケースでは、azurerm_resource_groupazurerm_application_insightsの2つのデータソースが使用されています。これらのブロックは、特定のリソースグループとアプリケーションインサイトのリソースの詳細を取得するために使われています。例えば、azurerm_application_insightsデータソースは、指定されたアプリケーションインサイトのconnection_stringとinstrumentation_keyのような情報に利用しています。

この機能を使用することで、既存のリソースに対するハードコーディングや不必要な重複を避けることができ、モジュラリティと再利用性を高めることができます。特に、複数の環境やモジュール間で共有されるリソースの情報を一元管理する場合に有効です。

Functionsの作成について

Azure Functionsは下記を参考にしました。

上記を見ていると、Insightに関連する設定について下記が必要ということがわかります。

terraformのコードについて

このプロジェクトでは、Azure Function Appのデプロイに必要なリソースを以下のTerraformファイルで管理しています。

  • /modules/functions/output.tf
  • /modules/functions/main.tf
  • /modules/functions/variables.tf
  1. output.tf: デプロイされたリソースの情報を出力する設定です。ここでは、Storage Account、App Service Plan、Function Appの各ID、名前、主要な接続文字列などの重要な情報を出力します。これにより、デプロイ後のリソース管理が容易になります。
  2. main.tf: Function App、Storage Account、およびService Planを定義しています。ここにはリソースの実際の定義が含まれています。azurerm_storage_accountでStorage Accountを、azurerm_service_planでApp Service Planを、そしてazurerm_linux_function_appでFunction App自体を作成します。これらは、リソースの名前、場所、および必要な設定で構成されています。
  3. variables.tf: リソースの構成に使用する変数を定義しています。デプロイするリソースの構成をカスタマイズするための変数が定義されています。これには、リソースの場所、名前、タグなどの情報が含まれます。これらの変数を使用することで、様々な環境や要件に応じたリソースのデプロイが可能になります。

azure-terraform-templates/terraform/modules/functions/output.tf

# storage account
output "storage_account_id" {
  description = "The ID of the Storage Account."
  value       = azurerm_storage_account.az_storage.id
}

output "storage_account_name" {
  description = "The name of the Storage Account."
  value       = azurerm_storage_account.az_storage.name
}

output "storage_account_primary_access_key" {
  description = "The primary access key for the Storage Account."
  value       = azurerm_storage_account.az_storage.primary_access_key
}

output "storage_account_primary_connection_string" {
  description = "The primary connection string for the Storage Account."
  value       = azurerm_storage_account.az_storage.primary_connection_string
}

# azurerm service plan
output "service_plan_id" {
  description = "The ID of the App Service Plan."
  value       = azurerm_service_plan.az_service_plan.id
}

output "service_plan_name" {
  description = "The name of the App Service Plan."
  value       = azurerm_service_plan.az_service_plan.name
}

# azurerm function app
output "function_app_id" {
  description = "The ID of the Function App."
  value       = azurerm_linux_function_app.az_func.id
}

output "function_app_name" {
  description = "The name of the Function App."
  value       = azurerm_linux_function_app.az_func.name
}

output "function_app_default_hostname" {
  description = "The default hostname of the Function App."
  value       = azurerm_linux_function_app.az_func.default_hostname
}

azure-terraform-templates/terraform/modules/functions/main.tf

provider "azurerm" {
  features {}
}

resource "azurerm_storage_account" "az_storage" {
  name                     = var.storage_account_name
  resource_group_name      = var.resource_group_name
  location                 = var.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_service_plan" "az_service_plan" {
  name                = "app-service-plan"
  resource_group_name = var.resource_group_name
  location            = var.location
  os_type             = "Linux"
  sku_name            = "Y1"
}

resource "azurerm_linux_function_app" "az_func" {
  name                = var.storage_account_name
  resource_group_name = var.resource_group_name
  location            = var.location

  storage_account_name       = azurerm_storage_account.az_storage.name
  storage_account_access_key = azurerm_storage_account.az_storage.primary_access_key
  service_plan_id            = azurerm_service_plan.az_service_plan.id
  # insight
  # application_insights_connection_string = var.application_insights_connection_string
  # application_insights_key               = var.application_insights_key
  app_settings = {
    "APPINSIGHTS_INSTRUMENTATIONKEY" = var.application_insights_key
    "APPINSIGHTS_CONNECTION_STRING" = var.application_insights_connection_string
  }
  site_config {}
}

azure-terraform-templates/terraform/modules/functions/variables.tf

variable "location" {
  default     = "japaneast"
  description = "The Azure location to provision the resource in"
  type        = string
}

variable "resource_group_name" {
  description = "The name for the resource group"
  type        = string
}

variable "tags" {
  default     = {}
  description = "The tags to include for each of the provisioned resources"
  type        = map(string)
}

variable "function_name" {
  description = "The name for the function app"
  type        = string
  default     = "func-xiaotian-dev-001"
}

variable "storage_account_name" {
  description = "The name for the storage account"
  type        = string
  default     = "staxiaotiandev001"
}

variable "application_insights_connection_string" {
  description = "The connection string for the application insights instance"
  type        = string
}

variable "application_insights_key" {
  description = "The key for the application insights instance"
  type        = string
}

outputについての注意点

Terraformでは、モジュール内で定義された出力変数は、そのモジュールを使用している親のTerraform構成で明示的に出力されない限り、ルートレベルで直接表示されません。つまり、modules/functions/output.tf で定義された出力変数をルートレベルで表示するには、それらを親のTerraform構成(通常はルートモジュール)で参照し、新たな出力変数として定義する必要があります。

modules/functions/output.tfに定義された出力変数をルートレベルで表示するには、以下の手順を踏みます。

  1. モジュールの出力を参照する: モジュール内で定義された出力変数を、親Terraform構成で明示的に参照します。たとえば、module.functions.storage_account_idのようにしてモジュールの出力変数を指定します。
  2. 新たな出力変数として定義する: 参照したモジュールの出力変数を、親構成の新たなoutputブロックで再定義します。これにより、その値はルートレベルの出力として利用可能になります。

下記のようになります。

output "storage_account_id" {
  description = "The ID of the Storage Account."
  value       = module.functions.storage_account_id
}

sensitiveプロパティに注意

outputブロックにsensitiveプロパティを指定することも重要です。これは、出力される情報が機密性の高いデータ(例えばアクセスキーなど)である場合に使用します。このプロパティがtrueに設定されていると、その出力値はTerraformのUIやログに表示されなくなります。

output "storage_account_primary_access_key" {
  description = "The primary access key for the Storage Account."
  value       = module.functions.storage_account_primary_access_key
  sensitive = true
}

output "storage_account_primary_connection_string" {
  description = "The primary connection string for the Storage Account."
  value       = module.functions.storage_account_primary_connection_string
  sensitive = true
}