Cloud Build for CICD

Christopher Grant
11 min readNov 28, 2022

--

In this tutorial you’ll walk through the steps needed to set up an end to end pipeline where GitHub triggers Cloud Build to process changes based on branches and deploy to GKE

What you’ll learn

  • Enable services
  • Prepare source files
  • Build simple Dockerfile
  • Build with a config file
  • Connect to Git repository
  • Setup automated builds
  • Configure branch based deploys

Start Cloudshell Editor

This lab was designed and tested for use with Google Cloud Shell Editor. To access the editor,

  • A new pane will open in the bottom of your window
  • You can toggle between the terminal and an editor by clicking the Open Editor button in the top bar
  • Then click Open Terminal to switch back to the terminal view

Environment Setup

In Cloud Shell, set your project ID and the project number for your project. Save them as PROJECT_ID and PROJECT_ID variables.

export PROJECT_ID=$(gcloud config get-value project)
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID \
--format='value(projectNumber)')

Enable services

Enable all necessary services:

gcloud services enable \
cloudbuild.googleapis.com \
container.googleapis.com \
containerregistry.googleapis.com \
artifactregistry.googleapis.com \
containerscanning.googleapis.com

Create a Docker repository in Artifact Registry

Artifact Registry is a single place for your organization to manage container images and language packages (such as Maven and npm). In this lab, we will use artifact registry to manage container images.

Create a new Docker repository named quickstart-docker-repo in the location us-west2 with the description “Docker repository”:

gcloud artifacts repositories create quickstart-docker-repo --repository-format=docker \
--location=us-west2 --description="Docker repository"

Verify that your repository was created:

gcloud artifacts repositories list

You will see quickstart-docker-repo in the list of displayed repositories.

Prepare source files

Download the source code from github.

git clone https://github.com/GoogleCloudPlatform/software-delivery-workshop.git

cd software-delivery-workshop && rm -rf .git
cd labs/gke-progression

In this section, you’ll build a simple Python app and Dockerfile. A Dockerfile is a text document that contains instructions for Docker to build an image.

Review the sample application located in src/app.py

import os

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
return 'Hello World v1.0'

if __name__ == "__main__":
app.run(debug=True,host='0.0.0.0',port=8080)

Review the file src/Dockerfile with the following contents:

FROM python:3.7-slim

ENV APP_HOME /app
WORKDIR $APP_HOME
COPY . ./

RUN pip install Flask gunicorn

CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 app:app

Build simple Dockerfile

Cloud Build allows you to build a Docker image using a Dockerfile. You don’t require a separate Cloud Build config file.

To build using a Dockerfile:

gcloud builds submit --region=us-west2 --tag us-west2-docker.pkg.dev/${PROJECT_ID}/quickstart-docker-repo/quickstart-image:tag1 src/

Note the value assigned to the tag parameter in the above command. You’ve just built a container image named quickstart-image using a Dockerfile and pushed the image to Artifact Registry into the docker repository created earlier.

Build with a config file

In this section you will use a Cloud Build config file to build the same container image as above. The build config file instructs Cloud Build to perform tasks based on your specifications.

In the same directory that contains quickstart.sh and the Dockerfile, create a file named cloudbuild.yaml with the following contents.

cat > ./cloudbuild.yaml << EOF
steps:
- name: 'gcr.io/cloud-builders/docker'
args: [ 'build', '-t', 'us-west2-docker.pkg.dev/${PROJECT_ID}/quickstart-docker-repo/quickstart-image:tag1', './src' ]
images:
- 'us-west2-docker.pkg.dev/${PROJECT_ID}/quickstart-docker-repo/quickstart-image:tag1'
EOF

Start the build specifying the use of the cloudbuild.yaml by running the following command:

gcloud builds submit --region=us-west2 --config cloudbuild.yaml

You’ve just built an image using the build config file and pushed the image to Artifact Registry.

Substitution Variables

It’s often useful to provide variables to a build to configure specific parameters. In the cloudbuild.yaml file you just created, the region was hard coded to us-west2. In this step you will utilize substitution variables to provide the region dynamically

Built in Cloud Build Variables are accessible using the standard convention of ${VAR}, and user-defined substitutions must begin with an underscore (_) such as ${_VAR}

Rewrite the cloudbuild.yaml to use the build id variable provided by Cloud Build for the tag name and also use a user-defined variable for the region which will be passed in to the build.

