📝

【Terraform 入門】Azureストレージアカウントを作成するデモを通してTerraformを理解する

2024/09/21に公開

概要

Terraform入門のため、Azureストレージアカウントを作ってみるデモです。

Azure CLIをインストールする

Azureへの認証用にAzure CLIが必要なため、インストールします。

https://learn.microsoft.com/en-us/cli/azure/install-azure-cli

$ brew update && brew install azure-cli

$ az --version
azure-cli                         2.63.0 *

core                              2.63.0 *
telemetry                          1.1.0

Dependencies:
msal                              1.30.0
azure-mgmt-resource               23.1.1

Terraformをインストールする

Terraformをインストールします。

https://developer.hashicorp.com/terraform/tutorials/azure-get-started/install-cli#install-terraform

# HashiCorpのリポジトリをhomebrewに追加
$ brew tap hashicorp/tap

# Terraformインストール
$ brew install hashicorp/tap/terraform

$ brew update

$ terraform -v                        
Terraform v1.9.3
on darwin_arm64

ディレクトリ構成

terraform-sample
  |--modules
      |--storage_account
          |-- main.tf
          |-- outputs.tf
          |-- variables.tf
  |--envs
      |--development
          |-- main.tf
          |-- outputs.tf
          |-- provider.tf
          |-- backend.tf
          |-- terraform.tfvars
          |-- variables.tf
      |--production
          |-- main.tf
          |-- outputs.tf
          |-- provider.tf
          |-- backend.tf
          |-- terraform.tfvars
          |-- variables.tf

ディレクトリ構成のポイントは以下です。

  • 各リソース(今回はBlob Storage)は、再利用できるようにmoduleとして定義する
  • main.tfなどの主要ファイルは、環境別にディレクトリを分ける

環境別にディレクトリを分けているため、各環境のディレクトリに移動してterraformコマンドを叩き、リソースを作成していきます。

$ cd envs/development
# 各環境のディレクトリに移動してから、実行
$ development % terraform init
$ development % terraform plan
$ development % terraform apply

各ファイルの内容と役割

TerraformによるAzureリソース作成方法は、azurermのドキュメントに記載されています。

https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs

各ファイルの内容と役割について、整理します。

ストレージアカウントの定義

まずは、ストレージアカウントの定義からです。

https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account

ストレージアカウントの作成方法については、上記ドキュメントに記載されています。ここでは、ストレージアカウントとBlobコンテナを定義しています。

今回はストレージアカウントをmoduleに分割する形を取りました。こうすることで、各環境(Dev/Prod)でモジュールを再利用でき、各環境でリソースを定義する必要がなくなります。

modules/storage_account/main.tf

ここでは、AzureストレージアカウントとBlobコンテナを定義しています。

modules/storage_account/main.tf
resource "azurerm_storage_account" "sample_storage_account" {
  name                     = var.storage_account_name
  resource_group_name      = var.resource_group_name
  location                 = var.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_storage_container" "sample_blob_container" {
  name                  = var.container_name
  storage_account_name  = azurerm_storage_account.sample_storage_account.name
  container_access_type = "private"
}

resourceブロックについては、【初心者向け】Terraformのresourceブロックを理解する:Azureストレージアカウントを例にで解説しています。

https://zenn.dev/shimiyu/articles/7db8ddc9443c82

modules/storage_account/variables.tf

同じ階層のvariables.tfでは、main.tf内で使用する変数を定義します。

https://developer.hashicorp.com/terraform/language/values/variables

modules/storage_account/variables.tf
variable "storage_account_name" {
  type = string
}

variable "container_name" {
  type = string
}

variable "resource_group_name" {
  type = string
}

variable "location" {
  type = string
}

variableブロックについても、別記事variableブロックで解説しています。

modules/storage_account/outputs.tf

後ほど登場するenvs/{dev/prod}側のoutputブロックで、モジュール内で定義した情報(ここではストレージアカウント名やBlobコンテナ名)を使用しています。envs/{dev/prod}側のoutputでモジュール内で定義した情報を利用する場合、モジュール側でoutputを定義し、公開しておく必要があります。

outputs.tf
output "storage_account_name" {
  value = azurerm_storage_account.sample_storage_account.name
}

output "container_name" {
  value = azurerm_storage_container.sample_blob_container.name
}

この点については、別記事でも言及しています。

https://zenn.dev/shimiyu/articles/58973d36b9dd69

envs/{dev/prod}/main.tf

