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

Bicepを使ってAzure Container AppsとDaprのマイクロサービスをデプロイ

(更新) (公開)

はじめに

前回記事「Node.js,Python,React で Dapr の状態管理アプリを作成してローカル環境で動作確認」のアプリを Azure Container Apps + Dapr へ Bicep、Azure Container Registry、GitHub Actions を使ってデプロイしてみました。一連の手順を紹介していきたいと思います。


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 アプリ/マイクロサービス:画面は、React をビルドしたものです。node のサーバーで画面、 API 要求( Dapr 経由でアクセス)を振り分けています。マイクロサービスとは言うものの、python の /order API 一つです。これが役割毎に増えていけばマイクロサービスアーキテクチャということです。


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


【 コンテナレジストリ 】

Azure Container Registry、GitHub Container Registry(ghcr.io)、Docker Registry、etc...の内、今回は、Azure Container Registry を使います。

Dapr についての説明は、前回記事「Node.js,Python,ReactでDaprの状態管理アプリを作成してローカル環境で動作確認」にありますので、省略します。


基本的には、learn.microsoft.com の「チュートリアル: Azure Container Apps に GitHub Actions を使用して Dapr アプリケーションをデプロイする」を見ながら進めましたが、参考にしただけで、同一ではありません。Go で実装されたサービスは省略しました。
また、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-dapr-bicep-simple-app にアップしました。

アプリ作成(前回記事「Node.js,Python,React で Dapr の状態管理アプリを作成してローカル環境で動作確認」で作成したものです。)

コンテナビルドのための 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/key-vault.bicep
./deploy/cosmosdb.bicep
./deploy/key-vault-secret.bicep
./deploy/dapr-component.bicep
./deploy/container-http.bicep
作成
と準備していきます。


結果、以下のようになります。

.
├── dapr-components
│   └── local
│       └── statestore.yaml
├── deploy
│   ├── container-http.bicep
│   ├── cosmosdb.bicep
│   ├── create-acr.bicep
│   ├── dapr-component.bicep
│   ├── environment.bicep
│   ├── key-vault.bicep
│   ├── key-vault-secret.bicep
│   └── main.bicep
├── node-service
│   ├── client
│   │   ├── build
│   │   │   ├── asset-manifest.json
│   │   │   ├── favicon.ico
│   │   │   ├── index.html
│   │   │   ├── logo192.png
│   │   │   ├── logo512.png
│   │   │   ├── manifest.json
│   │   │   ├── robots.txt
│   │   │   └── static
│   │   │       ├── css
│   │   │       │   ├── main.073c9b0a.css
│   │   │       │   └── main.073c9b0a.css.map
│   │   │       └── js
│   │   │           ├── 787.c4e7f8f9.chunk.js
│   │   │           ├── 787.c4e7f8f9.chunk.js.map
│   │   │           ├── main.a92ca6f7.js
│   │   │           ├── main.a92ca6f7.js.LICENSE.txt
│   │   │           └── main.a92ca6f7.js.map
│   │   ├── package.json
│   │   ├── package-lock.json
│   │   ├── public
│   │   │   ├── favicon.ico
│   │   │   ├── index.html
│   │   │   ├── logo192.png
│   │   │   ├── logo512.png
│   │   │   ├── manifest.json
│   │   │   └── robots.txt
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── App.css
│   │   │   ├── App.test.tsx
│   │   │   ├── App.tsx
│   │   │   ├── index.css
│   │   │   ├── index.tsx
│   │   │   ├── logo.svg
│   │   │   ├── react-app-env.d.ts
│   │   │   ├── reportWebVitals.ts
│   │   │   └── setupTests.ts
│   │   └── tsconfig.json
│   ├── Dockerfile
│   ├── index.js
│   ├── package.json
│   └── package-lock.json
└── python-service
    ├── app.py
    ├── Dockerfile
    └── requirements.txt

Dockerfile 作成

node サービス側の Dockerfile を準備します。
node サービスは、
/order
/delete
のアクセスの時、自身のサイドカーの Dapr へ転送して、結果、python サービスの方へリクエストが行きます。
それ以外のアクセスの時、node-service/client/build/ の生成物(html,js)を返します。

./node-service/Dockerfile
FROM node:17-alpine
WORKDIR /usr/src/app
COPY . .
RUN npm install

# Build the client
RUN --mount=type=secret,id=REACT_APP_MY_API_URL \
export REACT_APP_MY_API_URL=$(cat /run/secrets/REACT_APP_MY_API_URL) && \
cd client && npm i && npm run build

