Implement secure CI/CD with Workload Identity Federation, GitLab CI & Cloud Deploy.

EZEKIAS BOKOVE
7 min readJan 7, 2023
Implement secure CI/CD with Workload Identity Federation, GitLab CI & Cloud Deploy.

Currently, there are several articles and tutorials on setting up CI/CD pipelines with GitLab and Cloud Build. These different articles have in common the use of service account keys (which is not recommended) and Cloud Build.

  • The use of service account keys is not good practice and should be replaced by Workload Identity Federation.
  • In most cases Cloud Build is used as a deployment tool which is not bad, but a tool specifically designed for deployment like Cloud Deploy would be better.

Here we will look at how :

  • Establish a secure connection between GitLab and Google Cloud with Workload Identity Federation.
  • Setting up CI/CD with GitLab CI and Cloud Deploy. Continuous integration (CI) operations will be managed by GitLab CI and continuous delivery (CD) operations by Cloud Deploy.

- Secure connection between GitLab and Google Cloud with Workload Identity Federation.

For the configuration, we will follow the GitLab instructions which are listed in this article👇.

The most important for us here are the first two steps :

If you are interested in the Provider attributes parameter, here are some attributes from the GitLab sample project.

    "google.subject"           = "assertion.sub", # Required
"attribute.aud" = "assertion.aud",
"attribute.project_path" = "assertion.project_path",
"attribute.project_id" = "assertion.project_id",
"attribute.namespace_id" = "assertion.namespace_id",
"attribute.namespace_path" = "assertion.namespace_path",
"attribute.user_email" = "assertion.user_email",
"attribute.ref" = "assertion.ref",
"attribute.ref_type" = "assertion.ref_type",

Once this is done, we will create a service account that we will attach to our Workload Identity Pool.

Our service account will have the following roles : Artifact Registry Writer ,Cloud Deploy Operator ,Service Account User ,Storage Admin

Now we go to IAM & Admin > Workload Identity Federation from the Google Cloud Console. Once in the interface, click on the name of the Pool we created earlier (if you have followed it from the beginning, the name should be GitLab). Then press Grant Access and select the service account we created. Finally, save and click on DISMISS.

Workload Identity Pool with service account

To finalize the configuration, we will add four environment variables to GitLab. Among the environment variables, there are two that are essential for Workload Identity Federation, namely GCP_SERVICE_ACCOUNT and GCP_WORKLOAD_IDENTITY_PROVIDER.

GCP_PROJECT_ID : PROJECT ID of Google Cloud.

GCP_REGION : the region of your Artifact Registry and Cloud Deploy.

GCP_SERVICE_ACCOUNT : the email of the service account we have created. (For example: xxxxxxxxxxx@xxxxxxxxx.iam.gserviceaccount.com)

GCP_WORKLOAD_IDENTITY_PROVIDER : projects/<project_number>/locations/global/workloadIdentityPools/gitlab/providers/gitlab-gitlab

Note : replace <project_number> with its value.

GitLab variable

We have finished configuring the secure connection between GitLab and Google Cloud. It’s time to configure our CI/CD.

- Configure a CI/CD with GitLab CI and Cloud Deploy.

The configuration requires a number of files that we will analyse.

.gitlab-ci.yml

In this file, we had our CI which contains 3 steps :

  • build : build and push our docker image into the GitLab registry.
  • push : get the docker image stored in the GitLab registry and push it to the Google Cloud Artifact Registry.
  • deploy : with the command gcloud iam workload-identity-pools create-cred-config we create a configuration file for the generated credentials to connect to Google Cloud. Then, we create our Pipeline with the command gcloud deploy apply. Finally, we will create a Cloud Deploy release with the gcloud deploy releases create command.
# File: .gitlab-ci.yml
stages:
- build
- push
- deploy

docker-build:
# Use the official docker image.
stage: build
only:
refs:
- dev
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
# 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
when: on_success

push:
# Use the official docker image.
stage: push
only:
refs:
- dev
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- apk upgrade --update-cache --available
- apk add openssl
- apk add --update --no-cache python3 py-crcmod bash libc6-compat
- wget https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-412.0.0-linux-x86_64.tar.gz > /tmp/google-cloud-sdk.tar.gz -O /tmp/google-cloud-sdk.tar.gz | bash
- mkdir -p /usr/local/gcloud && tar -C /usr/local/gcloud -xvf /tmp/google-cloud-sdk.tar.gz && /usr/local/gcloud/google-cloud-sdk/install.sh --quiet
- export PATH=$PATH:/usr/local/gcloud/google-cloud-sdk/bin
- echo ${CI_JOB_JWT_V2} > .ci_job_jwt_file
- gcloud iam workload-identity-pools create-cred-config ${GCP_WORKLOAD_IDENTITY_PROVIDER}
--service-account="${GCP_SERVICE_ACCOUNT}"
--output-file=.gcp_temp_cred.json
--credential-source-file=.ci_job_jwt_file
- gcloud auth login --cred-file=`pwd`/.gcp_temp_cred.json
- gcloud config set project $GCP_PROJECT_ID
- printf 'yes' | gcloud auth configure-docker $GCP_REGION-docker.pkg.dev
- docker pull "$CI_REGISTRY_IMAGE${tag}"
- docker images
- docker tag "$CI_REGISTRY_IMAGE${tag}" $GCP_REGION-docker.pkg.dev/$GCP_PROJECT_ID/Artifact-Registry-Name/hello-word:latest
- docker push $GCP_REGION-docker.pkg.dev/$GCP_PROJECT_ID/Artifact-Registry-Name/hello-word:latest