ここからは、各環境(dev/prod)のディレクトリ内のファイルの解説に入ります。

main.tfでは、リソースグループとストレージアカウントが定義されています。このmain.tfに記載されているリソースは、実際にAzure上に作成されるリソースです。

ストレージアカウントの定義で説明したように、ストレージアカウントはmoduleとして分割しています。main.tfでは、このmoduleを使用して、ストレージアカウントを宣言しています。

main.tf
resource "azurerm_resource_group" "rg" {
  name     = var.resource_group_name
  location = var.location
}

module "storage_account" {
  // 利用するmoduleファイルを指定します
  // ここでは、modulesディレクトリ内にあるストレージアカウントを定義したファイルを指定します
  source = "../../modules/storage_account"
  
  // 上記sourceで指定されたモジュール内の変数に与える値を渡します(`modules/storage_account/variables.tf`で定義されている変数)
  storage_account_name = var.storage_account_name
  resource_group_name  = azurerm_resource_group.rg.name
  location             = var.location
  container_name       = var.container_name
}

envs/{dev/prod}/terraform.tfvars

variableブロックで定義した変数に与える値を定義するファイルです。

実際の値が入りますので、devprodでそれぞれ異なる値が指定されることになります。

以下は、devの例です。

envs/dev/terraform.tfvars
resource_group_name    = "terraform-sample-dev-rg"
location               = "Japan EAST"
storage_account_name   = "samplestoragedev202409"
container_name         = "sample-container"

envs/{dev/prod}/outputs.tf

output.tf
output "storage_account_name" {
  value = module.storage_account.storage_account_name
}

output "container_name" {
  value = module.storage_account.container_name
}

terraform applyを実行すると、

Outputs:

container_name = "sample-container"
storage_account_name = "samplestoragedev202409"

のような情報が出力されます。

この出力内容は、outputs.tfで定義したものに基づいています。

この出力情報は、

  • 作成されたリソースの確認
  • 他のリソースや外部システムで利用

などの用途で使われます。

envs/{dev/prod}/provider.tf

Azureの場合は、azurerm(Azure Resource Manager)を指定します。

provider "azurerm" {
  features {}
}

envs/{dev/prod}/backend.tf

tfstateファイルを保存しておくAzureストレージアカウントやBlobコンテナやファイル名を指定します。

envs/dev/backend.tf
terraform {
  backend "azurerm" {
    resource_group_name   = "tfstate-dev-rg"
    storage_account_name  = "sampletfstatestoragedev"
    container_name        = "tfstate"
    key                   = "development.terraform-sample.tfstate"
  }
}

状態管理用のストレージアカウントを作成する

Terraformを使ってAzureリソースを作成する前に、状態管理用のストレージアカウントを用意する必要があります。

tfstateファイルとは何か?

Terraformが管理対象とするリソースの現在の状態を保存するファイルのことです。

Terraformは、リソース作成の際、"現在の状態"と"Terraformファイルに定義されている状態"を比較します。そして、検出した差分に応じて、リソースを変更(作成・更新・削除)します。この"現在の状態"を保存するのがtfstateファイルです。

ストレージアカウントを手動作成する

tfstateファイルをリモートで管理するため、Azureのストレージアカウントを事前に作成しておきます。CLIやAzureポータルを使用してenvs/development/backend.tfの設定に基づき、ストレージアカウントを作成します。

同様に、tfstateファイルを保存するためのBlobコンテナも作成します。

なぜtfstateファイルを外部ストレージに保存するのか

tfstateファイルは、AzureストレージアカウントやAWSのS3のような外部ストレージに保存することが推奨されます。Gitでtfstateファイルを管理することは推奨されません。その理由は、複数のユーザーが同時にTerraformを操作した際に、コンフリクトが発生しやすく、状態が正しく管理できなくなる可能性があるからです。

外部ストレージにtfstateファイルを保存した場合、Terraformはロック機能を使用します。このロック機能により、あるユーザーがリソースの操作を行っている間、他のユーザーが同時に変更を加えることができなくなり、コンフリクトが起きなくなります。

上記の理由により、tfstateファイルは外部ストレージに保存するのが推奨されます。

https://developer.hashicorp.com/terraform/language/state/locking

リソースを作成する

ここからは、実際にTerraformコマンドを打ち込んでリソースを作成していきます。

ここでは、develop環境のリソースを作成します。

Azure CLIでログイン

作業にあたり、envs/developの階層に移動します。

cd terraform-sample/envs/development