EXPOSE 3000

CMD [ "npm", "run", "start" ]

python サービス側の Dockerfile を準備します。

./python-service/Dockerfile
FROM python:3.9
COPY requirements.txt /app/
WORKDIR /app
RUN pip install -r requirements.txt
COPY . .
ENTRYPOINT ["python"]
EXPOSE 5000
CMD ["app.py"]

app.py を起動しているだけですので、起動すると、以下の警告が出力されます。ちゃんと WSGI を使ったサーバーにした方が良いと思いますが、あくまでもお試し版ということで、このままでいきます。

WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.


build-and-deploy.yaml 作成

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

意味の説明は、以前の記事「Azure Container Appsへbicep,Azure Container Registry,GitHub Actionsを使ってデプロイ」に詳しく書きましたので、そちらを参照してください。このときとやっていることはだいたい同じです。

./.github/workflows/build-and-deploy.yaml
name: Build and Deploy
on:
  push:
    branches: [main]
    tags: ["v*.*.*"]
    paths-ignore:
      - "README.md"
      - ".vscode/**"
  workflow_dispatch:

jobs:
  set-env:
    name: Set Environment Variables
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.main.outputs.version }}
      created: ${{ steps.main.outputs.created }}
      repository: ${{ steps.main.outputs.repository }}
    steps:
      - id: main
        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

  package-services:
    runs-on: ubuntu-latest
    needs: set-env
    permissions:
      contents: read
      packages: write
    outputs:
      containerImage-node: ${{ steps.image-tag.outputs.image-node-service }}
      containerImage-python: ${{ steps.image-tag.outputs.image-python-service }}
    strategy:
      matrix:
        services:
          [
            { "appName": "node-service", "directory": "./node-service" },
            { "appName": "python-service", "directory": "./python-service" },
          ]
    steps:
      - name: Checkout repository
        uses: actions/checkout@v2
      - name: Log into registry ${{ secrets.REGISTRY_LOGIN_SERVER }}
        if: github.event_name != 'pull_request'
        uses: azure/docker-login@v1
        with:
          login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}
      - name: Extract Docker metadata
        id: meta
        uses: docker/metadata-action@v3
        with:
          images: ${{ secrets.REGISTRY_LOGIN_SERVER }}/${{ needs.set-env.outputs.repository }}/${{ matrix.services.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
        uses: docker/build-push-action@v2
        with:
          context: ${{ matrix.services.directory }}
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          secrets: |
            "REACT_APP_MY_API_URL=${{ secrets.REACT_APP_MY_API_URL }}"
      - name: Output image tag
        id: image-tag
        run: |
          echo image-${{ matrix.services.appName }}=$GITHUB_REPOSITORY/${{ matrix.services.appName }}:sha-${{ needs.set-env.outputs.version }} | tr '[:upper:]' '[:lower:]' >> $GITHUB_OUTPUT

  deploy:
    runs-on: ubuntu-latest
    needs: package-services
    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - name: Azure Login
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Deploy bicep
        uses: azure/CLI@v1
        with:
          inlineScript: |
            az deployment group create -g ${{ secrets.RESOURCE_GROUP }} -f ./deploy/main.bicep \
              -p \
                minReplicas=1 \
                nodeImage='${{ secrets.REGISTRY_LOGIN_SERVER }}/${{ needs.package-services.outputs.containerImage-node }}' \
                nodePort=3000 \
                pythonImage='${{ secrets.REGISTRY_LOGIN_SERVER }}/${{ needs.package-services.outputs.containerImage-python }}' \
                pythonPort=5000 \
                containerRegistry=${{ secrets.REGISTRY_LOGIN_SERVER }} \
                containerRegistryUsername=${{ secrets.REGISTRY_USERNAME }} \
                containerRegistryPassword='${{ secrets.REGISTRY_PASSWORD }}'

ACR デプロイ用 Bicep 作成

Azure Container Registry デプロイ用に ./deploy/create-acr.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

ACA 他デプロイ用 Bicep 作成

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


Key Vault について

今回、Dapr - Cosmos DB 紐付けの為だけに、Key Vault を使っています。


なぜかと言うと、

output primaryMasterKey string = listKeys(accountName_resource.id, accountName_resource.apiVersion).primaryMasterKey

の行が、
function list*(resourceNameOrIdentifier: string, apiVersion: string, [functionValues: object]): any
The syntax for this function varies by name of the list operations. Each implementation returns values for the resource type that supports a list operation. The operation name must start with list. Some common usages are listKeys, listKeyValue, and listSecrets.

Outputs should not contain secrets. Found possible secret: function 'listKeys'bicep corehttps://aka.ms/bicep/linter/outputs-should-not-contain-secrets
の警告になったからです。
primaryMasterKey は、daprComponents のデプロイ箇所で以下のように渡す必要があるのですが、秘密の値を output するなと怒られています。

resource stateDaprComponent 'Microsoft.App/managedEnvironments/daprComponents@2022-01-01-preview' = {
  name: '${environmentName}/orders'
  dependsOn: [
    environment
  ]
  properties: {
    componentType: 'state.azure.cosmosdb'
    version: 'v1'
    secrets: [
      {
        name: 'masterkey'
        value: cosmosdb.outputs.primaryMasterKey
      }
    ]

これだけのために、


Key Vault デプロイ

Key Vault Secret に primaryMasterKey の値登録

daprComponents のデプロイ箇所で渡す


としています。


ただし、Key Vault から取り出した値を渡せば良いかと言うとそうでもなく、

resource stateDaprComponent 'Microsoft.App/managedEnvironments/daprComponents@2022-01-01-preview' = {
  name: '${environmentName}/orders'
  dependsOn: [
    environment
  ]
  properties: {
    componentType: 'state.azure.cosmosdb'
    version: 'v1'
    secrets: [
      {
        name: 'masterkey'
        value: kv.getSecret('CosmosDbPrimaryMasterKey')
      }
    ]

のように、kv.getSecret('CosmosDbPrimaryMasterKey') を直接渡すと、
Function "getSecret" is not valid at this location. It can only be used when directly assigning to a module parameter with a secure decorator.
のエラーになりますので、
@secure() で保護した param を渡す必要がありました。

bicep CosmosDbPrimaryMasterKeyのところ


Bicep 内容

意味の説明は、コメントとして記述しました。ただし、Dapr に関する事、Cosmos DB に関する事、Key Vault に関する事以外は省略しています。その他の部分の説明は、以前の記事「Azure Container Appsへbicep,Azure Container Registry,GitHub Actionsを使ってデプロイ」に詳しく書きましたので、そちらを参照してください。

./deploy/main.bicep
param location string = resourceGroup().location
param environmentName string = 'env-${uniqueString(resourceGroup().id)}'

param appName string = 'hello-dapr'

param minReplicas int = 0

param nodeImage string
param nodePort int = 3000
var nodeServiceAppName = 'node-app'

param pythonImage string
param pythonPort int = 5000
var pythonServiceAppName = 'python-app'

param isPrivateRegistry bool = true

param containerRegistry string
@secure()
param containerRegistryUsername string = ''
@secure()
param containerRegistryPassword string = ''
#disable-next-line secure-secrets-in-params
param registryPassword string = 'registry-password'

// Container Apps Environment
module environment 'environment.bicep' = {
  name: '${deployment().name}--environment'
  params: {
    environmentName: environmentName
    location: location
    appInsightsName: '${environmentName}-ai'
    logAnalyticsWorkspaceName: '${environmentName}-la'
  }
}

// Key Vault
module keyvault 'key-vault.bicep' = {
  name: 'keyvault-deployment'
  params: {
    location: location
    appName: appName
    tenantId: tenant().tenantId
  }
}

// Cosmosdb
module cosmosdb 'cosmosdb.bicep' = {
  name: '${deployment().name}--cosmosdb'
  params: {
    location: location
    primaryRegion: location
    keyVaultName: keyvault.outputs.keyVaultName
  }
}

resource kv 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
  name: keyvault.outputs.keyVaultName
}

// Dapr Component
// マネージド環境で Dapr コンポーネントを作成
module daprComponent 'dapr-component.bicep' = {
  name: '${deployment().name}--daprComponent'
  // dependsOn: (module[] | (resource | module) | resource[])[]
  // リソースをデプロイするとき、一部のリソースが他のリソースより前に確実にデプロイされるようにすることが必要な場合があります。
  // たとえば、データベースをデプロイする前に論理 SQL
  // このリレーションシップは、あるリソースが他のリソースに依存しているとマークすることで確立します。
  // 明示的な依存関係は、dependsOn プロパティで宣言されます。
  dependsOn: [
    environment
    keyvault
  ]
  params: {
    cosmosDbPrimaryMasterKey: kv.getSecret('CosmosDbPrimaryMasterKey')
    documentEndpoint: cosmosdb.outputs.documentEndpoint
    environmentName: environmentName
    pythonServiceAppName: pythonServiceAppName
  }
}

// Python App
module pythonService 'container-http.bicep' = {
  name: '${deployment().name}--${pythonServiceAppName}'
  dependsOn: [
    environment
  ]
  params: {
    enableIngress: true
    isExternalIngress: false
    location: location
    environmentName: environmentName
    containerAppName: pythonServiceAppName
    containerImage: pythonImage
    containerPort: pythonPort
    isPrivateRegistry: isPrivateRegistry
    minReplicas: minReplicas
    containerRegistry: containerRegistry
    registryPassword: registryPassword
    containerRegistryUsername: containerRegistryUsername
    revisionMode: 'Single'
    secrets: [
      {
        name: registryPassword
        value: containerRegistryPassword
      }
    ]
  }
}

// Node App
module nodeService 'container-http.bicep' = {
  name: '${deployment().name}--${nodeServiceAppName}'
  dependsOn: [
    environment
  ]
  params: {
    enableIngress: true
    isExternalIngress: true
    location: location
    environmentName: environmentName
    containerAppName: nodeServiceAppName
    containerImage: nodeImage
    containerPort: nodePort
    minReplicas: minReplicas
    isPrivateRegistry: isPrivateRegistry
    containerRegistry: containerRegistry
    registryPassword: registryPassword
    containerRegistryUsername: containerRegistryUsername
    revisionMode: 'Multiple'
    env: [
      {
        name: 'PYTHON_SERVICE_NAME'
        value: pythonServiceAppName
      }
    ]
    secrets: [
      {
        name: registryPassword
        value: containerRegistryPassword
      }
    ]
  }
}

output nodeFqdn string = nodeService.outputs.fqdn
output pythonFqdn string = pythonService.outputs.fqdn

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

resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2020-03-01-preview' = {
  name: logAnalyticsWorkspaceName
  location: location
  properties: any({
    retentionInDays: 30
    features: {
      searchVersion: 1
    }
    sku: {
      name: 'PerGB2018'
    }
  })
}

resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: appInsightsName
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    WorkspaceResourceId:logAnalyticsWorkspace.id
  }
}

resource environment 'Microsoft.App/managedEnvironments@2022-03-01' = {
  name: environmentName
  location: location
  properties: {
    daprAIInstrumentationKey:appInsights.properties.InstrumentationKey
    appLogsConfiguration: {
      destination: 'log-analytics'
      logAnalyticsConfiguration: {
        customerId: logAnalyticsWorkspace.properties.customerId
        sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey
      }
    }
  }
}

output location string = location
output environmentId string = environment.id

./deploy/key-vault.bicep
param appName string

// Key Vault名(最大長24文字)
// 今回の場合、kv-hello-dapr-q4******gv(リソースグループID)
@maxLength(24)
param vaultName string = '${'kv-'}${appName}-${substring(uniqueString(resourceGroup().id), 0, 23 - (length(appName) + 3))}' // must be globally unique
param location string = resourceGroup().location
param sku string = 'Standard'
param tenantId string // テナントID

// 下記参照(渡しているところに説明あり)
param enabledForDeployment bool = true
param enabledForTemplateDeployment bool = true
param enabledForDiskEncryption bool = true
param enableRbacAuthorization bool = true
param softDeleteRetentionInDays int = 90

// ネットワークアクセスルール(特に無し)
param networkAcls object = {
  ipRules: []
  virtualNetworkRules: []
}

resource keyvault 'Microsoft.KeyVault/vaults@2022-07-01' = {
  name: vaultName
  location: location
  properties: {
    tenantId: tenantId
    sku: {
      family: 'A'
      name: sku
    }
    // シークレットとして格納されている証明書をキー コンテナーから取得することを
    // Azure Virtual Machines に許可するかどうかを指定するプロパティ
    enabledForDeployment: enabledForDeployment
    // Azure Disk Encryption がコンテナーからシークレットを取得し、
    // キーをアンラップすることを許可するかどうかを指定するプロパティ
    enabledForDiskEncryption: enabledForDiskEncryption
    // Azure Resource Manager がキー コンテナーからシークレットを
    // 取得することを許可するかどうかを指定するプロパティ
    enabledForTemplateDeployment: enabledForTemplateDeployment
    // softDelete(論理削除)データ保持日数(7以上、90以下)
    softDeleteRetentionInDays: softDeleteRetentionInDays
    // データ アクションの承認方法を制御するプロパティ。
    // true の場合、キー コンテナーはデータ アクションの承認に役割ベースの
    // アクセス制御 (RBAC) を使用し、コンテナーのプロパティで指定された
    // アクセス ポリシーは無視されます (警告: これはプレビュー機能です)。
    // false の場合、キー コンテナーはコンテナーのプロパティで指定された
    // アクセス ポリシーを使用し、Azure Resource Manager に格納されている
    // ポリシーはすべて無視されます。 null または指定されていない場合、
    // vault はデフォルト値の false で作成されます。 管理アクションは常に
    // RBAC で承認されることに注意してください。
    enableRbacAuthorization: enableRbacAuthorization
    // 特定のネットワークの場所からキー コンテナーへのアクセスを管理する規則
    networkAcls: networkAcls
  }
}

output keyVaultName string = keyvault.name
output keyVaultId string = keyvault.id

./deploy/cosmosdb.bicep
// function description(text: string): any
// @description というデコレータをつけて、パラメータの説明を記載
// @description デコレータによる説明は、パラメータ入力で ? を入力した際に表示される
// VSCode拡張機能の場合、マウスオーバー時の説明に表示される。
@description('Cosmos DB アカウント名、最大長 44 文字、小文字')
param accountName string = 'cosmos-${uniqueString(resourceGroup().id)}'

@description('Cosmos DB アカウントのロケーション')
param location string

@description('Cosmos DB アカウントのプライマリ レプリカ リージョン')
param primaryRegion string

@description('Cosmos DB アカウントの既定の整合性レベル')
// @allowed
// パラメーターに使用できる値を定義できます。 使用できる値は配列で指定します。
// 使用できる値の 1 つではない値がパラメーターに渡された場合、検証時にデプロイは失敗します。
// ここに書かれているのは、Cosmos DB 整合性の種類
// Eventual(最終的)
// Consistent Prefix(一貫性のあるプレフィックス)
// Session(セッション)
// Bounded Staleness(有界整合性制約)
// Strong(強固)
@allowed([
  'Eventual'
  'ConsistentPrefix'
  'Session'
  'BoundedStaleness'
  'Strong'
])
param defaultConsistencyLevel string = 'Session'

// @minValue
// @maxValue
// 文字列と配列のパラメーターの最小長と最大長を指定できます。 一方または両方の制約を設定できます。
// 文字列の場合、長さは文字数を示します。 配列の場合、長さは配列内の項目数を示します。
@description('古いリクエストの最大数。BoundedStaleness(有界整合性制約)に必要です。有効な範囲、シングル リージョン: 10 ~ 1000000。マルチ リージョン: 100000 ~ 1000000。')
@minValue(10)
@maxValue(2147483647)
param maxStalenessPrefix int = 100000

@description('最大遅延時間 (分)。BoundedStaleness(有界整合性制約)に必要です。有効な範囲、シングル リージョン: 5 ~ 84600。マルチ リージョン: 300 ~ 86400。')
@minValue(5)
@maxValue(86400)
param maxIntervalInSeconds int = 300

@description('データベースの名前')
param databaseName string = 'ordersDb'

// ここでいうコンテナとは、Cosmos DBの「コンテナ」(テーブルに相当)
@description('コンテナの名前')
param containerName string = 'orders'

// コンテナーの最大スループット
@description('Maximum throughput for the container')
@minValue(4000)
@maxValue(1000000)
param autoscaleMaxThroughput int = 4000

param keyVaultName string

// 指定された文字列を小文字に変換します。
var accountNameVar = toLower(accountName)
var consistencyPolicy = {
  Eventual: {
    // ConsistencyLevel=整合性レベル
    // Eventual (最終的)
    // Write が複数発生した場合の順序保証無し
    // https://qiita.com/everpeace/items/cbbace418f7bc297631f
    // 読み込めるデータが 最新である保証はない
    // 読み込むプロセスによって(同一プロセスであっても)は 先祖返りするかもしれない
    // でも、すべてのreplicaが いつか同じ状態に収束する
    // これは5つの内一番整合性が弱いかわりに読込書込がとても速い
    defaultConsistencyLevel: 'Eventual'
  }
  // 一貫性のあるプレフィックス
  // 読み込めるデータが最新である保証はない,先祖返りするかもしれない, いつか同じ状態に収束する のはEventualと同じ
  // それに加えてreplicaに適用される書込リクエストの順序は同じ
  // 例えば、A, B, Cという書込リクエストが複数のreplicaに適用されるとすると、クライアントからみると、
  // A, A,B, A,B,Cと書込リクエストが処理されたデータは読み込まれる可能性はあるが、
  // A,Cとか、B,A,Cという風な順で書込がなされたデータが見える可能性はない
  ConsistentPrefix: {
    defaultConsistencyLevel: 'ConsistentPrefix'
  }
  // セッション
  // Consistent Prefixは、先祖返りが起きたり、
  // 読込リクエストごとに見えるデータの世代が違う、ことが起きるけれど、
  // Sessionでは、いわゆる
  // Monotonic Read, Monotonic Write, Read Your own Write(RYW)を保証する
  // Monotonic Read: あるプロセスから見て読み込めるデータは先祖返りしない
  // Monotonic Write: あるプロセスからなされるデータXへの書込順序は保存される
  // Read Your own Write(RYW): あるプロセスが書込処理を行ったら、
  // そのプロセスはすぐその書込処理完了後のデータが読める
  Session: {
    defaultConsistencyLevel: 'Session'
  }
  // 有界整合性制約
  // 読み込めるデータは古いことはあるが、
  // t単位時間以降はK世代以内のデータが読めることを保証する
  // ユーザはこのK, tをチューニングできる。
  // この整合性レベルは、強い整合性を求めたいけどデータの
  // availabilityが99.99%でよくて、かつ低レイテンシが欲しい場合に有効
  BoundedStaleness: {
    defaultConsistencyLevel: 'BoundedStaleness'
    // 古いリクエストの最大数。 BoundedStaleness に必要。
    // 有効な範囲、シングル リージョン: 10 ~ 1000000。
    // マルチ リージョン: 100000 ~ 1000000。
    maxStalenessPrefix: maxStalenessPrefix
    // 最大遅延時間 (分)。 BoundedStaleness に必要。
    // 有効な範囲、シングル リージョン: 5 ~ 84600。マルチ リージョン: 300 ~ 86400。
    maxIntervalInSeconds: maxIntervalInSeconds
  }
  // 厳密、強固
  // いわゆるLinearizabileを保証する、あるプロセスが書き込めたら、
  // どのプロセスが読んでも常に最新の値が読める。
  // majority quorumで実装できる
  // Linerizabile(線形化可能性)
  // 並行プログラミングにおいて操作(または操作の集合)は、
  // 呼び出しイベントと応答イベント
  // (コールバック)の順序付きリストで構成されており、
  // 応答イベントを追加することで以下のように拡張できる場合、線形化可能である。
  // 1.拡張されたリストは逐次履歴として再表現することができる(直列化可能である)。
  // 2.その逐次履歴は元の拡張されていないリストの部分集合である。
  // quorumとは分散システムにおいて、分散トランザクションが処理を
  // 実行するために必要な最低限の票の数である。
  // quorumベースの技術は分散システムにおいて、処理の整合性をとるために実装される。
  Strong: {
    defaultConsistencyLevel: 'Strong'
  }
}
var locations = [
  {
    locationName: primaryRegion
    // フェールオーバー優先度
    failoverPriority: 0
    // ゾーン冗長
    isZoneRedundant: false
  }
]

// Cosmos DB 作成
// データベースアカウント
// Azure Cosmos DB リソース モデル
// https://learn.microsoft.com/ja-jp/azure/cosmos-db/account-databases-containers-items
resource accountName_resource 'Microsoft.DocumentDB/databaseAccounts@2021-01-15' = {
  name: accountNameVar
  // 作成する Cosmos DB データベース アカウントの種類
  // GlobalDocumentDB, MongoDB, Parse
  kind: 'GlobalDocumentDB'
  // リソースが属するリソース グループの場所
  location: location
  properties: {
    // consistencyPolicy=整合性についての設定
    // 上で、param defaultConsistencyLevel string = 'Session' があるため、
    // 今回の場合、Session。
    consistencyPolicy: consistencyPolicy[defaultConsistencyLevel]
    locations: locations
    // 申し込みタイプ?Standardしかない?
    databaseAccountOfferType: 'Standard'
  }
}

// Cosmos DB 子リソース 個別のデータベース
resource accountName_databaseName 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2021-01-15' = {
  parent: accountName_resource
  name: databaseName
  // Azure Cosmos DB SQL データベースを作成および更新するためのプロパティ
  properties: {
    resource: {
      // Cosmos DB SQL データベースの名前
      id: databaseName
    }
  }
}

// Cosmos DB 子リソース 個別のデータベース コンテナ―
// データが格納される場所
resource accountName_databaseName_containerName 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2021-01-15' = {
  parent: accountName_databaseName
  name: containerName
  // Azure Cosmos DB コンテナを作成および更新するためのプロパティ
  properties: {
    resource: {
      // Cosmos DB SQL コンテナの名前
      id: containerName
      // 論理パーティション(パーティションキー)
      partitionKey: {
        // コンテナー内のデータをパーティション分割できるパスのリスト
        paths: [
          '/partitionKey'
        ]
        // kind: 'Hash' | 'MultiHash' | 'Range' | string
        // パーティショニングに使用されるアルゴリズムの種類。
        // MultiHash の場合、コンテナの作成で複数のパーティション キー (最大 3 つまで) が
        // サポートされる。
        kind: 'Hash'
      }
    }
    options: {
      autoscaleSettings: {
        // maxThroughput: int
        // リソースがスケールアップできる最大スループット。
        // 上で定義されている4000。param autoscaleMaxThroughput int = 4000
        maxThroughput: autoscaleMaxThroughput
      }
    }
  }
}

