🚀

【Azure】App Service にTerraform を使ってコンテナイメージをデプロイする

2022/12/10に公開

はじめに

みなさん、こんにちは。Azure App Service をTerraform で構築 + コンテナイメージをデプロイというのをやってみたので、備忘的に書いていきたいと思います。Azure は調べていても情報が少なかったり古かったりするため、少しでも役に立てたらな、というモチベーションです。

今回は以下の流れで実践してます。

  1. Terraform でApp Service を作成する。
  2. Terrafrom でAzure Container Registry(ACR) を作成する。
  3. GitHub Actions からアプリのコンテナイメージを作成し、ACR にPUSHすると共に、App Service にデプロイする。

Azure でのコンテナサービス

2022/12/10現在、Azure でコンテナイメージを利用してアプリを構築する際は、以下のサービスが選択肢として挙がります。

  • Azure Kubernetes Service (AKS)
  • Azure Red Hat OpenShift
  • Azure Container Apps
  • Asure Functions
  • Web App for Containers (App Service)👈今回はコチラ
  • Azure Container Instance

👇こちらにAzure のコンテナサービスがまとまっています。
https://azure.microsoft.com/ja-jp/products/category/containers/

今回はGraphQL + NestJS のアプリをコンテナイメージとし、単一のBFF として動かすことが目的だったため、シンプルで社内にナレッジも溜まっているであろうWeb App for Containers(App Service) を採用しました。初めて見たときは「こんなサービスがあるんだな」と思ったのですが、どうやらApp Service にコンテナイメージを載せることを指しているだけのようです。分かりづらい…実際、ページに行くと「App Service -> Web App for Containers」とあります。
https://azure.microsoft.com/ja-jp/products/app-service/containers/#overview

混乱するだけなので、サービスの多さで競おうとせずに、名前変えて欲しいところですが…ということで、Terraform で構築するのもApp Service になるわけです。

App Service の作成

まずはApp Service を作っていきます。App Serivice 構築にあたって、最初はAzure のページを見ていたのですが、Terraform サンプルはだいたい古くなっていました。そのため、HashiCorp のドキュメントを頼るのですが、これまたHashiCorp のサンプルも古いことが多かったので、Arguments を参考にしつつ、GitHub を見に行く、という動き方が最善だと思います。

なお、App Serivice の今回の要件としては以下のとおりです。

  • App Service のOS タイプはLinux を選択
  • App Service をVNet 統合する
  • コンテナイメージの指定はマイクロソフトのサンプルを利用し、2回目以降は差分を無視する

最後の要件は、GitHub Actions からアプリのコンテナイメージをデプロイしたあとに、Terraform の再実行でイメージを書き換えないようにするためです。今回はlifecycle内にてignore_changesを利用していますが、もっと良い方法がある気がしています…詳しい方がいたらコメントくださると嬉しいです。