az loginでログインします。ブラウザが立ち上がり、認証ページが表示されますのでログインします。

$ development % az login

az account showで、現在アクティブになっているサブスクリプションを確認できます。リソースを作成するサブスクリプションとして問題ないかを確認しておきます。

$ development % az account show
{
  "environmentName": "AzureCloud",
  "homeTenantId": "****",
  "id": "****",
  "isDefault": true,
  "managedByTenants": [],
  "name": "Azure サブスクリプション 1",
  "state": "Enabled",
  "tenantDefaultDomain": "****.onmicrosoft.com",
  "tenantDisplayName": "test",
  "tenantId": "****",
  "user": {
    "name": "****.com",
    "type": "user"
  }
}

init:初期化する

terraform initによって、Terraformプロジェクトを初期化します。

具体的には、

  • Terraformのプロバイダプラグインのインストール
  • バックエンドの設定(どのtfstateを使うかを決める)
  • モジュールの取得
  • Terraform実行に必要な.terraformディレクトリの作成

などが実施されます。

$ development % terraform init

# バックエンドの設定
# -backend-configオプションを使って明示的に指定しない場合は、コマンドを実行したディレクトリ内のバックエンド設定(`backend`ブロックで定義されたもの)が使用されます
Initializing the backend...

Successfully configured the backend "azurerm"! Terraform will automatically
use this backend unless the backend configuration changes.
# モジュールの取得
Initializing modules...
# Terraformプロバイダープラグインのインストール
# providerブロックで定義したものが対象となります(ここでは`azurerm`プロバイダーがインストールされます)
Initializing provider plugins...
- Finding latest version of hashicorp/azurerm...
- Installing hashicorp/azurerm v4.1.0...
- Installed hashicorp/azurerm v4.1.0 (signed by HashiCorp)

# ロックファイルの作成
# このファイルをバージョン管理に含めることで、再度`terraform init`を実行する際に同じプロバイダーのバージョンがインストールされることが保証されます。
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.

terraform init後、.terraformディレクトリや.terraform.lock.hclファイルが作成されているのが確認できます。

状態管理用のBlobコンテナがなくてエラー

tfstateファイルを保存するBlobコンテナを作成していないと、ContainerNotFoundエラーが発生します。

$ development % terraform init

Initializing the backend...
Initializing modules...
- storage_account in ../../modules/storage_account
╷
│ Error: Failed to get existing workspaces: containers.Client#ListBlobs: Failure responding to request: StatusCode=404 -- Original Error: autorest/azure: Service returned an error. Status=404 Code="ContainerNotFound" Message="The specified container does not exist.\nRequestId:2366d8ca-c01e-005d-1b2c-fd82aa000000\nTime:2024-09-02T11:37:31.8065640Z"
│ 
│ 
╵

ストレージアカウントを手動作成するの手順通りにBlobコンテナを作っていれば、問題ありません。

plan:実行計画を確認する

terraform planは、最新のコードが適用された時にどのような変更がされるのかを確認するためのコマンドです。より具体的には、リソースの現在の状態(tfstateファイル)とTerraformコードを比較し、リソースを追加・更新・削除するのかを判断し、その実行計画を示します。

terraform planを実行しても、実際のリソースは何も変更されません。

