【Terraform 入門】Azureストレージアカウントを作成するデモを通してTerraformを理解する
概要
Terraform入門のため、Azureストレージアカウントを作ってみるデモです。
Azure CLIをインストールする
Azureへの認証用に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をインストールします。
# 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のドキュメントに記載されています。
各ファイルの内容と役割について、整理します。
ストレージアカウントの定義
まずは、ストレージアカウントの定義からです。
ストレージアカウントの作成方法については、上記ドキュメントに記載されています。ここでは、ストレージアカウントとBlobコンテナを定義しています。
今回はストレージアカウントをmodule
に分割する形を取りました。こうすることで、各環境(Dev/Prod)でモジュールを再利用でき、各環境でリソースを定義する必要がなくなります。
modules/storage_account/main.tf
ここでは、AzureストレージアカウントとBlobコンテナを定義しています。
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ストレージアカウントを例にで解説しています。
modules/storage_account/variables.tf
同じ階層のvariables.tf
では、main.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
を定義し、公開しておく必要があります。
output "storage_account_name" {
value = azurerm_storage_account.sample_storage_account.name
}
output "container_name" {
value = azurerm_storage_container.sample_blob_container.name
}
この点については、別記事でも言及しています。
envs/{dev/prod}/main.tf
ここからは、各環境(dev/prod)のディレクトリ内のファイルの解説に入ります。
main.tf
では、リソースグループとストレージアカウントが定義されています。このmain.tf
に記載されているリソースは、実際にAzure上に作成されるリソースです。
ストレージアカウントの定義で説明したように、ストレージアカウントはmodule
として分割しています。main.tf
では、このmodule
を使用して、ストレージアカウントを宣言しています。
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
ブロックで定義した変数に与える値を定義するファイルです。
実際の値が入りますので、dev
とprod
でそれぞれ異なる値が指定されることになります。
以下は、dev
の例です。
resource_group_name = "terraform-sample-dev-rg"
location = "Japan EAST"
storage_account_name = "samplestoragedev202409"
container_name = "sample-container"
envs/{dev/prod}/outputs.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コンテナやファイル名を指定します。
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
ファイルは外部ストレージに保存するのが推奨されます。
リソースを作成する
ここからは、実際に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