Gitlab CI: How to avoid rebuilding of images on each commit - gitlab

I'm using Gitlab CI to store and deploy docker images but I got a big issue. Gitlab CI is rebuilding all images on every commit.
The first step is to build my common image which takes around 8 minutes. Currently I only modify child images but the common image is still rebuild on every commit.
Thus the build is a waste of time because the push won't have an effect as the image is already inside the Gitlab repository.
How to avoid the rebuild of images when it's already inside your Gitlab repository?
Below gitlab-ci.yml
build:
tags:
- docker_ci_build
services:
- docker:dind
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- >
docker build
-t $CI_PROJECT/common:6.0.1 common
Below a dockerfile:
FROM ubuntu:bionic
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive \
apt-get install -y packages && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
COPY file.conf /etc/common/

Caching appears to be more complicated than what I first thought, and the docker documentation for the various pull options fails to mention any nuances.
If the docker image may or may not yet exist, you can actually adjust the docker pull command to pass your build even if the pull fails:
- docker image pull $LATEST || true
This will pull the image $LATEST if it exists, otherwise will carry on with your build. The next step is to make sure that your build can actually take use of it.
The --pull option for docker build seemed like what you wanted, and docker's documentation says
--pull Always attempt to pull a newer version of the image
but it appears that it doesn't actually do anything for the actual image you're building. More promising is --cache-from which lets you name images that docker can cache from. In my simple test however, I was not seeing docker actually use my cache, probably because it doesn't have enough layers in the docker image to leverage the cache.
- docker image pull $LATEST_IMAGE || true
- docker build --pull -t $TAGGED_IMAGE --cache-from $LATEST_IMAGE .
- docker tag $TAGGED_IMAGE $LATEST_IMAGE
- docker push $LATEST_IMAGE
- docker push $TAGGED_IMAGE
This may be more useful if your build image is multi-stage build, and can actually leverage the cache. Optionally, you could adjust your gitlab-ci script to avoid building if the image exists.
Optionally, you may want to look at gitlab-ci pipelines and creating optional stages and triggers for when you build and push common, and when you build your sub-projects.
I define the variables in a variables section at the beginning of the file
Latest is defined with the :latest tag which is basically the most recent build from any CI branch for the project. $TAGGED_IMAGE is defined with :$CI_COMMIT_SHORT_SHA which tags the docker image with the git SHA, which makes kubernetes deployments to a development environment easier to track.

Related

How to integrate various services for building a project in GitLab CI/CD?

