doodle-on-web

自分で調べたことや、仕事の中で質問されたことなどをまとめています。

ACIでACRのイメージPullに失敗してImageInaccessible / 401 Unauthorizedになった話 — Logic Apps × Managed Identity の落とし穴

スポンサーリンク

まず症状

検索でここに来た人向けに、症状を先に置きます。同じならビンゴです。

  • ACI(Azure Container Instances)の 作成自体は成功する
  • でもコンテナは起動せず、az container show の events にこれだけが出る:
Failed to pull image "myacr.azurecr.io/myapp:latest":
[...] 401 Unauthorized
ImageInaccessible
  • ローカルでは docker pull できる
  • az acr logindocker pull も通る
  • ACR の admin user を ON にすると 動く
  • ACI に Managed Identity を割り当てて AcrPull を付けても 変わらない

ACI ImageInaccessible ACR 401 Unauthorized Managed Identity AcrPull 効かない あたりで検索して来た人は、たぶんこれです。

原因は imageRegistryCredentials.identity の指定漏れ。先に結論:

{
  "properties": {
    "imageRegistryCredentials": [
      {
        "server": "myacr.azurecr.io",
        "identity": "/subscriptions/.../userAssignedIdentities/<uami>"
      }
    ]
  }
}

ARM / Bicep テンプレートのこの一行が、解説記事だとよく抜けてます。 詳細は下に書きます。


構成

Logic Apps (Standard)
   │  HTTP トリガー
   ▼
[Create or update a container group]   ← ACI を都度作るアクション
   │
   ▼
ACI (Container Group)
   │  起動時にイメージを Pull
   ▼
ACR (Premium, Private)

リクエストが来たら使い捨てのコンテナを ACI で起動して、終わったら消す、というよくあるバッチ処理パターン。 ACR のネットワークは検証中なので一旦パブリック開放、つまりネットワークの問題は除外できている状態でこれが起きました。


最初の勘違い

正直に書きます。最初に events を見たとき、こう思いました。

「あー権限ね。ACI に Managed Identity 付けて、ACR に AcrPull 付けたら終わるやつでしょ」

これが全部間違ってた

何を勘違いしていたかを並べておきます。AI が書く記事には絶対出ない部分なので、ここが一番役に立つかもしれません。

勘違い1: 「Managed Identity を付けたら Pull もできる」

違います。 ACI の Managed Identity は コンテナが起動したあとに AAD トークンを取るためのもので、起動時のイメージ Pull には使えない。 理屈で考えれば当然で、コンテナがまだ起動してないのにコンテナ内の Identity が動くわけがない。でもハマってる時は気づかない。

勘違い2: 「AcrPull を付ければどの Identity でも Pull できる」

違います。 RBAC は「誰に」付けたかが全て。 Pull を実行する Identity に対して付いてないと意味がない。 じゃあ「Pull を実行する Identity」って誰?という話を ARM テンプレートで明示してないと、Azure はそもそも誰の Identity を使えばいいか分からない。

勘違い3: 「Contributor 持ってるから読めるはず」

違います。 Azure の RBAC には2種類あって、ContributorOwnerControl Plane(ARM 操作)の権限。 イメージや Blob やシークレットの中身を読むのは Data Plane の権限で、AcrPull / Storage Blob Data Reader / Key Vault Secrets User といった完全に別系統のロールが必要。 ACR のイメージ Pull は Control Plane 権限だけでは足りず、Pull に使われる Identity 側に AcrPull 相当の権限が必要になります。Azure を始めて最初にハマる罠の代表格。

勘違い4: 「401 Unauthorized は認証失敗、つまり権限不足」

惜しいけど違います。 401 は「認証情報が渡ってない / 誰として実行してるか分からない」状態。 403 が「認証は通ってるが権限がない」状態。 今回のは 401 なので、そもそも Identity が指定できてないということ。RBAC をいくらいじっても直りません。

この区別、エラーメッセージに unauthorized と書いてあるのに「権限の問題だ」と思い込んで RBAC を触り続けて時間を溶かしました。


試したこと(時系列)

試行1: ACI の System-Assigned Managed Identity に AcrPull を付ける

az role assignment create \
  --assignee <aci-system-assigned-mi> \
  --role AcrPull \
  --scope <ACR-resource-id>

→ 401 のまま。勘違い1 のせい。

試行2: ACR の admin user を一時的に ON にする

imageRegistryCredentials にユーザー名/パスワードを直接渡す。

動いた

これで「ネットワーク・イメージ・ACI 自体は正常」「純粋に認証の話」と確定。 admin user は OFF に戻す(本番で ON のままにするのは絶対 NG)。

試行3: Logic Apps の Managed Identity に AcrPull を付ける

「ACI を起動するのは Logic Apps なんだから、こっちじゃね?」と試したが変わらず。 これも勘違い2 のせい。Logic Apps の MI は ARM 操作には使われるけど、ACI が ACR から Pull するときの Identity ではない

