1. 記事一覧 >
  2. ブログ記事
Bicep
category logo

Azure Container Appsへbicep,Azure Container Registry,GitHub Actionsを使ってデプロイ

(更新) (公開)

はじめに

Azure Container Apps へ bicep、Azure Container Registry、GitHub Actions を使って React Web アプリをデプロイしてみました。一連の手順を紹介していきたいと思います。


先日アップした記事「Azure Container Appsへbicep,GitHub Container Registry,GitHub Actionsを使ってデプロイ」の Azure Container Registry 版です。その他は、まったく同一で、コンテナレジストリを GitHub Container Registry(GitHub Packages)から Azure Container Registry に変更したのみです。


Azure Container Apps:フル マネージド サーバーレス コンテナー サービスです。Azure Container Apps を使うと、Kubernetes のオーケストレーションやインフラストラクチャを気にすることなく、コンテナー化されたアプリケーションを実行できます。
bicep:Bicep は、宣言型の構文を使用して Azure リソースをデプロイするドメイン固有言語 (DSL) です。無人でAzure をいじるときに使うプログラミング言語のようなものです。
GitHub Actions:GitHub Actions は、GitHub が提供する CI/CD サービスです。 GitHub と高度に統合されており、GitHub に公開されたコードを自動でビルド・テスト・デプロイを行うのが主目的です。
Azure Container Registry:Azureのコンテナレジストリです。Docker HubのAzure版のようなものです。有料です。
Web アプリ:create-react-app で作成したものそのままです。


Azure Container Appsデプロイ全体像の図


パイプラインや CI/CD については、別記事「Kotlin + Spring Boot のアプリを GitLab CI/CD による Docker デプロイまで全手順」に詳しく書きましたので、こちらを参照してください。

【 コンテナレジストリ 】

Azure Container Registry、GitHub Container Registry(ghcr.io)、Docker Registry、etc...の内、今回は、Azure Container Registry を使います。Azure Container Registry、GitHub Container Registry(ghcr.io)以外の場合、どうなるかは検証していません。


基本的には、learn.microsoft.com の「チュートリアル: Azure Container Apps に GitHub Actions を使用して Dapr アプリケーションをデプロイする」を見ながら進めましたが、参考にしただけで、同一ではありません。Dapr 未使用で、Azure Cosmos DB は作成せず、コンテナも一つで、かなり単純化しています。
learn.microsoft.com は、GitHub Container Registry(ghcr.io)を使っていますが、今回は、Azure Container Registry に差し替えています。


本記事情報により何らかの問題が生じても、一切責任を負いません。

コンソールは、Ubuntu 20.04 LTS の bash で作業しています。

git コマンドや az コマンド、npx などはインストール済みで使えるものとします。


ソースコード準備

作成済みの全体ソースコードは、 https://github.com/itc-lab/azure-container-apps-bicep-acr-example にアップしました。

create-react-app で作成

コンテナビルドのための Dockerfile

GitHub Actions ワークフローファイル .github/workflows/build-and-deploy.yaml

Azure Container Registry デプロイ用に ./deploy/create-acr.bicep

Azure Container Apps 他、Azure へのリソースデプロイ用に ./deploy/main.bicep./deploy/environment.bicep./deploy/container-http.bicep
と準備していきます。


create-react-app

$ npx create-react-app aca-app --template typescript
$ cd aca-app
$ npm start

自動的に最低限の Web アプリが作成されて、http://localhost:3000 で以下の画面が表示されます。

Webアプリ画面


Dockerfile

yarn build した静的ファイルを nginx で参照する形のコンテナを作成するための、Dockerfile を準備します。

./Dockerfile
# Dockerマルチステージビルド
FROM node:16 as build

WORKDIR /app

COPY package.json /app/

RUN yarn

COPY . /app/

RUN yarn build


FROM nginx:latest

COPY --from=build /app/build /usr/share/nginx/html

COPY --from=build /app/nginx.conf /etc/nginx/conf.d/default.conf

nginx.conf を準備します。
3000 番ポートを開いています。

