Azure PipelinesのジョブでDBのコンテナを立ててタスクで利用する


要約
---------

ドキュメントを見て試して、Pipelinesのジョブの中でPostgreSQLのコンテナを立ち上げてDBを使ったテストを動かしたという話です。

https://docs.microsoft.com/ja-jp/azure/devops/pipelines/process/service-containers


手順
------------

Azure Pipelinesではジョブ単位でコンテナを立ち上げておき、その後タスクを実行できます。

これを利用して.Net coreなプロジェクトのテストプロジェクトで実際のDBを使ってみました。

```
- project.sln
- Fukasawah.Sample1
  - Fukasawah.Sample1.csproj
- Fukasawah.Sample1.Tests
  - Fukasawah.Sample1.Tests.csproj
```

という感じのファイル構成をしているとき、次のようなパイプラインを書きます。

``` yml
trigger:
  - master

variables:
  DotnetVersion: 3.1.x
  BuildConfiguration: Release
  ProjectName: Fukasawah.Sample1 # 適宜書き換え
  TestDbName: testdb
  TestDbUser: testuser
  TestDbPassword: testpass

# ジョブで使うコンテナを定義して名前を付ける
resources:
  containers:
    - container: 'postgres'
      image: 'postgres:11-bullseye'
      ports:
        - 5432
      env:
        POSTGRES_DB: $(TestDbName)
        POSTGRES_USER: $(TestDbUser)
        POSTGRES_PASSWORD: $(TestDbPassword)
      # ヘルスチェックはコマンドに応じて
      options: >-
        --health-cmd pg_isready
        --health-interval 10s
        --health-timeout 5s
        --health-retries 5

stages:
  - stage: Build_and_Publish
    jobs:
      - job: Build
        pool:
          vmImage: "ubuntu-latest"

        services:
          # `サービス名: コンテナ名` で書く（複数同じサービスがあることはあまりないので、コンテナ名と一緒にすると紛れがないと思う）
          postgres: postgres
        variables:
          # テストプロジェクトで使う接続文字列を環境変数CONNECTION_STRINGで渡す。ポートだけは変わるので変数から埋め込む
          CONNECTION_STRING: "Host=localhost;Port=$(agent.services.postgres.ports.5432);Database=$(TestDbName);Username=$(TestDbUser);Password=$(TestDbPassword)"
        steps:
          - task: UseDotNet@2
            inputs:
              packageType: "sdk"
              version: $(DotnetVersion)
            displayName: "Install .NET Core SDK $(DotnetVersion)"

          - task: DotNetCoreCLI@2
            inputs:
              command: "build"
              projects: "**/$(ProjectName).csproj"
              arguments: "-c $(BuildConfiguration)"
            displayName: "dotnet build"

# TODO: DBの作成を行うジョブを書く場合はこのあたり。
#   接続文字列を読み取ってデータを流すスクリプトを用意したりする（dotnet ef migration && dotnet ef database updateとか。）
#   今回は、テストコード中で「dbContext.Database.EnsureCreated()」を呼び出して作成するので、やっていない。

          - task: DotNetCoreCLI@2
            inputs:
              command: "test"
              projects: "**/$(ProjectName).Tests.csproj"
              arguments: '-c $(BuildConfiguration) --collect:"XPlat Code Coverage"'
            displayName: "dotnet test"

          - task: PublishCodeCoverageResults@1
            inputs:
              codeCoverageTool: "Cobertura"
              summaryFileLocation: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"

          - task: DotNetCoreCLI@2
            inputs:
              # publish時にzipまで行うには一工夫
              # ref: https://devadjust.exblog.jp/27915866/
              command: "publish"
              publishWebProjects: false
              projects: "**/$(ProjectName).csproj"
              arguments: "-c $(BuildConfiguration) -o $(Build.SourcesDirectory)/publish --no-restore --no-build -p:DebugType=None -p:DebugSymbols=false"
            displayName: "dotnet publish"

          - task: CopyFiles@2
            inputs:
              contents: '$(ProjectName)/$(ProjectName)*.zip'
              targetFolder: $(Build.ArtifactStagingDirectory)
            displayName: "Copy zip to artifacts staging directory"

          - task: PublishBuildArtifacts@1
            inputs:
              pathToPublish: $(Build.ArtifactStagingDirectory)
              artifactName: BuildOutputs
            displayName: "Publish build artifacts"
```

ポイントは以下です。[ドキュメント](https://docs.microsoft.com/ja-jp/azure/devops/pipelines/process/service-containers)に書いてある通りです。

- `resources.containers[]:` に利用するコンテナの定義を書く
- `stages[].jobs[].services` にresourcesで書いたコンテナを参照する
- `$(agent.services.サービス名.ports.ポート番号)`でポート番号を変数から参照して埋め込む
    - コンテナの定義のポート番号はこの変数に使われるもので繋ぐためには使えない。うっかり重複してエラーになっても面倒なので変数で取るほうが良いと思う
    - ただ、dockerコマンドで起動しているだけなので、ポートマッピングしている場合はそのまま行けるかもしれない（`5432`ではなく`5432:5432`と書いたり）
 
docker-compose.ymlをazure-pipelines.ymlで書くようなイメージですね。



これを実行すると、こんな感じに実行されます。

![パイプラインのジョブの実行結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/1dedbffa-26e8-1c60-e665-d9c9e026a68d.png)


ジョブの最初の方のInitialize containersタスクでイメージのPullが行われるので、若干時間がかかっています。


### コンテナが動く状態になるまで待つ（healthcheck）

コンテナの起動はしたものの、プロセスが立ち上がりきらない場合もあり、その時にテストが走るとダメです。
ビルド時間的に間に合うでしょうと高をくくっても良いのですが、optionsでDockerのHealthcheckオプションを指定できます。
PostgreSQLでは[pg_isready](https://www.postgresql.jp/document/11/html/app-pg-isready.html)というユーティリティコマンドがあり、これを使うのが一般的なようです。(その分、次のタスクを実行するまでの待ち時間は増えます。)

``` yml
resources:
  containers:
    - container: 'postgres'
      image: 'postgres:11-bullseye'
      ports:
        - 5432
      env:
        POSTGRES_DB: $(TestDbName)
        POSTGRES_USER: $(TestDbUser)
        POSTGRES_PASSWORD: $(TestDbPassword)
      options: --health-cmd pg_isready --health-interval 3s --health-timeout 3s --health-retries 10
```