Deployment Pipeline
Let's setup a deployment pipeline using GitHub actions. We just need something to deploy so let's create a new website.
Create a Dockerfile with the following contents:
FROM nginx:1.19-alpine
COPY index.html /usr/share/nginx/html
and add index.html with the following content
<!DOCTYPE html>
<html>
<body style="background-color: gray;">
<p>
Content
</p>
</body>
</html>
Let's make sure that everything works by doing docker build . -t colorcontent && docker run -p 3000:80 colorcontent
to build and run it and then accessing it through http://localhost:3000. Next is the addition of manifests for our website.
manifests/service.yaml
apiVersion: v1
kind: Service
metadata:
name: dwk-environments-svc
spec:
type: LoadBalancer
selector:
app: dwk-environments
ports:
- port: 80
protocol: TCP
targetPort: 80
manifests/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: dwk-environments
spec:
replicas: 1
selector:
matchLabels:
app: dwk-environments
template:
metadata:
labels:
app: dwk-environments
spec:
containers:
- name: dwk-environments
image: jakousa/colorcontent
Next, to test our manifests deploy this into our cluster. Above I had pushed the built image using docker push
.
$ kubectl apply -f manifests/service.yaml
$ kubectl apply -f manifests/deployment.yaml
Kustomize
Applying multiple files like this gets bothersome. We can of course point it at a directory like so: kubectl apply -f manifests/
, but this is an excellent moment to turn our attention to Kustomize.
Kustomize is a tool that helps with configuration customization and is baked into kubectl. In this case we'll use it to define which files are meaningful for Kubernetes. Alternatively, we could use Helm and maybe Helmsman, but we will leave that outside of the context of this course.
For us a new file kustomization.yaml in the root of the project will work. The kustomization.yaml should include instructions to use the deployment.yaml and service.yaml.
kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- manifests/deployment.yaml
- manifests/service.yaml
Now we can deploy this using the -k
flag identifying that we want to use Kustomize.
$ kubectl apply -k .
We can preview the file with kubectl kustomize .
. Kustomize will be an essential tool for our deployment pipeline. It'll allow us to individually choose which image to use. For this let's declare the image inside the kustomization.yaml.
kustomization.yaml
# ...
images:
- name: PROJECT/IMAGE
newName: jakousa/colorcontent
This will replace the image "IMAGE:TAG" with the one defined in newName. Next setting a placeholder value inside the deployment.yaml for the image:
deployment.yaml
# ...
containers:
- name: dwk-environments
image: PROJECT/IMAGE
Test that everything works
$ kubectl kustomize .
...
spec:
containers:
- image: jakousa/colorcontent
name: dwk-environments
Kustomize has a few additional tools you can test out if you want to install it - but we'll see the usage in the next section.
The documentation of Kustomize in probably not the best one around. You will find more or less useful stuff by Google. One that you might want to check out is Kustomize Cheat Sheet.
Github Actions
GitHub Actions will be the CI/CD tool of choice for this course. Google also offers Cloud Build, and a step-by-step guide to deploying to GKE with it. You can return here to implement deployment with Cloud Build if you have credits left over after the course!
Create a file .github/workflows/main.yaml. We'll want the workflow to do 3 things:
- build the image
- publish the image to a container registry
- deploy the new image to our cluster
The initial config will look something like this:
main.yaml
name: Release application
on:
push:
env:
PROJECT_ID: ${{ secrets.GKE_PROJECT }}
GKE_CLUSTER: dwk-cluster
GKE_ZONE: europe-north1-b
IMAGE: dwk-environments
SERVICE: dwk-environments
BRANCH: ${{ github.ref_name }}
We set the workflow to run whenever changes are pushed to the repository and set the environment variables accordingly - we'll need them later on.
Next is adding the jobs. For simplicity, we'll add everything into a single job that'll build, publish and deploy.
# ...
jobs:
build-publish-deploy:
name: Build, Publish and Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
This sets the environment for the job and triggers the checkout action as the first step.
Next we'll use some additional actions, mainly from google-github-actions that are designed to help on deployments to Google Cloud. We'll start with authentication, followed by setup:
# ...
- uses: google-github-actions/auth@v2
with:
credentials_json: '${{ secrets.GKE_SA_KEY }}'
- name: 'Set up Cloud SDK'
uses: google-github-actions/setup-gcloud@v2
- name: 'Use gcloud CLI'
run: gcloud info
The secrets used in authentication are not from the environment variables but are included as environment secrets in the project GitHub:
Read here more on GitHub secrets.
Give these roles to your service account:
- Kubernetes Engine Service Agent - Gives Kubernetes Engine account access to manage cluster resources. Includes access to service accounts.
- Storage Admin - Grants full control of buckets and objects.
- Artifact Registry Administrator - Administrator access to create and manage repositories.
- Artifact Registry Create-on-Push Repository Administrator - Access to manage artifacts in repositories, as well as create new repositories on push.
After creating a service account for GKE called "github-actions" I created the key using gcloud:
$ gcloud iam service-accounts keys create ./private-key.json --iam-account=github-actions@dwk-gke-331210.iam.gserviceaccount.com
The entire JSON generated needs to be added to GKE_SA_KEY.
Next, use gcloud commands to configure Docker.
# ...
- run: gcloud --quiet auth configure-docker
This will enable us to push images to Google Container Registry, which we'll use instead of Docker Hub. We could use Docker Hub if we wanted to do so, but GCR is an excellent option now that we have access to it. GCR is a lot more performant and has a low network latency. Cutting down on the time we spend moving images around will ensure our deployments are quick. Read more about it here https://cloud.google.com/container-registry/. Note that the registry is not free and you'll probably want to delete the images from there during and after this course.
One more step is needed until we are ready to go, we still need to use action get-gke-credentials to our Google Kubernetes cluster to get credentials :
# ...
- name: 'Get GKE credentials'
uses: 'google-github-actions/get-gke-credentials@v2'
with:
cluster_name: '${{ env.GKE_CLUSTER }}'
project_id: '${{ env.PROJECT_ID }}'
location: '${{ env.GKE_ZONE }}'
And finally let's write out the desired image with a tag. The image will be gcr.io/PROJECT_ID/IMAGE:GITHUB_BRANCH-GITHUB_SHA
. And building the image:
# ...
- name: Build
run: docker build --tag "gcr.io/$PROJECT_ID/$IMAGE:$BRANCH-$GITHUB_SHA" .
We use the project name (from the env $IMAGE) as the image name and the tag is formed by concatenating the branch name, that comes from the env that we defined and the GitHub commit sha that is taken from env $GITHUB_SHA, that is automatically provided by the workflow.
Publish similarly:
# ...
- name: Publish
run: docker push "gcr.io/$PROJECT_ID/$IMAGE:$BRANCH-$GITHUB_SHA"
Last step is the deployment. We'll setup Kustomize first:
# ...
- name: Set up Kustomize
uses: imranismail/setup-kustomize@v2.1.0
Now we can use Kustomize to set the image we want the pipeline to publish. Here I pipe the output of kustomize build .
to kubectl apply
, if you are unsure what is happening you can output the kustomize build .
and check what was built in the middle of the pipeline!
Finally we'll preview the rollout and confirm that the release was a success. Rollout will wait until the deployment has rolled out. Here we use the same name of the image, dwk-environments, as the deployment name so we can use the $IMAGE environment variable.
# ...
- name: Deploy
run: |-
kustomize edit set image PROJECT/IMAGE=gcr.io/$PROJECT_ID/$IMAGE:$BRANCH-$GITHUB_SHA
kustomize build . | kubectl apply -f -
kubectl rollout status deployment $SERVICE
kubectl get services -o wide
Separate environment for each branch
A quite popular choice when using a deployment pipeline is having a separate environment for every branch - especially when using feature branching.
Let's implement our own version of this. Let's extend the previously defined pipeline. The current state of the pipeline looks as follows:
main.yaml
name: Release application
on:
push:
env:
PROJECT_ID: ${{ secrets.GKE_PROJECT }}
GKE_CLUSTER: dwk-cluster
GKE_ZONE: europe-north1-b
IMAGE: dwk-environments
DEPLOYMENT: dwk-environments
BRANCH: ${{ github.ref_name }}
jobs:
build-publish-deploy:
name: Build, Publish and Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: google-github-actions/auth@v2
with:
credentials_json: '${{ secrets.GKE_SA_KEY }}'
- name: 'Set up Cloud SDK'
uses: google-github-actions/setup-gcloud@v2
- name: 'Use gcloud CLI'
run: gcloud info
- run: gcloud --quiet auth configure-docker
- name: 'Get GKE credentials'
uses: 'google-github-actions/get-gke-credentials@v2'
with:
cluster_name: '${{ env.GKE_CLUSTER }}'
project_id: '${{ env.PROJECT_ID }}'
location: '${{ env.GKE_ZONE }}'
- name: Build and publish
run: |-
docker build --tag "gcr.io/$PROJECT_ID/$IMAGE:$BRANCH-$GITHUB_SHA" .
docker push "gcr.io/$PROJECT_ID/$IMAGE:$BRANCH-$GITHUB_SHA"
- name: Set up Kustomize
uses: imranismail/setup-kustomize@v2
- name: Deploy
run: |-
kustomize edit set image PROJECT/IMAGE=gcr.io/$PROJECT_ID/$IMAGE:$BRANCH-$GITHUB_SHA
kustomize build . | kubectl apply -f -
kubectl rollout status deployment $DEPLOYMENT
kubectl get services -o wide
What we'll want to do is deploy each branch into a separate namespace so that each branch has its own separate environment.
The namespace can be changed with kustomize:
kustomize edit set namespace ${GITHUB_REF#refs/heads/}
With this command, the namespace name will be equal to the branch name.
If the namespace is not defined, the command causes an error, so we need to create it:
kubectl create namespace ${GITHUB_REF#refs/heads/} || true
We also need to take the namespace to use with command:
kubectl config set-context --current --namespace=${GITHUB_REF#refs/heads/}
So the deploy step changes as follows:
- name: Deploy
run: |-
kubectl create namespace ${GITHUB_REF#refs/heads/} || true
kubectl config set-context --current --namespace=${GITHUB_REF#refs/heads/}
kustomize edit set namespace ${GITHUB_REF#refs/heads/}
kustomize edit set image PROJECT/IMAGE=gcr.io/$PROJECT_ID/$IMAGE:${GITHUB_REF#refs/heads/}-$GITHUB_SHA
kustomize build . | kubectl apply -f -
kubectl rollout status deployment $DEPLOYMENT
kubectl get services -o wide
To test this, edit the index.html and publish the changes to a new branch.
The next step would be to configure the domain names for each branch so that we'd have "www.example.com" as the production and e.g. "featx.example.com" as the featx branch. If you have any credits left after the course you can return here and implement it. Google Cloud DNS and this guide can get you started.