Skip to content

FEA506 - Implement automated build and deployment pipeline

Feature ID FEA506
Subsystem the feature is part of Backend/Infrastructure
Responsible person Matias Korpela
Status Implemented

Description

This feature integrates CI/CD pipelines for the frontend and backend repositories. It automates the process of building Docker images and pushing them to the GitLab container registry. After this, the images are automatically deployed to either the staging or production environments.

All relevant issues related to or contributing to the definition of the feature are gathered here

ID Description
Use Case 1 Development and production pipelines
FUNC-REQ-C0017 Automated build and deployment pipeline
FUNC-REQ-C0021 Account for staging and production environments in deployment pipeline

Preliminary user stories

  • US021 As a developer, I want to set up a CI/CD pipeline that automatically builds, tests, and deploys the web app to different environments, such as staging and production. #79
  • US029 As a platform engineer, I want to implement an automated build and deployment pipeline using tools like GitLab CI/CD to streamline the release process and ensure consistent deployments. #80

User interface mock-up

uml diagram

Implementation

Gitlab CI/CD Pipelines were created for frontend and backend repositories. A Gitlab runner was installed on the development server to run the pipeline jobs. The pipeline first builds a Docker image from the source and pushes it to GitLab container registry. After this, the images are automatically deployed to either the staging or production environments.

Gitlab container registry

Container registry

Frontend

# .gitlab-ci.yml

# Build a Docker image with CI/CD and push to the GitLab registry.
# Deploy to staging or production environment.

# Based on docker template:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Docker.gitlab-ci.yml

variables:
  DOCKER_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG

# All branches but main are tagged with $DOCKER_IMAGE_NAME (defaults to commit ref slug)
# Main branch is tagged with `latest`

workflow:
  rules:
    - if: $CI_COMMIT_REF_NAME == "main"
      variables:
        DOCKER_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest
    - if: $CI_COMMIT_BRANCH
      when: always

docker-build:
  # Use the official docker image.
  image: docker
  stage: build
  services:
    - docker:dind
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
    - |
      if [[ "$CI_COMMIT_BRANCH" == "main" ]]; then
        docker build --pull -t "$DOCKER_IMAGE_NAME" .
      else
        docker build --pull --build-arg BUILD_MODE=staging -t "$DOCKER_IMAGE_NAME" .
      fi
    - docker push "$DOCKER_IMAGE_NAME"
  tags:
    - ff-2024-t7-runner
  # Run this job in a branch where a Dockerfile exists
  rules:
    - if: $CI_COMMIT_BRANCH
      exists:
        - Dockerfile

deploy-staging:
  image: ubuntu
  stage: deploy
  script:
    - |
      if docker ps -a --format {{.Names}} | grep -q "tukko-frontend"; then
        docker container stop tukko-frontend
        docker rm tukko-frontend
      fi
    - docker run -d -p 80:80 --name tukko-frontend $DOCKER_IMAGE_NAME
    - docker image prune --force
  tags:
    - ff-2024-t7-runner
  # Run this job in a development branch where a Dockerfile exists
  rules:
    - if: $CI_COMMIT_BRANCH == "dev"
      exists:
        - Dockerfile

deploy-production:
  image: ubuntu
  stage: deploy
  # Set up SSH credentials
  before_script:
    - install -m 600 -D /dev/null ~/.ssh/id_rsa
    - echo "$SSH_PROD_PRIVATE_KEY" | base64 -d > ~/.ssh/id_rsa
    - ssh-keyscan "$SSH_PROD_HOST"  > ~/.ssh/known_hosts
  # Connect to the server and start docker
  script:
    - ssh $SSH_PROD_USER@$SSH_PROD_HOST << ENDSSH
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
    - |
      if docker ps -a --format {{.Names}} | grep -q "tukko-frontend"; then
        docker container stop tukko-frontend
        docker rm tukko-frontend
      fi
    - docker run -d -p 80:80 --pull always --name tukko-frontend $DOCKER_IMAGE_NAME || err=1
    - docker image prune --force || err=1
    - exit \$err && ENDSSH
  # Delete SSH credentials
  after_script:
    - rm -rf ~/.ssh
  tags:
    - ff-2024-t7-runner
  # Run this job in main branch where a Dockerfile exists
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      exists:
        - Dockerfile

