Building an Elixir Release into Docker-image using gitlab-ci. Part 1
Well, we are actively using Phoenix/Elixir in our projects for backend development, we also have RoR project as a frontend-service for Admin UI. Our project consists of a bunch of microservices written in Elixir/Erlang, and we are running it in production in Docker-containers linked together and composed by docker-compose.
On every push to project branch on gitlab, gitlab-ci runs tests, style checking, and other tasks. These tasks configured using .gitlab-ci.yml, and on every merge to master gitlab builds a release image for us and uploads it to gitlab container registry. After all we run `docker-compose pull && docker-compose up -d` on servers to download the latest release images and upgrade containers.
So following I will describe our release pipeline for Elixir services, using snippets from our project’s .gitlab-ci.yml.
We are using docker:latest image for our runner, and several stages:
Passing some variables:
Those variables used during release build, they will be available to all stages. E.g CONTAINER_RELEASE_IMAGE is used on release stage, as a link to push release image to. The POSTGRES_* variables are used to configure postgres service, and to connect later from containers.
Our build stage:
- docker build -f Dockerfile.build -t ci-project-build-$CI_PROJECT_ID:$CI_BUILD_REF .
- docker create
--name build_data_$CI_PROJECT_ID_$CI_BUILD_REF busybox /bin/true
- docker run --volumes-from build_data_$CI_PROJECT_ID_$CI_BUILD_REF --rm -t ci-project-build-$CI_PROJECT_ID:$CI_BUILD_REF
Before running this stage we create container which provides volumes for building artifacts, btw gitlab-ci has a cache volume itself for similar purposes, but I couldn’t make it working correctly with gitlab runner using docker image.
- docker run --rm
--volumes-from build_data_$CI_PROJECT_ID_$CI_BUILD_REF ci-project-build-$CI_PROJECT_ID:$CI_BUILD_REF sh -c "mix ecto.setup && mix test"
Notice what we must pass variables and link postgres manually since gitlab runner is passing variables only to the first level of docker, but we go deeply ;)
We could link any services as we want, e.g. we are using Kafka in production and on our test stage we make Kafka service available to running tests.
- docker run --rm
--volumes-from build_data_$CI_PROJECT_ID_$CI_BUILD_REF ci-project-build-$CI_PROJECT_ID:$CI_BUILD_REF sh -c "mix credo --strict"
Release task, we run it only on pushes to master:
- docker run
-e MIX_ENV=prod --rm -t ci-project-build-$CI_PROJECT_ID:$CI_BUILD_REF
sh -c "mix deps.get && mix compile && mix release"
- docker cp build_data_$CI_PROJECT_ID_$CI_BUILD_REF:/build/rel/$APP_NAME/releases/$APP_VERSION/$APP_NAME.tar.gz .
- docker build -t $CONTAINER_RELEASE_IMAGE .
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN gitlab.example.org:4567
- docker push $CONTAINER_RELEASE_IMAGE
We are using conform to achieve runtime configuration of release using environment variables. I use approach described in this blog-post: http://carlo-colombo.github.io/2016/05/04/The-3-E-Elixir-Exrm-and-Environment-Variables/
And task to cleanup things:
- docker rm -v build_data_$CI_PROJECT_ID_$CI_BUILD_REF
- docker rmi ci-project-build-$CI_PROJECT_ID:$CI_BUILD_REF
It removes container with volumes created for build artifacts and removes image used during pipeline. This task is running every time, despite results of any previous tasks.
Below is our Dockerfiles:
RUN apk add postgresql-client erlang-xmerl erlang-tools --no-cache
ADD . /build
CMD mix deps.get
this image is used to create a container for run tests and style checks.
RUN apk — update add postgresql-client erlang erlang-sasl erlang-crypto erlang-syntax-tools && rm -rf /var/cache/apk/*
ENV APP_NAME project
ENV PORT 4000
RUN mkdir -p /app
COPY $APP_NAME.tar.gz /app/
RUN tar -zxvf $APP_NAME.tar.gz
CMD trap exit TERM; /app/bin/$APP_NAME foreground & wait
This Dockerfile is used to build an actual image with Elixir release.
- Now we don’t use erlang hot upgrade feature;
- We don’t test if release is correctly starting, now we are testing it manually and locally;
- Every container uses it’s own epmd and intercommunication between services now made using rest apis and I’m working on integration of Erlang-In-Docker approach: https://github.com/Random-Liu/Erlang-In-Docker to use native erlang messaging between services.
I have a plan to write and publish several articles about our release pipeline, to answer the following questions:
- How do we compile and publish assets?
- How do we run our database migrations, since mix tasks aren’t available from release image?
- What problems are we faced during implementation of this pipeline, and what solutions we found.
Thanks for reading!