I have a project that requires npm and gradle for build, and docker for building and pushing the image.
At first I thought that I should create my own ubuntu image with gradle and npm setup, but I found out that is not what docker images are for.
So I hoped to run official Gradle and node images as a service so that my script can call those commands, but that is not happening for some reason.
My .gitlab-ci.yml:
variables:
IMAGE_NAME: my.registry.production/project
IMAGE_TAG: $CI_COMMIT_BRANCH
GIT_SUBMODULE_STRATEGY: recursive
stages:
- build
- deploy
build_project:
stage: build
image: ubuntu:jammy
services:
- name: node:12.20
alias: npm
- name: gradle:6.3.0-jre8
alias: gradle
before_script:
- git submodule init && git submodule update --remote --recursive
script:
- cd project-server && npm install && gradle clean build -Pprod -Pwar -x test -x integrationTest
deploy_image:
stage: deploy
image: docker:20.10.17
services:
- name: docker:20.10.17-dind
alias: docker
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_DRIVER: overlay2
script:
- docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD my.registry.production
- docker build -t $IMAGE_NAME:$IMAGE_TAG .
- docker push $IMAGE_NAME:$IMAGE_TAG
If anyone has any info on how to solve this I would greatly appreciate it, since I’m a novice DevOps.
Edit 1:
My Dockerfile for custom image with Gradle and Node installed.
FROM ubuntu:jammy
LABEL key=DevOps
SHELL ["/bin/bash", "--login", "-i", "-c"]
RUN apt update && apt upgrade -y && apt install curl -y
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
RUN source /root/.bashrc && nvm install 12.14.1
RUN nvm install 12.20.0
RUN apt install zip unzip
RUN curl -s "https://get.sdkman.io" | bash
RUN source "$HOME/.sdkman/bin/sdkman-init.sh"
RUN sdk install java 8.0.302-open
RUN sdk install gradle 3.4.1
SHELL ["/bin/bash", "--login", "-c"]
CMD [ "bin/bash" ]
After I run it, it says that npm is not found in $PATH, I tried Java, Gradle as well but they weren't found in the path as well.
I don't know why since I installed them as you can tell from the Dockerfile.
As I know, a docker image is equal to one build. So if you have multiple services you need to build each one into docker image then you can encapsulate all images into docker-compose.yml file.
I think you can do the following:
Build the npm project into a docker image
Build the Gradle project into a docker image
Write the docker-compose.yml file and put both images.
Once you have done it, the pipeline calls the docker-compose.yml file.
I hope this will be helpful.
Consider a few suggestions based on the fundamental concepts about the deployment in your CI/CD pipeline:
Remove the services keyword. Reference GitLab's official documents on what the services keyword inside gitlab-ci.yaml file is not for. The feature is used
to provide network accessable services to your job runtime (like
a database): https://docs.gitlab.com/ee/ci/services/index.html
Your project uses npm as a dependency management system, Gradle is
a build tool. Both of these pieces of software are more than
appropriate to run on the host operating system of the container
runtime inside GitLab's Pipeline job. You need these tools to assemble some build artifact as a result of the job on the same host your code has been downloaded on in the Runner.
Think about the overall size of the base image in your build_project job and consider how time to download the image over the network on to the Runner will impact your job and overall pipeline duration. If performance can be improved by baking build dependencies into a custom Dockerfile do this. If your image is too large, instead use shell commands inside the script keyword block to download them at the runtime of the job. There can be pros and cons for both.
Break shell scripts to one command per line for easier troubleshooting of failures in your scripts. You will be able to see the line number of the command which returned a non-zero exit code in your job logs:
...
script:
- cd project-server
- npm install
- gradle clean build -Pprod -Pwar -x test -x integrationTest
...
It's recommended to use the Gradle wrapper (gradlew) most of the time instead of the gradle executable directly. Configure this within your project and check the configuration files for the wrapper into your version control system and this will simplify your build dependency: https://docs.gradle.org/current/userguide/gradle_wrapper.html

What is docker node:9-stretch?

I'm looking at a Dockefile we have in one of our projects that essentially builds our UI.
I see it's using FROM node:9-stretch which ships with npm 5.6.0. I want to use npm ci which requires 5.7.0 so i need to update my dockerfile node base image.
I do not see a node:9-stretch in docker hub https://hub.docker.com/_/node/
Where is this pulling node:9-stretch from and what is a -stretch version of a base image?
The node 9 build was dropped after this commit https://github.com/nodejs/docker-node/commit/b22fb6c84e3cac83309b083c973649b2bf6b092d. You can find Dockerfile in diff.
The node:9-stretch image you can pull build before the commit, and persisted in docker hub. The 9-stretch tag exists in Tags page, as for now https://hub.docker.com/_/node?tab=tags&page=18.

Dockerizing Nodejs dependencies for Gitlab CI

I'm using Gitlab CI in order to implement CI for my Node.js app. I'm already using artifacts and sharing the dependencies between jobs, however, I would like to make it faster. Every time a pipeline starts, it installs the dependencies during the first job and I'm thinking to prevent this by having all dependencies in a Docker image and pass that image to test & production stages. However, I have been unable to do so. Apparently Gitlab doesn't run the code inside my image's WORKDIR.
Following is my Dockerfile:
FROM node:6.13-alpine
WORKDIR /home/app
COPY package.json .
RUN npm install
CMD [“sh”]
And following is my gitlab-ci.yml:
test:
image: azarboon/dependencies-test
stage: test
script:
 — pwd
  — npm run test
Looking at logs, pwd results in /builds/anderson-martin/lambda-test, which is different from the defined WORKDIR and also installed dependencies are not found. Do you have any recommendation for me how can I Dockerize my dependencies and speed up the build stage?
Probably the easiest way to solve your issue is to symlink the node_modules folder from your base image into the gitlab CI workspace like this:
test:
image: azarboon/dependencies-test
stage: test
script:
 — ln -s /home/app/node_modules ./node_modules
  — npm run test
The syntax for symlinking is ln -s EXISTING_FILE_OR_DIRECTORY SYMLINK_NAME.
Please note that /home/app/ is the workspace which you´re using in your base image.
Gitlab also provides other functionality to share dependencies. On the one hand you have caching and on the other job artifacts.

Where should i run my grunt build step when building my docker image for staging and production environments?

