Github Action + TerraformでAzure リソースをプロビジョニングする

この記事は1年以上前に投稿されました。情報が古い可能性がありますので、ご注意ください。
とある業務にてAzure リソースのプロビジョニングを頻繁に行う必要があり、Terraformを利用してコードを作成しデプロイしていました。しかしながら、Terraformを理解している人が少なく個人に依存し過ぎている傾向がありました。またターミナルからコマンド実行も手間がかかり面倒と感じることがありました。 そこでプロビジョニング作業簡略化を目指しGitHub ActionでTerraformを実行するツールを作成してみましたので今回はその一例を紹介したいと思います。ツールを利用することで誰でも簡単にAzure リソースのプロビジョニングを行うことができます。
ツール要件
- (今回の例として)Azure Kubernates Service(以下AKS)をデプロイしたい
 - コマンド操作やTerraform,AKS構築に不慣れな人のためにGUIから簡単に操作できるようにしたい
 - ユーザーは必要項目を入力するだけでAKSをプロビジョニングしたい
 - 使い終わったら手軽に削除したい
 
構成図
- Github ActionからTerraformを実行しAzureにAKSを作成する(リソースグループも同時に作成)
 - Terraform実行アカウントはAzure サービスプリンシパルを利用する
 - Terraform実行時に生成されるtfstateファイルはBlob Strageに保管する
 

手順
サービスプリンシパルの作成
サブスクリプションへのContributor権限を持つAzure サービスプリンシパルを作成します。出力結果は後の手順にて利用しますので控えときます。
$ az ad sp create-for-rbac --name "gha-terraform-demo-sp" --role contributor --scopes /subscriptions/{SubID}
{
  "appId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "displayName": "gha-terraform-demo-sp",
  "password": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "tenant": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx"
}
Azure Blob Storageの作成
Azure Blob Storageとtfstateファイル保管先となるコンテナーも作成します。
// リソースグループ・Blob Storage作成
$ BLOB_STORAGE_NAME=stghaterraformdemo
$ RG_NAME=rg-gha-terraform-demo
$ LOCATION=japaneast
$ az group create --location ${LOCATION} --resource-group ${RG_NAME}
$ az storage account create -n ${BLOB_STORAGE_NAME} -g ${RG_NAME} -l ${LOCATION}
// Blob Storage内にコンテナーを作成する
$ CONNECTION_STRING=$(az storage account show-connection-string -n ${BLOB_STORAGE_NAME} -g ${RG_NAME} --output tsv)
$ CONTAINER_NAME=ghaterraformtfstate
$ az storage container create -n ${CONTAINER_NAME} --connection-string ${CONNECTION_STRING}
Github Secretの登録
GithubにSecretを登録します。サービスプリンシパル作成時に控えた出力結果を使用します。
ARM_CLIENT_ID: <appId>
ARM_CLIENT_SECRET: <password>
ARM_SUBSCRIPTION_ID: <利用するAzure サブスクリプションID>
ARM_TENANT_ID: <tenant>

コードをgithubリポジトリにpushする
以下のディレクトリを作成します。
- .github/workflows
 - terraform
 
terraform
ファイル、ディレクトリは以下のように構成します。
- mainディレクトリ: コードのメイン処理、tfstateファイルの管理設定、プロバイダ、バージョンを定義するファイルを配置する
 - modulesディレクトリ: 作成するリソースをモジュール別に管理し呼び出すモジュールはmain/main.tfで定義する
 
terraform
├── main
│   ├── backend.tf
│   ├── main.tf
│   ├── provider.tf
│   └── variable.tf
└── modules
    ├── kubernetes
    │   ├── main.tf
    │   └── variable.tf
    └── resource_group
        ├── main.tf
        ├── output.tf
        └── variable.tf