$ development % terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions
are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # azurerm_resource_group.rg will be created
  + resource "azurerm_resource_group" "rg" {
      + id       = (known after apply)
      + location = "japaneast"
      + name     = "terraform-sample-dev-rg"
    }

  # module.storage_account.azurerm_storage_account.sample_storage_account will be created
  + resource "azurerm_storage_account" "sample_storage_account" {
      + access_tier                        = (known after apply)
      + account_kind                       = "StorageV2"
      + account_replication_type           = "LRS"
      + account_tier                       = "Standard"
      + allow_nested_items_to_be_public    = true
      + cross_tenant_replication_enabled   = false
      + default_to_oauth_authentication    = false
      + dns_endpoint_type                  = "Standard"
      + https_traffic_only_enabled         = true
      + id                                 = (known after apply)
      + infrastructure_encryption_enabled  = false
      + is_hns_enabled                     = false
      + large_file_share_enabled           = (known after apply)
      + local_user_enabled                 = true
      + location                           = "japaneast"
      + min_tls_version                    = "TLS1_2"
      + name                               = "samplestorageaccountdev"
      + nfsv3_enabled                      = false
      + primary_access_key                 = (sensitive value)
      + primary_blob_connection_string     = (sensitive value)
      + primary_blob_endpoint              = (known after apply)
      + primary_blob_host                  = (known after apply)
      + primary_blob_internet_endpoint     = (known after apply)
      + primary_blob_internet_host         = (known after apply)
      + primary_blob_microsoft_endpoint    = (known after apply)
      + primary_blob_microsoft_host        = (known after apply)
      + primary_connection_string          = (sensitive value)
      + primary_dfs_endpoint               = (known after apply)
      + primary_dfs_host                   = (known after apply)
      + primary_dfs_internet_endpoint      = (known after apply)
      + primary_dfs_internet_host          = (known after apply)
      + primary_dfs_microsoft_endpoint     = (known after apply)
      + primary_dfs_microsoft_host         = (known after apply)
      + primary_file_endpoint              = (known after apply)
      + primary_file_host                  = (known after apply)
      + primary_file_internet_endpoint     = (known after apply)
      + primary_file_internet_host         = (known after apply)
      + primary_file_microsoft_endpoint    = (known after apply)
      + primary_file_microsoft_host        = (known after apply)
      + primary_location                   = (known after apply)
      + primary_queue_endpoint             = (known after apply)
      + primary_queue_host                 = (known after apply)
      + primary_queue_microsoft_endpoint   = (known after apply)
      + primary_queue_microsoft_host       = (known after apply)
      + primary_table_endpoint             = (known after apply)
      + primary_table_host                 = (known after apply)
      + primary_table_microsoft_endpoint   = (known after apply)
      + primary_table_microsoft_host       = (known after apply)
      + primary_web_endpoint               = (known after apply)
      + primary_web_host                   = (known after apply)
      + primary_web_internet_endpoint      = (known after apply)
      + primary_web_internet_host          = (known after apply)
      + primary_web_microsoft_endpoint     = (known after apply)
      + primary_web_microsoft_host         = (known after apply)
      + public_network_access_enabled      = true
      + queue_encryption_key_type          = "Service"
      + resource_group_name                = "terraform-sample-dev-rg"
      + secondary_access_key               = (sensitive value)
      + secondary_blob_connection_string   = (sensitive value)
      + secondary_blob_endpoint            = (known after apply)
      + secondary_blob_host                = (known after apply)
      + secondary_blob_internet_endpoint   = (known after apply)
      + secondary_blob_internet_host       = (known after apply)
      + secondary_blob_microsoft_endpoint  = (known after apply)
      + secondary_blob_microsoft_host      = (known after apply)
      + secondary_connection_string        = (sensitive value)
      + secondary_dfs_endpoint             = (known after apply)
      + secondary_dfs_host                 = (known after apply)
      + secondary_dfs_internet_endpoint    = (known after apply)
      + secondary_dfs_internet_host        = (known after apply)
      + secondary_dfs_microsoft_endpoint   = (known after apply)
      + secondary_dfs_microsoft_host       = (known after apply)
      + secondary_file_endpoint            = (known after apply)
      + secondary_file_host                = (known after apply)
      + secondary_file_internet_endpoint   = (known after apply)
      + secondary_file_internet_host       = (known after apply)
      + secondary_file_microsoft_endpoint  = (known after apply)
      + secondary_file_microsoft_host      = (known after apply)
      + secondary_location                 = (known after apply)
      + secondary_queue_endpoint           = (known after apply)
      + secondary_queue_host               = (known after apply)
      + secondary_queue_microsoft_endpoint = (known after apply)
      + secondary_queue_microsoft_host     = (known after apply)
      + secondary_table_endpoint           = (known after apply)
      + secondary_table_host               = (known after apply)
      + secondary_table_microsoft_endpoint = (known after apply)
      + secondary_table_microsoft_host     = (known after apply)
      + secondary_web_endpoint             = (known after apply)
      + secondary_web_host                 = (known after apply)
      + secondary_web_internet_endpoint    = (known after apply)
      + secondary_web_internet_host        = (known after apply)
      + secondary_web_microsoft_endpoint   = (known after apply)
      + secondary_web_microsoft_host       = (known after apply)
      + sftp_enabled                       = false
      + shared_access_key_enabled          = true
      + table_encryption_key_type          = "Service"

      + blob_properties (known after apply)

      + network_rules (known after apply)

      + queue_properties (known after apply)

      + routing (known after apply)

      + share_properties (known after apply)
    }

  # module.storage_account.azurerm_storage_container.sample_blob_container will be created
  + resource "azurerm_storage_container" "sample_blob_container" {
      + container_access_type             = "private"
      + default_encryption_scope          = (known after apply)
      + encryption_scope_override_enabled = true
      + has_immutability_policy           = (known after apply)
      + has_legal_hold                    = (known after apply)
      + id                                = (known after apply)
      + metadata                          = (known after apply)
      + name                              = "sample-container"
      + resource_manager_id               = (known after apply)
      + storage_account_name              = "samplestorageaccountdev"
    }

