Azure DevOps で Dev Container Build and Run を使う

# Dev Container Build and Run とは？
Azure DevOpsの拡張機能として提供されており、
Azure PipelinesでDev Containersのコンテナをビルド、Azure Container Registryにイメージをpushするタスクが利用できます。

Dev Containersはクライアント環境でビルドされたそれぞれのコンテナ イメージを利用するシナリオが多いですが、
ビルドをAzure上で実行させ利用者(開発者)はビルドされたイメージをpullするだけになり、より固定的な環境として開発チーム内で共通化する事ができます。

Dev Containersは開発環境をコンテナ上で構築する事により、セットアップ手順の省力化やチーム内での共通化、他のプロジェクトに影響のない環境の独立化など、様々なメリットのあるVisual Stucio Code 開発環境のソリューションの1つです。
ここではDev Containersについての詳しい説明は割愛します。

https://code.visualstudio.com/docs/devcontainers/containers

https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers

# 前提
説明では、Azure DevOps(Repos、Pipelines)、Azure Container Registryを利用しているものとします。

# プロジェクト

## リポジトリ

https://github.com/devcontainers/ci

## DevOps 拡張機能

https://marketplace.visualstudio.com/items?itemName=devcontainers.ci

以下機能が提供されます。

- PipelineでのDevcontainersCiタスクの実行
- Pipeline エディタ上でのDevcontainersCiタスクの追加

## ライセンス
MIT

https://marketplace.visualstudio.com/items/devcontainers.ci/license

# Azure DevOpsへの拡張機能のインストール
1. Marketplace から **Get it free** をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/f1db0eb1-4a57-d2e6-f002-23dd62d08fb2.png)

1. インストール先の組織を選択し、**Install** をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/4c53dcad-822c-6c5d-8bd6-40dc2badd347.png)

1. **Proceed to organization** をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/b5ef87ee-7f9b-10bf-c8da-bf3ca5b5c3dc.png)

1. Organization Settings の Extensions に追加されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/a661a737-6ee6-6f03-cf1a-854abd7812bb.png)

# Dev Containers 作成

1. Azure Reposに新しいリポジトリを作成し、`.devcontainers/devcontainer.json`を作成、リポジトリにpush
(Debian 環境を作る例)

    ```javascript:.devcontainer/devcontainer.json
    // For format details, see https://aka.ms/devcontainer.json. For config options, see the
    // README at: https://github.com/devcontainers/templates/tree/main/src/debian
    {
        "name": "Debian",
        // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
        "image": "mcr.microsoft.com/devcontainers/base:bullseye"
    
        // Features to add to the dev container. More info: https://containers.dev/features.
        // "features": {},
    
        // Use 'forwardPorts' to make a list of ports inside the container available locally.
        // "forwardPorts": [],
    
        // Configure tool-specific properties.
        // "customizations": {},
    
        // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
        // "remoteUser": "root"
    }
    ```

    チュートリアル: [Developing inside a Container using Visual Studio Code Remote Development](https://code.visualstudio.com/docs/devcontainers/containers)

# パイプライン 作成

## パイプラインがContainer Registryにpushするためのトークンを作成

1. Container Registryのポータルから、**トークン** → **追加** をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/2e3b1237-161c-d5a1-2081-aaa2134f1a46.png)

1. **トークン** に名前を、**スコープ マップ** から `_repositries_push` を選択し、**作成** をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/dc16c973-37d4-1172-0c0b-bb956268ea8b.png)

1. 作成されたトークンをクリックし、**password1** の **アクション** から **↻** (生成)をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/ab9af41d-d963-a552-f34b-a6944d11b463.png)

1. **有効期限を設定しますか？** をクリックし、有効期限を設定する(省略可能だが、セキュアにするために設定した方がよい)。  
  **生成** をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/145962c9-f253-c1a0-b104-c272bd3feb0a.png)

1. **はい** を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/7e4fd942-c8e5-003b-0715-57c25d7e8edc.png)

1. パスワードが生成されるので、記録しておく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/da845c0a-b478-6c8f-2cd9-7edc338607ca.png)