// Key Vault に primaryMasterKey 格納
module setCosmosDbPrimaryMasterKey 'key-vault-secret.bicep' = {
  name: 'setCosmosDbPrimaryMasterKey'
  params: {
    // Key Vault の名前
    keyVaultName: keyVaultName
    // キー名
    secretName: 'CosmosDbPrimaryMasterKey'
    // 値
    secretValue: listKeys(accountName_resource.id, accountName_resource.apiVersion).primaryMasterKey
  }
}

output documentEndpoint string = accountName_resource.properties.documentEndpoint
// Cosmos DB のエンドポイント。
// 以下のように Dapr の設定に使用。
// metadata: [
//   {
//     name: 'url'
//     value: documentEndpoint
//   }

./deploy/key-vault-secret.bicep
param keyVaultName string
param secretName string
@secure()
param secretValue string

// 渡されたシークレットの キーと値をKey Vault に格納
// 今回は、Cosmos DB の PrimaryMasterKey のみに使用。
resource keyVaultSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = {
  name: '${keyVaultName}/${secretName}'
  properties: {
    value: secretValue
  }
}

./deploy/dapr-component.bicep
param environmentName string
param documentEndpoint string
param pythonServiceAppName string

@secure()
param cosmosDbPrimaryMasterKey string = ''