実際にはモジュール化をしていたり、変数用のvariabels.tfterraform.tfvarsを利用していますが、ここでは記事用に1ファイルにまとめてみました。(そのため、このまま利用しても動かないかもです…
また、サンプルで見かけるazurerm_app_service_planazurerm_app_serviceは廃止が予定されているため注意が必要です。

appservice.tf
# Resource Group
resource "azurerm_resource_group" "rg" {
  name     = "appservice-rg"
  location = "japaneast"
}

# VNet
resource "azurerm_virtual_network" "vnet" {
  name                = "vnet"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  address_space       = ["10.0.0.0/16"]
}

# VNet統合用サブネット(integration_subnet)
resource "azurerm_subnet" "integrationsubnet" {
  name                 = "integrationsubnet"
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = ["10.0.1.0/24"]
  delegation {
    name = "delegation"
    service_delegation {
      name = "Microsoft.Web/serverFarms"
    }
  }
}

# App Service Plan
resource "azurerm_service_plan" "app_service_plan" {
  name                = "appserviceplan"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  os_type             = "Linux"
  sku_name            = "B1"
}

# App Serivice
resource "azurerm_linux_web_app" "example" {
  name                = "web-app-for-containers-example"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  service_plan_id     = azurerm_service_plan.app_service_plan.id
  site_config {
    application_stack {
      # Docker Image は初期値のみ指定
      docker_image     = "jackofallops/azure-containerapps-python-acctest"
      docker_image_tag = "v0.0.1"
    }
  }
  # Docer Image 初期登録後は変更されていても無視する
  lifecycle {
    ignore_changes = [
      site_config[0].application_stack[0].docker_image,
      site_config[0].application_stack[0].docker_image_tag
    ]
  }
  # VNet統合のサブネットを指定
  virtual_network_subnet_id = azurerm_subnet.integrationsubnet.id
  # その他省略...
}

Azure Container Registry(ACR) 作成

続いて、GitHub Actions からコンテナイメージをPUSHするためのAzure Container Registry を作成します。ここはHashiCorp のドキュメント通りで問題ないですが、GitHub Actions から管理者ユーザーでのアクセスを行うため、admin_enabledtrueを設定しています。anonymous_pull_enabledは指定しなければfalseになりますが、明示的に指定しています。

acr.tf
# Azure Container Registy
resource "azurerm_container_registry" "example" {
  name                = "acr-example"
  resource_group_name = "rg-example"
  location            = "japaneast"
  sku                 = "Standard"
  # 管理者ユーザーからのアクセス可否
  admin_enabled = true
  # パブリックアクセスはtrue
  public_network_access_enabled = true
  # 匿名(未認証)プルアクセス許可設定:デフォルトfalse(trueでアクセス許可されるため注意)
  anonymous_pull_enabled = false
}

GitHub Actions 作成

作成前の下準備

GitHub Actions を利用したAzure へのアクセスには、サービスプリンシパルとOIDCを利用しています。サービスプリンシパルというのがAzure 初心者の私には中々理解できなかったのですが、AWS でいうIAM ロール的なものだろうと一旦判断しました(タブン違う…ので、ADをしっかり理解しないといけないな、と思いながら年を越してしまいそうです…

以下のページにGitHub Actions からAzure への接続方法についてまとまっていますので、こちらを確認するのが良いと思います。これはドキュメント通りで問題なかったです。
https://learn.microsoft.com/ja-jp/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux

また、GitHub Actions からAzure Container Registy(ACR) へのアクセスは管理者ユーザーを使っているため、Azure ポータルからユーザー名とパスワードを取得し、GitHub のシークレットに登録します。より安全にACR を利用する場合は、こちらもサービスプリンシパルを利用するのが良いのだと思うのですが、サポートの方とやり取りしても中々上手くいかなかったため、暫定でこのようにしています。

GitHub Actions Workflow 作成

以下のように作成しました。デプロイは手動で行うためworkflow_dispatchが入っていることと、入力値によって環境シークレットを使い分けるようにしています。こうすることで、GitHub Actions 実行時に選択した環境ごとに異なる値が設定されるため、実行ファイルを一つにできます。

name: Build and deploy docker image to Azure App Service

on:
  # Allow manually trigger
  workflow_dispatch:
    inputs:
      environment:
        type: environment
        description: deploy environment

# OIDC
permissions:
  id-token: write
  contents: read

# Login Azure container registry and build and deploy docker image.
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    environment: ${{ github.event.inputs.environment }}

    steps:
      - name: Checkout to the branch
        uses: actions/checkout@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Azure Login
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Log in to container registry
        uses: azure/docker-login@v1
        with:
          login-server: ${{ secrets.REGISTRY_URL }}
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}

      - name: Build and push container image to registry
        uses: docker/build-push-action@v3
        with:
          push: true
          tags: ${{ secrets.REGISTRY_URL }}/${{ secrets.CONTAINER_REPOSITORY_NAME }}:${{ github.sha }}
          file: ./hoge/Dockerfile
          context: ./hoge

      - name: Deploy to Azure Web App
        id: deploy-to-webapp
        uses: azure/webapps-deploy@v2
        with:
          app-name: ${{ secrets.AGGREGATE_WEB_APP_FOR_CONTAINER }}
          images: "${{ secrets.REGISTRY_URL }}/${{ secrets.CONTAINER_REPOSITORY_NAME }}:${{ github.sha }}"

まとめ

今回はAzure App Service へGitHub Actions からコンテナイメージをデプロイしてみました。Azure の知識が全然無い中、必死に格闘していましたが、Terraform を介して対話することで知識がついたような気がします(タブン...
その他にも、プライベートエンドポイントというものを作成したりもしていますが、GUIで作成するとサービス側が自動で作成して意識する必要がないものも、Terraform だと自分で定義しなくてはならなかったりするので、IaC での構築はサービスに対しての理解を深めるためにも非常に重要だと再認識できました。
これからも自分が詳しいサービスばかり携われるわけではないですし、知見のないサービスを素早くキャッチアップする力を養うためにも、頑張っていきたいと思います。

それでは、この記事がどなたかのお役に立てば幸いです。
最後までお読みいただき、ありがとうございました!

Discussion