CI/CD pipeline with Cloud Build and Cloud Run  - Part 2

CI/CD pipeline with Cloud Build and Cloud Run  - Part 2

Before you begin please take the time to read the article on pipline architecture. CI/CD pipeline with Cloud Build and Cloud Run. *Development & production environment with Cloud Build and Cloud Run.

Here we will see how to configure this architecture in a development and production environment.

- Configuration of services on GCP

  • Create two GCP projects. One project for the development environment and another for the production environment. The configuration will be the same for both environments simply because we will focus only on the configurations needed for deployment on Cloud Run.

  • Activate the APIs of the Cloud Build, Artifact Registry and Cloud Run services.

  • Go to the Cloud Build settings and assign the Cloud Run Admin and Service Account User roles to Cloud Build.

  • Create a Docker repository in Artifact Registry.

  • Create a service account with the Cloud Build Service Account role and Project > Viewer.

  • Then generate a new key pair and upload it.

Don’t forget to repeat the same configurations for both environments.

- CI/CD environment variable on Gitlab

Click on Settings > CI/CD > Variables > Expand

Then, add the following variables:

GCP_PROJECT_ID : project ID of the production environment.

GCP_PROJECT_ID_DEV : project ID of the development environment.

GCP_SERVICE_KEY : the content of the json file of the service account key pair created in the production environment.

GCP_SERVICE_KEY_DEV : the content of the json file of the service account key pair created in the development environment.

- .gitlab-ci.yaml Configuration

Let’s analyze the .gitlab-ci.yml file step by step.

# File: .gitlab-ci.yml 
stages:
  - build
  - test
  - deploy-dev
  - deploy-prod

default:
  image: docker:latest
  services:
    - docker:dind
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY

docker-build:
  # Use the official docker image.
  stage: build
  only:
    refs:
      - master
      - develop
  # Default branch leaves tag empty (= latest tag)
  # All other branches are tagged with the escaped branch name (commit ref slug)
  script:
    - |
      if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
        tag=""
        echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'"
      else
        tag=":$CI_COMMIT_REF_SLUG"
        echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag"
      fi
    - docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" .
    - docker push "$CI_REGISTRY_IMAGE${tag}"
  # Run this job in a branch where a Dockerfile exists
  interruptible: true
  environment:
    name: build/$CI_COMMIT_REF_NAME


test:
  # Use the official docker image.
  stage: test
  only:
    refs:
      - master
      - develop

  script:
    - docker run -p 3000:8080 --name my-app -d "$CI_REGISTRY_IMAGE${tag}"
    - apk --no-cache add curl
    - chmod u+x ./test.sh && ./test.sh
    #- docker inspect --format='{{range $p, $conf := .NetworkSettings.Ports}} {{$p}} -> {{(index $conf 0).HostPort}} {{end}}' $(docker ps -aq)
    #- docker port my-app
  after_script:
    - docker stop my-app && docker rm my-app

  environment:
    name: test/$CI_COMMIT_REF_NAME
  when: on_success


deploy-dev:
  stage: deploy-dev
  only:
    refs:
      - develop
  image: google/cloud-sdk
  services:
    - docker:dind
  script:
    - echo $GCP_SERVICE_KEY_DEV > gcloud-service-key-dev.json # Google Cloud service accounts
    - gcloud auth activate-service-account --key-file gcloud-service-key-dev.json
    - gcloud config set project $GCP_PROJECT_ID_DEV
    - gcloud builds submit . --config=cloudbuild-dev.yaml
  environment:
    name: develop/$CI_COMMIT_REF_NAME
    action: stop

deploy-prod:
  stage: deploy-prod
  only:
    refs:
      - master
  image: google/cloud-sdk
  services:
    - docker:dind
  script:
    - echo $GCP_SERVICE_KEY > gcloud-service-key.json # Google Cloud service accounts
    - gcloud auth activate-service-account --key-file gcloud-service-key.json
    - gcloud config set project $GCP_PROJECT_ID
    - gcloud builds submit . --config=cloudbuild.yaml

  environment:
    name: production/$CI_COMMIT_REF_NAME
    action: stop

#!/bin/bash
#HTTP="$(docker inspect --format='{{(index (index .NetworkSettings.Ports "8080/tcp") 0).HostPort}}' $(docker ps -aq))"
HTTP=$(curl -I docker:3000 2> /dev/null | grep -c "200 OK")
DOCKER=$(docker ps | grep -c "$CI_REGISTRY_IMAGE${tag}")
if [  $DOCKER -eq 1 ]
then
    #if [ $HTTP = "8080" ]
    if [ $HTTP -eq 1 ]  
    then
        echo Container running and visible on port 3000, good job!
    else
        echo The container is running, but the website is not published on port 3000
    fi
else
    echo No container running
fi

In the first step, we will build our docker image.

Once that’s done, we’ll run our docker image and check if our container is working properly. First, we will check if the container is running and secondly, we will check the port of the container. Depending on the needs of your project, you can add as many tests (Dependency-Scanning, Container-Scanning, Secret-Detection, etc.) as you want.

After the success of the previous steps, we will call our cloudbuild-dev.yaml file if we are on the develop branch for the deployment of our application. On the other hand, if we are on the master branch, we will use our cloudbuild-prod.yaml file.

You can also perform other rounds of tests with Cloud Build before deploying your application to Cloud Run. It will all depend on your goals.

# File: cloudbuild.yaml
steps:
    # build the container image
  - name: 'gcr.io/cloud-builders/docker'
    args: [ 'build', '-t', 'us-central1-docker.pkg.dev/$PROJECT_ID/docker-repository-name/image_name', '.' ]
    # push the container image
  - name: 'gcr.io/cloud-builders/docker'
    args: [ 'push', 'us-central1-docker.pkg.dev/$PROJECT_ID/docker-repository-name/image_name']
    # deploy to Cloud Run
  - name: "gcr.io/cloud-builders/gcloud"
    args: [ 'beta', 'run', 'deploy', 'service_name', '--image', 'us-central1-docker.pkg.dev/$PROJECT_ID/docker-repository-name/image_name', '--region', 'us-central1', '--tag', 'dev', '--platform', 'managed', '--port', '8080', '--allow-unauthenticated']
# File: cloudbuild.yaml
steps:
    # build the container image
  - name: 'gcr.io/cloud-builders/docker'
    args: [ 'build', '-t', 'us-central1-docker.pkg.dev/$PROJECT_ID/docker-repository-name/image_name', '.' ]
    # push the container image
  - name: 'gcr.io/cloud-builders/docker'
    args: [ 'push', 'us-central1-docker.pkg.dev/$PROJECT_ID/docker-repository-name/image_name']
    # deploy to Cloud Run
  - name: "gcr.io/cloud-builders/gcloud"
    args: [ 'beta', 'run', 'deploy', 'service_name', '--image', 'us-central1-docker.pkg.dev/$PROJECT_ID/docker-repository-name/image_name', '--region', 'us-central1', '--no-traffic', '--platform', 'managed', '--port', '8080', '--allow-unauthenticated']

As you can see, the deployment in the development environment receives 100% of the traffic while the production environment receives 0%. This is quite normal because it is continuous delivery. The scaling of the application in the production environment will be done manually.

NB : when you use the “ — no-traffic” setting for your first deployment, you will get an error message, because the first revision of your first deployment must receive 100% traffic.

Thank you for your attention.