cat > ./cloudbuild.yaml << EOF
steps:
- name: 'gcr.io/cloud-builders/docker'
args: [ 'build', '-t', '\${_REGION}-docker.pkg.dev/${PROJECT_ID}/quickstart-docker-repo/quickstart-image:\${BUILD_ID}', './src' ]
images:
- '\${_REGION}-docker.pkg.dev/$PROJECT_ID/quickstart-docker-repo/quickstart-image:\${BUILD_ID}'
EOF

Start the build by running the following command that passes in the region variable:

gcloud builds submit --region=us-west2 --config cloudbuild.yaml \
--substitutions=_REGION="us-west2"

Connect to Git repository

If you don’t have one already, create an account on GitHub.

NOTE: If you have two-factor authentication set up on GitHub, create a personal access token with Repo permissions to use instead of a GitHub password with the command line. Save the token, you will use it multiple times in the future steps.

  • Set your github username in an environment variable for later use
export GIT_USER=<your user name>
  • Set the git user name and email
git config --global user.email "$(gcloud config list account --format "value(core.account)")"
git config --global user.name "$(gcloud config list account --format "value(core.account)")
  • Optionally store credentials so you don’t have to enter them multiple times
git config --global credential.helper store
  • Create a new public repository in GitHub called cloud-build-cicd
  • Initialize a local git repo and push the contents to Github

Note: If using an access token, use the generated access token when prompted for password.

git init && git add . && git commit -m "initial commit"
git remote add origin https://github.com/${GIT_USER}/cloud-build-cicd.git
git branch -M main
git push -u origin main

To build source code from GitHub, you must first connect Cloud Build to your GitHub repository. In this section, you’ll use one way to connect your cloud-build-cicd repository to Cloud Build, but other mechanisms are also available.

  1. In the Google Cloud console navigation menu, click Cloud Build > Triggers.
    Open Triggers page
  2. At the bottom of the page, Click Connect repository.
  3. Under Select source, select GitHub (Cloud Build GitHub App).
  4. Click Continue.
  5. Authenticate your GitHub account.
  6. Under Repository, select GITHUB_USERNAME/cloud-build-cicd as your repository.
  7. Click the checkmark to agree to terms and conditions for trigger connection.
  8. Click Connect.
  9. Click Done.

Setup automated builds

Trigger

Open the Triggers page in the Google Cloud console:
Open Triggers page

At the bottom of the Triggers page, click Create trigger.

On the Create trigger page, enter the following settings:

  • Name: Enter sample-trigger as the name of your trigger.
  • Event: Select Push to a branch as the repository event to invoke your trigger.
  • Source: Select the GITHUB_USERNAME/cloud-build-cicd repository as your source, which contains your source code and your build config file.
  • Configuration: Choose Cloud Build configuration file as your build config file.
  • Cloud Build configuration file location: Specify the path to your Cloud Build configuration file as /cloudbuild.yaml

Under Substitution variables click Add Variable and enter the following values:

  • Variable 1 = _REGION
  • Value = us-west2

Click Create to save your build trigger.

Make a change

In this section, you will commit a change to your cloud-build-cicd repository on your GitHub account.

Open the src/app.py file and update the line containing “Hello World v1.0” to “Hello World v1.1”

@app.route('/')
def hello_world():
return 'Hello World v1.1'

Commit your changes to GitHub by running the following commands:

git add .
git commit -m "update to v1.1"
git push origin main

You may be prompted to enter your credentials when pushing code to your repository. If prompted, enter your username and an authentication token.

You have now pushed a change to your repository. Your push will result in an automatic build by your trigger.

Review build details

In this section, you will view the build details associated with your invoked build after committing a change.

  1. In the Google Cloud console navigation menu, click Cloud Build > History.
    Open the Cloud Build page
  2. You will see the Build history page:
  3. In the Build column, click the name of a build.
  4. On the Build details page, click Build Artifacts.
    Review the output locations of the assets build by this job

To view the build log, click the download icon and view the downloaded file.

You have successfully invoked a Cloud Build build using a trigger and viewed the build details.

Delete the trigger

You won’t use this trigger for the remainder of the lab so run the command below to delete it

gcloud beta builds triggers delete sample-trigger

Capstone: Full CI/CD Pipeline

In this section you will use what you’ve learned so far to build a typical developer workflow that incorporates dynamic branch based deployments for dev testing, Canary testing of commits to the main branch, and production rollout for tagged releases.

Create a GKE Cluster

