Nowadays, the DevOps culture is present in many projects and companies of all sizes. The main concepts of this culture are Continuous Integration and Continuous Delivery/Deployment (CI/CD), which consists of a method for frequent delivery of apps to customers using automation for the main stages of app development. CI/CD is a strongly recommended solution to the integration of new code, as problems can occur with operations and development teams (the infamous “integration hell”).
Let’s describe CI/CD’s main concepts:
Continuous Integration or “CI” is about regularly doing specific and important software development steps after new code is written (build, test, and merge) in an automated way on a shared repository. CI is a solution for the problem of branch conflicts on an active codebase.
There are two concepts tied to the CD part of CI/CD. The first, Continuous Delivery, is about being able to send changes that will be tested and uploaded to a shared repository and can later be deployed to any environment by an operations team. So it’s a solution to the problem of poor visibility and communication between dev and business teams and ensures minimal effort to deploy new code.
Finally, the second concept consists of automatically releasing new changes from the repository to the production environment, where it is usable by stakeholders/customers. It solves the problem of overloading operations teams with manual processes that slow down app delivery.
In this article, we will use the GitLab CI/CD tool to apply all the concepts described above. It takes care of catching bugs and errors early in the development cycle, ensuring that all the code deployed to the production environment complies with the code standards that you and your team established for your app/project. It can handle and automatize the steps of building, testing, deploying, and monitoring the code/app.
First of all, the usage of this tool starts with writing a setup file, named “gitlab-ci.yml” that describes all the steps or stages that our REST API example app needs to pass. This API consists of a simple app written in Ruby using the Sinatra library to help to define a couple of HTTP REST routes.
First step: we need to define the stages. For this example, we have:
- Security: consists of ensuring that app dependencies versions are all correct and if vulnerabilities exist;
- Test: consists of running the automatized tests;
- Deploy: consists of sending the most recent app changes to staging and production envs. In this example, I choose Heroku to make our app available.
stages: - security - test - deploy
Now, we define a default job that will be reused at the security and test stages.
.default_job: image: ruby:2.7 variables: RAILS_ENV: test cache: paths: - "vendor/bundle" before_script: - apt-get update -yy && apt-get install build-essential libpq-dev -y - gem i bundler - bundle install --binstubs -j $(nproc) - bundle config set path 'vendor/bundle' - bundle update only: - merge_requests - main
This job is responsible to run a ruby docker image with env variables, defining a path to save cache (cache usage helps to reuse artifacts that have already been downloaded), and running commands to install required libs and app dependencies. Those are only executed for the merge request and integration operations of the
The security stage takes care of finding vulnerabilities in dependencies. For that, we shall write the following instructions:
brakeman_and_audit: extends: .default_job stage: security before_script: - gem install brakeman - gem install bundler-audit - bundle audit --update script: - brakeman -z -q - bundle audit
Now, the test stage is responsible for running the test suite written for your application. In our case, we just need to run the command
bundle exec rspec spec.
testing: extends: .default_job stage: test script: - bundle exec rspec spec
Finally, the last steps are deploying to staging and production. Both steps extend the
deployment_job. This job is executed only on the
main branch and consists of installing required dependencies to deploy to Heroku. For this, we will use
dpl, a command-line tool to deploy code, html, packages, or build artifacts to various service providers.
.deployment_job: image: ruby:latest before_script: - apt-get update -qy - apt-get install -y ruby-dev - curl https://cli-assets.heroku.com/install-ubuntu.sh | sh - gem install dpl only: - main
Also, we need to set the env variables that will allow us to auth to Heroku and send our example app to staging and production environments. The variables are:
- HEROKU_API_KEY: Token for authentication
- HEROKU_APP_STAGING: staging environment app name
- HEROKU_APP_PRODUCTION: production environment app name
Now, defining our steps for staging and production environment is easy. We just need to extend
.deployment_job and execute the command
dpl --provider=heroku --app=$HEROKU_APP_[STAGING/PRODUCTION] --api-key=$HEROKU_API_KEY
The production and staging sections are very similar, the only difference being the keyword
when that allows us to send the new version of our application manually.
staging: extends: .deployment_job stage: deploy script: - dpl --provider=heroku --app=$HEROKU_APP_STAGING --api-key=$HEROKU_API_KEY production: extends: .deployment_job stage: deploy when: manual script: - dpl --provider=heroku --app=$HEROKU_APP_PRODUCTION --api-key=$HEROKU_API_KEY
And that’s it! We have a CI/CD process configured!
We are hiring new talent. Check out our careers portal!