I’m going to attempt to catalog how I’m using Docker to test and build containers that are for deployment into Amazon ECS.
Build Process
- Use
Dockerfile.build
- Uses Cake:
dotnet restore
dotnet build
dotnet test
dotnet publish
- Uses Cake:
- Save running image to container
- Copy
publish
directory out of container - Use
Dockerfile
- Copy
publish
directory into image
- Copy
- Push built image to ECS
Driving the build: Cake
I love Cake and have contributed some minor things to it. It does support .NET Core. However, the nuget.exe
used to drive some critical things like nuget push
does not. push
is actually the only command I need that isn’t on .NET Core. So I standardized on requiring Mono for just the build container.
My base Cake file: build.cake
var target = Argument("target", "Default"); var tag = Argument("tag", "cake"); Task("Restore") .Does(() => { DotNetCoreRestore("src/\" \"test/\" \"integrate/"); }); Task("Build") .IsDependentOn("Restore") .Does(() => { DotNetCoreBuild("src/**/project.json\" \"test/**/project.json\" \"integrate/**/project.json"); }); Task("Test") .IsDependentOn("Build") .Does(() => { var files = GetFiles("test/**/project.json"); foreach(var file in files) { DotNetCoreTest(file.ToString()); } }); Task("Publish") .IsDependentOn("Test") .Does(() => { var settings = new DotNetCorePublishSettings { Framework = "netcoreapp1.1", Configuration = "Release", OutputDirectory = "./publish/", VersionSuffix = tag }; DotNetCorePublish("src/Server", settings); }); Task("Default") .IsDependentOn("Test"); RunTarget(target);
I broke out all the steps as I often run Cake for each step during development. You’ll notice that each dotnet
command behaves differently. It’s very annoying.
I have a project structure that usually goes like this:
src
– Source filestest
– Unit tests for those source filesintegrate
– Integration tests that should run separately from unit tests.misc
– Other code stuff
Other things to notice:
Default
is test. Don’t want to accidently publishpublish
has a hard-coded entry point. Probably should make that argument.tag
is a tag I want to tag the published build with. I want to see something unique for each publish. I default this withcake
for local publishes.
The Build Container: Dockerfile.build
I actually started with following the little HOW-TO from the ASP.NET team from here:
FROM cl0sey/dotnet-mono-docker:1.1-sdk ARG TAG=docker ENV TAG ${TAG} WORKDIR /app RUN mkdir /publish COPY . . RUN ./build.sh -t publish --scriptargs "--tag=${TAG}"
Notice the source image: cl0sey/dotnet-mono-docker:1.1-sdk
Someone was nice enough to already make a Docker image with Mono on top of the base microsoft/dotnet:1.1-sdk-projectjson
image. The SDK image is what is needed for using all of the dotnet cli
commands that aren’t just running.
Notice:
ARG
andENV
declarations for specifying thetag
variable. I thinkARG
declares it andENV
allows it to be used as abash
-like variable.- creating a
publish
directory. - How I pass the
tag
variable to theCake
script.
The Deployment Container: Dockerfile
FROM microsoft/dotnet:1.1.0-runtime COPY ./publish /app WORKDIR /app EXPOSE 5000 ENV ASPNETCORE_ENVIRONMENT beta ENTRYPOINT ["dotnet", "Server.dll"]
Notice:
- I actually use the official runtime image.
COPY
command to grab the localpublish
directory and put it in theapp
directory inside the container.- I keep the default 5000 port. Why not? It’s all hidden in AWS.
- I just declared my environment to be
beta
instead ofstaging
ENTRYPOINT
has to be an array of strings.Server.dll
is the executable assembly.
Hanging It All Together: CircleCI
I’m using CircleCI as my CI service because it’s free/cheap. Also, it runs Docker and can do Docker inside Docker. The docker
commands will work just about anywhere though.
machine: services: - docker dependencies: override: - docker info test: override: - docker build -t build-image --build-arg TAG="${CIRCLE_BRANCH}-${CIRCLE_BUILD_NUM}" -f Dockerfile.build . - docker create --name build-cont build-image deployment: beta: branch: master commands: - docker cp build-cont:/app/publish/. publish/ - docker build -t server-api:latest . - docker tag server-api:latest $AWS_ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com/server-api:$CIRCLE_BUILD_NUM - ./push.sh
Notice:
test
phase- The
test
phase doesdocker build
onDockerfile.build
This file does everything, including publish. The image is tagged asbuild-image
. test
phase also creates a container calledbuild-cont
for possible deployment.- My
tag
is made of the branch name plus the build number. These areCircleCI
variables.
- The
deployment
phase- named
beta
I could have more environments for deployment, I guess. - locked to the
master
branch. When I push feature branches, only thetest
phase runs to test things. Only when merged intomaster
does it deploy. docker cp
copies thepublish
directory out of thebuild-cont
container.Dockerfile
is used withdocker build
and tagged asserver-api:latest
- I also explicitly tag the image with my AWS ECS specific name.
CircleCI
hides my AWS account id in an environment variable for me. push.sh
actually does the push to AWS.
- named
push.sh to AWS ECS
Finally, I want to save my Docker image.
#!/usr/bin/env bash configure_aws_cli(){ aws --version aws configure set default.region eu-west-1 aws configure set default.output json } push_ecr_image(){ eval $(aws ecr get-login --region eu-west-1) docker push $AWS_ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com/visibility-api:$CIRCLE_BUILD_NUM } configure_aws_cli push_ecr_image
The bash script is copied in part from something else more complicated. You can’t just do the push command from the circle.yaml
because of the need to use eval
to login to AWS. My AWS push creds are also locked in a CircleCI environment variable that the aws ecr get-login
command expects.