しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

Terraform と Azure Pipelines を使って App Service / Azure Functions をコード化して管理する

ぼちぼち ARM Template を JSON で書くのが限界と思い始めてきたので、Terraform を使って Azure リソースの管理をやっていこうという気持ちになりました。

Terraform で使われている HCL は JSON で書く ARM Template よりも読み書きがしやすいのと、CI/CD に必要なバリデーションや Dry-run も簡単に行えるので便利に使えるはずです。ちょっと前に Cloud Shell にインストールされていたり、Azure と Terraform の関係も悪くない感じです。

多分、自分は Azure VM を作ることはないので App Service でとりあえず試しました。最近よく使うのは Azure Functions なので一緒に試しておきました。*1

Terraform (HCL) で Azure リソースを定義する

AWS Provider では触ったことがあったので HCL に対する抵抗はほぼなく、書き方を思い出すぐらいでしたが、思ったより AzureRM Provider の情報がなかったので公式のドキュメントを中心に触っていったほうが良さそうです。今回もわからないことがあれば、まず以下のページで調べました。

https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs

全ての Azure サービスに対する Provider が用意されているわけでは無さそうなので、必要なサービスに対応しているか先に調べておいたほうが後で苦労しないです。

Azure 側にも Terraform のドキュメントがありますが、基本は VM / IaaS 寄りという感じです。VM には興味はないので適当に流し読んでおきました。

Terraform の状態を Azure Storage に保存するページは有用そうな雰囲気がありますが、後で紹介する Azure Pipelines の Terraform Tasks を使うとほぼ気にする必要がなくなるので、雰囲気だけ掴んでおけばよいです。

早速、それぞれのリソースを作るために HCL を書いていくことにします。結局はバックエンドは ARM なので ARM Template の知識があれば、HCL を少し覚えるだけで簡単に書けるはずです。

App Service を作る

App Service を作るためには azurerm_app_serviceazurerm_app_service_plan を使います。リファレンスを読めばサンプル定義が載っているので簡単です。

https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/app_service

どうしても分からない場合は terraform import を使ってしまうのも手です。App Service はプロパティがかなり多いので、デフォルト値を上手く使っていかないとメンテナンス性が下がる可能性があります。

Premium V2 な App Service を作る定義は以下のようになります。ぱっと見は ARM Template に近いです。

resource "azurerm_app_service_plan" "main" {
  name                = "${var.prefix}-asp"
  location            = "${azurerm_resource_group.main.location}"
  resource_group_name = "${azurerm_resource_group.main.name}"

  sku {
    tier = "PremiumV2"
    size = "P1v2"
  }
}

resource "azurerm_app_service" "main" {
  name                = "${var.prefix}-appservice"
  location            = "${azurerm_resource_group.main.location}"
  resource_group_name = "${azurerm_resource_group.main.name}"
  app_service_plan_id = "${azurerm_app_service_plan.main.id}"

  site_config {
    dotnet_framework_version = "v4.0"
  }
}

ARM Template と Terraform でプロパティ名が対応していない部分があるので、その辺りだけ注意です。とはいっても、基本的には Terraform の方が書きやすくなっています。

Azure Functions (Consumption) を作る

ARM Template を使っても少し複雑になっていた Azure Functions (Consumption) ですが、Terraform は Azure Functions 向けの Provider が用意されているので、少しだけ楽になっていました。

https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/function_app

Consumption の場合は Azure Storage が必要になるので、その辺りも同時に作っておく必要があります。あとはモニタリング用に Application Insights も必要なので追加します。

リソースに必要な定義は以下のようになります。ARM Template よりは簡略化されています。

resource "azurerm_storage_account" "function" {
  name                     = "${var.prefix}azfunctions"
  resource_group_name      = "${azurerm_resource_group.main.name}"
  location                 = "${azurerm_resource_group.main.location}"
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_app_service_plan" "consumption" {
  name                = "${var.prefix}-consumption-asp"
  location            = "${azurerm_resource_group.main.location}"
  resource_group_name = "${azurerm_resource_group.main.name}"
  kind                = "FunctionApp"

  sku {
    tier = "Dynamic"
    size = "Y1"
  }
}

resource "azurerm_application_insights" "function" {
  name                = "${var.prefix}-appinsights"
  location            = "japaneast"
  resource_group_name = "${azurerm_resource_group.main.name}"
  application_type    = "web"

  tags = {
    "hidden-link:${azurerm_resource_group.main.id}/providers/Microsoft.Web/sites/${var.prefix}-function" = "Resource"
  }
}