./nginx.conf
server {
  listen 3000;

  gzip on;
  gzip_vary on;
  gzip_min_length 10240;
  gzip_proxied expired no-cache no-store private auth;
  gzip_http_version 1.1;
  gzip_types text/plain text/css text/xml text/javascript application/javascript application/x-javascript application/xml image/png;
  gzip_disable "MSIE [1-6]\.";

  location / {
    root /usr/share/nginx/html;
    index index.html index.htm;
    try_files $uri $uri/ /index.html =404;
  }
  include /etc/nginx/extra-conf.d/*.conf;
}

build-and-deploy.yaml

GitHub Actions パイプラインのワークフローファイル .github/workflows/build-and-deploy.yaml を作成します。

意味は、中のコメントを見てください。

./.github/workflows/build-and-deploy.yaml
name: Build and Deploy
on:
  push:
    # mainブランチにpushまたは、v*.*.* タグがreleaseされたときに発動
    branches: [main]
    tags: ["v*.*.*"]
    # このファイルの変更だけの場合、パイプラインを実行しない。
    # パイプラインとは、一連の自動化処理のこと。GitHubでは、ワークフローと言っている。
    # ここに書いてある実行するかどうかの条件は、ワークフロー トリガーと言う。
    paths-ignore:
      - "README.md"
      - ".vscode/**"
      - "assets/**"
      - "build-and-run.md"
  # 手動実行(GUIからRun workflowボタン)有効
  workflow_dispatch:

jobs:
  set-env:
    name: Set Environment Variables
    runs-on: ubuntu-latest
    outputs:
      # mainステップの実行結果を保存→他のjobで変数として使える。
      # needs.set-env.outputs.version のように参照
      version: ${{ steps.main.outputs.version }}
      created: ${{ steps.main.outputs.created }}
      repository: ${{ steps.main.outputs.repository }}
      appname: ${{ steps.main.outputs.appname }}
    steps:
      # $GITHUB_SHA:ワークフローをトリガーしたコミット SHA。
      # $GITHUB_REPOSITORY:リポジトリオーナー/リポジトリ名。例:octocat/Hello-World
      - id: main
        # set-outputコマンドを利用することで出力パラメータに値を代入することができたが、
        # set-outputの使用は非推奨になった。
        # echo "::set-output name={name}::{value}" で{name}に{value}を代入。
        # ↓
        # echo "{name}={value}" >> $GITHUB_OUTPUT で{name}に{value}を代入。
        # cut -c1-7 = 先頭7文字
        run: |
          echo version=$(echo ${GITHUB_SHA} | cut -c1-7) >> $GITHUB_OUTPUT
          echo created=$(date -u +'%Y-%m-%dT%H:%M:%SZ') >> $GITHUB_OUTPUT
          echo repository=$GITHUB_REPOSITORY >> $GITHUB_OUTPUT
          echo appname=react-frontend >> $GITHUB_OUTPUT

  # コンテナ build & コンテナレジストリへ push のジョブ
  package-services:
    # ランナー イメージ(ジョブを実行する仮想マシン)を指定。windows-latest、windows-2022、macos-12とかいろいろある。
    # ランナーは自分で作ることもできて、自分で作ったランナーをセルフホストランナー(self-hosted runners)という。
    runs-on: ubuntu-latest
    # needs=先行のジョブを指定(指定しないと、並列実行される。)
    # この場合、needs: set-env が無いと、set-envジョブと同時にpackage-servicesジョブが動く。
    needs: set-env
    # secrets.GITHUB_TOKENの権限を設定。このワークフローで必要な権限を設定。
    permissions:
      # ソースコードリポジトリは、読み取りのみ
      contents: read
      # コンテナレジストリは書き込み可
      packages: write
    outputs:
      # image-tagステップにて、echo image-${{ needs.set-env.outputs.appname }}=*・・・ が実行されていて、
      # その値をcontainerImage-reactという名称で、他ジョブでも使えるように出力している。(最後に実行するコマンドのパラメータに使っている)
      containerImage-react: ${{ steps.image-tag.outputs.image-react-frontend }}
    steps:
      - name: Checkout repository
        # actions/checkout@v2アクション
        # pushされた時にはそのpushされたコードをチェックアウト
        # プルリク時には、そのプルリクされたコードをチェックアウト
        uses: actions/checkout@v2
      - name: Log into registry ${{ secrets.REGISTRY_LOGIN_SERVER }}
        # プルリク以外だったら実行
        if: github.event_name != 'pull_request'
        # Azure Container Registryにdocker imageをpushするためにdocker login。
        # azure/docker-login@v1アクションを利用。
        # withでアクションに必要なパラメータを定義。
        uses: azure/docker-login@v1
        with:
          # ログインするAzureコンテナレジストリ。
          login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
          # ログインするユーザー=事前にpush許可が与えられているサービスプリンシパルのclientId
          username: ${{ secrets.REGISTRY_USERNAME }}
          # ログインするユーザーのパスワード=事前にpush許可が与えられているサービスプリンシパルのclientSecret
          password: ${{ secrets.REGISTRY_PASSWORD }}
      - name: Extract Docker metadata
        id: meta
        # docker/metadata-action@v3アクション
        # 動的なタグ生成を実施→docker build時に指示できる
        # ビルドされたimageが
        # acrtestyou.azurecr.io/xxx/aaa:latest、acrtestyou.azurecr.io/xxx/aaa:1.2.3、acrtestyou.azurecr.io/xxx/aaa:1.2、acrtestyou.azurecr.io/xxx/aaa:1、
        # acrtestyou.azurecr.io/xxx/aaa:sha-ad132f5、acrtestyou.azurecr.io/xxx/aaa:main
        # といったタグでpushされる。
        uses: docker/metadata-action@v3
        with:
          images: ${{ secrets.REGISTRY_LOGIN_SERVER }}/${{ needs.set-env.outputs.repository }}/${{ needs.set-env.outputs.appname }}
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=ref,event=branch
            type=sha
      - name: Build and push Docker image
        # docker/build-push-action@v2アクション
        # コンテナ build & コンテナレジストリへ push
        uses: docker/build-push-action@v2
        with:
          # context: Dockerfileが置いてある場所
          # 今回は、リポジトリ直下に置いているが、ディレクトリの中なら、'./frontend'、'./backend' とか。
          context: "."
          # コンテナレジストリにpushするかどうか。プルリク以外は、push。
          push: ${{ github.event_name != 'pull_request' }}
          # 上で設定したタグを適用
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
      - name: Output image tag
        id: image-tag
        # image-react-frontend = acrtestyou.azurecr.io/<github-username>/aca-app2-repo/react-frontend:sha-<コミットSHA(先頭7文字)>
        # tr '[:upper:]' '[:lower:]' は、小文字化するLinuxコマンド
        # 以下のようにsecretsを含んでechoはダメ!
        # run: |
        #   echo image-${{ needs.set-env.outputs.appname }}=${{ secrets.REGISTRY_LOGIN_SERVER }}/$GITHUB_REPOSITORY/${{ needs.set-env.outputs.appname }}:sha-${{ needs.set-env.outputs.version }} | tr '[:upper:]' '[:lower:]' >> $GITHUB_OUTPUT
        # image-react-frontend = <github-username>/aca-app2-repo/react-frontend:sha-<コミットSHA(先頭7文字)>
        run: |
          echo image-${{ needs.set-env.outputs.appname }}=$GITHUB_REPOSITORY/${{ needs.set-env.outputs.appname }}:sha-${{ needs.set-env.outputs.version }} | tr '[:upper:]' '[:lower:]' >> $GITHUB_OUTPUT

  deploy:
    runs-on: ubuntu-latest
    # package-servicesジョブ終了後に実行
    # 複数終了待ちの場合:[job1, job2] で job1とjob2 両方が終わっている必要があるという意味になる。
    needs: package-services
    steps:
      - name: Checkout repository
        # actions/checkout@v2アクション
        # リポジトリをチェックアウト
        uses: actions/checkout@v2

      - name: Azure Login
        # azure/login@v1アクション
        # Azureにログイン
        uses: azure/login@v1
        with:
          # サービス プリンシパル シークレット(事前にAzureに作成して、シークレットをGitHubのGUIで登録)
          # Azure CLI で bicep を実行、Azure Container Appsにデプロイする権限を得るために必要。
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Deploy bicep
        # azure/CLI@v1アクション
        # Azure CLI (Azure内のターミナル)で実行
        uses: azure/CLI@v1
        with:
          # Azureリソースグループを東日本リージョン(japaneast)に作成
          # az deployment group create -g <リソース グループ> -f <bicepファイル> でAzureへのデプロイを開始。
          # -p は引数指定(main.bicep内で使うパラメータ)
          # minReplicas:最小レプリカ(Pod)数
          # reactImage:React App の containerImage
          # reactPort:React App の containerPort
          # containerRegistry:コンテナレジストリ。ここでは、Azure Container Registry (今回の場合は、acrtestyou.azurecr.io)
          # containerRegistryUsername:コンテナレジストリのログインユーザー名(事前にpull許可が与えられているサービスプリンシパルのclientId)
          # containerRegistryPassword:コンテナレジストリのパスワード(事前にpull許可が与えられているサービスプリンシパルのclientSecret)
          # リソース作成済み前提で動かすため、以下は除外(作成済みでもinlineScript:の最初に実行して構わない。)
          # az group create -g ${{ secrets.RESOURCE_GROUP }} -l japaneast
          inlineScript: |
            az deployment group create -g ${{ secrets.RESOURCE_GROUP }} -f ./deploy/main.bicep \
              -p \
                minReplicas=0 \
                reactImage='${{ secrets.REGISTRY_LOGIN_SERVER }}/${{ needs.package-services.outputs.containerImage-react }}' \
                reactPort=3000 \
                containerRegistry=${{ secrets.REGISTRY_LOGIN_SERVER }} \
                containerRegistryUsername=${{ secrets.REGISTRY_USERNAME }} \
                containerRegistryPassword='${{ secrets.REGISTRY_PASSWORD }}'

Bicep

Azure Container Registry デプロイ用に ./deploy/create-acr.bicep
Azure Container Apps 他、Azure へのリソースデプロイ用に ./deploy/main.bicep./deploy/environment.bicep./deploy/container-http.bicep を準備します。
これは、GitHub Actions の 最後に az deployment group create -g ${{ secrets.RESOURCE_GROUP }} -f ./deploy/main.bicep・・・ で実行されています。

./deploy/create-acr.bicep
@minLength(5)
@maxLength(50)
@description('Provide a globally unique name of your Azure Container Registry')
param acrName string = 'acr${uniqueString(resourceGroup().id)}'

@description('Provide a location for the registry.')
param location string = resourceGroup().location

@description('Provide a tier of your Azure Container Registry.')
param acrSku string = 'Basic'

resource acrResource 'Microsoft.ContainerRegistry/registries@2021-06-01-preview' = {
  name: acrName
  location: location
  sku: {
    name: acrSku
  }
  properties: {
    adminUserEnabled: false
  }
}

@description('Output the login server property for later use')
output loginServer string = acrResource.properties.loginServer

./deploy/main.bicep
// param <parameter-name> <parameter-data-type> = <default-value>
// 注意:=の右側 <default-value> は、-p で与えられなかった場合、適用される。
// 今回、build-and-deploy.yaml にて以下のように実行されている。
// -p \
// minReplicas=0 \
// reactImage='${{ needs.package-services.outputs.containerImage-react }}' \
// reactPort=3000 \
// containerRegistry=${{ secrets.REGISTRY_LOGIN_SERVER }} \
// containerRegistryUsername=${{ secrets.REGISTRY_USERNAME }} \
// containerRegistryPassword=${{ secrets.REGISTRY_PASSWORD }}

// function resourceGroup(): resourceGroup
// 現在のリソース グループのスコープを返す
param location string = resourceGroup().location
// function uniqueString(... : string): string
// パラメータとして提供された値に基づいてハッシュ文字列を作成。戻り値は 13 文字。
param environmentName string = 'env-${uniqueString(resourceGroup().id)}'

param minReplicas int = 0

param reactImage string
param reactPort int = 3000
// var で始まる部分はコマンドライン等からは変更できないファイル内で利用する内部変数
var reactFrontendAppName = 'react-app'

param isPrivateRegistry bool = true

param containerRegistry string
// ログインするユーザー=事前にpush許可が与えられているサービスプリンシパルのclientId
// function secure(): any
// @secure() 修飾子
// パラメーターの値はデプロイ履歴に保存されず、ログにも記録されない。
@secure()
param containerRegistryUsername string = ''
// ログインするユーザーのパスワード=事前にpush許可が与えられているサービスプリンシパルのclientSecret
@secure()
param containerRegistryPassword string = ''
// コンテナレジストリシークレットのキー名
// ただのキー名だけど、"Password"という言葉に反応して、secure-secrets-in-params警告が出るため、
// #disable-next-line で沈黙させる。
#disable-next-line secure-secrets-in-params
param registryPassword string = 'registry-password'

// Container Apps 環境モジュール
// module <symbolic-name> '<path-to-file>' = {
//   name: '<linked-deployment-name>'
//   params: {
//     <parameter-names-and-values>
//   }
// }
// <path-to-file>は、テンプレートのようなもので、値を渡して何回も使用可能。
// 注意:今回は、1回しか使っていない。
// 各パラーメータの説明は、environment.bicep、ontainer-http.bicep 内に記載。
module environment 'environment.bicep' = {
  // function deployment(): deployment
  // 現在のデプロイメント操作に関する情報を返す。
  // ここでは、name: "main--environment" になる。
  name: '${deployment().name}--environment'
  params: {
    environmentName: environmentName
    location: location
    appInsightsName: '${environmentName}-ai'
    logAnalyticsWorkspaceName: '${environmentName}-la'
  }
}

// React App
module reactFrontend 'container-http.bicep' = {
  name: '${deployment().name}--${reactFrontendAppName}'
  dependsOn: [
    environment
  ]
  params: {
    enableIngress: true
    isExternalIngress: true
    location: location
    environmentName: environmentName
    containerAppName: reactFrontendAppName
    containerImage: reactImage
    containerPort: reactPort
    minReplicas: minReplicas
    isPrivateRegistry: isPrivateRegistry
    containerRegistry: containerRegistry
    registryPassword: registryPassword
    containerRegistryUsername: containerRegistryUsername
    revisionMode: 'Multiple'
    secrets: [
      {
        name: registryPassword
        value: containerRegistryPassword
      }
    ]
  }
}

output reactFqdn string = reactFrontend.outputs.fqdn

./deploy/environment.bicep
param environmentName string
param logAnalyticsWorkspaceName string
param appInsightsName string
param location string

// ログ分析ワークスペース 作成
// resource キーワードを使用してリソースを追加。
// リソースのシンボリック名を設定。シンボリック名はリソース名と同じものではない。
// シンボリック名は、Bicep ファイルの他の部分にあるリソースを参照するために使用。
// resource <symbolic-name> '<full-type-name>@<api-version>' = {
//   <resource-properties>
// }
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2020-03-01-preview' = {
  name: logAnalyticsWorkspaceName
  location: location
  properties: any({
    // ワークスペースのデータ保持日数。 -1 は、Unlimited Sku の場合、無制限。
    // 他のすべての Sku で許可される最大日数は 730 日。
    retentionInDays: 30
    // ?どこにもこの項目の説明が無い。
    features: {
      searchVersion: 1
    }
    // SKU(Stock Keeping Unit)
    // Basic、Standardなどの課金形態のこと。
    // 2018 年 4 月の価格モデルを選択したサブスクリプションで Log Analytics ワークスペースを作成または構成する場合、
    // 有効な Log Analytics 価格レベルは PerGB2018 のみ。
    sku: {
      name: 'PerGB2018'
    }
  })
}

// Application Insights 作成
// Application Insights は Azure Monitor の拡張機能であり、
// アプリケーション パフォーマンス監視 ("APM" とも呼ばれる) 機能を提供。
// APM ツールは、開発、テスト、運用環境からアプリケーションを監視するのに役立つ。
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: appInsightsName
  location: location
  // このコンポーネントが参照するアプリケーションの種類。
  // 任意の文字列で、値は通常、web、ios、other、store、java、phone のいずれか。
  kind: 'web'
  properties: {
    // 監視アプリケーションの種類
    Application_Type: 'web'
    // データが取り込まれるログ分析ワークスペースのリソース ID
    // (logAnalyticsWorkspace は上で作成しているリソースを参照している)
    WorkspaceResourceId:logAnalyticsWorkspace.id
  }
}

// マネージド環境(Azure Container Apps の環境)作成
resource environment 'Microsoft.App/managedEnvironments@2022-03-01' = {
  // 環境名:'env-${uniqueString(resourceGroup().id)}'
  name: environmentName
  location: location
  properties: {
    // サービスをサービス通信テレメトリにエクスポートするために Dapr によって使用される Azure Monitor インストルメンテーション キー
    // Application Insights インストルメンテーション キー。
    // アプリケーションが Azure Application Insights に送信されるすべてのテレメトリの送信先を識別するために使用できる読み取り専用の値。
    // この値は、新しい各 Application Insights コンポーネントの構築時に提供される。
    // appInsightsは、上で作成しているApplication Insights。
    // 今回は、Dapr を導入しないため、意味無いかも。
    daprAIInstrumentationKey:appInsights.properties.InstrumentationKey
    appLogsConfiguration: {
      // ログ記録先。 現在、「log-analytics」のみがサポートされている。
      destination: 'log-analytics'
      logAnalyticsConfiguration: {
        // Log AnalyticsのcustomerIdとsharedKey
        customerId: logAnalyticsWorkspace.properties.customerId
        // function list*([apiVersion: string], [functionValues: object]): any
        // この関数の構文は、リスト操作の名前によって異なる。
        // 一般的な使用法には、listKeys、listKeyValue、および listSecrets がある。
        // 各実装は、リスト操作をサポートするリソース タイプの値を返す。
        sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey
      }
    }
  }
}

// output <name> <data-type> = <value>
// デプロイが成功すると、出力値はデプロイの結果で自動的に返される。
// Azure CLI または Azure PowerShell スクリプトでデプロイ履歴から出力値を参照できる。
// az deployment group show \
// -g <resource-group-name> \
// -n <deployment-name> \
// --query properties.outputs.resourceID.value
output location string = location
output environmentId string = environment.id

./deploy/container-http.bicep
param containerAppName string
param location string
param environmentName string
param containerImage string
param containerPort int
param isExternalIngress bool
param containerRegistry string
@secure()
param containerRegistryUsername string
param isPrivateRegistry bool
param enableIngress bool = true
@secure()
param registryPassword string
param minReplicas int = 0
param secrets array = []
param env array = []
param revisionMode string = 'Single'

// existing
// デプロイ済みのリソースを参照。→この後、environment.id で使っている。
// existing キーワードを使って参照した場合、リソースは再デプロイされない。
// 今回の場合、environment.bicepで作成される マネージド環境(Azure Container Apps の環境)への参照。
resource environment 'Microsoft.App/managedEnvironments@2022-03-01' existing = {
  name: environmentName
}

var resources = [
  {
    cpu: '0.25'
    memory: '0.5Gi'
  }
  {
    cpu: '0.5'
    memory: '1.0Gi'
  }
  {
    cpu: '0.75'
    memory: '1.5Gi'
  }
  {
    cpu: '1.0'
    memory: '2.0Gi'
  }
  {
    cpu: '1.25'
    memory: '2.5Gi'
  }
  {
    cpu: '1.5'
    memory: '3.0Gi'
  }
  {
    cpu: '1.75'
    memory: '3.5Gi'
  }
  {
    cpu: '2.0'
    memory: '4.0Gi'
  }
]

// Azure Container Apps 作成
resource containerApp 'Microsoft.App/containerApps@2022-03-01' = {
  name: containerAppName
  location: location
  properties: {
    // コンテナー アプリの環境のリソース ID
    managedEnvironmentId: environment.id
    configuration: {
      // activeRevisionsMode: 'Multiple' | 'Single' | string
      // ActiveRevisionsMode は、コンテナー アプリのアクティブなリビジョンの処理方法を制御。
      // Multiple: 複数のリビジョンをアクティブにできる。
      // Single: 一度にアクティブにできるリビジョンは 1 つだけ。
      // Singleモードでは、リビジョン ウェイトは使用できない。
      // 値が指定されていない場合、Singleモードがデフォルト。(今回は、Multipleを指定)
      activeRevisionsMode: revisionMode
      // コンテナ アプリで使用されるシークレットのコレクション
      // 今回は、main.bicepから以下が渡されている。docker pull するために必要。
      // [
      //   {
      //     name: registryPassword
      //     value: containerRegistryPassword
      //   }
      // ]
      secrets: secrets
      // コンテナー アプリで使用されるコンテナーのプライベート コンテナー レジストリ資格情報のコレクション
      // 今回は、プライベート(非公開)で、isPrivateRegistry=trueのため、nullではなく、セットされる。
      registries: isPrivateRegistry ? [
        {
          // コンテナレジストリ。ここでは、Azure Container Registry (今回の場合は、acrtestyou.azurecr.io)
          server: containerRegistry
          // コンテナレジストリのログインユーザー名(事前にpull許可が与えられているサービスプリンシパルのclientId)
          username: containerRegistryUsername
          // コンテナレジストリのパスワード(事前にpull許可が与えられているサービスプリンシパルのclientSecret)
          passwordSecretRef: registryPassword
        }
      ] : null
      // イングレス設定
      // Ingress:外部からのアクセス(主にHTTP)を管理するもの
      // インターネット - イングレス - コンテナ
      ingress: enableIngress ? {
        // httpsのみ
        allowInsecure: false
        // インターネットに公開(true)
        external: isExternalIngress
        // コンテナ側のポート(3000)
        targetPort: containerPort
        // ポート転送方式(auto,http,http2,tcp)
        transport: 'auto'
        // トラフィック ルールを設定
        traffic: [
          {
            // デプロイされた最新のリビジョン
            latestRevision: true
            // リビジョンに割り当てられたトラフィックの重み→100=トラフィック分割しない。
            weight: 100
            // weight: 70 //ブルーグリーン・デプロイメントする場合、ここに割り振っておく。
          }
          // {
          //   revisionName: 'react-app--prh39yx'←直近のリビジョン(自動で指定されるような工夫が必要だとは思うが、とりあえず、即値)
          //   weight: 30
          // }
        ]
      } : null
      // Daprの設定
      dapr: {
        // Daprサイドカー無効
        enabled: false
      }
    }
    // コンテナー アプリのバージョン管理されたアプリケーション定義。
    template: {
      // コンテナ アプリのコンテナ定義のリスト。
      containers: [
        {
          // コンテナイメージタグ
          // 例:acrtestyou.azurecr.io/xxxxx/aca-app-repo/react-frontend:sha-734785c
          image: containerImage
          // カスタムコンテナ名
          name: containerAppName
          // env: EnvironmentVar[]
          // コンテナ環境変数。
          // 今回は、特に設定しないが、
          // env: [
          //   {
          //     name: 'HOGEHOGE'
          //     value: 'foobar'
          //   }
          // ]
          // とした場合、例えば、nodeアプリの場合、アプリ側ソースコードで、process.env.HOGEHOGE のように参照できる。
          env: env
          // cpu: '0.25' memory: '0.5Gi'
          resources: resources[0]
          // 正常性プローブ
          // Container Apps では、次のプローブがサポートされている。
          // Liveness: レプリカの全体的な正常性を報告。
          // Readiness: レプリカがトラフィックを受け入れる準備ができていることを示す。
          // Startup: startup probe を使用して、遅いアプリの健全性または準備状態のレポートを遅延。
          probes: [
            {
              // プローブが成功した後に失敗したと見なされる最小連続失敗回数。
              // デフォルトは 3。最小値は 1。最大値は 10。
              // failureThreshold: 3
              // 実行する http 要求を指定
              httpGet: {
                // 接続先のホスト名。デフォルトはポッド IP。
                // host: 'string'
                // リクエストに設定するカスタム ヘッダー。
                // httpHeaders: [
                //   {
                //     name: 'string'
                //     value: 'string'
                //   }
                // ]
                // アクセス先のパス、ポート番号
                path: '/'
                port: 3000
                // ホストへの接続に使用するスキーム。 デフォルトは HTTP
                scheme: 'HTTP'
              }
              // liveness プローブが開始されるまでのコンテナーの起動後の秒数。 最小値は 1。最大値は 60。
              initialDelaySeconds: 60
              // プローブを実行する頻度 (秒単位)。 デフォルトは 10 秒。 最小値は 1。最大値は 240。
              periodSeconds: 30
              // プローブが失敗した後に成功したと見なされるための最小連続成功。デフォルトは 1。LivenessとStartupには 1 である必要がある。 最小値は 1。最大値は 10。
              // successThreshold: 1
              // TCPSocket は、TCP ポートに関するアクションを指定。 TCP フックはまだサポートされていない。
              // tcpSocket: {
              //   host: 'string'
              //   port: int
              // }
              // プローブの失敗時に Pod が正常に終了する必要があるオプションの期間 (秒単位)。
              // terminationGracePeriodSeconds: int
              // プローブがタイムアウトになるまでの秒数。 デフォルトは 1 秒。 最小値は 1。最大値は 240。
              timeoutSeconds: 10
              // type: 'Liveness' | 'Readiness' | 'Startup' | string
              // プローブの種類
              type: 'Startup'
            }
          ]
        }
      ]
      scale: {
        // minReplicas: int
        // オプション。 コンテナー レプリカの最小数。0を指定して、使われていないと、無課金の待機状態になる。
        minReplicas: minReplicas
        // オプション。 コンテナー レプリカの最大数。 設定されていない場合、デフォルトは 10。
        maxReplicas: 1
      }
    }
  }
}

// fqdn = ホスト名
// 今回、全コンテナ enableIngress=true のため、ホスト名が返る。
output fqdn string = enableIngress ? containerApp.properties.configuration.ingress.fqdn : 'Ingress not enabled'

コンテナレジストリ作成

コンテナを push できる場所のコンテナレジストリ(Azure Container Registry のインスタンス)を作成します。


Azure CLI で、サインインします。

$ az login --use-device-code
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code H*******W to authenticate.

https://microsoft.com/devicelogin にブラウザでアクセスして、表示されているコード(H*******W)を入力して、サインインします。

Azure CLI 最新版を使っているか確認します。

$ az upgrade

東日本リージョン japaneast(任意)に
リソースグループ my-containerapp-store(任意)を作成します。

$ az group create --name my-containerapp-store --location japaneast

作成したリソースに Azure Container Registry のインスタンスを作成します。

$ az deployment group create --resource-group my-containerapp-store --template-file ./deploy/create-acr.bicep --parameters acrName=acrtestyou

ここで、bicepを使っていますが、特に重要な意味はありません。普通にコマンドラインで作成しても良いです。

コマンドライン:az acr create --resource-group $RES_GROUP --name $ACR_NAME --sku Basic --location japaneast

my-containerapp-store は、先ほど作成したリソースグループです。acrtestyou は任意ですが、以下の注意点があります。


注意:
aclName=acrtestyou とすると、acrtestyou.azurecr.io が作成されます。これは、全世界で重複しない必要があります。重複すると、以下のエラーになります。
The registry DNS name aaaaa.azurecr.io is already in use. You can check if the name is already claimed using following API:


aclName=acr-testyou のように記号は使えません。アルファベットと数字のみです。また、5~50文字である必要があります。名前がまずい場合、以下のエラーになります。
Invalid resource name: 'react-frontend'. Resource names may contain alpha numeric characters only and must be between 5 and 50 characters.. For more information, please refer resource name requirements at


サービスプリンシパル作成

共同作成者のロールを持ち、コンテナー レジストリのリソース グループをスコープとするサービス プリンシパルを作成します。

$ groupId=$(az group show \
  --name my-containerapp-store \
  --query id --output tsv)
$ az ad sp create-for-rbac \
  --scope $groupId \
  --role Contributor \
  --sdk-auth

my-containerapp-store は、先ほど作成したリソースグループ名です。


成功したら、以下のような出力が得られます。 この出力は後で使います。

{
  "clientId": "d9******-****-****-****-**********e3",
  "clientSecret": "V9************************************wS",
  "subscriptionId": "ea******-****-****-****-**********80",
  "tenantId": "0c******-****-****-****-**********2b",
  "activeDirectoryEndpointUrl": "https://login.microsoftonline.com",
  "resourceManagerEndpointUrl": "https://management.azure.com/",
  "activeDirectoryGraphResourceId": "https://graph.windows.net/",
  "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/",
  "galleryEndpointUrl": "https://gallery.azure.com/",
  "managementEndpointUrl": "https://management.core.windows.net/"
}

Azure サービス プリンシパルの資格情報を更新して、コンテナー レジストリに対するプッシュとプルのアクセスを許可します。
この手順により、GitHub ワークフローでサービス プリンシパルを使用して、コンテナー レジストリに対する認証と、
Docker イメージのプッシュおよびプルを実行できます。
コンテナー レジストリのリソース ID を取得します。
環境変数 registryId に一旦、先ほど作成した Azure Container Registry のインスタンス のリソースIDを格納します。

$ registryId=$(az acr show \
  --name acrtestyou \
  --resource-group my-containerapp-store \
  --query id --output tsv)
$ echo $registryId
/subscriptions/ea0c9***-****-****-****-*******a0c80/resourceGroups/my-containerapp-store/providers/Microsoft.ContainerRegistry/registries/acrtestyou

my-containerapp-store は、先ほど作成したリソースグループ名です。acrtestyou は、先ほど作成した Azure Container Registry です。


az role assignment create を使用して、レジストリに対するプッシュおよびプル アクセスを付与する AcrPush ロールを割り当てます。

$ az role assignment create \
  --assignee d9******-****-****-****-**********e3 \
  --scope $registryId \
  --role AcrPush

--assignee d9******-****-****-****-**********e3 のところは、先ほど JSON 出力にあった clientId です。


GitHub 準備

リポジトリ作成

GitHub にプライベートリポジトリ aca-app-repo を作成します。(リポジトリの作り方については省略します。)
リポジトリ名は任意です。(サービスプリンシパルのアプリ名に合わせる必要もありません。)

リポジトリ作成


Personal access tokens

・GitHub ワークフロー実行権限(workflow
・GitHub Container Registry への push 権限(write:packages
を有効にします。


Settings -> 左下の Developer settings -> Personal access tokens -> Tokens (classic)
にて、workflowwrite:packages にチェックを入れ、Update token をクリックします。

Personal access tokens手順 Settings


Personal access tokens手順 Developer settings


Personal access tokens手順 Update token


シークレット

シークレット(ワークフロー内で使う秘密の値)を設定します。


リポジトリ aca-app-repo に戻って、

Settings -> Secrets -> Actions -> New repository secret をクリックします。

New repository secret


ここに、以下を設定します。
AZURE_CREDENTIALS:先ほどのサービス プリンシパルの作成の JSON 出力全体
REGISTRY_LOGIN_SERVER:レジストリのログイン サーバー名(今回は、acrtestyou.azurecr.io
REGISTRY_USERNAME:サービス プリンシパルの作成 JSON 出力の clientId(今回は、d9******-****-****-****-**********e3
REGISTRY_PASSWORD:サービス プリンシパルの作成 JSON 出力の clientSecret(今回は、V9************************************wS
RESOURCE_GROUP:サービス プリンシパルのスコープ指定に使用したリソース グループの名前(今回は、my-containerapp-store

AZURE_CREDENTIALS、RESOURCE_GROUP


commit & push

リポジトリ aca-app-repo main ブランチに push します。

$ git init
$ git config --local user.name "AAAAA BBBBB"
$ git config --local user.email "xxxxx@example.com"
$ git add .
$ git commit -m "first commit"
$ git branch -M main
$ git remote add origin https://github.com/<github user name>/aca-app-repo.git
$ git push -u origin main

<github user name> 部分は、GitHub ユーザー名です。


Run workflow

Actions -> Build and Deploy -> Run workflow
をクリックして、Branch: main のままにして、Run workflow をクリックします。

Run workflow


Set Environment Vairables ジョブ
後続のジョブで使用する値を生成しています。

package-services ジョブ
docker コンテナをビルドして、コンテナレジストリ(今回の場合、acrtestyou.azurecr.io)にビルドしたコンテナを push しています。

deploy ジョブ
Azure リソースをデプロイしています。
(既にデプロイ済みの場合は、Azure Container Apps アプリreact-appのリビジョンが一つ増えます。)


と進んで、全て緑色のチェックが付いたら、デプロイ完了です。

デプロイ完了

deploy ジョブで作成されるリソースは、以下です。
・ログ分析ワークスペース
・Application Insights
・Azure Container Apps の環境
・Azure Container Apps のアプリ


Azure Container Apps アプリ react-app のアプリ名は、./deploy/main.bicep
var reactFrontendAppName = 'react-app'
に書かれています。(アプリ名は任意です。)


動作確認1

Azure Portalhttps://portal.azure.com/) -> コンテナ― アプリ -> react-app -> リビジョン管理
を見ると、リビジョンが一つだけ有ることが分かります。
コンテナ― アプリ選択


リビジョン管理 リビジョンが一つだけ有る


概要 から アプリケーション URL にアクセスすると、アプリが起動しています。 概要 アプリケーション URL


アプリ起動確認


CI/CD

App.tsx のメッセージに "New" という言葉を足して、commit & push します。

Newという言葉追加

$ git add .
$ git commit -m "second commit"
$ git push

動作確認2

Azure Portalhttps://portal.azure.com/) -> コンテナ― アプリ -> react-app -> リビジョン管理 を見ると、リビジョンが二つ有ることが分かります。 リビジョン管理 リビジョンが二つ有る


概要 から アプリケーション URL にアクセスすると、アプリが起動しています。"New" という言葉が反映された最新アプリということが分かります。 Newという言葉が追加されている


一連の作成物は、以下のコマンドでリソースグループごと削除することで、まとめて削除できます。

$ az group delete \
  --resource-group my-containerapp-store

トラフィック変更

70%,30%

トラフィックを70%,30%の配分にして、保存します。

リビジョントラフィック 70%,30%配分 図


リビジョントラフィック 70%,30%配分 設定



10回に3回くらい、前("New"無し)の状態に戻ります。 リビジョントラフィック 70%,30%配分 結果


100% 入れ替え

前("New"無し)のリビジョンを 100% にして、保存します。

リビジョントラフィック 0%,100%配分 図


リビジョントラフィック 0%,100%配分 設定



前("New"無し)の状態のままになります。 リビジョントラフィック 0%,100%配分 結果


スケーリングについて

今回の場合、Pod のスケーリングに関して、最小レプリカ数 0 です。
したがって、しばらくすると、0 になり、無課金状態に移行します。
その代わり、Pod レプリカ数 0 → 1 に復帰する時、1分近く待ちますので、注意が必要です。
今回の場合、(Pod レプリカ数 0 の状態で)ブラウザでアクセスすると、読み込み待ちが1分近く続きました。

loading...