Open35

既に動いているAzureをTerraformで管理し始めるための手順をメモる

nobjasnobjas
nobjasnobjas

Terraform実行用のサービスプリンシパルを作成すると良さそう。
個人でもいいけど属人化するね

nobjasnobjas

CloudShell を追加する時にストレージが作られるらしい。ボタンを押したら自動で作成された

nobjasnobjas

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
nobjasnobjas

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
nobjasnobjas

使用するサブスクリプションを切り替える

$ 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
nobjasnobjas

terraform 実行用のサービスプリンシパルを作成する

サービスプリンシパルが何なのかは色んな記事があるので省略

https://zenn.dev/mizti/articles/1abb7067fbfa97

$ 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 に環境変数を書き込む

.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*
nobjasnobjas

リソースグループ単位で作るのが整理しやすいってことだろう
https://learn.microsoft.com/ja-jp/azure/developer/terraform/create-resource-group

nobjasnobjas

リソースグループ作成のためのterraformコードを作成する

terraform コードを置くフォルダを作成して移動

$ mkdir iac_terraform
$ cd iac_terraform/

providers.tf をつくる

$ vi providers.tf
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
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
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
outputs.tf
output "resource_group_name" {
  value = azurerm_resource_group.rg.name
}
nobjasnobjas

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"
nobjasnobjas

実行してみる

リソースグループをつくるだけなので危なくなさそうなので実行してみる

$ 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でも確認できた

nobjasnobjas

作ったのリソースグループは不要なので削除する

削除するプランの作成

$ terraform plan -destroy -out main.destroy.tfplan

削除を実行

$ terraform apply main.destroy.tfplan

消えたログが出てればOK。Azure Portalでも消えてた

nobjasnobjas
nobjasnobjas

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
nobjasnobjas

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
nobjasnobjas

export が成功すると

  • main.tf
  • provider.tf
  • aztfexportResourceMapping.json

が書き出される。書き出せないリソースが合った場合には aztfexportSkippedResources.txt というファイルも書き出されるとのこと

nobjasnobjas

対象のリソースを削除する

丁寧にプランから作ってみる

$ 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

nobjasnobjas
nobjasnobjas

まずはstorageを作成する

$ mkdir tfstate
$ cd tfstate

Azureでストレージアカウントを作ってすぐにコンテナーを作成しようとするとエラーになってしまう状態が発生しているらしく、現在PRが出されていて解決に向かっています。
https://github.com/hashicorp/terraform-provider-azurerm/pull/23002

私が作業した時点では同様のエラーが出ていました。

│ 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" {
│ 

これを回避するために、先にストレージアカウントだけ作成します

main.tf
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"

その後コメント部分のコメントアウトを外します

main.tf
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"
nobjasnobjas

アクセスキーを取得する

この例では、Terraform はアクセス キーを使用して Azure ストレージ アカウントに対して認証を行います。 運用環境のデプロイでは、azurerm バックエンドでサポートされている使用可能な 認証オプション を評価し、ユース ケースに最も安全なオプションを使用することをお勧めします。

上記のtfで作成すると、ストレージアカウント名はランダムで作成される。
作成されたストレージアカウント名をoutputとして出力するように main.tf に追加する

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
nobjasnobjas

main.tf の terraform.backend に指定する

main.tf
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"
  }
}
nobjasnobjas

Azure Export for Terraform のインストール

https://github.com/azure/aztfexport

Cloud Shell にインストールを試みる

nobjasnobjas

Azure Cloud Shell でやろうとしたが特権アカウントではないので、sudoなど使えず。ローカルで実施

インストール

$ brew install aztfexport
nobjasnobjas

Terraformもローカルにインストール

$ brew tap hashicorp/tap
$ brew install hashicorp/tap/terraform
nobjasnobjas

途中でハマった

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を管理させるために必要な権限一覧が載っているので参考に

https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/guides/service_principal_configuration

nobjasnobjas

マネージドIDとサービスプリンシパル

  • マネージドIDは Azure 内のリソースから Azure内のリソースに繋げる時に認証情報の管理がいらないID
    • 外部のアプリケーションからは利用出来ない
    • Azure VMとかからfunction実行する時とかはこれでやるとセキュアだし設定が少なくて簡単
  • サービスプリンシパルは外部からも繋げられるけど認証情報の管理が必要

この記事が丁寧に説明されていて勉強になった
https://milestone-of-se.nesuke.com/sv-advanced/azure/az-managedid-keyvault-service-principal/