はじめに
Azure Container Apps で System Assigned Managed Identity を使用してコンテナイメージをプルしようとすると、「鶏が先か、卵が先か」のような問題に直面します。
- アプリを作成するには、コンテナイメージをプルできる必要がある(Azure Container Apps は作成前にイメージの検証を行う)
- 一方、システム割り当てマネージド ID を使ってコンテナイメージをプルするには、アプリが存在している必要がある(アプリが作成されるまでシステム割り当てマネージド ID は発行されない)
この問題をどのように解決すればよいのでしょうか?本記事では、その具体的な方法を解説します。
なお、サンプルコードは以下のリポジトリにて公開されています。
リソース一覧
まずは、今回のテーマに登場する Azure リソースを簡単に紹介します。
Azure Container Apps
Container Apps は、フルマネージドのサーバーレスコンテナサービスです。開発者がアプリケーション開発 (=ビジネスの差別化要因) に集中できることを目指して作られたサービスとなります。
Azure Container Registry
Azure Container Registry は、マネージドなプライベート Docker レジストリ サービスです。コンテナイメージのビルド機能も備えており、ビルドからデプロイまでを一元管理できます。
マネージド ID
マネージド ID(Managed Identity)は、Azure が提供する ID 管理の仕組みで、Azure サービスへの安全なアクセスを可能にします。
マネージド ID の種類
1. ユーザー割り当てマネージド ID
- 手動で作成・管理可能(複数リソース間で共用可能)
- リソース削除後も ID は残る
- 用途: 共通 ID を使う場合(例: 複数アプリが同じ Key Vault にアクセス)
2. システム割り当てマネージド ID
- リソースごとに自動作成・割り当て
- リソース削除時に ID も削除
- 用途: 個々のリソースが Azure サービスへアクセスする場合
Azure Container Apps に安全にイメージを pull する方法
さて、本題に入ります。
Azure Container Registry では、管理者ユーザーを有効化できますが、これはテスト用途向けであり、本番環境では推奨されません。
Docs にも以下のような記載があります。
そのため、以下の 2 つの方法を紹介します。
ユーザー割り当てマネージド ID を使った方法(推奨)
Azure 公式ドキュメントでは、可能な限りユーザー割り当てマネージド ID を使用することが推奨されています。
When possible, you should use a user-assigned managed identity to pull images.
(イメージのプルには可能な限りユーザー割り当てマネージド ID を使用するべきです。)
ユーザー割り当てマネージド ID を使用すると、Bicep や Terraform などの IaC を利用し、1 ステップでコンテナイメージのプルとデプロイが可能になります。
後述する「Container Apps のシステム割り当てマネージド ID を使った方法」と比べてみると一目瞭然でしょう。
ポイントとなる Bicep の箇所を記載します。
明示的にユーザー割り当てマネージド ID を作成し、Azure Container Registry からイメージを pull するロールを割り当てます。
// ユーザー割り当てマネージド ID
resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31-preview' = {
name: userAssignedIdentityName
location: location
}
// ロール割り当て
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(resourceGroup().id, azureContainerRegistry, 'AcrPullTestUserAssigned')
properties: {
principalId: identity.properties.principalId
principalType: 'ServicePrincipal'
// acrPullDefinitionId has a value of 7f951dda-4ed3-4680-a7ca-43fe172d538d
roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', acrPullDefinitionId)
}
}
// ・・・
マネージド ID に割り当て可能なロール一覧は、以下の Docs をご参照ください。
Bicep を使ったコード例
param environmentName string
param logAnalyticsWorkspaceName string
param appInsightsName string
param containerAppName string
param azureContainerRegistry string
param azureContainerRegistryImage string
param azureContainerRegistryImageTag string
param acrPullDefinitionId string
param userAssignedIdentityName string
param location string = resourceGroup().location
// ユーザー割り当てマネージド ID
resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31-preview' = {
name: userAssignedIdentityName
location: location
}
// ロール割り当て
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(resourceGroup().id, azureContainerRegistry, 'AcrPullTestUserAssigned')
properties: {
principalId: identity.properties.principalId
principalType: 'ServicePrincipal'
// acrPullDefinitionId has a value of 7f951dda-4ed3-4680-a7ca-43fe172d538d
roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', acrPullDefinitionId)
}
}
// Container Apps の Log Analytics ワークスペース
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
name: logAnalyticsWorkspaceName
location: location
properties: any({
retentionInDays: 30
features: {
searchVersion: 1
}
sku: {
name: 'PerGB2018'
}
})
}
// Application Insights
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
name: appInsightsName
location: location
kind: 'web'
properties: {
Application_Type: 'web'
WorkspaceResourceId: logAnalyticsWorkspace.id
}
}
// Container Apps 環境
resource appEnvironment 'Microsoft.App/managedEnvironments@2024-02-02-preview' = {
name: environmentName
location: location
properties: {
daprAIInstrumentationKey: appInsights.properties.InstrumentationKey
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalyticsWorkspace.properties.customerId
sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey
}
}
}
}
// Container Apps
resource containerApp 'Microsoft.App/containerApps@2022-06-01-preview' = {
name: containerAppName
location: location
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${identity.id}': {}
}
}
properties: {
environmentId: appEnvironment.id
configuration: {
ingress: {
targetPort: 8080
external: true
}
registries: [
{
server: '${azureContainerRegistry}.azurecr.io'
identity: identity.id
}
]
}
template: {
containers: [
{
image: '${azureContainerRegistry}.azurecr.io/${azureContainerRegistryImage}:${azureContainerRegistryImageTag}'
name: 'dockercontainersshexamples-dotnet-alpine'
resources: {
cpu: 1
memory: '2Gi'
}
}
]
scale: {
minReplicas: 1
maxReplicas: 1
}
}
}
}
システム割り当てマネージド ID を使った方法
システム割り当てマネージド ID を使用する場合、以下の 2 ステップが必要です。
- パブリックイメージ(ダミーコンテナ)を使用してアプリを作成
- 作成したアプリに AcrPull ロールを付与し、プライベートイメージを設定
システム割り当てマネージド ID は、アプリ作成後に発行されるため、まずは認証不要なパブリックイメージを使用し、その後適切な権限を付与することでプライベートレジストリからの pull を可能にします。
ポイントとなる Bicep の箇所を記載します。
まず、ダミーのコンテナイメージとしてパブリックイメージを使用してアプリを作成します。
// パブリックイメージを使用した Container Apps
resource containerApp 'Microsoft.App/containerApps@2024-02-02-preview' = {
...
identity: {
type: 'SystemAssigned'
}
properties: {
...
template: {
containers: [
{
name: 'main'
image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
resources: {
cpu: json('0.5')
memory: '1Gi'
}
}
]
}
...
}
// ロール割り当て
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(resourceGroup().id, azureContainerRegistry, 'AcrPullSystemAssigned')
scope: containerApp
properties: {
principalId: containerApp.identity.principalId
principalType: 'ServicePrincipal'
roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', acrPullDefinitionId)
}
}
次に、Container Apps で展開済みのアプリを入れ替えるために、プライベートイメージを指定します。
// パブリックイメージを使用した Container Apps
resource containerApp 'Microsoft.App/containerApps@2024-02-02-preview' = {
...
identity: {
type: 'SystemAssigned'
}
properties: {
...
template: {
containers: [
{
name: 'main'
- image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
+ image: '${azureContainerRegistry.properties.loginServer}/private-image:latest'
resources: {
cpu: json('0.5')
memory: '1Gi'
}
}
]
}
...
}
// ロール割り当て
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(resourceGroup().id, azureContainerRegistry, 'AcrPullSystemAssigned')
scope: containerApp
properties: {
principalId: containerApp.identity.principalId
principalType: 'ServicePrincipal'
roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', acrPullDefinitionId)
}
}
Bicep を使ったコード例(ステップ1)
param environmentName string param logAnalyticsWorkspaceName string param appInsightsName string param containerAppName string param azureContainerRegistry string param acrPullDefinitionId string param location string = resourceGroup().locationresource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
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 appEnvironment 'Microsoft.App/managedEnvironments@2022-06-01-preview' = {
name: environmentName
location: location
properties: {
daprAIInstrumentationKey: appInsights.properties.InstrumentationKey
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalyticsWorkspace.properties.customerId
sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey
}
}
}
}
resource containerApp 'Microsoft.App/containerApps@2022-06-01-preview' = {
name: containerAppName
location: location
identity: {
type: 'SystemAssigned'
}
properties: {
environmentId: appEnvironment.id
configuration: {
ingress: {
targetPort: 80
external: true
}
}
template: {
containers: [
{
image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
name: 'dotnet'
resources: {
cpu: 1
memory: '2Gi'
}
}
]
scale: {
minReplicas: 1
maxReplicas: 1
}
}
}
}
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(resourceGroup().id, azureContainerRegistry, 'AcrPullSystemAssigned')
scope: containerApp
properties: {
principalId: containerApp.identity.principalId
principalType: 'ServicePrincipal'
// acrPullDefinitionId has a value of 7f951dda-4ed3-4680-a7ca-43fe172d538d
roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', acrPullDefinitionId)
}
}
Bicep を使ったコード例(ステップ2)
param environmentName string param logAnalyticsWorkspaceName string param appInsightsName string param containerAppName string param azureContainerRegistry string param acrPullDefinitionId string param location string = resourceGroup().locationresource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
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 appEnvironment 'Microsoft.App/managedEnvironments@2022-06-01-preview' = {
name: environmentName
location: location
properties: {
daprAIInstrumentationKey: appInsights.properties.InstrumentationKey
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalyticsWorkspace.properties.customerId
sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey
}
}
}
}
resource containerApp 'Microsoft.App/containerApps@2024-10-02-preview' = {
name: containerAppName
location: location
identity: {
type: 'SystemAssigned'
}
properties: {
environmentId: appEnvironment.id
configuration: {
ingress: {
targetPort: 80
external: true
}
}
template: {
containers: [
{
image: '${azureContainerRegistry.properties.loginServer}/private-image:latest'
name: 'dotnet'
resources: {
cpu: 1
memory: '2Gi'
}
}
]
scale: {
minReplicas: 1
maxReplicas: 1
}
}
}
}
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(resourceGroup().id, azureContainerRegistry, 'AcrPullSystemAssigned')
scope: containerApp
properties: {
principalId: containerApp.identity.principalId
principalType: 'ServicePrincipal'
// acrPullDefinitionId has a value of 7f951dda-4ed3-4680-a7ca-43fe172d538d
roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', acrPullDefinitionId)
}
}
マネージド ID のライフサイクルを活用して最小権限のセキュリティ原則に従う
今回のシナリオのように、コンテナイメージの pull のみであれば、マネージド ID のライフサイクルを指定して、必要最小限の使用に制御することができます。
利用可能なオプション
以下のオプションを使用して、どのフェーズでマネージド ID を有効にするかを制御できます。
オプション | 説明 | 主な用途 |
---|---|---|
Init | init コンテナーでのみ使用可能。メインコンテナーでは使用不可。ワークロードプロファイル従量課金環境でのみサポート。 | 初期化時のみマネージド ID が必要な場合(例: Key Vault からシークレットを取得後、不要になるケース)。 |
Main | メインコンテナーでのみ使用可能。init コンテナーでは使用不可。 | init コンテナーにマネージド ID が不要な場合。 |
All | すべてのコンテナー(init と main の両方)で使用可能。 | マネージド ID をフル活用する場合(デフォルト)。 |
None | どのコンテナーのコードからでも使用不可。ただし、ACR のイメージプルやスケールルール、Key Vault シークレット参照には利用可能。 | 実行中のコンテナーにマネージド ID を不要にし、より厳密なアクセス制御を実現する場合。 |
例えば、本記事のシナリオの場合は None
を指定することで、最小権限のセキュリティ原則に従うことができます。
resource containerApp 'Microsoft.App/containerApps@2024-10-02-preview' = {
name: appName
location: resourceGroup().location
properties: {
// ・・・
registries: [
{
server: acr.properties.loginServer
identity: 'system'
}
]
identitySettings: [
{
identity: 'system'
lifecycle: 'None'
}
]
}
// ・・・
おわりに
今回紹介した Azure Container Apps のマネージド ID により、最小権限の原則(Least Privilege Principle) に従い、より安全かつ柔軟なアクセス制御が可能になります。
特に、Init フェーズ と Main フェーズ でのマネージド ID の使用を分離できることで、セキュリティリスクを低減し、不要な権限を排除できる点が大きな利点です。
また、ACR のイメージプルや Key Vault シークレットの取得にのみマネージド ID を使用する設定(None
オプション) を活用することで、アプリケーション内部での ID 使用を完全に排除し、より強固なセキュリティを確保することもできます。
適切なマネージド ID の設定を行うことで、不要なアクセス権を削減し、よりセキュアな Azure 環境を構築できるので、用途に応じた設定をぜひ活用してみてください。