ここで 30 分くらい経って、だんだん自分の理解そのものが怪しくなってきました。 「AcrPull 付けたのに何で動かないんだ」が30分続くと、「もしかして Azure 側のバグでは?」という現実逃避が始まる。これ、Azure 沼の典型症状です。だいたい自分のミスです。

試行4: events をちゃんと読む

冷静になって、events を全文出す:

az container show \
  --resource-group <rg> \
  --name <container-group-name> \
  --query "containers[0].instanceView.events"

エラー全文:

Failed to pull image: […] unauthorized: authentication required, visit https://aka.ms/acr/authorization for more information.

aka.ms/acr/authorization を素直に開いて、公式の YAML サンプルと自分のテンプレートを並べて目で diff したら、imageRegistryCredentials.identity という行が自分側にだけ無かった

最初からドキュメントを横に置いて diff してれば、たぶん 30 分で済んでました。 ハマった時に自分の頭で考え続けるのは、だいたい逆効果というのが今回の一番の学び。


本当の原因

ACI が ACR から Managed Identity で Pull するには、3つ全部揃える必要がある

  1. User-Assigned Managed Identity (UAMI) を作る
  2. ACI のテンプレートで「この UAMI を割り当てる」 + 「Pull にこの UAMI を使う」を両方書く
  3. その UAMI に対して、ACR リソーススコープで AcrPull を付ける

Bicep だとこう:

resource aci 'Microsoft.ContainerInstance/containerGroups@2023-05-01' = {
  name: 'mycg'
  location: location
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${uami.id}': {}    // ← ① ACI に UAMI を割り当て
    }
  }
  properties: {
    imageRegistryCredentials: [
      {
        server: 'myacr.azurecr.io'
        identity: uami.id  // ← ② Pull にこの UAMI を使うと明示
      }
    ]
    containers: [ ... ]
    osType: 'Linux'
    restartPolicy: 'Never'
  }
}

そして UAMI に AcrPull:

az role assignment create \
  --assignee <uami-principal-id> \
  --role AcrPull \
  --scope <ACR-resource-id>     # ← Resource Group ではなく ACR リソース直

これで動きました。


図で整理:Azureは「誰が・いつ・何をする」が全部分離している

今回の事故の本質はこれです。1枚で書くとこうなります。

┌──────────────────────────────────────────────────────────────┐
│ Logic Apps (System-Assigned MI)                               │
│   │ Control Plane: ARM API 経由で ACI を「作る」              │
│   │ 必要権限: Contributor on RG                                │
│   ▼                                                            │
│ ┌────────────────────────────────────────────────────────┐    │
│ │ ACI Container Group                                     │    │
│ │   │                                                      │    │
│ │   │ ★ Pull タイミング ★                                  │    │
│ │   │ Data Plane: ACR からイメージを Pull                  │    │
│ │   │ 使う Identity: imageRegistryCredentials.identity     │    │
│ │   │   で指定された UAMI                                   │    │
│ │   │ 必要権限: AcrPull on ACR (Data Plane)               │    │
│ │   ▼                                                      │    │
│ │ ACR                                                      │    │
│ │   │                                                      │    │
│ │   │ コンテナ起動後 → User-Assigned MI で Key Vault 等     │    │
│ │   │ にアクセス可能(これは Pull とは別の話)             │    │
│ │   ▼                                                      │    │
│ │ Key Vault / Storage / etc                                │    │
│ └────────────────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────────┘

軸:
  誰が   = どの Identity か(Logic Apps MI / UAMI / ACI System MI)
  いつ   = Control Plane の瞬間か / Data Plane の瞬間か / 起動後か
  何を   = ARM 操作 / イメージ Pull / シークレット取得
  権限   = Control Plane RBAC か Data Plane RBAC か

「Managed Identity 付けて AcrPull 付けたのに動かない」が起きるのは、この4軸のどれかが噛み合ってないから。今回は「誰が Pull するか」が ARM テンプレートで指定されてなかったので、Azure 側は「誰として Pull すればいいか分からない」状態になり 401 を返していた、というのが正解。


同じ構造の他サービス

実はこの「Pull 用 Identity を明示する」問題は ACI 固有じゃないです。Azure でコンテナを Pull する場面はだいたい同じ構造で、サービスごとに微妙に違うのが余計にハマる原因。

サービス Pull する主体 設定が必要な場所
ACI imageRegistryCredentials.identity で指定した UAMI ARM / Bicep の imageRegistryCredentials.identity
App Service for Containers App Service の Managed Identity App Setting DOCKER_REGISTRY_SERVER_* を消した上で、ACR 連携を Identity ベースで構成
Azure Functions (Container) Functions の Managed Identity App Service と同じ仕組み
AKS kubelet identity(クラスタ作成時に自動生成される別 MI) az aks update --attach-acr か、kubelet identity に直接 AcrPull

