danielwertheim

danielwertheim


notes from a passionate developer

Share


Sections


Tags


Disclaimer

This is a personal blog. The opinions expressed here represent my own and not those of my employer, nor current or previous. All content is published "as is", without warranty of any kind and I don't take any responsibility and can't be liable for any claims, damages or other liabilities that might be caused by the content.

Azure DevOps - Service Containers & Docker Compose

I've been wrestling a bit with using service containers in an Azure DevOps pipeline. The idea is to spin up some services (in my case three instances of NATS) that I then use to run my tests against. I failed miserably. I spent several hours trying different combinations. And finally gave up and used Docker Compose instead.

Before presenting the working solution, using good "old" Docker Compose, I'll present what I tried and found not working. This so that we can learn. Or hopefully, so that you or some else knows how to fix it and can guide me to get it working.

Almost got it

It worked great, until I needed to pass configuration to the services. Lets look at one of my attempts.

resources:
  containers:
  - container: nats
    image: nats:2.1
    ports:
    - 4222:4222

  - container: nats-auth
    image: nats:2.1
    env:
      MyNats_credentials__user: $(MyNats_credentials__user)
      MyNats_credentials__pass: $(MyNats_credentials__pass)
    ports:
    - 4223:4222
    volumes:
    - /docker_vol_config:/config
    command: '--config /config/auth.config'

It maps the volumes etc and starts the containers. If you pass an invalid path via the -c switch, the nats-server will output an error about it and Azure DevOps will warn you that the container didn't start.

The referenced auth.config has the following contents:

authorization {
  user: $MyNats_credentials__user
  password: $MyNats_credentials__pass
  timeout: 10
}

I then define usage of the resource containers like this:

stages:
- stage: Build
  jobs:
  - job: BuildTestPack
    displayName: 'Build, test & pack'
    timeoutInMinutes: 5
    cancelTimeoutInMinutes: 2
    services:
      nats: nats
      nats-auth: nats-auth

Most of my tests runs against localhost:4222 which works fine as it has no specific config. But the test running against localhost:4223 "nats-auth" fails as it can not connect. And I'm not talking about the expected connection failures due to e.g. passing invalid credentials.

It's nothing with the ENV-variables. I can see that Azure DevOps resolves them and assigns them via -e switches.

Although, something is fishy, as I do believe this should work:

command: '--user $MyNats_credentials__user --pass $MyNats_credentials__pass'

But that also fails. I can get it to work by hard-coding the values and not using an auth.config file:

resources:
  containers:
  - container: nats
    image: nats:2.1
    ports:
    - 4222:4222

  - container: nats-auth
    image: nats:2.1
    ports:
    - 4223:4222
    volumes:
    - /docker_vol_config:/config
    command: '--user foo --pass bar'

I've tried various things. And finally just gave up.

Docker Compose - a working solution

I just stopped using Azure DevOps resource- and service containers and started using Docker Compose instead. I was already using Docker Compose for my local development so it was simple to get going.

First lets fix the pipeline. Remove all resource containers and service containers.

azure-pipelines-ci.yml

name: $(SemVer)

variables:
  BuildRev: $[counter(format('{0:yyyyMMdd}', pipeline.startTime), 1)]
  SemVer: $[format('{0:yyyy}.{0:MM}.{0:dd}-pre{1}', pipeline.startTime, variables.BuildRev)]
  CommitId: $(Build.SourceVersion)

trigger:
  batch: true
  branches:
    include:
    - master

pr:
  autoCancel: true
  branches:
    include:
    - master

pool:
  vmImage: ubuntu-18.04

stages:
- template: azure-templates/stage-build.yml

Then add tasks in the job to start and stop the "services" via Docker Compose.

azure-templates/stage-build.yml
I've removed some tasks, but the project is open source so...

stages:
- stage: Build
  jobs:
  - job: BuildTestPack
    displayName: 'Build, test & pack'
    timeoutInMinutes: 5
    cancelTimeoutInMinutes: 2
    steps:
    - task: DockerCompose@0
      displayName: 'Start NATS Services'
      inputs:
        containerregistrytype: 'Container Registry'
        dockerComposeFile: 'docker-compose.yml'
        dockerComposeFileArgs: |
          MyNats_credentials__user=$(MyNats_credentials__user)
          MyNats_credentials__pass=$(MyNats_credentials__pass)
        action: 'Run a Docker Compose command'
        dockerComposeCommand: 'up -d'

    - task: DotNetCoreCLI@2
      displayName: 'Build Solution'
      inputs:
        command: build
        projects: 'src/*.sln'
        arguments: '-c $(BuildConfiguration) --no-incremental --nologo -p:TreatWarningsAsErrors=true -p:Version=$(SemVer) -p:InformationalVersion=$(CommitId)'

    - task: DotNetCoreCLI@2
      displayName: 'IntegrationTests'
      inputs:
        command: test
        projects: 'src/**/IntegrationTests.csproj'
        arguments: '-c $(BuildConfiguration) --no-build'
        testRunTitle: 'IntegrationTests'

    - task: DockerCompose@0
      displayName: 'Stop NATS Services'
      condition: always()
      inputs:
        containerregistrytype: 'Container Registry'
        dockerComposeFile: 'docker-compose.yml'
        action: 'Run a Docker Compose command'
        dockerComposeCommand: 'down'

I'm not using Docker Compose to build a custom container that builds my code and runs my test. I'm just using it to start the services I'm dependent on to test my code.

The tasks involving Docker Compose are the following tasks:

docker-compose up -d

- task: DockerCompose@0
  displayName: 'Start NATS Services'
  inputs:
    containerregistrytype: 'Container Registry'
    dockerComposeFile: 'docker-compose.yml'
    dockerComposeFileArgs: |
      MyNats_credentials__user=$(MyNats_credentials__user)
      MyNats_credentials__pass=$(MyNats_credentials__pass)
    action: 'Run a Docker Compose command'
    dockerComposeCommand: 'up -d'

docker-compose down

- task: DockerCompose@0
  displayName: 'Stop NATS Services'
  condition: always()
  inputs:
    containerregistrytype: 'Container Registry'
    dockerComposeFile: 'docker-compose.yml'
    action: 'Run a Docker Compose command'
    dockerComposeCommand: 'down'

I've not included any scripts to ensure that my containers are running. The down task is probably not necessary as I hope Azure DevOps will clean all resources when the build completes, but I've included a task for tearing them down and I selected it to always run via: condition: always().

docker-compose-yml

version: "3"
services:
  nats:
    image: "nats"
    ports:
      - "4222:4222"

  nats-auth:
    image: "nats"
    ports:
      - "4223:4222"
    environment:
      - MyNats_credentials__user
      - MyNats_credentials__pass
    command: "-c /nats_config/auth.config"
    volumes:
      - ./nats_config:/nats_config

  nats-tls:
    image: "nats"
    ports:
      - "4224:4222"
    command: "-c /nats_config/tls.config"
    volumes:
      - ./nats_config:/nats_config

Feel free to guide me

Even though I like the working solution, due to the fact that I't works the same way locally, I would really, really like if someone could say: "but you have an error here. You just need to change this..."

Anyways. Maybe it solves an issue for you. If, then "your welcome". If not...

Merry Christmas,

//Daniel

View Comments