Setting up Symfony continuous integration using GitLab CI/CD

Once your application starts growing, checking your code quality, running all sorts of tests, and maintaining versioning start to be a heavy task to handle. And we, developers, as lazy creatures that we are, tends to automate all those repeatedly tasks. A continuous integration (CI) service once configured well with our application environment, can launch all those tasks automatically each time the code is changed in our repositories.

Today’s article will describe how to integrate one of those services which is GitLab CI/CD, with our Symfony application. This article is part of a series about PHP and containers eco-system.

Let’s rock!

After pushing the code to our repository (GitLab in this case), our continuous integration service (GitLab CI) will automatically trigger its runners to first build the project by installing all the necessary dependencies and launch the tests.

To build our eco-system, we need a bunch of tools, so let’s get started.

GitLab

GitLab is sort of Github for enterprises, we’ll use it in this case for our code versioning, you can pick whatever edition you want. Personally, the community edition is more than enough, since it’s free and which is more important contains GitLab CI/CD pipelines.

To install the community edition, the server must respect some requirements, and as a Docker enthusiast, I used Docker Compose to put GitLab CE behind a NGINX proxy with SSL, here is the docker-compose.yml file:

version: "2"  
services:  
  nginx-proxy:
    image: jwilder/nginx-proxy:latest
    ports:
     - "443:443"
     - "80:80"
    volumes:
      - '/home/docker/nginx-proxy/ssl:/etc/nginx/certs:ro'
      - '/etc/nginx/vhost.d'
      - '/usr/share/nginx/html'
      - '/var/run/docker.sock:/tmp/docker.sock:ro'
      - './nginx/my_proxy.conf:/etc/nginx/conf.d/my_proxy.conf'
  letsencrypt-nginx-proxy-companion:
    image: jrcs/letsencrypt-nginx-proxy-companion:latest
    volumes_from:
      - nginx-proxy
    volumes:
      - '/home/docker/nginx-proxy/ssl:/etc/nginx/certs:rw'
      - '/var/run/docker.sock:/var/run/docker.sock:ro'
  gitlab-server:
    image: gitlab/gitlab-ce:latest
    hostname: gitlab.lekode.com
    ports:
      - 2224:22/tcp
    environment:
      VIRTUAL_PORT: 80
      VIRTUAL_HOST: gitlab.lekode.com
      LETSENCRYPT_HOST: gitlab.lekode.com
      LETSENCRYPT_EMAIL: [email protected]
      GITLAB_OMNIBUS_CONFIG: |
          external_url 'https://gitlab.lekode.com'
          gitlab_rails['gitlab_shell_ssh_port'] = 2224
          nginx['listen_port'] = 80
          nginx['listen_https'] = false
          nginx['proxy_set_headers'] = {
            "X-Forwarded-Proto" => "https",
            "X-Forwarded-Ssl" => "on"
          }
          gitlab_rails['smtp_enable'] = true
          gitlab_rails['smtp_address'] = "smtp.postmarkapp.com"
          gitlab_rails['smtp_port'] = 587
          gitlab_rails['smtp_user_name'] = "smtp-user"
          gitlab_rails['smtp_password'] = "smtp-secret"
          gitlab_rails['smtp_domain'] = "lekode.com"
          gitlab_rails['smtp_authentication'] = "login"
          gitlab_rails['smtp_enable_starttls_auto'] = true
          gitlab_rails['gitlab_email_from'] = '[email protected]'
    volumes:
      - /gitlab/config:/etc/gitlab
      - /gitlab/logs:/var/log/gitlab
      - /gitlab/data:/var/opt/gitlab

The nginx-proxy service is the NGINX proxy, the port 80 is exposed to verify the domain name in the SSL certificate generation processes by Let’s Encrypt, the port 443 is the default port for https and need to be exposed to access GitLab domain. We also need to set client_max_body_size which is by default 2MB, so in nginx/my_proxy.conf:

client_max_body_size 1000m;  

The letsencrypt-nginx-proxy-companion service generates SSL certificate for our domain name, this certificate is updated each three months automatically.

And finally, gitlab-server is the service of GitLab CE, we need to expose SSH port (2224 in this case) and indicate the exposed port in the OMNIBUS config file. The OMNIBUS configuration can be set by the GITLAB_OMNIBUS_CONFIG environment variable. I exposed the major configuration I need, but you can check the full list of parameters you can customize here.