Backend

# .gitlab-ci.yml

# Build a Docker image with CI/CD and push to the GitLab registry.
# Deploy to staging or production environment.

# Based on docker template:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Docker.gitlab-ci.yml

variables:
  DOCKER_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
  # mkdir /opt/gdcdeployment
  # chown gitlab-runner /opt/gdcdeployment/ | chown gitlab-runner /opt/gdcdeployment/traffic-visualizer-backend/
  DEPLOYMENT_PATH: /opt/gdcdeployment/traffic-visualizer-backend

# All branches but main are tagged with $DOCKER_IMAGE_NAME (defaults to commit ref slug)
# Main branch is tagged with `latest`

workflow:
  rules:
    - if: $CI_COMMIT_REF_NAME == "main"
      variables:
        DOCKER_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest
    - if: $CI_COMMIT_BRANCH
      when: always

docker-build:
  # Use the official docker image.
  image: docker
  stage: build
  services:
    - docker:dind
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
    - docker build --pull -t "$DOCKER_IMAGE_NAME" .
    - docker push "$DOCKER_IMAGE_NAME"
  tags:
    - ff-2024-t7-runner
  # Run this job in a branch where a Dockerfile exists
  rules:
    - if: $CI_COMMIT_BRANCH
      exists:
        - Dockerfile

deploy-staging:
  image: ubuntu
  stage: deploy
  script:
    - pwd
    - rm -rf $DEPLOYMENT_PATH
    - mkdir $DEPLOYMENT_PATH
    - cp docker-compose.yml redis-stack.conf .env $DEPLOYMENT_PATH/
    - echo "DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME" >> $DEPLOYMENT_PATH/.env
    - ls -la $DEPLOYMENT_PATH
    - docker compose -f $DEPLOYMENT_PATH/docker-compose.yml up -d
    - docker image prune --force
  tags:
    - ff-2024-t7-runner
  # Run this job in a development branch where a Dockerfile exists
  rules:
    - if: $CI_COMMIT_BRANCH == "dev"
      exists:
        - Dockerfile

deploy-production:
  image: ubuntu
  stage: deploy
  # Set up SSH credentials
  before_script:
    - install -m 600 -D /dev/null ~/.ssh/id_rsa
    - echo "$SSH_PROD_PRIVATE_KEY" | base64 -d > ~/.ssh/id_rsa
    - ssh-keyscan "$SSH_PROD_HOST"  > ~/.ssh/known_hosts
  # Connect to the server and start docker
  script:
    - scp docker-compose.yml redis-stack.conf .env $SSH_PROD_USER@$SSH_PROD_HOST:$DEPLOYMENT_PATH
    - ssh $SSH_PROD_USER@$SSH_PROD_HOST << ENDSSH
    - find $DEPLOYMENT_PATH -type f -not \( -name "docker-compose.yml" -or -name "redis-stack.conf" -or -name ".env" \) -delete
    - echo "DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME" >> $DEPLOYMENT_PATH/.env
    - ls -la $DEPLOYMENT_PATH
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
    - docker compose -f $DEPLOYMENT_PATH/docker-compose.yml up --pull always -d ||err=1
    - docker image prune --force || err=1
    - exit \$err && ENDSSH
  # Delete SSH credentials
  after_script:
    - rm -rf ~/.ssh
  tags:
    - ff-2024-t7-runner
  # Run this job in main branch where a Dockerfile exists
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      exists:
        - Dockerfile

Testing / possible acceptance criteria

Testcase Test source Responsible
Testcase 1 FUNC-REQ-C0017 Ops