I'm really struggling to figure out where i should put my grunt build step when building my docker image and deploying to dockerhub.
My workflow at the moment is as follows:
Push branch to github
CircleCI installs all dependencies, builds project, and runs tests on branch.
Merge branch branch to staging branch
CircleCI installs all dependencies, builds project, and runs tests on branch.
If tests pass, package the built files into the docker image with the source and also run npm install --production. CircleCI then deploys this staging image to dockerhub
Tutum is linked to dockerhub and deploys my image to DigitalOcean whenever a new image is pushed.
I do the same workflow as above, when merging to master, and a production image is created instead.
It feels a bit weird that i'm created 2 separate docker images. Is this standard practice?
I've seen quite a lot of people including the grunt/gulp build step in their dockerfiles, but that doesn't feel right either as all the devDependencies, and bower_components will then be in the image along with the built code.
What's the best practice for running build steps and building docker images? Is it better to have CI do it, or dockerhub do it from the dockerfile? I'm also after the most efficient way to create my docker image for staging and production.
Below is my circleCI.yml file, followed by my Dockerfile.
circle.yml:
machine:
node:
version: 4.2.1
# Set the timezeone - any value from /usr/share/zoneinfo/ is valid here
timezone:
Europe/London
services:
- docker
pre:
- sudo curl -L -o /usr/bin/docker 'http://s3-external-1.amazonaws.com/circle-downloads/docker-1.8.2-circleci'; sudo chmod 0755 /usr/bin/docker; true
dependencies:
pre:
- docker --version
- sudo pip install -U docker-compose==1.4.2
- sudo pip install tutum
override:
- npm install:
pwd: node
post:
- npm run bower_install:
pwd: node
- npm run grunt_build:
pwd: node
test:
override:
- cd node && npm run test
deployment:
staging:
branch: staging
commands:
- docker-compose -f docker-compose.production.yml build node
# - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
- tutum login -u $DOCKER_USER -p $DOCKER_PASS -e $DOCKER_EMAIL
- docker tag dh_node:latest tutum.co/${DOCKER_USER}/dh_stage:latest
- docker push tutum.co/${DOCKER_USER}/dh_stage:latest
master:
branch: master
commands:
- docker-compose -f docker-compose.production.yml build node
# - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
- tutum login -u $DOCKER_USER -p $DOCKER_PASS -e $DOCKER_EMAIL
- docker tag dh_node:latest tutum.co/${DOCKER_USER}/dh_prod:latest
- docker push tutum.co/${DOCKER_USER}/dh_prod:latest
Dockerfile:
FROM node:4.2
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY package.json /usr/src/app/
RUN npm install --production
COPY . /usr/src/app
#
#
# Commented the following steps out, as these
# now run on CircleCI before the image is built.
# (Whether that's right, or not, i'm not sure.)
#
# Install bower
# RUN npm install -g bower # grunt-cli
#
# WORKDIR src/app
# RUN bower install --allow-root
#
# Expose port
EXPOSE 3000
# Run app using nodemon
CMD ["npm", "start"]
What's the best practice for running build steps and building docker images? Is it better to have CI do it, or dockerhub do it from the dockerfile?
It's better to run the build steps themselves outside of docker. Thus the same steps work for local development, non-docker deployment, etc. Keep your coupling to docker itself loose when you can. Thus build your artifacts with regular build tools and scripts and simply ADD built files to your docker image via your Dockerfile.
It feels a bit weird that i'm created 2 separate docker images. Is this standard practice?
I would recommend instead using exactly the image you have already built and tested on stage in production. Once you rebuild the image, you become vulnerable to discrepancies breaking your production image even though your stage image worked OK. At this point neither docker nor npm can deliver strictly reproducible builds across time, thus once it's built and tested gold, it's gold and goes to production bit-for-bit identical.
Your circle ci should download all the dependencies and then create docker image from that downloaded packages. All testing is passed with the specified dependencies and should be carry forwarded to production. Once. The image is pushed to docker hub with all dependencies and tumtum will deploy the same to your production and as the dependencies are already downloaded it will take seconds to create containers.
Answering to your second query of building the same image. I would suggest to deploy the same image to production. This will guarantee you that what worked great on staging is also working the same on production.

GitLab-CI PHPunit Composer laravel