## 新しいパイプラインを作成

1. DevOps プロジェクトの **Pipelines** から **New pipeline** をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/983697dc-1189-36e0-5f05-f45694bb2ea3.png)

1. **Azure Repos Git** を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/abffeaf3-8e2d-0b6b-23a3-e9e05bb19bc7.png)

1. ビルド対象のリポジトリを選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/d7e339f3-d9ff-d4f9-fc3b-b27c8d97d5eb.png)

1. **Starter pipeline** を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/20663681-291b-8c30-21cd-7b7833a904ad.png)

1. `azure-pipelines.yml`ファイルが作成されるので、`script`に`docker login`のコマンドと環境変数`ACR_TOKEN`の設定を記述する。  
  `docker login`の`-u`パラメータ(ユーザー名)にはContainer Registryに作成したトークンの名前、`-p`パラメータ(パスワード)に変数`ACR_TOKEN`を指定
    ```yaml:azure-pipelines.yml
    # Starter pipeline
    # Start with a minimal pipeline that you can customize to build and deploy your code.
    # Add steps that build, run tests, deploy, and more:
    # https://aka.ms/yaml
    
    trigger:
    - main
    
    pool:
      vmImage: ubuntu-latest
    
    steps:
    - script: |
          docker login -u build-container -p $ACR_TOKEN yourregistry.azurecr.io
      displayName: 'Log in to Container Registry'
      env:
        ACR_TOKEN: $(ACR_TOKEN)
    ```

1. **Show assistant** をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/51814c8b-9abd-1659-655d-48326b07f220.png)

## Devcontainers CI Taskの追加

1. **Devcontainers CI Task** をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/766b82e4-0c10-a025-02d6-5cec8f1c72b2.png)