This section will deploy to GKE so create a small cluster with the following commands

export CLUSTER=mycluster
export ZONE=us-central1-c

gcloud container clusters create ${CLUSTER} --zone=${ZONE} --num-nodes "2"

Give Cloud Build rights to your cluster.

Cloud Build will be deploying the application to your GKE Cluster and will need rights to do so.

gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member=serviceAccount:${@cloudbuild.gserviceaccount.com">PROJECT_NUMBER}@cloudbuild.gserviceaccount.com \
--role=roles/container.developer

Replace placeholder values in the sample repository with your PROJECT_ID:
This command creates instances of the various config files unique to your current environment. It searches all the template files with extension tmpl, replaces the environment variables with the values and creates new files without tmpl extension.


export APP_NAME=sample-app

for template in $(find . -name '*.tmpl'); do envsubst '${PROJECT_ID} ${ZONE} ${CLUSTER} ${APP_NAME}' < ${template} > ${template%.*}; done

Branch Triggers for developers

Open the cloudshell editor in the current directory with the following command

cloudshell workspace .

Create a file called cloudbuild-branch.yaml with the following contents

steps:

### Build
- id: 'build'
name: 'gcr.io/cloud-builders/docker'
entrypoint: 'bash'
args:
- '-c'
- |
docker build -t ${_REGION}-docker.pkg.dev/${PROJECT_ID}/quickstart-docker-repo/$_APP_NAME:${BRANCH_NAME}-${SHORT_SHA} ./src

### Publish
- id: 'publish'
name: 'gcr.io/cloud-builders/docker'
entrypoint: 'bash'
args:
- '-c'
- |
docker push ${_REGION}-docker.pkg.dev/${PROJECT_ID}/quickstart-docker-repo/$_APP_NAME:${BRANCH_NAME}-${SHORT_SHA}



### Deploy
- id: 'deploy'
name: 'gcr.io/cloud-builders/gcloud'
env:
- 'KUBECONFIG=/kube/config'
entrypoint: 'bash'
args:
- '-c'
- |

PROJECT=$(gcloud config get-value core/project)


gcloud container clusters get-credentials "${_CLUSTER}" \
--project "${PROJECT}" \
--zone "${_ZONE}"

sed -i 's|gcr.io/$PROJECT_ID/$_APP_NAME:.*|${_REGION}-docker.pkg.dev/${PROJECT_ID}/quickstart-docker-repo/$_APP_NAME:${BRANCH_NAME}-${SHORT_SHA}|' ./k8s/deployments/dev/*.yaml

kubectl get ns ${BRANCH_NAME} || kubectl create ns ${BRANCH_NAME}
kubectl apply --namespace ${BRANCH_NAME} --recursive -f k8s/deployments/dev
kubectl apply --namespace ${BRANCH_NAME} --recursive -f k8s/services

images:
- '${_REGION}-docker.pkg.dev/${PROJECT_ID}/quickstart-docker-repo/$_APP_NAME:${BRANCH_NAME}-${SHORT_SHA}'

Toggle back to the terminal by clicking the Open Terminal button in the top nav bar

Create the trigger

gcloud beta builds triggers create github \
--name="branch-trigger" \
--repo-name=cloud-build-cicd \
--repo-owner=${GIT_USER} \
--branch-pattern="[^(.main)]" \
--build-config=cloudbuild-branch.yaml \
--substitutions=_APP_NAME="sample-app",_CLUSTER="mycluster",_REGION="us-west2",_ZONE="us-central1-c"

You can view the newly create trigger on the Cloud Build Triggers page

The trigger you just created will fire for commits to any branch that is not named main.

Create a branch

git checkout -b mybranch

Edit src/app.py change the output to v2.0

Commit the change and push to the repository

git add .
git commit -m "Updated to v2.0"
git push origin mybranch
  1. Review the build in progress on the build history page

Open the Cloud Build page

Review GKE workloads. A new namespace ‘mybranch’ was created that matches the name of the branch and a sample-app-dev application was deployed.

Main Triggers for Canary deploys

Create a file called cloudbuild-main.yaml with the following contents

steps:

### Build

- id: 'build'
name: 'gcr.io/cloud-builders/docker'
entrypoint: 'bash'
args:
- '-c'
- |
docker build -t ${_REGION}-docker.pkg.dev/${PROJECT_ID}/quickstart-docker-repo/$_APP_NAME:${SHORT_SHA} ./src


### Test


### Publish
- id: 'publish'
name: 'gcr.io/cloud-builders/docker'
entrypoint: 'bash'
args:
- '-c'
- |
docker push ${_REGION}-docker.pkg.dev/${PROJECT_ID}/quickstart-docker-repo/$_APP_NAME:${SHORT_SHA}



### Deploy
- id: 'deploy'
name: 'gcr.io/cloud-builders/gcloud'
env:
- 'KUBECONFIG=/kube/config'
entrypoint: 'bash'
args:
- '-c'
- |
PROJECT=$$(gcloud config get-value core/project)

gcloud container clusters get-credentials "${_CLUSTER}" \
--project "$${PROJECT}" \
--zone "${_ZONE}"


sed -i 's|gcr.io/$PROJECT_ID/$_APP_NAME:.*|${_REGION}-docker.pkg.dev/${PROJECT_ID}/quickstart-docker-repo/$_APP_NAME:${SHORT_SHA}|' ./k8s/deployments/canary/*.yaml

kubectl get ns production || kubectl create ns production
kubectl apply --namespace production --recursive -f k8s/deployments/canary
kubectl apply --namespace production --recursive -f k8s/services

images:
- '${_REGION}-docker.pkg.dev/${PROJECT_ID}/quickstart-docker-repo/$_APP_NAME:${SHORT_SHA}'

Add the new yaml you just created file to the repo

git add . && git commit -m "Added Main Trigger"

Create the trigger for commits to the main branch

gcloud beta builds triggers create github \
--name="main-trigger" \
--repo-name=cloud-build-cicd \
--repo-owner=${GIT_USER} \
--branch-pattern=main \
--build-config=cloudbuild-main.yaml \
--substitutions=_APP_NAME="sample-app",_CLUSTER="mycluster",_REGION="us-west2",_ZONE="us-central1-c"

You can view the newly created trigger on the Cloud Build Triggers page

Merge the branch into main

git checkout main
git merge mybranch
git push origin main

This will trigger the build job for the main branch

Review the build in progress on the build history page

Open the Cloud Build page

Review GKE workloads. Production name space was created and a sample-app-canary application was deployed.

Tag Triggers for production releases

Create a file called cloudbuild-tag.yaml with the following contents

steps:

### Add Tag

- id: 'add-tag'
name: 'gcr.io/cloud-builders/gcloud'
entrypoint: 'bash'
args:
- '-c'
- |
gcloud artifacts docker tags add ${_REGION}-docker.pkg.dev/${PROJECT_ID}/quickstart-docker-repo/$_APP_NAME:${SHORT_SHA} \
${_REGION}-docker.pkg.dev/${PROJECT_ID}/quickstart-docker-repo/$_APP_NAME:$TAG_NAME \
--quiet



### Deploy
- id: 'deploy'
name: 'gcr.io/cloud-builders/gcloud'
env:
- 'KUBECONFIG=/kube/config'
entrypoint: 'bash'
args:
- '-c'
- |
PROJECT=$$(gcloud config get-value core/project)

gcloud container clusters get-credentials "${_CLUSTER}" \
--project "$${PROJECT}" \
--zone "${_ZONE}"


sed -i 's|gcr.io/$PROJECT_ID/$_APP_NAME:.*|${_REGION}-docker.pkg.dev/${PROJECT_ID}/quickstart-docker-repo/$_APP_NAME:$TAG_NAME|' ./k8s/deployments/prod/*.yaml

kubectl apply --namespace production --recursive -f k8s/deployments/prod
kubectl apply --namespace production --recursive -f k8s/services

Notice that in this configuration you’re not rebuilding the source, but reusing the previously built image.

Add the new yaml file to the repo so it’s available to the trigger

git add . && git commit -m "Added Tag Trigger" && git push origin main

Wait for the main build to finish

Open the Cloud Build page

Create the trigger

gcloud beta builds triggers create github \
--name="tag-trigger" \
--repo-name=cloud-build-cicd \
--repo-owner=${GIT_USER} \
--tag-pattern=.* \
--build-config=cloudbuild-tag.yaml \
--substitutions=_APP_NAME="sample-app",_CLUSTER="mycluster",_REGION="us-west2",_ZONE="us-central1-c"

Exercise the trigger by pushing a tag to the repo

git tag 2.0
git push origin 2.0

Open the Cloud Build page

Review GKE workloads. Sample-app-production application was deployed to production namespace.

--

--