はじめに
前回、Azure Container Appsとコンテナの学び直し ではAzure Container Appsの基本機能をKubernetesと見比べながら整理しました。
今回はAzure Container Apps(以下 ACA)と ACA Jobs を中核としたエンタープライズレベルの構成を一例ですが組み上げ、に実際にアプリケーションをリリースした内容をまとめます。
結構ボリュームが多くなってしまったので前編・後編で構成していこうと思っています。
| 内容 | |
|---|---|
| 前編(本記事) | 閉域に関わる構成、Bicep、マネージドID、Private Endpoint/DNS、Application Gateway、インターナルアクセスとの共存 |
| 後編 | アプリの登録、クライアントシークレット、他テナントからの情報取得、シークレット管理、Dockerfile、イメージのビルドとCI/CD、デプロイとリリース |
アプリケーションおよびインフラのコードは抜粋で、テナントID・リソース名などはダミー値(<...>)に置き換えています。
またソース全量をお見せできる準備ができておりませんので、その点ご了承を頂ければと思います。
使ったアプリケーション
管理テナント(今回作っている環境)から、管理対象となるAzureテナント・サブスクリプション内のリソース状態を一元的に確認できる運用機能を持ったWebアプリケーション。
以下のような情報を管理対象テナントから取得し表示している。
| 表示項目 | 内容 |
|---|---|
| VM | ホスト、VMと稼働状態、OS内訳 |
| PaaS | 利用しているPaaSの一覧 |
| 料金 | 当月の合計(Cost Management) |
| Advisor | 推奨事項の件数 |
言語はPython(FastAPI)、画面はHTML/CSS/JavaScriptの静的ファイルである。
Container Apps 環境内では、一覧表示機能と情報収集機能を別の実行単位に分けている。
| 機能 | 実行単位 | 役割 |
|---|---|---|
| 一覧表示 | コンテナー アプリ | Cosmos DBに保存した最新のスナップショットを読んで画面に返す |
| 情報収集 | コンテナー アプリ ジョブ | 日次で各テナントの情報を集め、Cosmos DBに保存する |
一覧表示の画面を開くたびに他テナントへ情報を取りに行くわけではなく、ジョブが日次で集めて保存し、一覧表示機能は保存済みのデータを読むだけにしている。常時稼働のオンライン処理はコンテナー アプリ、決まった処理をして終わるバッチ処理はジョブ、という前回学び直した使い分けそのままである。
特性的にパブリック向けのアプリケーションでは全くないが、公開サイトをセキュアに構成するためのアーキテクチャの題材として用いている。
プラットフォームについて
リソース一覧
| リソース | 役割 | SKU・プラン | アクセス |
|---|---|---|---|
| Application Gateway | 利用者向けの入口 | Standard_v2(最小1・最大2インスタンス) | 公開IPあり |
| Container Apps 環境 | 一覧表示アプリとジョブを動かす環境 | ワークロードプロファイル:Consumption | パブリックアクセス無効、PE経由 |
| コンテナー アプリ/ジョブ | 一覧表示本体と日次の収集処理 | Consumption | Container Apps環境内 |
| Azure Container Registry (ACR) | イメージの置き場 | Premium | パブリックアクセス無効、PE経由 |
| Azure Cosmos DB | スナップショットの保存先 | Serverless | パブリックアクセス無効、PE経由 |
| Key Vault (キーコンテナ) | クライアントシークレットの保管 | Standard | パブリックアクセス無効、PE経由 |
| DMZ/バックエンド仮想ネットワーク | DMZにAGWとACA用PE、バックエンドにACA統合・データ用PE・ビルド用VMを配置 | - | ピアリングなし |
| NAT Gateway | ジョブが他テナントのAPIを呼ぶための外向き通信 | - | アウトバウンドのみ |
| マネージドID | アプリ用・ジョブ用。ACR・Key Vault・Cosmos DBへのアクセス | ユーザー割り当て | - |
| Log Analytics/Application Insights | ログとテレメトリの保存先 | Log Analytics:PerGB2018(保持30日)、Application Insights:ワークスペースベース | AMPLSは未使用 |
| ビルド用VM | イメージのビルドとpush | Standard_D2s_v3(Windows Server) | プライベートIPのみ |
| Azure Bastion | VMへの接続経路 | Bastion:Basic | 通常はERやVPNで代替 |
PEはプライベート エンドポイント(Private Endpoint)の略。
構成図
プラットフォームの概要
- 仮想ネットワーク間(DMZとバックエンド)は、仮想ネットワーク ピアリングをしていない
- Container Apps 環境は、DMZ仮想ネットワークにプライベート エンドポイントを、バックエンド仮想ネットワークに仮想ネットワーク統合を行っている
- リクエストは、AGWのバックエンドプールに登録されたACA用プライベート エンドポイントを経由してACAに届く
- ジョブは他テナントのAPIを呼ぶため、外向きだけNAT Gatewayで確保している
- 一般的な構成では、Spokeであるバックエンド仮想ネットワークとHubネットワークを仮想ネットワークピアリングで接続し、Hubネットワークに配置されるAzure Firewallからアウトバウンド通信を制御するためNAT Gatewayは不要である
- あるいはHubネットワークからオンプレミスへ強制トンネリングをしオンプレミスのファイアウォール機能からアウトバウンドする構成もある
各リソースのポイントを少しずつ見ていこうと思う。
Container Apps 環境
環境は専用のサブネットに統合して作成し、パブリックアクセスは無効にする。
ワークロードプロファイルは従量課金(Consumption)。
resource environment 'Microsoft.App/managedEnvironments@2025-07-01' = {
name: '<ACA環境名>'
location: location
properties: {
publicNetworkAccess: 'Disabled'
vnetConfiguration: {
infrastructureSubnetId: <networking.outputs.acaSubnetId>
internal: false
}
workloadProfiles: [{ name: 'Consumption', workloadProfileType: 'Consumption' }]
appLogsConfiguration: { destination: 'azure-monitor' }
}
}
publicNetworkAccess: 'Disabled' と internal: false として、プライベート エンドポイントからアプリに到達させる。後述するIngressはどこからでもトラフィックを受け入れる external: true にしているが公開アクセスを無効にしているので、インターネットから直接は到達できない。前回記事の「環境側とアプリ側の2段階」の具体的な構成である。
仮想ネットワーク統合先のサブネットには Microsoft.App/environments への委任が必要で、ワークロードプロファイル環境では最小で/27になる。
作成後にサイズを変更できないので、余裕を持って決める。外向き通信にはNAT Gatewayを関連付け、既定のアウトバウンド(defaultOutboundAccess)は無効にしている。
環境の診断設定で、ログとメトリックをLog Analyticsワークスペース(30日保持)へ送っている。Application Insightsはワークスペースに紐付け、Entra IDによる認証のみにしている。
コンテナー アプリとジョブ
一覧表示アプリは、Ingressを有効にしてプライベート エンドポイントからのリクエストを受ける。イメージの取得は、パスワードではなくマネージドIDで行う(registries の identity)。
dependsOn には、アプリ用マネージドIDへの3つのロール割り当て(ACRのAcrPull、Key VaultのKey Vault Secrets User、Cosmos DBのデータロール)を入れている。(マネージドIDの詳細は後述)
ACAはコンテナーを起動するたびにACRからイメージを取得するため、アプリの作成時点でAcrPullが付いていないと、最初のイメージ取得に失敗する。
Key VaultとCosmos DBの権限は、起動したアプリが最初にアクセスする時点で必要になるため dependsOn で明示し、権限が付く前にアプリの作成(イメージの取得)が始まらないようにしている。
resource app 'Microsoft.App/containerApps@2026-07-01' = {
name: '<一覧表示アプリ名>'
location: location
identity: { type: 'UserAssigned', userAssignedIdentities: { '${appIdentity.id}': {} } }
properties: {
managedEnvironmentId: environment.id
workloadProfileName: 'Consumption'
configuration: {
activeRevisionsMode: 'Single'
registries: [{ server: registry.properties.loginServer, identity: appIdentity.id }]
ingress: { external: true, targetPort: 8000, transport: 'auto' }
}
template: {
containers: [{
name: 'app'
image: '<ACR名>.azurecr.io/<イメージ名>:${imageTag}'
resources: { cpu: json('0.5'), memory: '1Gi' }
}]
scale: { minReplicas: 1, maxReplicas: 3 }
}
}
dependsOn: [acrPullRole, cosmosRole, kvSecretsUserRole]
}
ジョブは、同じイメージの起動コマンドを上書きして、収集処理だけを動かす。
resource job 'Microsoft.App/jobs@2026-07-01' = {
name: '<収集ジョブ名>'
location: location
identity: { type: 'UserAssigned', userAssignedIdentities: { '${jobIdentity.id}': {} } }
properties: {
environmentId: environment.id
workloadProfileName: 'Consumption'
configuration: {
triggerType: 'Schedule'
scheduleTriggerConfig: { cronExpression: '0 0 * * *', parallelism: 1, replicaCompletionCount: 1 }
replicaRetryLimit: 1
replicaTimeout: 1800
registries: [{ server: registry.properties.loginServer, identity: jobIdentity.id }]
}
template: {
containers: [{
name: 'collector'
image: '<ACR名>.azurecr.io/<イメージ名>:${imageTag}'
command: ['python']
args: ['-m', '<収集処理のモジュール名>']
resources: { cpu: json('0.5'), memory: '1Gi' }
}]
}
}
}
| 項目 | 一覧表示アプリ | 収集ジョブ |
|---|---|---|
| リソース | 0.5 vCPU / 1 GiB | 0.5 vCPU / 1 GiB |
| スケール・実行 | 最小1・最大3レプリカ、ポート8000 | 並列1、完了数1、再試行1、タイムアウト1800秒 |
ACAジョブのcronの 0 0 * * * はUTCの00:00で、日本時間(JST)では毎日09:00に動く。
ACR・Cosmos DB・Key Vault
ACAが連携するACR・Cosmos DB・Key Vaultは、パブリックアクセスを無効にして、後述するプライベート エンドポイントからだけ接続できるようにしている。
Key Vaultは、AGWが証明書を取得する場合にだけTrusted Servicesの例外を許可している(後述のApplication Gatewayの証明書(Key Vault)参照)。
// Azure Key Vault
resource keyVault 'Microsoft.KeyVault/vaults@2026-02-01' = {
name: take('akv${suffix}', 24)
location: location
properties: {
tenantId: subscription().tenantId
sku: { family: 'A', name: 'standard' }
enableRbacAuthorization: true
enableSoftDelete: true
softDeleteRetentionInDays: 90
publicNetworkAccess: 'Disabled'
networkAcls: { bypass: 'AzureServices', defaultAction: 'Deny' }
}
}
// Azure Cosmos DB
resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2026-03-15' = {
name: cosmosName
location: location
kind: 'GlobalDocumentDB'
properties: {
databaseAccountOfferType: 'Standard'
locations: [{ locationName: location, failoverPriority: 0, isZoneRedundant: false }]
enableAutomaticFailover: true
consistencyPolicy: { defaultConsistencyLevel: 'Session' }
capabilities: [{ name: 'EnableServerless' }]
disableLocalAuth: true
publicNetworkAccess: 'Disabled'
minimalTlsVersion: 'Tls12'
}
}
// Azure Container Registry
resource registry 'Microsoft.ContainerRegistry/registries@2025-11-01' = {
name: registryName
location: location
sku: { name: 'Premium' }
properties: {
adminUserEnabled: false
publicNetworkAccess: 'Disabled'
networkRuleBypassOptions: 'None'
policies: { azureADAuthenticationAsArmPolicy: { status: 'enabled' } }
}
}
ACRは、プライベート エンドポイントを使うためにPremiumを選択し、管理者ユーザーを無効にしてパブリックアクセスも無効にしている。
ACRのパブリックアクセスとビルド
ACRのビルドは az acr build で、ソースのアップロードからビルド、ACRへのpushまでをACR側で実行でき、手元やビルド用のサーバーにDockerを用意しなくてよい。
この az acr build はACR Tasksという機能で実行され、既定ではMicrosoftが管理するエージェントがビルドを行う。
このエージェントは利用者の仮想ネットワークの外で動くため、パブリックアクセスを無効にしたACRに、ネットワークの許可なしにはpushできない。
ACR Tasksがパブリックアクセスを無効にしたACRへ接続する方法は、次の通りである。
- 信頼されたサービスのバイパスを許可し、さらにタスク用のポリシー(
networkRuleBypassAllowedForTasks)を有効にする。2025年6月1日以降は、このポリシーを有効にしていないと、システム割り当てマネージドIDのトークンを使うタスクのバイパスが既定で拒否され、403エラーになる - 仮想ネットワークに専用のエージェントプールを作る(Premium、プレビュー。ただし東日本リージョン未提供)
引用元:Manage network bypass policy for tasks - Azure Container Registry | Microsoft Learn
引用元:Use Dedicated Pool to Run Tasks in Azure Container Registry - Azure Container Registry | Microsoft Learn
今回は、ACRを仮想ネットワークの外から一切受け付けない方針で、networkRuleBypassOptions を None にして信頼されたサービスのバイパスを許可していない。また専用のエージェントプールの利用もできないため、az acr build によるリモートビルドは使えず、ACRにはpushだけを仮想ネットワーク内から行う。
イメージのビルドとpushの方法は後編で扱う。
パブリックアクセスの状況
ここまでの設定を、パブリックアクセスの観点で整理する。
パブリックにしているのは入口のAGWだけ。
Container Apps 環境とデータ系のサービスはパブリックアクセスを無効にしている。
| リソース | パブリックアクセス | 到達方法 |
|---|---|---|
| Application Gateway | あり(パブリックアクセスの入口) | 公開IP |
| Container Apps 環境 | 無効 | AGWからDMZのPE経由 |
| ACR | 無効 | バックエンドのPE経由 |
| Cosmos DB | 無効 | バックエンドのPE経由 |
| Key Vault | 無効(Trusted Servicesのみ例外) | バックエンドのPE経由。AGWはTrusted Servicesで証明書を取得 |
| ビルド用VM | パブリックIPなし | Bastion、またはER/VPN経由 |
認証とマネージドID
Container Apps 環境から他リソースへの接続はキーやパスワードを使用せず、原則マネージドIDで認証する構成としている。
認証方式
| 接続 | キーを使う場合 | 今回の方式 |
|---|---|---|
| Container Apps 環境 → ACR(イメージ取得) | 管理者ユーザーとパスワード | マネージドID+AcrPull |
| ビルド用VM → ACR(push) | 管理者ユーザーとパスワード | VMのマネージドID+AcrPush |
| アプリ/ジョブ → Cosmos DB | アカウントキー、接続文字列 | マネージドID+データプレーンRBAC |
| アプリ/ジョブ → Key Vault | 資格情報の埋め込み | マネージドID+Key Vault Secrets User |
| AGW → Key Vault(証明書) | 証明書ファイルの手動配置 | AGWのマネージドID+Trusted Services |
| アプリ → Application Insights | 接続文字列(キー)での取り込み | ローカル認証を無効にし、Entra認証のみ |
キーが残るのは、他テナントを読むためのクライアントシークレットが1つだけである。
マネージドIDは基本的に同じテナント内のリソースに対する仕組みなので、他テナントへの認証だけが例外になる。これは後編で扱う。
マネージドIDの種類
マネージドIDは、リソースにMicrosoft Entra IDのIDを持たせ、Entra認証に対応するサービスへ資格情報なしでアクセスさせる仕組みである。アクセス権はAzure RBACで必要最小限に付与できる。
コンテナー アプリとジョブには、ユーザー割り当てのマネージドIDを使う。
イメージのプルには可能な限りユーザー割り当てマネージド ID を使用するべきです。
引用元:マネージド ID を使用した Azure Container Apps イメージのプル | Microsoft Learn
ユーザー割り当てのマネージドIDは、コンテナー アプリの作成前にAcrPullを付与しておけるので、コードでインフラを構築する場合に楽である。
マネージドIDと権限
作成するマネージドIDの一覧を示す。
| マネージドID | 権限(ロール) | 権限の対象 | 用途 |
|---|---|---|---|
| コンテナー アプリ用 | AcrPull | ACR | イメージ取得 |
| コンテナー アプリ用 | Key Vault Secrets User | Key Vault | シークレット参照 |
| コンテナー アプリ用 | Cosmos DBの組み込みデータロール(Data Contributor) | Cosmos DBのスナップショット用コンテナー | スナップショットの読み書き |
| ジョブ用 | AcrPull | ACR | イメージ取得 |
| ジョブ用 | Key Vault Secrets User | Key Vault | シークレット参照 |
| ジョブ用 | Cosmos DBの組み込みデータロール(Data Contributor) | Cosmos DBのスナップショット用コンテナー | スナップショットの読み書き |
| ビルド用VM用 | AcrPush | ACR | イメージのpush |
| AGW用 | Key Vault Secrets User | Key Vault | TLS証明書の取得 |
Azure RBACのロール割り当ては、次のようなBicepで定義する(コンテナー アプリ用のマネージドIDへのAcrPullの例)。
resource acrPullRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(registry.id, appIdentity.id, 'AcrPull')
scope: registry
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
principalId: appIdentity.properties.principalId
principalType: 'ServicePrincipal'
}
}
Cosmos DBでは、Azure RBACではなくCosmos DB自身のデータプレーンRBACを使い、コンテナー単位を指定する。
00000000-0000-0000-0000-000000000002 は組み込みロール「Data Contributor」である。
resource cosmosRole 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2026-03-15' = {
parent: cosmos
name: guid(cosmos.id, appIdentity.id, 'CosmosDataContributor')
properties: {
principalId: appIdentity.properties.principalId
roleDefinitionId: '${cosmos.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002'
scope: '${cosmos.id}/dbs/<データベース名>/colls/<コンテナー名>'
}
}
キーを使えなくする設定
マネージドIDを使っていても、キーが有効なままだと誰かがキーを発行して使えてしまうため、サービス側でキーによる認証を無効にしておく。
- ACR:
adminUserEnabled: false(管理者ユーザーのパスワードを使わない)、azureADAuthenticationAsArmPolicy(マネージドIDでのイメージ取得に必要) - Cosmos DB:
disableLocalAuth: true(アカウントキーを使わせず、Entra認証のみにする) - Key Vault:
enableRbacAuthorization: true(アクセスポリシーではなくAzure RBACで認可する)
Application Insightsも、ローカル認証を無効にして、Entra認証のみで取り込む。
// Application Insights
resource insights 'Microsoft.Insights/components@2020-02-02' = {
name: insightsName
location: location
kind: 'web'
properties: {
Application_Type: 'web'
WorkspaceResourceId: workspace.id
DisableLocalAuth: true
publicNetworkAccessForIngestion: 'Enabled'
publicNetworkAccessForQuery: 'Enabled'
}
}
アプリケーションからの利用
一覧表示アプリと収集ジョブは、ACAに割り当てたユーザー割り当てマネージドIDを使ってKey VaultとCosmos DBに接続する。
コードにキーやパスワードを書く代わりに、Azure SDKがマネージドIDでアクセストークンを取得する。
# ACAに割り当てたマネージドIDでAzureに接続するための資格情報
def credential() -> DefaultAzureCredential:
return DefaultAzureCredential(exclude_interactive_browser_credential=True)
# マネージドIDでKey Vaultに接続し、シークレットを取得
secret = SecretClient(settings.monitoring_key_vault_uri, credential()).get_secret(
settings.monitoring_client_secret_name
)
# 接続先とマネージドIDの資格情報をCosmos DBクライアントに渡す
client = CosmosClient(settings.cosmos_endpoint, credential())
接続先と使用するIDは、ACAの環境変数に設定する。COSMOS_ENDPOINT はCosmos DBの接続先、AZURE_CLIENT_ID はAzure SDKに使用するマネージドIDを知らせる値である。
一覧表示アプリには一覧表示アプリ用ID、収集ジョブにはジョブ用IDを設定する。
// 一覧表示アプリのコンテナーに設定する例
env: [
{ name: 'COSMOS_ENDPOINT', value: cosmos.properties.documentEndpoint }
{ name: 'MONITORING_KEY_VAULT_URI', value: keyVault.properties.vaultUri }
{ name: 'MONITORING_CLIENT_SECRET_NAME', value: '<Key Vaultのシークレット名>' }
{ name: 'AZURE_CLIENT_ID', value: appIdentity.properties.clientId }
]
収集ジョブでは、最後の行をジョブ用IDのクライアントIDに置き換える。両方のIDには、接続先であるKey VaultとCosmos DBへのアクセス権をあらかじめ付与しておく。
これでコードに接続キーや接続文字列を持たせずに、各サービスへ接続できる。
Private EndpointとPrivate DNS
Container Apps 環境のプライベート エンドポイントはDMZに、それ以外のプライベート エンドポイントはバックエンドに置く。
| 対象 | groupId | 配置先 | Private DNSゾーン |
|---|---|---|---|
| Container Apps 環境 | managedEnvironments |
DMZ | privatelink.<region>.azurecontainerapps.io |
| ACR | registry |
バックエンド | privatelink.azurecr.io |
| Cosmos DB(SQL API) | Sql |
バックエンド | privatelink.documents.azure.com |
| Key Vault | vault |
バックエンド | privatelink.vaultcore.azure.net |
Container Apps 環境は、コンテナーを起動するたびにイメージの新しいバージョンを確認するため、スケールアウトやジョブの実行、リビジョンの作成の度にContainer Apps 環境からACRのプライベート エンドポイントに到達できる必要がある。
resource zoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = {
parent: endpoint
name: '<DNSゾーングループ名>'
properties: {
privateDnsZoneConfigs: [{
name: '<DNSゾーン構成名>'
properties: { privateDnsZoneId: zone.id }
}]
}
}
DNSゾーンは、そのプライベート エンドポイントを名前解決する必要がある仮想ネットワークにだけリンクする。
Container Apps 環境のゾーンは、AGWが引くDMZ仮想ネットワークに、ACR・Cosmos DB・Key Vaultのゾーンは、アプリ/ジョブとビルド用VMが引くバックエンド仮想ネットワークにリンクする。
DMZ仮想ネットワークにリンクすると、DMZ仮想ネットワークからFQDNを引いたときに、ピアリングのないバックエンド側のプライベート エンドポイントのIP アドレスが返ってしまうためである。
// プライベート エンドポイントのモジュール側:DMZにリンクするかどうか
param linkDmz bool = false
resource dmzLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (linkDmz) {
parent: zone
name: '<DMZ用リンク名>'
location: 'global'
properties: { registrationEnabled: false, virtualNetwork: { id: dmzVnetId } }
}
// 呼び出し側:Container Apps 環境のプライベート エンドポイントだけtrue
linkDmz: endpoint.name == 'aca'
Application Gateway
AGWはContainer Apps 環境へHTTPS 443で接続する。
ヘルスプローブ用に、一覧表示アプリケーションには /healthz を用意している。
@app.get('/healthz')
def healthz() -> dict[str, str]:
return {'status': 'ok'}
Container Apps 環境のIngressはHostヘッダーでアプリケーションを振り分けるので、バックエンド設定でContainer Apps 環境のFQDNをHost/SNIとしてそのまま渡す。
backendHttpSettingsCollection: [{
name: '<バックエンド設定名>'
properties: {
port: 443
protocol: 'Https'
pickHostNameFromBackendAddress: true // ACAのFQDNをHost/SNIとして渡す
probe: { id: '<ヘルスプローブ(/healthz)のID>' }
}
}]
Application Gatewayの証明書(Key Vault)
AGWでHTTPSを受けるには、リスナーにTLS証明書が要り、証明書はKey Vaultで管理してAGWから参照させたい。
ただしKey Vaultはパブリックアクセスを無効にしてバックエンドのプライベート エンドポイントだけで受けており、DMZとバックエンドはピアリングしていないので、AGWからはKey Vaultに届かない。
Key Vaultのプライベート エンドポイントをDMZにも置けば届くが、同じKey Vaultに対して2つの仮想ネットワークにプライベート エンドポイントを作って同じPrivate DNSゾーンに登録すると、どちらの仮想ネットワークからも両方のIP アドレスが返り得るため、ピアリングのない状態では届かない側のIP アドレスが返る場合がある。
仮想ネットワークリンクは名前解決の参照先を決めるだけで通信経路は作らないため、リンクを増やしても届かない側のIP アドレスが返る状況は変わらない。
仮想ネットワークごとにゾーンを分ければ返るIP アドレスを分けられるが、同じ名前のPrivate DNSゾーンは同じリソースグループに2つ作れず、DMZ用のリソースグループを別に用意することになる。
これのひとつの解決策は、Key VaultのTrusted Services(信頼されたMicrosoftサービスによるバイパス)でAGWに証明書を取得させることである。Trusted Servicesはパブリックアクセスを無効にしたKey Vaultにも使える。
設定内容
Trusted Servicesを使う場合に必要な設定は、次の通りである。
- Key Vaultのネットワークで、Trusted Servicesのバイパスを許可する(パブリックアクセスは無効のままにする)
- AGWにユーザー割り当てのマネージドIDを付け、Key Vault Secrets Userを付与する
- 証明書は、秘密キーをエクスポート可能なPFXにして、SANを利用者向けのDNS名と一致させる
- AGWから証明書を、バージョンなしのシークレットURI(
https://<Key Vault名>.vault.azure.net/secrets/<証明書名>/)で参照する - Key Vaultのゾーンは、バックエンドだけにリンクし、DMZにはリンクしない
引用元:Azure Key Vault 証明書を使用した TLS 終端処理 | Microsoft Learn
Key Vaultは、Trusted Servicesのバイパスを許可して、パブリックアクセスを無効にする。AGWにはKey Vault Secrets Userを付与する。
resource keyVault 'Microsoft.KeyVault/vaults@2026-02-01' = {
name: '<Key Vault名>'
properties: {
enableRbacAuthorization: true
publicNetworkAccess: 'Disabled'
networkAcls: { bypass: 'AzureServices', defaultAction: 'Deny' }
}
}
resource gatewayKvRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(keyVault.id, gatewayIdentity.id, 'Key Vault Secrets User')
scope: keyVault
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')
principalId: gatewayIdentity.properties.principalId
principalType: 'ServicePrincipal'
}
}
AGW側は、マネージドIDを付けて、証明書をバージョンなしのURIで参照する。Key Vaultで証明書を更新すればAGWが自動で新しいバージョンに切り替わり、AGWは4時間ごとにKey Vaultを確認する。
identity: {
type: 'UserAssigned'
userAssignedIdentities: { '${gatewayIdentity.id}': {} }
}
// properties の中
sslCertificates: [{
name: '<証明書名>'
properties: { keyVaultSecretId: '<バージョンなしのシークレットURI>' }
}]
インターナルアクセスとの共存
本構成例は、インターネット側の利用者がAGW経由でACAに入る構成であるが、社内イントラネットなどインターナルネットワークからも同じACAにアクセスさせる場合は、ExpressRoute/VPNで延伸した別の仮想ネットワーク(Hubネットワーク側)から入ることになる。
- インターネットアクセス用とインターナルアクセス用でContainer Apps 環境を分離する
- 社内からのアクセスも一度インターネットに出てAGW経由で接続する
とできれば容易であるが、同一のContainer Apps 環境としたい要件があった場合の設計上の考え方を整理する。
DMZと社内用を分けたまま送信元で経路を分けるなら、Container Apps 環境のプライベート エンドポイントをDMZ側と社内用の両方に作る。
同じContainer Apps 環境のFQDNを、DMZではDMZのプライベート エンドポイントのIP アドレスに、社内では社内のプライベート エンドポイントのIP アドレスに解決させたいが、Private DNSゾーンを共有するとリンクしたすべての仮想ネットワークに同じ答えが返ってしまうため、社内向けには別のホスト名を用意する。
- 社内向けのホスト名(カスタムドメイン)を、社内のDNSまたはAzure Private DNSゾーンで、社内側のプライベート エンドポイントのIP アドレスに解決させる
- そのホスト名を、コンテナー アプリにカスタムドメインとして登録する(TLS/SSL証明書が必要)
- 外部向けは従来どおり、AGWからDMZのプライベート エンドポイントに届ける
社内のDNSからAzure側のプライベート エンドポイントのIP アドレスを引かせるには、Azure DNS Private Resolverのインバウンド エンドポイントと、オンプレミスDNSの条件付きフォワーダーなどを使う。
ACAに限った話ではないが、インターネット側にプライベート エンドポイントを配置したリソースを社内からも使う場合は、DNSの設計が必要になる。
まとめ
前編では、実際のアプリケーションを展開するにあたって、パブリックアクセスを絞りマネージドIDでセキュアに構成する例を示しました。
- パブリックにしているのは入口のAGWだけで、Container Apps 環境・ACR・Cosmos DB・Key Vaultはパブリックアクセスを無効にしてプライベート エンドポイントで接続
- 接続はすべてマネージドIDで認証し、サービス側でもキー(ACRの管理者ユーザー、Cosmos DBのアカウントキー、Application Insightsのローカル認証)を無効化
- DMZ仮想ネットワークとバックエンド仮想ネットワークはピアリングせず、プライベート エンドポイントのゾーンはそのプライベート エンドポイントを名前解決する仮想ネットワークにだけリンク
- AGWの証明書はTrusted Servicesで取得のやり方、インターネットアクセスとインターナルアクセスの共存時の考え方
後編では、イメージのビルドとCI/CD、リリース、他テナントへの認証を学び直したいと思います。
ここまでお読みくださりありがとうございました。