resource "azurerm_function_app" "function" {
  name                      = "${var.prefix}-function"
  location                  = "${azurerm_resource_group.main.location}"
  resource_group_name       = "${azurerm_resource_group.main.name}"
  app_service_plan_id       = "${azurerm_app_service_plan.consumption.id}"
  storage_connection_string = "${azurerm_storage_account.function.primary_connection_string}"
  client_affinity_enabled   = false
  version                   = "~2"

  app_settings = {
    "APPINSIGHTS_INSTRUMENTATIONKEY" = "${azurerm_application_insights.function.instrumentation_key}"
    "FUNCTIONS_WORKER_RUNTIME"       = "dotnet"
  }
}

azurerm_function_app のデフォルトだと Runtime v1 が使われるらしいので、明示的に version で Runtime v2 を使うようにしておきます。v1 はメンテナンス状態になっているはずです。

Terraform だと接続文字列を自分で組み立てる必要がないので、見た目かなりすっきりしました。

Azure Pipelines で Terraform を実行する

当然ながら作成した Terraform の定義は Git に入れて管理するわけですが、デプロイまで自動化しないと意味がなくなります。なので Azure Pipelines を使って自動的にデプロイするようにします。

残念ながら標準では Terraform に対応していないですが、Marketplace から Terraform の拡張をインストールできるので、これを使うと簡単に Terraform の各コマンドが利用できるようになります。

インストールすると CLI と Installer の 2 つが追加されます。Hosted Agent には Terraform が入っていますが、予期せぬバージョンアップを避けるためにも Installer で明示的に入れた方が良さそうです。

f:id:shiba-yan:20191009191359p:plain

デフォルトで Terraform は入っていますが、そのままだと Azure Resource Manager の操作に必要な Service Principal を上手く扱えないので、専用の Task を使ったほうが便利です。Azure CLI でログイン済みであっても、Service Principal でログインしている場合は Terraform で直接設定が必要になります。

Terraform CLI を使うと AzureRM の Service Connection の設定が簡単になる以外に、State Backend を簡単に Azure Storage を使うように設定できます。

f:id:shiba-yan:20191009191408p:plain

Storage Account などは Task に自動で作ってもらうことも出来ますが、今回は手動で Storage Account と Blob Container を作成して、その情報を設定するようにしました。

とりあえず最低限必要な処理を用意した YAML は以下のようになります。本来なら apply は Approval を使ったり、 Multi-stage pipelines にした方が良いですが、本質的な部分からずれるので今回は省略します。

trigger:
- master

pool:
  vmImage: 'ubuntu-latest'

variables:
  TerraformVersion: '0.12.10'

steps:
- task: TerraformInstaller@0
  inputs:
    terraformVersion: $(TerraformVersion)
  displayName: 'Install terraform $(TerraformVersion)'

- task: TerraformCLI@0
  inputs:
    command: 'init'
    backendType: 'azurerm'
    backendServiceArm: 'Azure Sponsorship'
    backendAzureRmResourceGroupName: 'tfstate-rg'
    backendAzureRmStorageAccountName: 'shibayantfstate'
    backendAzureRmContainerName: 'tfstate'
    backendAzureRmKey: 'terraform.tfstate'
  displayName: 'terraform init'

- task: TerraformCLI@0
  inputs:
    command: 'plan'
    environmentServiceName: 'Azure Sponsorship'
  displayName: 'terraform plan'

- task: TerraformCLI@0
  inputs:
    command: 'apply'
    environmentServiceName: 'Azure Sponsorship'
  displayName: 'terraform apply'

認証情報は Service Connection が使われるので、YAML には名前だけしか出てこないため安全です。

よくあるドキュメントでは Azure CLI で Service Principal を作成して、環境変数に設定といった手順が書かれていますが、Terraform CLI の Task を使うことで全て省略できます。

作成した Pipeline を動かすと、ちゃんと各処理が実行されて Azure にリソースがデプロイされます。

f:id:shiba-yan:20191009192340p:plain

Azure Portal からリソースグループを開けば、定義したリソースが作成されていることが確認できます。

Terraform はデフォルトで差分を見てアップデートしてくれるので、ARM Template の Incremental Mode を使うより簡単だと思います。

f:id:shiba-yan:20191009192349p:plain

これで Terraform と Azure Pipelines を組み合わせて、リソースのデプロイを行う基盤が完成しました。

あとは HCL を使って必要なリソース定義を書いていくことになりますが、ARM Template より柔軟に書けるので Template Deployment が不要かつ、リソースをちゃんと管理したい規模の場合に使っていこうと思います。

*1:Azure Functions は複数のリソースが必要なのでコード化して管理したい気持ちが強い