Finally, we should not forget to launch the containers :)

docker-compose up -d  

Protip: To update the GitLab version you run, pull the Docker new image and restart the containers:

docker pull gitlab/gitlac-ce:latest  
docker-compose stop  
docker-compose rm -f  
docker-compose up -d  

Admin Area

When we first connect to our GitLab Repository, we’re asked to add our root password, once done, we can access the Admin Area by login as root user.

The admin area is accessible from the wrench icon on the top, here we can manage the preferences of our GitLab, I will not cover this in our article.

GitLab CI/CD pipelines

One of the great features of GitLab is their integrated CI and CD, using this tool, we will describe each step of our continuous integration workflow, those steps are going to be processed distributed on separate machines by Runners processes. A Runner is launched as a Docker container, we will not use a proxy in this case because our Runner will communicate only with our GitLab server. First, let’s pull the GitLab Runner image by executing this command:

docker run -d --name gitlab-runner --restart always \  
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /srv/gitlab-runner/config:/etc/gitlab-runner \
  gitlab/gitlab-runner:latest

Next, we can register a new Runner for our application by executing the register command in the Runner container

docker exec -it gitlab-runner gitlab-runner register -n \  
  --url https://gitlab.lekode.com/ci \
  --registration-token Secret-token \
  --tag-list "test" \
  --executor docker \
  --description "Docker Tests Runner" \
  --docker-image "php:fpm-alpine"

You can get your own GitLab CI coordinator URL, and token from your project menu: Settings > CI/CD Pipelines > Specific Runners.

The default Docker image, in this case, is a PHP image since we will run PHP commands (installing our application dependencies, running tests, …)

Protip: GitLab offers some shared runners that you can enable to your project, but who say shared say you need to wait your turn each time.

Once the Runner is launched, you can check its status in your project settings

.gitlab-ci.yml

Finally, the .gitlab-ci.yml file describe the different steps of our CI workflow, the configuration file describe in first the different stages of the process (in our case only test stage), in a second time, we need to describe each stage jobs, by defining the container image where the stage will be running, and the commands to run. So let’s create our .gitlab-ci.yml file in our application folder:

stages:  
  - test

test:  
  stage: test
  image: kariae/symfony-php
  tags:
    - test
  services:
    - postgres

  variables:
    POSTGRES_USER: lekode-test
    POSTGRES_PASSWORD: lekode-test-pass
    POSTGRES_DB: lekode-db
    DATABASE_HOST: postgres
    DATABASE_PORT: "5432"

  artifacts:
    expire_in: 1 day
    paths:
      - vendor/
      - app/config/parameters.yml
      - var/bootstrap.php.cache

  before_script:
    - composer config cache-files-dir /cache/composer

  cache:
    paths:
      - /cache/composer
      - ./vendor

  script:
    - composer install --optimize-autoloader
    - vendor/bin/simple-phpunit

The test stage run on kariae/symfony-php Docker image, which is an image I created based on the official PHP image, that also includes the PHP extensions we used in the previous article to respect the Symfony requirements.

services parameter represent the services (mysql, postgresql, redis, …) that our application use to run the CI scripts.

variables parameter contains all the environment variables that our Symfony application needs.

We will use also GitLab artifacts, so we do not have to re-run composer install in the other jobs, and GitLab cache to set Composer cache folder.

To use the database information from our environment variables, Symfony 3.2 can read the runtime environment variables , so our parameters.yml.dist will contains something like:

parameters:  
  database_host: '%env(DATABASE_HOST)%'
  database_port: '%env(DATABASE_PORT)%'
  database_name: '%env(POSTGRES_DB)%'
  database_user: '%env(POSTGRES_USER)%'
  database_password: '%env(POSTGRES_PASSWORD)%' 

And finally, the scripts parameter contains the commands we want to run, which are in this case install and tests commands.

Protip: The GitLab CI supports also running scripts before and after the execution of the current job, the full description of .gitlab-ci.yml file can be found here.

Now, the next time we push our codes, the pipelines run automatically, we can check the status of pipelines which can be seen from the Pipelines menu in GitLab.

Conclusion

In this article, we saw a simple continuous integration workflow, that runs our Symfony application tests using Runners from GitLab CI/CD. The next article and the last one from this series will present the continuous deployment part using Rancher which is a powerful and easy-to-use container manager, if you have any question or note about this article, please feel free to write a comment here.