Plan: 3 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + container_name       = "sample-container"
  + storage_account_name = "samplestorageaccountdev"

──────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take
exactly these actions if you run "terraform apply" now.

apply:実際にリソースを作成する

terraform applyは、実際にリソースを作成するためのコマンドです。

実行すると、作成されるリソース内容が出力され、「本当に実行して良いか」を聞かれます。

$ development % terraform apply
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with
the following symbols:
  + create

Terraform will perform the following actions:

  # azurerm_resource_group.rg will be created
  + resource "azurerm_resource_group" "rg" {
      + id       = (known after apply)
...以下略

Plan: 3 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + container_name       = "sample-container"
  + storage_account_name = "samplestorageaccountdev"

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  # 問題なければ、yesと入力する
  Enter a value:

yesと入力すると、Terraformコードに従って、リソースが作成されます。ここでは、Azureリソースグループやストレージアカウントが作成されます。

Enter a value: yes

module.storage_account.azurerm_storage_account.sample_storage_account: Creating...
module.storage_account.azurerm_storage_account.sample_storage_account: Still creating... [10s elapsed]
module.storage_account.azurerm_storage_account.sample_storage_account: Still creating... [20s elapsed]
module.storage_account.azurerm_storage_account.sample_storage_account: Still creating... [30s elapsed]
module.storage_account.azurerm_storage_account.sample_storage_account: Still creating... [40s elapsed]
module.storage_account.azurerm_storage_account.sample_storage_account: Still creating... [50s elapsed]
module.storage_account.azurerm_storage_account.sample_storage_account: Still creating... [1m0s elapsed]
module.storage_account.azurerm_storage_account.sample_storage_account: Creation complete after 1m8s [id=/subscriptions/****/resourceGroups/terraform-sample-dev-rg/providers/Microsoft.Storage/storageAccounts/samplestoragedev202409]
module.storage_account.azurerm_storage_container.sample_blob_container: Creating...
module.storage_account.azurerm_storage_container.sample_blob_container: Creation complete after 0s [id=https://samplestoragedev202409.blob.core.windows.net/sample-container]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

container_name = "sample-container"
storage_account_name = "samplestoragedev202409"

apply時にエラーが発生することもある

Azure上の他のリソースとの整合性が取れていないようなエラーは、plan時には検出されません。apply実行時のエラーとなります。例えば、仮想ネットワークのアドレス空間の重複や同一サブスクリプション内の名前被りなどがそれに該当します。

今回の例で言うと、apply実行時に、StorageAccountAlreadyTaken: The storage account named samplestorageaccountdev is already taken.というエラーが発生しました。

module.storage_account.azurerm_storage_account.sample_storage_account: Creating...
╷
│ Error: creating Storage Account (Subscription: "****"
│ Resource Group Name: "terraform-sample-dev-rg"
│ Storage Account Name: "samplestorageaccountdev"): performing Create: unexpected status 409 (409 Conflict) with error: StorageAccountAlreadyTaken: The storage account named samplestorageaccountdev is already taken.
│ 
│   with module.storage_account.azurerm_storage_account.sample_storage_account,
│   on ../../modules/storage_account/main.tf line 1, in resource "azurerm_storage_account" "sample_storage_account":1: resource "azurerm_storage_account" "sample_storage_account" {

これは、ストレージアカウントの名前が他の名前と被っているエラーです。ストレージアカウント名は、Azure上のどのストレージアカウントとも被らないようにする必要があります(グローバルに一意であることが必須)。

このようなエラーは、apply実行時のエラーとなります。

作成したリソースを確認する

Azureポータルで作成されたリソースを確認してみます。

まず、リソースグループとストレージアカウントです。dev/terraform.tfvarsで指定した名前の通りにが作成されています。

次にBlobコンテナです。作成したストレージアカウントの中にコンテナが1つ作成されています。

最後に

以上です。

Azureストレージアカウントを作成するデモを通して、Terraformの各要素について学んだことを整理してみました。

Discussion