Azure Red Hat OpenShift を Terraform でコード化!
2022-12-15
azblob://2022/12/14/eyecatch/2022-12-15-create-aro-with-terraform-000.png

こちらは  FIXER Advent Calendar 2022  の15日目の記事です。

はじめに

皆さんお久しぶりです、FIXERの石井です。

なかなかブログに手が出せずに半年以上間隔があいてしまいました。

今日は Terraform を使って ARO (Azure Red Hat OpenShift) を構築してみようと思います。

null_resource で作ることも可能ですが、今回は  azapi_resource で書いていこうと思います。

azapi_resource については知りたい方は下記の記事をご覧ください。

https://tech-blog.cloud-config.jp/2022-12-05-azapi-resource

今回 Terraform で ARO を構築する処理は、下記チュートリアルの

クラスターを作成する

に対応します。そのため、事前に

開始する前に

を参考に、AROの作成準備を行う必要があるのでご了承ください。

Terraform で ARO を構築する(コード)

variables.tf

variable "name" {
  type = string
}

variable "cluster_domain_name" {
  type = string
}

variable "resource_group_name" {
  type = string
}

variable "subscription_id" {
  type = string
}

variable "tenant_id" {
  type = string
}

variable "client_id" {
  type = string
}

variable "client_secret" {
  type = string
}

variable "virtual_network_name" {
  type = string
}

variable "master_subnet_name" {
  type = string
}

variable "worker_subnet_name" {
  type = string
}

variable "masternode_vm_size" {
  type    = string
  default = "Standard_D8s_v3"
}

variable "workernode_vm_size" {
  type    = string
  default = "Standard_D4s_v3"
}


variable "workernode_disk_size_gb" {
  type    = number
  default = 128
}

variable "workernode_node_count" {
  type    = number
  default = 3
}

variable "pod_cidr" {
  type    = string
  default = "10.16.0.0/14"
}

variable "service_cidr" {
  type    = string
  default = "10.20.0.0/16"
}

variable "pull_secret" {
  type = string
}

main.tf

terraform {
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
    }
    azuread = {
      source = "hashicorp/azuread"
    }
    azapi = {
      source = "Azure/azapi"
    }
  }
}

provider "azurerm" {
  subscription_id = var.subscription_id
  client_id       = var.client_id
  client_secret   = var.client_secret
  tenant_id       = var.tenant_id
}

provider "azuread" {
  client_id     = var.client_id
  client_secret = var.client_secret
  tenant_id     = var.tenant_id
}

provider "azapi" {
  subscription_id = var.subscription_id
  client_id       = var.client_id
  client_secret   = var.client_secret
  tenant_id       = var.tenant_id
}

locals {
  cluster_resource_group_name    = "aro-${var.cluster_domain_name}"
  service_principal_name_for_aro = "aro-${var.cluster_domain_name}"
  aro_resource_provider_name     = "Azure Red Hat OpenShift RP"
}

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

data "azurerm_virtual_network" "example" {
  name                = var.virtual_network_name
  resource_group_name = data.azurerm_resource_group.example.name
}

######################################################
## ARO用のサービスプリンシパルの作成&権限付与
######################################################

data "azuread_client_config" "current" {}

resource "azuread_application" "app" {
  display_name = local.service_principal_name_for_aro
  owners       = [data.azuread_client_config.current.object_id]
}

resource "azuread_service_principal" "app" {
  application_id               = var.azuread_application.app.application_id
  app_role_assignment_required = false
}

resource "azurerm_role_assignment" "contributor" {
  scope                = "/subscriptions/${var.subscription_id}"
  role_definition_name = "Contributor"
  principal_id         = azuread_service_principal.app.object_id
}

######################################################
## リソースプロバイダーサービスプリンシパルの権限付与
######################################################
data "azuread_service_principal" "resource_provider" {
  display_name = local.aro_resource_provider_name
}

resource "azurerm_role_assignment" "network_contributor" {
  scope                = data.azurerm_virtual_network.example.id
  role_definition_name = "Network Contributor"
  principal_id         = data.azuread_service_principal.resource_provider.object_id
}