// マネージド環境で Dapr コンポーネントを作成
resource stateDaprComponent 'Microsoft.App/managedEnvironments/daprComponents@2022-01-01-preview' = {
  name: '${environmentName}/orders'
  // Dapr コンポーネント リソース固有のプロパティ
  properties: {
    // コンポーネントの種類
    // 他の例(何を見たら分かる?)
    // secretstores.azure.keyvault
    // pubsub.azure.servicebus
    // state.azure.blobstorage
    // bindings.cron
    // bindings.smtp
    componentType: 'state.azure.cosmosdb'
    // コンポーネントバージョン
    version: 'v1'
    // Dapr コンポーネントによって使用されるシークレットのコレクション
    secrets: [
      {
        name: 'masterkey'
        value: cosmosDbPrimaryMasterKey
      }
    ]
    // コンポーネントメタデータ
    // Cosmos DBの接続情報
    metadata: [
      {
        name: 'url'
        value: documentEndpoint
      }
      {
        name: 'database'
        value: 'ordersDb'
      }
      {
        name: 'collection'
        value: 'orders'
      }
      {
        name: 'masterkey'
        // メタデータ プロパティ値を取得する Dapr コンポーネント シークレットの名前。
        secretRef: 'masterkey'
      }
    ]
    // この Dapr コンポーネントを使用できるコンテナー アプリの名前
    scopes: [
      pythonServiceAppName
    ]
  }
}

