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