######################################################
## OpenShift クラスターの作成
######################################################
resource "azapi_resource" "openshift_cluster" {
  depends_on = [
    azurerm_role_assignment.network_contributor
  ]
  type      = "Microsoft.RedHatOpenShift/openShiftClusters@2022-04-01"
  name      = var.name
  location  = data.azurerm_resource_group.example.location
  parent_id = data.azurerm_resource_group.example.id
  tags      = {}

  body = jsonencode({
    properties = {
      apiserverProfile = {
        visibility = "Public"
      },
      clusterProfile = {
        domain               = "${var.cluster_domain_name}",
        resourceGroupId      = "/subscriptions/${var.subscription_id}/resourcegroups/${local.cluster_resource_group_name}",
        pullSecret           = "${var.pull_secret}",
        fipsValidatedModules = "Disabled"
      },
      ingressProfiles = [
        {
          name       = "default",
          visibility = "Public"
        }
      ],
      networkProfile = {
        podCidr     = "${var.pod_cidr}",
        serviceCidr = "${var.service_cidr}"
      },
      servicePrincipalProfile = {
        clientId     = "${module.mo_service_principal.client_id}"
        clientSecret = "${module.mo_service_principal.client_secret}"
      },
      masterProfile = {
        vmSize           = "${var.masternode_vm_size}",
        subnetId         = "/subscriptions/${var.subscription_id}/resourcegroups/${var.resource_group_name}/providers/Microsoft.Network/virtualNetworks/${var.virtual_network_name}/subnets/${var.master_subnet_name}",
        encryptionAtHost = "Disabled"
      },
      workerProfiles = [
        {
          name             = "worker"
          vmSize           = "${var.workernode_vm_size}",
          diskSizeGB       = "${var.workernode_disk_size_gb}",
          subnetId         = "/subscriptions/${var.subscription_id}/resourcegroups/${var.resource_group_name}/providers/Microsoft.Network/virtualNetworks/${var.virtual_network_name}/subnets/${var.worker_subnet_name}",
          count            = "${var.workernode_node_count}",
          encryptionAtHost = "Disabled"
        }
      ]
    }
  })

  response_export_values = ["*"]

  // AROクラスターの構築処理がタイムアウトするので時間を延ばす
  timeouts {
    create = "60m"
    update = "60m"
    delete = "60m"
  }

  lifecycle {
    ignore_changes = [
      body
    ]
  }
}

Terraform で ARO を構築する(解説)

先ほど貼ったコードについて解説していきます。

variables.tf に関してはそれぞれの変数をざっと解説します。

variables.tf の解説

name                     ・・・ AROクラスター名
cluster_domain_name      ・・・ ARO固有のドメイン名
resource_group_name      ・・・ AROクラスターを構築するリソースグループ名
subscription_id          ・・・ サブスクリプションID
tenant_id                ・・・ テナントID
client_id                ・・・ クライアントID
client_secret            ・・・ クライアントシークレット
virtual_network_name     ・・・ 仮想ネットワーク名
master_subnet_name       ・・・ マスターノード用のサブネット名
worker_subnet_name       ・・・ ワーカーノード用のサブネット名
masternode_vm_size       ・・・ マスターノードのVMサイズ
workernode_vm_size       ・・・ ワーカーノードのVMサイズ
workernode_disk_size_gb  ・・・ ワーカーノードのディスクサイズ(GB)
workernode_node_count    ・・・ ワーカーノードの数
pod_cidr                 ・・・ Pod の CIDR
service_cidr             ・・・ Service の CIDR
pull_secret              ・・・ プルシークレット

注意が必要なものに個別で説明すると、

cluster_domain_name は先頭がアルファベットの小文字、それ以外はアルファベットの小文字+数字の全8桁で構成する必要があり、他の人が作ったリソースと名前が被ってはいけません。8桁ぴったりにする必要があるかは検証してないですが、先頭が数字だと処理が失敗することは確認しています。

virtual_network_name, master_subnet_name, worker_subnet_name はチュートリアルで説明されているARO用の空のサブネットを指定します。

pod_cidr, service_cidr は特にこだわりがなければそのままの値で大丈夫です。

pull_secret はプルシークレットの中身をそのまま渡せば動作します。(セキュリティ的にはよくないですが)

もし必要ない場合は適当な文字列を渡してあげれば大丈夫です。

それでは、肝心の処理の部分に関してコードを切り分けてそれぞれ説明していこうと思います。

初期宣言

terraform {
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
    }
    azuread = {
      source = "hashicorp/azuread"
    }
    azapi = {
      source = "Azure/azapi"
    }
  }
}

provider "azurerm" {
  subscription_id = var.subscription_id
  client_id       = var.client_id
  client_secret   = var.client_secret
  tenant_id       = var.tenant_id
}

provider "azuread" {
  client_id     = var.client_id
  client_secret = var.client_secret
  tenant_id     = var.tenant_id
}

provider "azapi" {
  subscription_id = var.subscription_id
  client_id       = var.client_id
  client_secret   = var.client_secret
  tenant_id       = var.tenant_id
}

初歩的な部分ですが、今回は azurerm, azuread, azapi の3つのプロバイダーを使っているのでそれぞれにパラメータを渡してあげます。

パラメータは同じものがそのまま使えるので楽ですね。

locals/data定義

locals {
  cluster_resource_group_name    = "aro-${var.cluster_domain_name}"
  service_principal_name_for_aro = "aro-${var.cluster_domain_name}"
  aro_resource_provider_name     = "Azure Red Hat OpenShift RP"
}

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

data "azurerm_virtual_network" "example" {
  name                = var.virtual_network_name
  resource_group_name = data.azurerm_resource_group.example.name
}

ここの処理も初歩的な部分なので、特筆すべき点はないですね。

locals の aro_resoruce_provider_name はARO固有のリソースプロバイダー名なので変更する必要はないですが、バージョンアップ等で変わるかもしれないのでご注意ください。