./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'


resource environment 'Microsoft.App/managedEnvironments@2022-03-01' existing = {
  name: environmentName
}

resource containerApp 'Microsoft.App/containerApps@2022-03-01' = {
  name: containerAppName
  location: location
  properties: {
    managedEnvironmentId: environment.id
    configuration: {
      activeRevisionsMode: revisionMode
      secrets: secrets
      registries: isPrivateRegistry ? [
        {
          server: containerRegistry
          username: containerRegistryUsername
          passwordSecretRef: registryPassword
        }
      ] : null
      ingress: enableIngress ? {
        external: isExternalIngress
        targetPort: containerPort
        transport: 'auto'
        traffic: [
          {
            latestRevision: true
            weight: 100
          }
        ]
      } : null
      dapr: {
        enabled: true
        appPort: containerPort
        appId: containerAppName
      }
    }
    template: {
      containers: [
        {
          image: containerImage
          name: containerAppName
          env: env
        }
      ]
      scale: {
        minReplicas: minReplicas
        maxReplicas: 1
      }
    }
  }
}

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=hellodapracrtest

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

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

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


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


acrName=hello-dapr-acr-test のように記号は使えません。アルファベットと数字のみです。また、5 ~ 50 文字である必要があります。名前がまずい場合、以下のエラーになります。
Invalid resource name: 'hello-dapr-acr-test'. 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 hellodapracrtest \
  --resource-group my-containerapp-store \
  --query id --output tsv)
