GKE · GitOps · 10 min read
Party started as a fairly standard application: a frontend, a backend and PostgreSQL.
Once the application worked, I started using it to learn the deployment side. I wanted to try GKE, Terraform, Helm and Argo CD on a real project. Certificates, external secrets and monitoring were added later.
I did not have the whole architecture planned from the start. I tried a few approaches, kept some and removed others.
Where the setup ended up
Terraform on one side, Helm on the other
I used Terraform for the GCP resources: the GKE cluster and what was needed to connect Google Secret Manager. The application and its Kubernetes resources lived in a Helm chart.
Terraform → GCP infrastructure
Helm → Kubernetes resources
Argo CD → sync from GitI could have managed the Kubernetes resources with Terraform too, but I did not. Once the cluster existed, I found Helm easier to work with when changing and redeploying the application.
I would probably keep that split today. It is not the only valid setup, but it remained understandable for this project.
Moving away from latest
My first workflow built two Docker images, one for the client and one for the server, then pushed both to GHCR using the latest tag.
That was enough at first. It became a problem when I wanted to check which version was actually running in the cluster. The image name did not answer that because latest could point to different content after every build.
party-server:<git-sha>
party-client:<git-sha>I switched to tags based on the commit SHA. I also tried using image digests with Argo CD Image Updater. That part took more attempts than I expected.
The loop caused by Argo CD Image Updater
I then added Argo CD Image Updater to automate image updates. It detected a new image, changed the configuration in Git and let Argo CD sync the cluster.
The automated commits worked. My CI workflow also ran on Git changes, though. Image Updater could create a commit that started a build, produced another image and triggered Image Updater again.
if: github.actor != 'argocd-image-updater'I added this condition so GitHub Actions would ignore commits created by Image Updater. It stopped the loop. I simply had not considered that interaction when I connected the two tools.
GitHub Actions was deploying the application too
In the final setup, Argo CD synchronized the application, but GitHub Actions also fetched the GKE credentials and ran helm upgrade --install directly.
helm upgrade --install party-app infra/helm/party \
--namespace party --create-namespace --reuse-values \
--set client.image.tag="<git-sha>" \
--set server.image.tag="<git-sha>"I had two separate paths capable of deploying the application. That was unnecessary and made it harder to know whether the cluster really matched the configuration in Git.
Today I would separate them more clearly. CI would test, build and push the images, then stop. It would not access the cluster. Argo CD would handle the deployment from the versioned configuration in Git.
Ingress, certificates and secrets
Getting the Deployments and Services to run was not what took most of my time. I spent more time going back and forth on NGINX Ingress and cert-manager: annotations, TLS secrets and certificate configuration all needed a few corrections.
For credentials, I did not want to keep them in Helm values. I used Google Secret Manager with External Secrets Operator to copy them into Kubernetes Secrets.
This added a few more resources to understand and configure, but the secrets stayed out of both the chart and the repository.
Monitoring came last
I added Prometheus and Grafana fairly late. The backend exposed a /metrics endpoint, and Prometheus collected application metrics alongside cluster metrics.
The Kubernetes metrics showed me the state of the containers. The backend metrics answered a different set of questions: were events being created, were people joining them, and was the application actually being used?
If the goal is to monitor an application, I would start by instrumenting the backend. Prometheus and Grafana can only display what the application exposes. Without useful application metrics, it is easy to end up with good-looking CPU and memory dashboards that still say very little about whether the product works.
What I would do now
Party did not need GKE to run. A simpler managed service would have been enough. The project was also how I was learning Kubernetes and the surrounding tools, so over-engineering it was part of the exercise.
I would keep Terraform, Helm, external secrets and monitoring. I would mostly simplify the deployment: no helm upgrade in GitHub Actions, no complicated digest handling until it solves a real problem, and only one path capable of changing what runs in the cluster.
I would not repeat every step in the same way. Several parts worked, but were more complicated than they needed to be.
What stayed with me
Before Party, I still thought about Kubernetes mostly in terms of Deployments, Services and Pods. The project made me spend most of my time on everything around them:
- tracking the exact version that was deployed;
- stopping several tools from deploying at the same time;
- getting secrets into Pods without putting them in Git;
- exposing the application over HTTPS and checking that it actually worked.
That was also when I started spending more time on the platform around the code than on the application itself.