以下のファイルを各ディレクトリに配置します
mainディレクトリ
// backend.tf 
terraform {
   backend "azurerm" {
       resource_group_name  = "rg-gha-terraform-demo" ##Blob Storageが存在するリソースグループ
       storage_account_name = "stghaterraformdemo" ##Blob Storage
       container_name       = "ghaterraformtfstate" ##コンテナ名
   }
}
// provider.tf
terraform {
  required_version = ">= 0.13"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "3.18.0"
    }
  }
}
provider "azurerm" {
  features {}
}
// main.tf
module "rg" {
source = "../modules/resource_group"
name = "rg-${var.id}"
location = var.location
}
module "aks" {
source = "../modules/kubernetes"
# common kubernetes config
location = var.location
resource_group_name = module.rg.name
name = "aks-${var.id}"
kubernetes_version = var.kubernetes_version
network_plugin = "azure"
ip_versions = ["IPv4"]
# default node_pool config
default_node_pool_auto_scaling = true
default_node_pool_node_count = 3
default_node_pool_vm_size ="Standard_E4a_v4"
default_node_pool_max_count = 5
default_node_pool_min_count = 1
# second node_pool config
second_node_pool_enabled = var.second_node_pool_enabled ? 1 : 0
second_node_pool_auto_scaling = true
second_node_pool_node_count = 3
second_node_pool_vm_size = "Standard_DS2_v2"
second_node_pool_max_count = 5
second_node_pool_min_count = 1
depends_on = [module.rg.rg]
}
// variable.tf
variable "id" {}
variable "location" {}
variable "kubernetes_version" {
    default = null
}
variable "second_node_pool_enabled" {
    type = bool
    default = false
}
modules/resource_group
// main.tf
resource "azurerm_resource_group" "rg" {
  name     = var.name
  location = var.location
}
// output.tf
output "name" {
  value     = azurerm_resource_group.rg.name
}
// variable.tf
variable "name" {}
variable "location" {}
modules/kubernates
// main.tf
resource "azurerm_kubernetes_cluster" "aks" {
    name                = var.name
    location            = var.location
    resource_group_name = var.resource_group_name
    dns_prefix          = "aks-${var.name}-dns"
    kubernetes_version  = "${var.kubernetes_version}"
    default_node_pool {
        name                   = "nodepool"
        zones                  = ["1","2","3"]
        node_count             = var.default_node_pool_node_count
        vm_size                = var.default_node_pool_vm_size
        max_pods               = "200"
        enable_auto_scaling    = var.default_node_pool_auto_scaling
        # If enable_auto_scaling is set to true, max_count and min_count is required.
        max_count              = var.default_node_pool_max_count
        min_count              = var.default_node_pool_min_count
    }
    network_profile {
        network_plugin     = var.network_plugin
        ip_versions        = var.ip_versions
    }
    identity {
        type = "SystemAssigned"
    }
}
resource "azurerm_kubernetes_cluster_node_pool" "secondnodepool" {
    count = var.second_node_pool_enabled
    name                   = "nodepool2"
    kubernetes_cluster_id  = azurerm_kubernetes_cluster.aks.id
    node_count             = var.second_node_pool_node_count
    vm_size                = var.second_node_pool_vm_size
    max_pods               = "200"
    enable_auto_scaling    = var.second_node_pool_auto_scaling
    # If enable_auto_scaling is set to true, max_count and min_count is required.
    max_count              = var.second_node_pool_max_count
    min_count              = var.second_node_pool_min_count
}
// variable.tf
variable "location" {}
variable "resource_group_name" {}
variable "kubernetes_version" {
    default = "1.26.3"
}
variable "name" {}
variable "default_node_pool_node_count" {
  default = 3
}
variable "default_node_pool_vm_size" {
  default = "Standard_DS2_v2"
}
variable "default_node_pool_max_count" {
  default = 3
}
variable "default_node_pool_min_count" {
  default = 3
}
variable "default_node_pool_auto_scaling" {
    default = false
}
variable "second_node_pool_enabled" {
  default = 0
}
variable "second_node_pool_node_count" {
  default = 3
}
variable "second_node_pool_vm_size" {
  default = "Standard_DS2_v2"
}
variable "second_node_pool_max_count" {
  default = 3
}
variable "second_node_pool_min_count" {
  default = 3
}
variable "second_node_pool_auto_scaling" {
    default = false
}
variable "network_plugin" {
    default = "azure"
}
variable "ip_versions" {
    default = ["IPv4"]
}
variable "network_policy" {
    default = null
}
.github/workflows
実行するworkflowファイルを配置します。今回はAKSのデプロイ/削除をしたいので2つのworkflowファイルを作成します。
deploy.yaml
name: Deploy gha-terraform-demo
on:
  workflow_dispatch:
    inputs:
      common_name:
        description: リソースid
        required: true
        type: string 
        default: gha-terraform-demo
      aks_version:
        description: AKSのバージョン
        required: true
        type: string   
        default: 1.26.3
      add_node_pool:
        description: ノードプールを追加する
        required: false
        type: boolean