$ echo $registryId
/subscriptions/ea0c9***-****-****-****-*******a0c80/resourceGroups/my-containerapp-store/providers/Microsoft.ContainerRegistry/registries/hellodapracrtest

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


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

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

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


リソースプロバイダーの登録

今回、Key Vault を使うのですが、リソースプロバイダーに登録が無いと、以下のエラーになります。
The subscription is not registered to use namespace 'Microsoft.KeyVault'.


このエラーにより、デプロイに失敗しますので、リソースプロバイダーを登録しておきます。

$ az provider register --namespace Microsoft.KeyVault
Registering is still on-going. You can monitor using 'az provider show -n Microsoft.KeyVault'

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:レジストリのログイン サーバー名(今回は、hellodapracrtest.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 します。

$ rm -rf node-service/client/.git
$ 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 コンテナをビルドして、コンテナレジストリ(今回の場合、hellodapracrtest.azurecr.io)にビルドしたコンテナを push しています。
今回、コンテナが2つありますので、2つ並行して実行されます。

deploy ジョブ
Azure リソースをデプロイしています。
(既にデプロイ済みの場合は、Azure Container Apps アプリ node-app のリビジョンが一つ増えます。python-app の方は、シングルリビジョンのため、リビジョンは増えません。)


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

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


Azure Container Apps の2つのアプリ名は、./deploy/main.bicep
var nodeServiceAppName = 'node-app'
var pythonServiceAppName = 'python-app'
に書かれています。(アプリ名は任意です。Dapr は、この名前で通信します。何でもいいように、環境変数になっています。


API の URL について

画面 →https://<デプロイにより得られたFQDN>/order
画面 →https://<デプロイにより得られたFQDN>/delete
という API アクセスがあるのですが、
<デプロイにより得られたFQDN> はデプロイ後に決まるため、React をビルドしている最中には分かりません。
そのため、以下の作業を行い、再度、パイプライン起動が必要です。
FQDN が最初から決まっている場合、最初から REACT_APP_MY_API_URL を Secrets に登録しておけば、問題無いです。


node-app の概要より、アプリケーション URL をコピーします。


リポジトリの Secrets に以下の値を登録します。
REACT_APP_MY_API_URL<コピーしたアプリケーション URL>

再度、ワークフローを起動します。

$ git add .
$ git commit -m "second commit"
$ git push -u origin main

注意:
Re-run jobs でもう一度ワークフローを起動しても Azure Container Apps のリビジョン名が commit id に関わっているため、更新されません。ここでは、改行を増やすなど、些細な修正をしたものとします。


動作確認

1. データ無しの状態で、GET してみます。

データが無いという結果が返りました。


2. POST してデータを入れます。

実装を最小限にするため、データは固定になっています。


3. 再びデータを GET します。

データが取り出されました。


4. データを DELETE します。

削除に成功しました。


5. 再びデータを GET します。 データ無しに戻ります。


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

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

Key Vault については、消したことを覚えているため、以下のように完全に削除します。

$ az keyvault list-deleted
[
  {
    "id": "/subscriptions/ea******-****-****-****-**********80/providers/Microsoft.KeyVault/locations/japaneast/deletedVaults/kv-hello-dapr-q4******gv",
    "name": "kv-hello-dapr-q4******gv",
    "properties": {
・・・
$ az keyvault purge --name kv-hello-dapr-q4******gv

削除しないと、再度同じ名前でデプロイする機会があったときに以下のエラーになります。
A vault with the same name already exists in deleted state. You need to either recover or purge existing key vault.


マルチリビジョンや、トラフィックの変更については、こちらで触れていますので、省略します。

Azure Container Apps へ bicep,Azure Container Registry,GitHub Actions を使ってデプロイ - 動作確認1~


ヨシ!

loading...