既に動いているAzureをTerraformで管理し始めるための手順をメモる
VscodeのExtenrion
Cloud ShellでTerraformを構成できる
Terraform実行用のサービスプリンシパルを作成すると良さそう。
個人でもいいけど属人化するね
CloudShell を追加する時にストレージが作られるらしい。ボタンを押したら自動で作成された
terraform の version を確認。古いようなので最新をインストールしよう
$ terraform version
Terraform v1.6.4
on linux_amd64
Your version of Terraform is out of date! The latest version
is 1.7.4. You can update by downloading from https://www.terraform.io/downloads.html
https://www.terraform.io/downloads.html で AMD64 を探してダウンロードリンクをコピー
今回はこれ
Terraform を最新版にする
DL
$ curl -O https://releases.hashicorp.com/terraform/1.7.4/terraform_1.7.4_linux_amd64.zip
zip展開
$ unzip terraform_1.7.4_linux_amd64.zip
binフォルダ作成、terraformフォルダを移動
$ mkdir bin
$ mv terraform bin/
$PATH
に ~/bin がもともと含まれているようなので sh再起動
$ echo $PATH
~/.local/bin:~/bin:...
再起動ボタンがある
最新になったか確認
$ terraform version
Terraform v1.7.4
on linux_amd64
使用するサブスクリプションを切り替える
$ az login
Cloud Shell is automatically authenticated under the initial account signed-in with. Run 'az login' only if you need to use a different account
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code xxxxxxxxx to authenticate.
https://microsoft.com/devicelogin にアクセスして表示されたコードを入力( xxxxxxxxx
の部分 )
ログインに成功するとサブスクリプションの一覧が表示される。私の環境はいくつかあるので複数表示された
The following tenants don't contain accessible subscriptions. Use 'az login --allow-no-subscriptions' to have tenant level access.
xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 'Hoge'
[
{
"cloudName": "AzureCloud",
"homeTenantId": "xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"id": "xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"isDefault": true,
"managedByTenants": [],
"name": "Azure サブスクリプション 1",
"state": "Enabled",
"tenantId": "xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"user": {
"name": "example@example.com",
"type": "user"
}
},
{
"cloudName": "AzureCloud",
"homeTenantId": "xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"id": "xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"isDefault": false,
"managedByTenants": [],
"name": "Azure サブスクリプション2",
"state": "Enabled",
"tenantId": "xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"user": {
"name": "example@example.com",
"type": "user"
}
}
]
上記のリストをtableで見やすく出力したい場合はこう
$ az account list --query "[].{Name:name, ID:id, User:user.name, Default:isDefault}" --output Table --all
Default を切り替えるのはこう
$ az account set --subscription "<サブスクリプションの名前かID>"
再度表示してみて指定したサブスクリプションになっていればOK
$ az account show
terraform 実行用のサービスプリンシパルを作成する
サービスプリンシパルが何なのかは色んな記事があるので省略
$ az ad sp create-for-rbac --name <サービスプリンシパルにつけたい名前> --role Contributor --scopes /subscriptions/$(az account show --query "id" --output tsv)
成功すると各種情報が表示されるので、メモる。 パスワードはここでしか表示されないので無くすとリセットしたりすることになるので必ずメモる。重要
{
"appId": "<service_principal_appid>",
"displayName": "<サービスプリンシパルにつけたい名前>",
"password": "<ここがパスワード!>",
"tenant": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
.bashrc
に環境変数を書き込む
export ARM_SUBSCRIPTION_ID="<azure_subscription_id>"
export ARM_TENANT_ID="<azure_subscription_tenant_id>"
export ARM_CLIENT_ID="<service_principal_appid>"
export ARM_CLIENT_SECRET="<先ほどメモったパスワード>"
読み込む&設定確認
$ . .bashrc
$ printenv | grep ^ARM*
リソースグループ単位で作るのが整理しやすいってことだろう
リソースグループ名は random_pet でランダムに生成するのが推奨されてるけど、わかりづらくないのかしら。
リソースグループ作成のためのterraformコードを作成する
terraform コードを置くフォルダを作成して移動
$ mkdir iac_terraform
$ cd iac_terraform/
providers.tf
をつくる
$ vi providers.tf
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~>3.0"
}
random = {
source = "hashicorp/random"
version = "~>3.0"
}
}
}
provider "azurerm" {
features {}
}
main.tf
をつくる
$ vi main.tf
resource "random_pet" "rg_name" {
prefix = var.resource_group_name_prefix
}
resource "azurerm_resource_group" "rg" {
location = var.resource_group_location
name = random_pet.rg_name.id
}
variables.tf
をつくる
ここでregionの設定をする。一覧はここ https://github.com/claranet/terraform-azurerm-regions/blob/master/REGIONS.md
$ vi variables.tf
variable "resource_group_location" {
type = string
default = "japaneast"
description = "リソースグループのリージョン"
}
variable "resource_group_name_prefix" {
type = string
default = "rg"
description = "リソースグループ名のプレフィックス(接頭辞)にランダムな ID を組み合わせたもので、Azure サブスクリプション内で名前が一意になります"
}
outputs.tf
をつくる
$ vi outputs.tf
output "resource_group_name" {
value = azurerm_resource_group.rg.name
}
terraform の初期化
$ terraform init -upgrade
terraform プランの作成
この時点では実際に変更はされない。体感、結構実行時間が長かった
$ terraform plan -out main.tfplan
こんなoutputが出る
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 = (known after apply)
}
# random_pet.rg_name will be created
+ resource "random_pet" "rg_name" {
+ id = (known after apply)
+ length = 2
+ prefix = "rg"
+ separator = "-"
}
Plan: 2 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ resource_group_name = (known after apply)
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Saved the plan to: main.tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "main.tfplan"
実行してみる
リソースグループをつくるだけなので危なくなさそうなので実行してみる
$ terraform apply main.tfplan
こんな出力
random_pet.rg_name: Creating...
random_pet.rg_name: Creation complete after 0s [id=rg-xxxxxx-xxxxxx]
azurerm_resource_group.rg: Creating...
azurerm_resource_group.rg: Creation complete after 2s [id=/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-xxxxxx-xxxxxx]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
resource_group_name = "rg-xxxxxx-xxxxxx"
結果を確認してみる
$ terraform output
resource_group_name = "rg-xxxxxx-xxxxxx"
resource "random_pet" "rg_name"
で指定しているとおり random_pet でランダムなペットの名前でリソースグループ名が作られている。私の場合は accepted-cougar
だったw
リソースグループの詳細を見てみる
$ az group show --name $(terraform output -raw resource_group_name)
{
"id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-xxxxxx-xxxxxx",
"location": "japaneast",
"managedBy": null,
"name": "rg-xxxxxx-xxxxxx",
"properties": {
"provisioningState": "Succeeded"
},
"tags": {},
"type": "Microsoft.Resources/resourceGroups"
}
正常にできていそう。Azure Portalでも確認できた
作ったのリソースグループは不要なので削除する
削除するプランの作成
$ terraform plan -destroy -out main.destroy.tfplan
削除を実行
$ terraform apply main.destroy.tfplan
消えたログが出てればOK。Azure Portalでも消えてた
Azureでのドキュメント一覧
export
https://learn.microsoft.com/ja-jp/azure/developer/terraform/azure-export-for-terraform/export-resources-hcl?tabs=azure-cli#create-the-test-azure-resources に書いてあるとおりまずはテストしてみる
まずはリソースグループを作成
$ az group create --name myResourceGroup --location japaneast
{
"id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup",
"location": "japaneast",
"managedBy": null,
"name": "myResourceGroup",
"properties": {
"provisioningState": "Succeeded"
},
"tags": null,
"type": "Microsoft.Resources/resourceGroups"
}
VMを作成
$ az vm create \
--resource-group myResourceGroup \
--name myVM \
--image Debian11 \
--admin-username azureadmin \
--generate-ssh-keys \
--public-ip-sku Standard
{
"fqdns": "",
"id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachines/myVM",
"location": "japaneast",
"macAddress": "60-45-BD-67-66-68",
"powerState": "VM running",
"privateIpAddress": "xxx.xxx.xxx.xxx",
"publicIpAddress": "xxx.xxx.xxx.xxx",
"resourceGroup": "myResourceGroup",
"zones": ""
}
テストするディレクトリを作成して移動
$ mkdir test-export
$ cd test-export
export してみる
$ aztfexport resource-group --non-interactive --hcl-only myResourceGroup
export 時にこんなエラーが出た。Terraform をインストールする必要がある。
Error: error finding a terraform exectuable: unable to find, install, or build from 1 sources: 1 error occurred:
* terraform: executable file not found in $PATH
export が成功すると
main.tf
provider.tf
aztfexportResourceMapping.json
が書き出される。書き出せないリソースが合った場合には aztfexportSkippedResources.txt
というファイルも書き出されるとのこと
対象のリソースを削除する
丁寧にプランから作ってみる
$ terraform plan -destroy -out main.destroy.tfplan
エラーが出た
╷
│ Error: Inconsistent dependency lock file
│
│ The following dependency selections recorded in the lock file are inconsistent with the current configuration:
│ - provider registry.terraform.io/hashicorp/azurerm: required by this configuration but no version is selected
│
│ To make the initial dependency selections that will initialize the dependency lock file, run:
│ terraform init
╵
terraform init
しろと。やってみる
$ terraform init
再度プラン作成しようとしたら別のエラー
$ terraform plan -destroy -out main.destroy.tfplan
No changes. No objects need to be destroyed.
Either you have not created any objects yet or the existing objects were already deleted outside of Terraform.
これはもしかしてAzureの状態が取得できていない( --hcl-only
で実行したから)のが原因かもしれない。と思い、状態も取得できるように --hcl-only
を外した状態で再度export
$ aztfexport resource-group --non-interactive --overwrite myResourceGroup
すると
- import.tf
- terraform.tf
- terraform.tfstate
- .terraform.lock.hcl
- .terraform/*
などが作成された。これで再度planを試してみる
$ terraform plan -destroy -out main.destroy.tfplan
main.destroy.tfplan
が正しく作成された。plan時の出力で何が削除されるのかが表示されるのでおかしなものが消えていない事を確認。
問題なさそうなので削除を実行
$ terraform apply main.destroy.tfplan
無事実行が完了し、Azure Portalでも削除が確認できた。また、削除実行後は手元の状態ファイルも更新され( terraform.tfstate
)、バックアップファイルも作成される( terraform.tfstate.backup
)
azure状態をstorageに保存
接続時の認証
まずはstorageを作成する
$ mkdir tfstate
$ cd tfstate
Azureでストレージアカウントを作ってすぐにコンテナーを作成しようとするとエラーになってしまう状態が発生しているらしく、現在PRが出されていて解決に向かっています。
私が作業した時点では同様のエラーが出ていました。
│ Error: building Queues Client: retrieving Account Key: Listing Keys for Storage Account "tfstatec0he7" (Resource Group "tfstate"): storage.AccountsClient#ListKeys: Failure responding to request: StatusCode=404 -- Original Error: autorest/azure: Service returned an error. Status=404 Code="ResourceNotFound" Message="The Resource 'Microsoft.Storage/storageAccounts/tfstatexxxxx' under resource group 'tfstate' was not found. For more details please go to https://aka.ms/ARMResourceNotFoundFix"
│
│ with azurerm_storage_account.tfstate,
│ on main.tf line 25, in resource "azurerm_storage_account" "tfstate":
│ 25: resource "azurerm_storage_account" "tfstate" {
│
これを回避するために、先にストレージアカウントだけ作成します
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~>3.0"
}
}
}
provider "azurerm" {
features {}
}
resource "random_string" "resource_code" {
length = 5
special = false
upper = false
}
resource "azurerm_resource_group" "tfstate" {
name = "tfstate"
location = "japaneast"
}
resource "azurerm_storage_account" "tfstate" {
name = "tfstate${random_string.resource_code.result}"
resource_group_name = azurerm_resource_group.tfstate.name
location = azurerm_resource_group.tfstate.location
account_tier = "Standard"
account_replication_type = "LRS"
allow_nested_items_to_be_public = false
tags = {
environment = "staging"
}
}
# resource "azurerm_storage_container" "tfstate" {
# name = "tfstate"
# storage_account_name = azurerm_storage_account.tfstate.name
# container_access_type = "private"
# }
これでTerraformを実行します
$ terraform plan -out main.tfplan
$ terraform apply "main.tfplan"
その後コメント部分のコメントアウトを外します
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~>3.0"
}
}
}
provider "azurerm" {
features {}
}
resource "random_string" "resource_code" {
length = 5
special = false
upper = false
}
resource "azurerm_resource_group" "tfstate" {
name = "tfstate"
location = "japaneast"
}
resource "azurerm_storage_account" "tfstate" {
name = "tfstate${random_string.resource_code.result}"
resource_group_name = azurerm_resource_group.tfstate.name
location = azurerm_resource_group.tfstate.location
account_tier = "Standard"
account_replication_type = "LRS"
allow_nested_items_to_be_public = false
tags = {
environment = "staging"
}
}
resource "azurerm_storage_container" "tfstate" {
name = "tfstate"
storage_account_name = azurerm_storage_account.tfstate.name
container_access_type = "private"
}
そしてplanを実行するとエラーが出る場合には、しばらく待ちます。トイレにでも行ってくるといいと思います。私は数分待ったら正しく実行されました。
$ terraform plan -out main.tfplan
$ terraform apply "main.tfplan"
アクセスキーを取得する
この例では、Terraform はアクセス キーを使用して Azure ストレージ アカウントに対して認証を行います。 運用環境のデプロイでは、azurerm バックエンドでサポートされている使用可能な 認証オプション を評価し、ユース ケースに最も安全なオプションを使用することをお勧めします。
上記のtfで作成すると、ストレージアカウント名はランダムで作成される。
作成されたストレージアカウント名をoutputとして出力するように main.tf
に追加する
...
output "storage_account_name" {
value = azurerm_storage_account.tfstate.name
}
$ terraform plan -out main.tfplan
$ terraform apply "main.tfplan"
アクセスキーを環境変数(私はdirenvを使っているので .envrc )に書き込む
$ ACCOUNT_KEY=$(az storage account keys list --resource-group tfstate --account-name $(terraform output -raw storage_account_name) --query '[0].value' -o tsv)
$ echo "export ARM_ACCESS_KEY=$ACCOUNT_KEY" >> .envrc
main.tf の terraform.backend に指定する
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~>3.0"
}
}
backend "azurerm" {
resource_group_name = "tfstate"
storage_account_name = "tfstatexxxxx"
container_name = "tfstate"
key = "terraform.tfstate"
}
}
接続はプライベートエンドポイントが良さそう
Azure Export for Terraform のインストール
Cloud Shell にインストールを試みる
Tips output に設定したsensitive な値を見る方法
row
$ terraform output -raw xxxx
-raw
つければ出力される
json
$ terraform output -json
-json
つければまとめて見える
途中でハマった
Terraform実行用のサービスプリンシパルを作成して、Terraform内で新たにサービスプリンシパルを作成しようとするとエラーが出る
ApplicationsClient.BaseClient.Post(): unexpected status 403 with OData error: Authorization_RequestDenied: Insufficient privileges to complete the operation.
アプリの登録 > Terraform実行用のサービスプリンシパル > APIのアクセス許可
で Microsoft.Graph の Application.ReadWrite.All
を設定すれば動く。
他にも下記URLにはAzure ADを管理させるために必要な権限一覧が載っているので参考に
マネージドIDとサービスプリンシパル
- マネージドIDは Azure 内のリソースから Azure内のリソースに繋げる時に認証情報の管理がいらないID
- 外部のアプリケーションからは利用出来ない
- Azure VMとかからfunction実行する時とかはこれでやるとセキュアだし設定が少なくて簡単
- サービスプリンシパルは外部からも繋げられるけど認証情報の管理が必要
この記事が丁寧に説明されていて勉強になった