ARO用のサービスプリンシパルの作成&権限付与

######################################################
## ARO用のサービスプリンシパルの作成&権限付与
######################################################

data "azuread_client_config" "current" {}

resource "azuread_application" "app" {
  display_name = local.service_principal_name_for_aro
  owners       = [data.azuread_client_config.current.object_id]
}

resource "azuread_service_principal" "app" {
  application_id               = var.azuread_application.app.application_id
  app_role_assignment_required = false
}

resource "azurerm_role_assignment" "contributor" {
  scope                = "/subscriptions/${var.subscription_id}"
  role_definition_name = "Contributor"
  principal_id         = azuread_service_principal.app.object_id
}

ARO専用のサービスプリンシパルを作る処理です。

ここではサブスクリプションの共同作成者権限を与えていますが、権限に関しては要件に合わせて適宜調整して使ってください。

リソースプロバイダーサービスプリンシパルの権限付与

######################################################
## リソースプロバイダーサービスプリンシパルの権限付与
######################################################
data "azuread_service_principal" "resource_provider" {
  display_name = local.aro_resource_provider_name
}

resource "azurerm_role_assignment" "network_contributor" {
  scope                = data.azurerm_virtual_network.example.id
  role_definition_name = "Network Contributor"
  principal_id         = data.azuread_service_principal.resource_provider.object_id
}

ここはTerraform化する際の引っかかりポイントです。

チュートリアルの az aro create のコマンドだと自動でやってくれているので、Terraform化する際に権限が足りないよエラーで沼ることになります。(N敗)

ここの処理を説明するためにこのブログを書いているといっても過言じゃないです。

OpenShift クラスターの作成

######################################################
## OpenShift クラスターの作成
######################################################
resource "azapi_resource" "openshift_cluster" {
  depends_on = [
    azurerm_role_assignment.network_contributor
  ]
  type      = "Microsoft.RedHatOpenShift/openShiftClusters@2022-04-01"
  name      = var.name
  location  = data.azurerm_resource_group.example.location
  parent_id = data.azurerm_resource_group.example.id
  tags      = {}

  body = jsonencode({
    properties = {
      apiserverProfile = {
        visibility = "Public"
      },
      clusterProfile = {
        domain               = "${var.cluster_domain_name}",
        resourceGroupId      = "/subscriptions/${var.subscription_id}/resourcegroups/${local.cluster_resource_group_name}",
        pullSecret           = "${var.pull_secret}",
        fipsValidatedModules = "Disabled"
      },
      ingressProfiles = [
        {
          name       = "default",
          visibility = "Public"
        }
      ],
      networkProfile = {
        podCidr     = "${var.pod_cidr}",
        serviceCidr = "${var.service_cidr}"
      },
      servicePrincipalProfile = {
        clientId     = "${module.mo_service_principal.client_id}"
        clientSecret = "${module.mo_service_principal.client_secret}"
      },
      masterProfile = {
        vmSize           = "${var.masternode_vm_size}",
        subnetId         = "/subscriptions/${var.subscription_id}/resourcegroups/${var.resource_group_name}/providers/Microsoft.Network/virtualNetworks/${var.virtual_network_name}/subnets/${var.master_subnet_name}",
        encryptionAtHost = "Disabled"
      },
      workerProfiles = [
        {
          name             = "worker"
          vmSize           = "${var.workernode_vm_size}",
          diskSizeGB       = "${var.workernode_disk_size_gb}",
          subnetId         = "/subscriptions/${var.subscription_id}/resourcegroups/${var.resource_group_name}/providers/Microsoft.Network/virtualNetworks/${var.virtual_network_name}/subnets/${var.worker_subnet_name}",
          count            = "${var.workernode_node_count}",
          encryptionAtHost = "Disabled"
        }
      ]
    }
  })

  response_export_values = ["*"]

  // AROクラスターの構築処理がタイムアウトするので時間を延ばす
  timeouts {
    create = "60m"
    update = "60m"
    delete = "60m"
  }

  lifecycle {
    ignore_changes = [
      body
    ]
  }
}

基本的にはチュートリアルと同じ構成で作るようにしています。

ポイントとしては、AROの構築は時間がかかるのでタイムアウトまでの時間を60分に伸ばしています。

更新と削除のタイムアウトは不要かもしれないですが、念のためつけています。

おわりに

今回は Terraform で ARO を構築してみました。さらっと Azure に関する Terraform プロバイダーを3つも使っているので、学習難易度はちょっと高いかもしれないです。

この3つのプロバイダーを使えば全ての Azure のリソース構築を自動化できると思うので、もっと使いこなせるようになって、いろんなリソースの自動化を進めていきたいと思います。

おまけ

後日 Microsoft 公式のドキュメントで azapi を使った実装コードを見つけました。今回苦労した部分もばっちりコード化されていて、ちょっぴり落ち込みました。

https://learn.microsoft.com/en-us/samples/azure-samples/aro-azapi-terraform/aro-azapi-terraform/