特に注意したいのが AKS。 AKS は「クラスタの Managed Identity」と「kubelet identity」が別物で、Pull するのは kubelet identity の方。クラスタ MI に AcrPull を付けても Pod は ImagePullBackOff になります。これも初見で必ずやらかすやつ。

つまり Azure でコンテナを動かす全サービスで、「Pull を実行する Identity は誰か」を毎回明示的に確認するのが安全です。デフォルトで察してくれません。


Azureの401/403を切り分ける順番(テンプレ)

このフローは ACI 以外でも使えます。App Service Container でも、Functions Container でも、AKS でも、構造は同じ。

① エラーが 401 か 403 か ImageInaccessible か、原文で確認する

unauthorized: authentication required → 401(Identity が渡ってない) Forbidden / does not have authorization → 403(Identity は分かるが権限不足) ImageInaccessible → 上記の総称(中身は 401 or 403)

ここを最初に分けないと、無関係な RBAC をいじり続けて溶ける

② admin user を一時的に ON にして切り分け

動けば「認証だけの問題」、動かなければ「ネットワーク or イメージ or ACI 自体」。 確認したら必ず OFF に戻す

③ アクティビティログより先にリソース自体のイベントを見る

リソース 見る場所
ACI az container show --query "containers[0].instanceView.events"
App Service Log Stream / Diagnose and solve
Functions Application Insights / Log Stream
AKS kubectl describe pod

アクティビティログには ARM 操作の成否しか出ない。実行時の認証エラーは載らないので、ここで時間を溶かさない。

④ Identity の「使われるタイミング」を意識する

  • 起動「前」に使う Identity(イメージ Pull)→ ARM テンプレートで明示が必要
  • 起動「後」に使う Identity(中からトークン取得)→ Managed Identity 割り当てだけでOK

これを混同してると永遠に直らない。

⑤ Control Plane と Data Plane を分けて考える

Control Plane の権限(Contributor / Owner 等)を持っていても、Data Plane の操作には対応する Data Plane ロールが別途必要になることが多い。 Reader でも Storage Blob Data Reader が無ければ Blob は読めない、というのと同じ構造。 ロール名に Data が入ってないものは原則 Control Plane、と覚えておくと早い。

⑥ スコープは「リソース直」から始める

Resource Group スコープで動かないことがある(継承タイミングの問題等)。 動作確認はまず ACR / Storage / Key Vault のリソース直スコープに付ける。広げるのはあとで。

⑦ ロール反映は数分かかる

付けた直後に動かないからといって構成をいじり倒さない。1〜5分待つ。 これでハマる人、本当に多い(自分含む)。


次回どう防ぐか(チェックリスト)

ACI / ACR / Identity が絡む案件で、最初に確認するリスト:

  • [ ] User-Assigned MI を先に作る(System-Assigned ではなく)
  • [ ] テンプレートに identity.type = UserAssigned割り当てる
  • [ ] テンプレートに imageRegistryCredentials.identity = <UAMI resource id>書く
  • [ ] UAMI に AcrPullACR リソーススコープ で付ける
  • [ ] ロール反映は数分待つ
  • [ ] エラーが出たら、まず events の原文を見る
  • [ ] それでも切り分け不能なら、admin user を一時的に ON にして認証問題か確認する
  • [ ] admin user は検証後に必ず OFF に戻す
  • [ ] エラー文の 401 / 403 / ImageInaccessible を区別する

AIに聞いた結果

ハマってる最中、Claude にも他の AI にも何度か聞きました。

返ってきた答え:

  • 「ACI に Managed Identity を割り当てて、ACR に AcrPull を付与してください」
  • 「ARM テンプレートの記述を確認してください」

合ってる。合ってるんだけど足りない。

imageRegistryCredentials.identity の存在を明示的に教えてくれた回答にはなかなか辿り着けませんでした。 これはたぶん、Web 上の解説記事の多くが「ACI に MI 付ける + AcrPull 付ける」までしか書いていなくて、AI もその平均値を返してくるからだと思います。

最終的に解決したのは、自分のテンプレートと公式ドキュメントの YAML サンプルを 目で diff した瞬間でした。

AI は平均的な解説は得意。 でも「ドキュメントには書いてあるけど解説記事には書かれてない一行」を当てるのは苦手。

これが今の AI 活用の現実だと思っていて、現場の人間が「自分が間違えた一行」をブログに書く意味は、まだはっきりあります。 AI を否定する話じゃなくて、AI が拾えてない隙間を人間が埋める、という分業の話。


おわりに

Azure の 401 / 403 はほぼ全部、

  • Identity(誰)
  • Scope(どこに対して)
  • Timing(いつ)
  • Plane(Control / Data)

の4軸に分解できます。 これが頭に入ると、3時間溶ける事故が30分になります。

同じ沼に落ちた人の役に立てたら嬉しいです。