I am new to GitLab-CI and Docker, I am stuck getting a runner to run my phpunit builds. Following the instructions here:
https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/install/linux-repository.md
However, The container per their instructions obviously doesn't contain the tools I need. So question is, what is the configuration when registering a multi-runner to have a runner that supports phpunit, composer so I can test my laravel builds.
Follow their instructions to register gitlab-ci-multi-runner with docker executor. Than in your .gitlab-ci.yml give an appropriate image that contains bare minimum for your requirements. Everything else you can install through command line in before_script. I'm posting a sample working config for you:
image: tetraweb/php
services:
- mysql
variables:
MYSQL_DATABASE: effiocms_db
MYSQL_USER: effio_user
MYSQL_PASSWORD: testpassword
WITH_XDEBUG: "1"
before_script:
# enable necessary php extensions
- docker-php-ext-enable zip && docker-php-ext-enable mbstring && docker-php-ext-enable gd && docker-php-ext-enable pdo_mysql
# composer update
- composer self-update && composer --version
- composer global require --no-interaction --quiet "fxp/composer-asset-plugin:~1.1.0"
- export PATH="$HOME/.composer/vendor/bin:$PATH"
- composer install --dev --prefer-dist --no-interaction --quiet
# codeception install
- composer global require --no-interaction --quiet "codeception/codeception=2.0.*" "codeception/specify=*" "codeception/verify=*"
# setup application
- |
php ./init --env=Development --overwrite=All
cd tests/codeception/backend && codecept build
cd ../common && codecept build
cd ../console && codecept build
cd ../frontend && codecept build
cd ../../../
- cd tests/codeception/bin && php yii migrate --interactive=0 && cd ../../..
codeception:
stage: test
script:
- |
php -S localhost:8080 > /dev/null 2>&1 &
cd tests/codeception/frontend
codecept run
Obviously this config is for my application running on Yii2. So you need to adjust it per your requirements.
Inline with what Arman P. said. When running your tests, make sure you have a docker image which contains all the tools that you will need for your build/test.
You have two options:
You can build your own image, with all the tools you need, and
maintain this as your project evolves; or,
you can simply install a basic image from the docker hub and install
all the tools before you run your jobs.
Both options have their pros and cons:
Option (1) gives you complete control, but you would need to add this to a private registry which the CI can pull from (Gitlab gives you a private registry support). The only slight con here is that you would have to setup the private registry (e.g Gitlab's) first. Not too difficult, and only need to do it once.
But then it is up to you to maintain the images, etc.
Option (2) allows you to run all the tools without having to maintain a private registry or the docker containers. You simply run the install scrips before your jobs as Arman P. mentioned. The disadvantage on that is that your jobs and builds/tests take longer to run as you now have to wait for the install to happen before every run.
A simple example: Using option (2)
We need PHPUnit and composer.
So, use a container from the public docker hub which has php: select one that has the version of PHP you want to test (e.g. 5.6 or 7). Let's assume 5.6.
In that container we need to install composer and PHPUnit before we can run our tests. This is what your CI file might look like:
.gitlab-ci.yml
image: php:5.6.25-cli
stages:
- build
- test
.install_composer_template:&install_composer
- apt-get install -y git
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
before_script:
<<:*install_composer
build app:
stage: build
script:
- composer install
- composer dump-autoload -o
test app:
stage: test
before_script:
<<:*install_composer
- composer global require -q phpunit/phpunit
script:
- phpunit --colors=always
Quick summary of what this actually does
Every job -- here we only have "build app" and "test app" -- will run through the official PHP 5.6 image.
We have two stages defined, build and test -- note: these are defined by default, but here we are being explicit for clarity.
The first job that runs will be the "build app" as it occurs in the first stage, build. Before the script runs in this job, the global before_script runs, which installs git and composer (Composer requires Git). Then the script runs and installs all our composer dependencies. Done.
The next stage then executes, which is test. So our jobs attached to that run (in parallel if we had more than one), which for us is just "test app". Here, we use YML features to reuse the install composer instructions: the local before_script overrides the global before_script, installing Composer (via the YML template) and PHPUnit. Then the actual script runs: which runs our unit tests. The unit tests assume you have a phpunit config in the root -- you can customise this command as you would on you own terminal.
Note here, that due to the stage setup, the output of the build stages are automatically made available in the test stage. So we don't have to do anything, Gitlab will pass all the files for us!
The main reason for using the install_composer template is to improve job execution time. Most things are going to require Composer. So we have a global template and a script that runs for every job. If you need something a bit more specific, with tools that are only required for that job, e.g. PHPUnit, then override the before_script locally in the job and install the required tools. This way we don't have to install a whole load of tools which we might not need in every job!

Resources