1. 対話形式で項目を入力し、**Add** をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/6d925241-80eb-d924-1f11-69c9c2d8ed73.png)

    設定内容は下記の"設定値"に反映されるため、記述式がわかる場合にはこの対話形式を利用する必要はありません。

    |項目|設定値|入力値|入力例|
    |---|---|---|---|
    |Image name (including registry)|imageName|イメージの名前(レジストリ含む)|`yourregistry.azurecr.io/example-dev-container`|
    |One or more comma-separated image tags (defaults to latest)|imageTag|pushされるイメージに設定されるタグ|`1.0,latest`|
    |Platforms for which the image should be built.<br>If omitted, defaults to the platform of the Azure DevOps Agent.<br>Multiple platforms should be comma separated.|platform|[Multiplatform Dev Container Builds](https://github.com/devcontainers/ci/blob/main/docs/multi-platform-builds.md)|`linux/amd64,linux/arm64`|
    |Specify the command to run after building the dev container image|runCmd|コンテナー イメージのビルド後に実行するコマンド|`make ci-build`|
    |Specify a child folder (containing a .devcontainer) instead of using the repository root|subFolder|`.devcontainer`フォルダーを含むフォルダーへのリポジトリ ルートからの相対パス|`folderB`|
    |Specify environment variables to pass to the docker run command|env|実行時に開発コンテナに渡す環境変数|`ACR_TOKEN: $(ACR_TOKEN)`|
    |Control when images are pushed to the registry|push|`filter`に設定すると、`sourceBranchFilterForPush`、`buildReasonsForPush`、及び`pushOnFailedBuild`の条件が満たされた場合にイメージがpushされる。`imageName`が設定されている場合は既定で`filter`|Never push(`never`)<br>Push if buildReasonsForPush, sourceBranchFilterForPush, and pushOnFailedBuild conditions are met(`filter`)<br>Always push(`always`)|
    |☑Control whether to push the image on failed builds (if push==filter)|pushOnFailedBuild|ビルド失敗時にpushを実行するかどうか||
    |Set the Build Reasons that should trigger a push of the dev container image (if push=filter).<br>Defaults to Manual, IndividualCI, BatchedCI.<br>(see https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&viewFallbackFrom=vsts&tabs=yaml)|buildReasonsForPush|レジストリにpushできる Build.Reason 値を指定|`Manual`<br>`InidividualCI`|
    |Set the source branches (e.g. refs/heads/main) that are allowed to trigger a push of the dev container image (if push=filter).<br>Leave empty to allow all.|sourceBranchFilterForPush|レジストリにpushされるブランチの指定<br>(※`main`等ではなく、[refs](https://git-scm.com/book/ja/v2/Git%E3%81%AE%E5%86%85%E5%81%B4-Git%E3%81%AE%E5%8F%82%E7%85%A7)等からコミット ハッシュの書き込まれている箇所を指定する必要があります)|`refs/heads/main`|
    |☑For non-root Dev Containers (i.e. where `remoteUser` is specified), the action attempts to make the container user UID and GID match those of the host user. Set this to true to skip this step (defaults to false)|skipContainerUserIdUpdate|ルート以外の Dev Container(`remoteUser`が指定されている)場合、コンテナー ユーザーのユーザーIDとグループIDをホスト ユーザーのものと一致させる||
    |Specify additional images to use for build caching|cacheFrom|ビルド キャッシュに使用する追加のイメージを指定||
    |☑Builds the image with `--no-cache` (takes precedence over `cacheFrom`)|noCache|ビルドに`--no-cache`オプションを指定||

1. YAMLファイルに`DevcontainersCi`のタスクが追加される。
    ```yaml:azure-pipelines.yml
    # Starter pipeline
    # Start with a minimal pipeline that you can customize to build and deploy your code.
    # Add steps that build, run tests, deploy, and more:
    # https://aka.ms/yaml
    
    trigger:
    - main
    
    pool:
      vmImage: ubuntu-latest
    
    steps:
    - script: |
          docker login -u build-container -p $ACR_TOKEN yourregistry.azurecr.io
      displayName: 'Log in to Container Registry'
      env:
        ACR_TOKEN: $(ACR_TOKEN)
    - task: DevcontainersCi@0
      inputs:
        imageName: 'yourregistry.azurecr.io/debian'
        push: 'filter'
        sourceBranchFilterForPush: 'refs/heads/main'
      displayName: 'Run a DevcontainersCi'
    ```

1. **Variables** をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/7ae9c9e3-1d61-6cf4-bb08-86eef92a9b6d.png)

1. **New variable** をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/84a4b786-efe3-4ce4-4452-97f74c9fb5f2.png)

1. **Name** に変数名を指定、**Keep this value secret** にチェックを入れ、**Value** にContainer Registryに作成したトークンのパスワードを入力し、**OK**をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/93901b3e-58d5-714f-eccb-23521cc8c2e5.png)

1. **Save** をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/835a4847-c8fd-a211-0a43-acd95ff36225.png)

1. **v**をクリックし、**Save** をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/9d4f7deb-823b-ce32-cda2-d5b928688cd4.png)

## パイプライン 実行

1. Azure Pipelinesから作成されたパイプラインをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/44261a49-fa29-81d4-03bf-2cb4a2a52561.png)

1. **Run pipeline** をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/0ec52509-ea81-4e2d-c694-67442d545e73.png)

1. 成功すると、Container Registryのリポジトリにイメージがpushされる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/397e6f9a-dbed-f2a5-76ef-484a5a874066.png)

# バージョン タグについて
パイプライン タスク内でカウンタが利用できます。
以下のように設定すれば、`latest`のタグを最新のイメージに更新、`ver_major`.`ver_minor`の値でタグが設定される。
`ver_major`は 1、`ver_minor`は`ver_major`が変化するまで0からインクリメントされ、
`ver_major`が 2 等に変化すると`ver_minor`は 0 にリセットされる。