environment:
name: push/$CI_COMMIT_REF_NAME
when: on_success

deploy:
stage: deploy
only:
refs:
- dev
image: google/cloud-sdk:latest

script:
- export RELEASE_TIMESTAMP=$(date '+%Y%m%d-%H%M%S')
- echo ${CI_JOB_JWT_V2} > .ci_job_jwt_file
- gcloud iam workload-identity-pools create-cred-config ${GCP_WORKLOAD_IDENTITY_PROVIDER}
--service-account="${GCP_SERVICE_ACCOUNT}"
--output-file=.gcp_temp_cred.json
--credential-source-file=.ci_job_jwt_file
- gcloud auth login --cred-file=`pwd`/.gcp_temp_cred.json
- gcloud config set project $GCP_PROJECT_ID
- gcloud deploy apply --file clouddeploy.yaml --region $GCP_REGION --project $GCP_PROJECT_ID
- gcloud deploy releases create release-$RELEASE_TIMESTAMP --delivery-pipeline cloud-run-pipeline --region $GCP_REGION --images app=$GCP_REGION-docker.pkg.dev/$GCP_PROJECT_ID/Artifact-Registry-Name/hello-word:latest --skaffold-file skaffold.yaml

environment:
name: deploy/$CI_COMMIT_REF_NAME

Note: replace Artifact-Registry-Name with the name of your Artifact Registry repository that you will create. For more information.

clouddeploy.yaml

This file contains the configuration of our pipeline. With requireApproval: true you require approval for the production environment.

apiVersion: deploy.cloud.google.com/v1
kind: DeliveryPipeline
metadata:
name: cloud-run-pipeline
description: application deployment pipeline
serialPipeline:
stages:
- targetId: dev-env
profiles: [dev]
- targetId: qa-env
profiles: [qa]
- targetId: prod-env
profiles: [prod]
---

apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
name: dev-env
description: Cloud Run development service
run:
location: projects/PROJECT_ID/locations/us-central1
---

apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
name: qa-env
description: Cloud Run QA service
run:
location: projects/PROJECT_ID/locations/us-west1
---

apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
name: prod-env
description: Cloud Run PROD service
requireApproval: true
run:
location: projects/PROJECT_ID/locations/us-south1

Note : replace PROJECT_ID by its real value in the clouddeploy.yaml file.

skaffold.yaml

Skaffold manages the workflow for deploying your application on Cloud Run.

apiVersion: skaffold/v3alpha1
kind: Config
metadata:
name: cloud-run-app
profiles:
- name: dev
manifests:
rawYaml:
- deploy-dev.yaml
- name: qa
manifests:
rawYaml:
- deploy-qa.yaml
- name: prod
manifests:
rawYaml:
- deploy-prod.yaml
deploy:
cloudrun: {}

deploy-dev.yaml

It contains the configuration file for the dev environment.

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: app-dev
spec:
template:
spec:
containers:
- image: app
resources:
limits:
cpu: 1000m
memory: 128Mi

deploy-qa.yaml

It contains the configuration file for the QA environment.

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: app-qa
spec:
template:
spec:
containers:
- image: app

deploy-prod.yaml

It contains the configuration file for the prod environment.

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: app-prod
spec:
template:
spec:
containers:
- image: app

It is in the deploy-dev.yaml, deploy-qa.yaml and deploy-prod.yaml files that you will put the configurations of your Cloud Run service. For the writing of these files do not hesitate to read this article 👇.

Using Cloud Console or Cloud Shell, you can switch from one release to another and from one environment to another with the Promote button or gcloud deploy releases promote .

As you can see, all resources are deployed.

Cloud Deploy provides you with a number of DORA metrics:

  • The Deployments metric shows the number of successful and failed deployments from the selected delivery pipeline to your production cluster.
  • The Deployment frequency metric shows how often the delivery pipeline successfully deploys to the production target per day. This is one of the four key metrics defined by DORA.
  • The Deployment failure rate metric shows the percentage of deployments that have failed.

Now it is up to you to adapt this example to your needs and objectives.

The code is available here 👉 https://gitlab.com/ricardobokove/gitlab-wif-deploy/

Thank you for reading this.

--

--

EZEKIAS BOKOVE

GDE & Champion Innovators for Google Cloud. Serverless & DevOps enthusiast. I like to learn from others, to share my knowledge with other people.