Dev Containers のベース イメージを Azure Container Registry から利用する

# はじめに
Dev Containers Build and Runにより、Azure Pipelineを利用してAzure Container Registry (ACR)にDev Containersのビルド済イメージを管理することができました。

https://qiita.com/fsdg-nishina/items/716621e66422fb17d488

通常のDev Containersのベース イメージは mcr.microsoft.com～ のような[Microsoft Artifact Registry](https://mcr.microsoft.com/)でオープンに提供されているため誰でも利用できますが、
一般的な開発シナリオでACRを公開して利用するシーンはあまりないと思うので、そうすると必ず認証手順が必要になり
そのままDev Containersのベース イメージにしただけではすぐに利用できません。

# 認証する方法
これに対応するアプローチはいくつかあると思います。

1. ユーザーに予めAzure CLIで認証しておいてもらう。
    Azure Powershellでも良いですが、一番ベタな方法です。
    - メリット
        - Dev Containersとしては何もする必要なし
    - デメリット
        - ユーザーの負担大。開発環境を起動するたびにコマンドをポチポチ入力する。

1. Dev Containers の [initializeCommand](https://containers.dev/implementors/json_reference/#lifecycle-scripts) でログインさせる(次項で解説)。
    initializeCommandはコンテナ実行(作成)前に呼び出されることを利用して、ここでスクリプトを呼び出してログインを実行させます。
    - メリット
        - ユーザーは起動時に受動的にログインを済ませ、環境を起動できる。
    - デメリット
        - スクリプトの設計はやや面倒。
        - 例えば "Dev Containers起動前にAzureにログインしていなければ、終了後にはまたログアウトした状態に戻してほしい" といった要求に応えるには更に面倒。  
        (今回のアプローチではログインしたままになる)

1. 拡張機能を利用する。
例えばDev ContainersでACRのイメージをpullする時に、自動的に保存された認証情報で認証できるような拡張機能があれば
それを利用する手があります。
今回は相応しいものが見つけられなかったため、見送っています。
([Docker拡張機能](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker)でGUIによる認証は可能ですが、フローに組み込む事は難しそうだった)
    - メリット
        - 認証がGUIで完結できる。完全にストレスレスな起動が期待できる。
    - デメリット
        - そんな拡張機能があるのだろうか？あるとすれば
            - 評価が必要。ニーズに合致しているか、不必要な機能がないか、信頼できるか、ライセンスに問題はないか、将来性は問題ないか...etc.
        - なければ
            - 拡張機能の開発が必要。手元に構築済のものがあれば良いが、そうでなければ本来の目的とずれた大きなタスクが必要となる。

解決したいポイントは
- ユーザーに毎回コマンドを入力させるのは、ストレスになる。
    - → コマンドをユーザーに打たせるくらいなら、スクリプト等であらかじめ呼び出すようにすれば良い。
- ユーザーに毎回アカウント名とパスワードを入力させるのは、ストレスになる。
    - → 認証情報を記憶できる何らかのヘルパーが利用できれば良い。

Dev ContainersではDockerを必ず利用するので、認証情報の記憶はDockerの認証情報ヘルパーを使うのが理に適っている感じがします。
しかしDockerの認証情報ヘルパーは環境により様々なものが用意されており、それぞれに設定内容が異なるためユーザーに設定してもらう前提とするとハードルが高くなります。

https://docs.docker.jp/engine/reference/commandline/login.html#id25

ここでは、Azureのサービスを利用するという事でAzure CLIをユーザー環境に用意しておいてもらうという点を妥協したアプローチを考えてみました。

Dev Containersを起動するホスト環境はWSL(Ubuntu)を想定しています。

# initializeCommand
Dev Containers 起動前に initialize.sh というスクリプトを実行します。

```javascript:.devcontainer/devcontainer.json
{
  "name": "Debian",
  "image": "yourregistry.azurecr.io/devcontainer:latest",
  "initializeCommand": "bash .devcontainer/initialize.sh"
}
```

```bash:.devcontainer/initialize.sh
#!/usr/bin/env bash
set -u

echo "Dev Containers Initialize Command"

echo "Azure CLIを確認します..."
which az
return_code=$?

if [ $return_code -ne 0 ]; then
    # Azure CLI がインストールされていない場合には中断
    echo "Azure CLIのインストールが必要です。"
    echo "https://learn.microsoft.com/ja-jp/cli/azure/install-azure-cli-linux"
    echo "  sudo apt-get update"
    echo "  sudo apt-get install ca-certificates curl apt-transport-https lsb-release gnupg"
    exit $return_code
fi
```

ACRを利用できるユーザーは、ACR上のIAMで設定された権限を持つユーザーです。

https://learn.microsoft.com/ja-jp/azure/container-registry/container-registry-roles

Azure CLIでは **az acr login** コマンドでACRへのログインができますが
当該レジストリ(ACRリソース)が利用できるサブスクリプションでないと失敗します。
サブスクリプションを変更するには **az acr** コマンドではなく **az login** 等のコマンドで指定しなければならないため、最初に実行するようにします。

**az login** コマンドでは標準ではブラウザでログインするようになっており、ここはユーザーに操作してもらう必要がありますが、ブラウザ側でCookieが保存されていれば毎回ログイン情報を入力する必要はありません。

但し、既にAzureにログイン済の場合は再ログインする必要はないのでスキップするようにした方がユーザーにとって煩雑とならず良いでしょう。

又、Azureにログイン済でもサブスクリプションが異なっていればサブスクリプションを変更する必要があります。
もしAzureにログインする時に `az login --tenant *****` のようなコマンドにより当該サブスクリプションの属さないテナントにログインしている場合、
`az account set --name *****` のようなコマンドでサブスクリプションを変更することができません。
よって、ログイン中でもテナントIDが異なっていれば強制的にテナントIDを指定してログインさせるようにします。

テナントを確認するには `az account show --output tsv` で出力された結果の8番目をawkコマンド等で見ることでテナントIDを照合できます。
但し恒久的に8番目になることが保証できないために `--query` オプションを使用して[JMESPath クエリ](https://learn.microsoft.com/ja-jp/cli/azure/query-azure-cli)(といっても列名を指定しただけ)により **tenantId** を明示的に指定しました。
この場合 `--output tsv` を指定する必要は無いように思いますが、YAMLやTSVで出力した場合はダブルクォーツ(`"`)が付かず、JSONで出力した場合には付く、等の違いがあります。

```bash:.devcontainer/settings
# ACR にログインする Azure アカウントのテナントID
AZURE_TENANT_ID=********-****-****-****-************
# ACR にログインする Azure アカウントのサブスクリプション名
AZURE_SUBSCRIPTION_NAME=YourSubscription
# Azure Container Registry の名前
ACR_NAME=yourregistry
```

```bash:.devcontainer/initialize.sh
echo "設定ファイルをロードします..."
source .devcontainer/settings

echo "Azure にログインしているかどうかを確認します..."
needs_login=0
az account show
return_code=$?

if [ $return_code -ne 0 ]; then
    # ログインしていない
    needs_login=1
elif [ $(az account show --query "tenantId" --output tsv) != "${AZURE_TENANT_ID}" ]; then
    # 異なるテナントにログイン済
    needs_login=1
fi

if [ $needs_login -eq 1 ]; then
    # 未ログイン又は異なるテナントIDの場合は az login
    echo "Azure にログインします..."
    az login --tenant "${AZURE_TENANT_ID}"
    return_code=$?

    if [ $return_code -ne 0 ]; then
        # 終了コードがエラーなら中断
        echo "Azure へのログインに失敗しました。"
        exit $return_code
    fi
fi

echo "Azure アカウントのサブスクリプションを ${AZURE_SUBSCRIPTION_NAME} に変更します..."
az account set --name $AZURE_SUBSCRIPTION_NAME
return_code=$?

if [ $return_code -ne 0 ]; then
    # 終了コードがエラーなら中断
    echo "サブスクリプション ${AZURE_SUBSCRIPTION_NAME} をアクティブにできません。"
    exit $return_code
fi
```

最後に、**az acr login** コマンドでACRにログインさせます。
Azureにログインしていれば、ログイン情報を入力する必要はありません。

```bash:.devcontainer/initialize.sh
echo "Azure Container Registry にログインします..."
az acr login --name $ACR_NAME
return_code=$?

if [ $return_code -ne 0 ]; then
    # 終了コードがエラーなら中断
    echo "Azure Container Registry ${ACR_NAME} にログインできません。"
    exit $return_code
fi
```

<details><summary>全てのコード</summary>

```bash:.devcontainer/initialize.sh
#!/usr/bin/env bash
set -u

echo "Dev Containers Initialize Command"

echo "Azure CLIを確認します..."
which az
return_code=$?

if [ $return_code -ne 0 ]; then
    # Azure CLI がインストールされていない場合には中断
    echo "Azure CLIのインストールが必要です。"
    echo "https://learn.microsoft.com/ja-jp/cli/azure/install-azure-cli-linux"
    echo "  sudo apt-get update"
    echo "  sudo apt-get install ca-certificates curl apt-transport-https lsb-release gnupg"
    exit $return_code
fi

echo "設定ファイルをロードします..."
source .devcontainer/settings

echo "Azure にログインしているかどうかを確認します..."
needs_login=0
az account show
return_code=$?

if [ $return_code -ne 0 ]; then
    # ログインしていない
    needs_login=1
elif [ $(az account show --query "tenantId" --output tsv) != "${AZURE_TENANT_ID}" ]; then
    # 異なるテナントにログイン済
    needs_login=1
fi

if [ $needs_login -eq 1 ]; then
    # 未ログイン又は異なるテナントIDの場合は az login
    echo "Azure にログインします..."
    az login --tenant "${AZURE_TENANT_ID}"
    return_code=$?

    if [ $return_code -ne 0 ]; then
        # 終了コードがエラーなら中断
        echo "Azure へのログインに失敗しました。"
        exit $return_code
    fi
fi

echo "Azure アカウントのサブスクリプションを ${AZURE_SUBSCRIPTION_NAME} に変更します..."
az account set --name $AZURE_SUBSCRIPTION_NAME
return_code=$?

if [ $return_code -ne 0 ]; then
    # 終了コードがエラーなら中断
    echo "サブスクリプション ${AZURE_SUBSCRIPTION_NAME} をアクティブにできません。"
    exit $return_code
fi

echo "Azure Container Registry にログインします..."
az acr login --name $ACR_NAME
return_code=$?

if [ $return_code -ne 0 ]; then
    # 終了コードがエラーなら中断
    echo "Azure Container Registry ${ACR_NAME} にログインできません。"
    exit $return_code
fi

echo "Azure Container Registry にログインします..."
az acr login --name $ACR_NAME
return_code=$?

if [ $return_code -ne 0 ]; then
    # 終了コードがエラーなら中断
    echo "Azure Container Registry ${ACR_NAME} にログインできません。"
    exit $return_code
fi
```

```bash:.devcontainer/settings
# ACR にログインする Azure アカウントのテナントID
AZURE_TENANT_ID=********-****-****-****-************
# ACR にログインする Azure アカウントのサブスクリプション名
AZURE_SUBSCRIPTION_NAME=YourSubscription
# Azure Container Registry の名前
ACR_NAME=yourregistry
```
</details>

## 実行結果
- 正常パターン
    ```
    Running the initializeCommand from devcontainer.json...
    
    Start: Run: /bin/sh -c bash .devcontainer/initialize.sh
    Dev Containers Initialize Command
    Azure CLIを確認します...
    /usr/bin/az
    設定ファイルをロードします...
    Azure にログインしているかどうかを確認します...
    Please run 'az login' to setup account.
    Azure にログインします...
    A web browser has been opened at https://login.microsoftonline.com/********-****-****-****-************/oauth2/v2.0/authorize. Please continue the login in the web browser. If no web browser is available or if the web browser fails to open, use device code flow with `az login --use-device-code`.
    tcgetpgrp failed: Not a tty
    [
      {
        "cloudName": "AzureCloud",
        "homeTenantId": "********-****-****-****-************",
        "id": "********-****-****-****-************",
        "isDefault": true,
        "managedByTenants": [],
        "name": "YourSubscription",
        "state": "Enabled",
        "tenantId": "********-****-****-****-************",
        "user": {
          "name": "azureuser@example.com",
          "type": "user"
        }
      }
    ]
    Azure アカウントのサブスクリプションを YourSubscription に変更します...
    Azure Container Registry にログインします...
    Login Succeeded
    ```
- Azure CLIがインストールされていない場合
    ```
    Start: Run: /bin/sh -c bash .devcontainer/initialize.sh
    Dev Containers Initialize Command
    Azure CLIを確認します...
    Azure CLIのインストールが必要です。
    https://learn.microsoft.com/ja-jp/cli/azure/install-azure-cli-linux
      sudo apt-get update"
      sudo apt-get install ca-certificates curl apt-transport-https lsb-release gnupg
    ```

- 異なるテナントにログインしていた場合
    ```
    Start: Run: /bin/sh -c bash .devcontainer/initialize.sh
    Dev Containers Initialize Command
    Azure CLIを確認します...
    /usr/bin/az
    設定ファイルをロードします...
    Azure にログインしているかどうかを確認します...
    {
      "environmentName": "AzureCloud",
      "homeTenantId": "********-****-****-****-************",
      "id": "********-****-****-****-************",
      "isDefault": true,
      "managedByTenants": [],
      "name": "AnotherSubscription ",
      "state": "Enabled",
      "tenantId": "********-****-****-****-************",
      "user": {
        "name": "azureuser@example.com",
        "type": "user"
      }
    }
    Azure にログインします...
    A web browser has been opened at https://login.microsoftonline.com/********-****-****-****-************/oauth2/v2.0/authorize. Please continue the login in the web browser. If no web browser is available or if the web browser fails to open, use device code flow with `az login --use-device-code`.
    tcgetpgrp failed: Not a tty
    [
      {
        "cloudName": "AzureCloud",
        "homeTenantId": "********-****-****-****-************",
        "id": "********-****-****-****-************",
        "isDefault": true,
        "managedByTenants": [],
        "name": "YourSubscription ",
        "state": "Enabled",
        "tenantId": "********-****-****-****-************",
        "user": {
          "name": "azureuser@example.com",
          "type": "user"
        }
      }
    ]
    Azure アカウントのサブスクリプションを YourSubscription  に変更します...
    Azure Container Registry にログインします...
    Login Succeeded
    ```
- 当該テナントが利用できない場合
    ```
    Start: Run: /bin/sh -c bash .devcontainer/initialize.sh
    Dev Containers Initialize Command
    Azure CLIを確認します...
    /usr/bin/az
    設定ファイルをロードします...
    Azure にログインしているかどうかを確認します...
    {
      "environmentName": "AzureCloud",
      "homeTenantId": "********-****-****-****-************",
      "id": "********-****-****-****-************",
      "isDefault": true,
      "managedByTenants": [],
      "name": "YourSubscription",
      "state": "Enabled",
      "tenantId": "********-****-****-****-************",
      "user": {
        "name": "azureuser@example.com",
        "type": "user"
      }
    }
    Azure にログインします...
    Failed to resolve tenant '********-****-****-****-************'.
    
    Error detail: {"error":"invalid_tenant","error_description":"AADSTS90002: Tenant '********-****-****-****-************' not found. Check to make sure you have the correct tenant ID and are signing into the correct cloud. Check with your subscription administrator, this may happen if there are no active subscriptions for the tenant.
    Trace ID: ********-****-****-****-************
    Correlation ID: ********-****-****-****-************
    Timestamp: 2023-05-24 05:57:31Z","error_codes":[90002],"timestamp":"2023-05-24 05:57:31Z","trace_id":"********-****-****-****-************","correlation_id":"********-****-****-****-************","error_uri":"https://login.microsoftonline.com/error?code=90002"}
    Azure へのログインに失敗しました。
    ```
- 当該サブスクリプションが利用できない場合
    ```
    Running the initializeCommand from devcontainer.json...
    
    Start: Run: /bin/sh -c bash .devcontainer/initialize.sh
    Dev Containers Initialize Command
    Azure CLIを確認します...
    /usr/bin/az
    設定ファイルをロードします...
    Azure にログインしているかどうかを確認します...
    {
      "environmentName": "AzureCloud",
      "homeTenantId": "********-****-****-****-************",
      "id": "********-****-****-****-************",
      "isDefault": true,
      "managedByTenants": [],
      "name": "YourSubscription",
      "state": "Enabled",
      "tenantId": "********-****-****-****-************",
      "user": {
        "name": "azureuser@example.com",
        "type": "user"
      }
    }
    Azure アカウントのサブスクリプションを YourSubscription に変更します...
    The subscription of 'yoursubscription' doesn't exist in cloud 'AzureCloud'.
    サブスクリプション YourSubscription をアクティブにできません。
    ```
- ACRにログインできない場合
    ```
    Start: Run: /bin/sh -c bash .devcontainer/initialize.sh
    Dev Containers Initialize Command
    設定ファイルをロードします...
    Azure CLIを確認します...
    /usr/bin/az
    Azure にログインしているかどうかを確認します...
    {
      "environmentName": "AzureCloud",
      "homeTenantId": "********-****-****-****-************",
      "id": "********-****-****-****-************",
      "isDefault": true,
      "managedByTenants": [],
      "name": "YourSubscription",
      "state": "Enabled",
      "tenantId": "********-****-****-****-************",
      "user": {
        "name": "azureuser@example.com",
        "type": "user"
      }
    }
    Azure アカウントのサブスクリプションを YourSubscription に変更します...
    Azure Container Registry にログインします...
    The resource with name 'yourregistry' and type 'Microsoft.ContainerRegistry/registries' could not be found in subscription 'YourSubscription (********-****-****-****-************)'.
    Using 'yourregistry.azurecr.io' as the default registry login server.
    Could not connect to the registry login server 'yourregistry.azurecr.io'. Please verify that the registry exists and the URL 'https://yourregistry.azurecr.io/v2/' is reachable from your environment.
    Try running 'az acr check-health -n yourregistry --yes' to diagnose this issue.
    Azure Container Registry yourregistry にログインできません。
    ```

いずれの場合も、エラーが発生すると以下のようなダイアログが表示され、Dev Containers環境は起動しません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/0ecbf4ff-d539-ddd6-c96f-9b453a7de4c9.png)

# 最後に
ベース イメージをビルド済のイメージにしても、Dev Containersとしてのビルドという動作は存在し
例えばACR上のイメージが更新された場合等にユーザー側でもリビルドが必要となります。
Dev Containersはわからない事が多いので、まだまだベストなプラクティスはよくわからないので
今後も研究予定です。