```yaml:azure-pipelines.yml
variables:
  ver_major: 1
  ver_minor: $[counter(variables['ver_major'], 0)]

- task: DevcontainersCi@0
  inputs:
    # ...
    imageTag: '$(ver_major).$(ver_minor),latest'
    # ...
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/e341f033-4f17-891a-5ed2-db2faf2cfc57.png)

## 参考
Pipelineで利用する変数、式について

https://learn.microsoft.com/ja-jp/azure/devops/pipelines/process/variables

https://learn.microsoft.com/ja-jp/azure/devops/pipelines/process/expressions

# Visual Studio Codeでの利用
WSL＋Ubuntu の場合

1. Dev ContainerはContainer Registryのイメージを利用するように変更します。

    ```javascript:.devcontainer/devcontainer.json
    {
        "name": "Debian",
        "image": "yourregistry.azurecr.io/debian/releases:latest"
    }
    ```

1. Azure CLIを利用し、Azureにログイン
  [az acr | Microsoft Learn](https://learn.microsoft.com/ja-jp/cli/azure/acr?view=azure-cli-latest#az-acr-login)

    ```bash
    $ az acr login --name (Container Registry名)
    ```

1. Visual Studio Codeを起動

    ```bash
    $ cd (リポジトリ クローン先ディレクトリ)
    $ code .
    ```

1. 左下の`><`をクリックし、**Reopen in Container** をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/f9c26e40-f8f9-5f37-41ac-a7a761771a63.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/6f8e5a29-2b70-fd2e-4918-d3581d7ca10b.png)

1. 左下に **Dev Container :~** と表示されればコンテナ環境での開発が利用できるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116331/f58da756-bf22-4d17-8215-f41df800fbed.png)

# 応用

> Pull Requestでmain以外のブランチでも実行してみたい。
> でもその時にバージョンがインクリメントされたら困る。

そのような場合のテクニック

- main ブランチ以外への push は Pipeline でトリガしない。  
  但し、Manual Run で実行は可能。
- マイナー バージョン番号はブランチ別に管理
- main ブランチの更新は releases ディレクトリに push
- Dev Container は devcontainer-build ディレクトリで開発
- 開発者の Dev Container は Azure Container Registry のイメージを使用

```javascript:.devcontainer/devcontainer.json
{
    "name": "Debian",
    "image": "yourregistry.azurecr.io/debian/releases:latest"
}
```

```javascript:devcontainer-build/.devcontainer/devcontainer.json
{
    "name": "Debian",
    "image": "mcr.microsoft.com/devcontainers/base:bullseye"
}
```

```yaml:azure-pipelines.yml
trigger:
  branches:
    include:
    # IndividualCI をトリガするのは main のみ (Manual は他のブランチも実行可能)
    - main
  paths:
    include:
    # トリガ対象のディレクトリ
    - devcontainer-build/*

pool:
  vmImage: ubuntu-latest

variables:
  ver_major: 1
  # ブランチ名.(major) 別に minor のカウンタを管理
  ver_minor: $[counter(format('{0}.{1}', variables['Build.SourceBranch'], variables['ver_major']), 0)]
  ${{ if eq(variables['Build.SourceBranchName'], 'main') }}:
    # main ブランチの更新は releases に push
    stage: 'releases'
  ${{ else }}:
    # main ブランチ以外はブランチ名に push
    stage: '$(Build.SourceBranchName)'

steps:
- script: |
      docker login -u build-container -p $ACR_TOKEN yourregistry.azurecr.io
  displayName: 'Log in to Container Registry'
  env:
    # Container Registry のトークン(パスワードは環境変数で管理)
    ACR_TOKEN: $(ACR_TOKEN)
- task: DevcontainersCi@0
  inputs:
    # ステージ別のディレクトリに push
    imageName: 'yourregistry.azurecr.io/debian/$(stage)'
    # インクリメントされるバージョンを設定、latest を更新
    imageTag: '$(ver_major).$(ver_minor),latest'
    # ビルド対象のディレクトリ
    subFolder: 'devcontainer-build'
  displayName: 'Run a DevcontainersCi'
```

# その他

Azure DevOpsに拡張機能をインストールする事により、これらの機能が提供されます。
拡張機能のバージョンが指定できない事により、破壊的変更があった場合の対応や
提供終了された場合の対応等は、考慮する必要があるかもしれません。
(但し幸いにも、プロジェクトは[オープンソース](https://github.com/devcontainers/ci)、ライセンスはMITとなっています)