jobs:
  terraform:
    name: 'Terraform'
    env:
      ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
      ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
      ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
      ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
      TF_VAR_location: Japan East
      TF_VAR_id: ${{ inputs.common_name }}
      TF_VAR_kubernetes_version: ${{ inputs.aks_version }}
      TF_VAR_second_node_pool_enabled: ${{ inputs.add_node_pool }}
    runs-on: ubuntu-latest
 
    defaults:
      run:
        shell: bash
    steps:
    - name: Checkout
      uses: actions/checkout@v2
    - name: 'Setup Terraform CLI'
      uses: hashicorp/setup-terraform@v1
      with:
        terraform_version: 1.4.6
    - name: 'Terraform init'
      run: terraform init  -backend-config=key=${TF_VAR_id}.tfstate
      working-directory: terraform/main
    - name: 'Terraform plan'
      run: terraform plan -lock=false
      working-directory: terraform/main
    - name: 'Terraform apply'
      run: terraform apply -auto-approve -lock=false
      working-directory: terraform/main
destroy.yaml
name: Destroy gha-terraform-demo
on:
  workflow_dispatch:
    inputs:
      common_name:
        description: リソースid
        required: true
        type: string 
        default: gha-terraform-demo
jobs:
  terraform:
    name: 'Terraform'
    env:
      ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
      ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
      ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
      ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
      TF_VAR_location: Japan East
      TF_VAR_id: ${{ inputs.common_name }}
    runs-on: ubuntu-latest
 
    defaults:
      run:
        shell: bash
    steps:
    - name: Checkout
      uses: actions/checkout@v2
    - name: 'Setup Terraform CLI'
      uses: hashicorp/setup-terraform@v1
      with:
        terraform_version: 1.4.6
    - name: 'Terraform init'
      run: terraform init  -backend-config=key=${TF_VAR_id}.tfstate
      working-directory: terraform/main
    - name: 'Terraform destroy'
      run: terraform destroy -auto-approve
      working-directory: terraform/main
Github ActionでWorkflowを実行する
作成したworkflowを実行します。[Run workflow]を押下し必要項目を入力し実行します。しばらくしたらJobが起動し成功(緑)/失敗(赤)の結果が確認できます。
- リソースid: 作成するAzure リソースにユニークな名前をつけます
 - AKSのバージョン:使いたいAKS バージョンを指定します
 - ノードプールを追加する:オプション機能としてノードプールを追加するかチェックします
 


Azure上にリソースが作成されていることが確認できます。

使い終わったら削除用workflowを実行してリソースを削除します。

ひとこと
ツールの導入によりプロビジョニングの手順が劇的に簡略化され、その過程で生じる手間やエラーが大幅に削減されました。また作業の効率化に寄与するだけでなく正確性も向上させる効果もあります。
この取り組みを通じて今後もこのような自動化ツールを導入することで作業の効率化や正確性向上を実現したいと思います。
