All posts
Misc

continuous what and continuous who?

9 min read
DevOpsCI/CDSoftware Engineering

Continuous what and continuous WHO? CICD… continuous integration and continuous deployment/delivery. Now more than ever, in an age where AI can fast track the actual creation of products, the ability to quickly integrate, verify, and ship changes to production separates those that can move fast and adapt to today's volatile environment from those who get left behind. To be specific, continuous integration and continuous delivery refers to the practice of automating building, testing, and releasing code changes. Using a variety of tools - Github, Jenkins, ArgoCD, etc - we can eliminate manual bottlenecks and make sure code is secure, functioning, and efficient the second any developer pushes a change.

Recently at work I've been a part of DevSecOps (development, security, & operations), the core team responsible for creating CICD standards and implementations for other teams via pipelines. Through the help of my team and lots of … exploration … I was able to build an end to end pipeline for one of our company's internal ai chat applications that utilized Jenkins, a shared Groovy library, security scanning, testing, artifact delivery, and an OpenShift deploy. In the end, while it did a great job at replacing a manual release process, I felt like enterprise tooling had a way of still hiding many of the fundamentals of what was being done. The shared library worked somewhat like a blackbox, a platform team owned the cluster and containers, and many of the scans performed were siloed off to other departments.

Overall, while the job was done, I felt like I didn't fully have a great grasp of the principles at hand, what actually was happening behind the scenes - from 'git push' to the new code now being reflected in production. Because of that desire to understand things end to end, I was prompted to create this project to hopefully learn by doing and, subsequently, teaching.

So … the plan: build something from scratch with the modern open-source stack - GitHub Actions, Docker, GHCR, k3s, Helm, Trivy, and ArgoCD. Two services (a Spring Boot API and a React frontend) ride along, but they're not the main point. Instead, everything around them is: the pipelines, the containers, the cluster, and the GitOps loop.

If you'd rather just go play with the finished thing, the live demo is right here and all the code lives on GitHub. Otherwise, stick around — let's get into it!

Considerations and Limitations

Before diving in, I wanted to be honest with myself about a few constraints — namely, keeping this thing interactive, educational, and (maybe most importantly for my wallet) affordable. So here are a few of the calls I made to balance all of that.

Keep the app boring on purpose. Since the whole point is the pipeline, the actual app barely does anything — a health check and some metadata, nothing more. Resisting shiny object syndrome is a lot easier when there's no shiny object to chase in the first place!

Run it on my laptop, not the cloud. In a real company these apps would live on the cloud — rented computers that are always on. Mine runs locally, on my own laptop, which … isn't always on. So when my "cluster" (the group of machines actually hosting the app) is asleep, the demo falls back to a clearly labeled simulated replay — the exact same steps you'd see if a real cluster were awake.

The demo rebuilds the very thing you're talking to. This one was a fun headache. When you hit Deploy, the change you make rebuilds and replaces the backend (the program on the server handling your request) — right in the middle of the conversation. The problem: the page needs to keep showing the pipeline's progress, but the exact server it's been tracking gets thrown out and swapped for a new one halfway through. My fix was to make the progress tracker "stateless" — it remembers nothing about the deploy. Every time the page asks "how's it going?", the server works out the answer fresh from outside facts (what GitHub's records say, and whether the live site is serving your new message yet) instead of leaning on its own memory. That way it doesn't matter which server answers — old or new, they both land on the same answer, and the tracking never breaks even though the thing being tracked is busy replacing itself.

The Deploy button isn't behind a login. Building real accounts and logins felt like overkill for a demo, so the button is gated by a simple shared token — a password-like string that keeps random bots out — and I'll be the first to admit it's a speed bump, not real security. The part that actually matters is the dangerous bit: the secret key allowed to write to my GitHub repo. That key never touches your browser — it lives only on the server, tucked away in a Kubernetes Secret (the cluster's built-in vault for sensitive values). So even with the button wide open, the powerful capability behind it stays locked up. A real product would put a proper login out front.

The security scan can't cry wolf. Every image gets checked for known security flaws — called CVEs (publicly catalogued vulnerabilities) — by a tool called Trivy, and the pipeline refuses to ship if it finds anything serious. Here's the tricky part though: sometimes a flaw gets reported in something I depend on before its maintainers have even released a fix. If the build failed on those, it'd be stuck red forever on a problem I literally can't solve — and people learn real quick to ignore a scanner that's always whining. So I set it to fail only on flaws that have an available fix (which forces me to actually go upgrade), and for the rare one I've reviewed and accepted, I jot it down in a file with a written reason. The idea is simple: the build should only stop you for problems you can actually do something about.

Final Product & How It Works!

So, after all that, here's the full modern CI/CD loop the app pulls off:

git push → GitHub Actions (test, build, Trivy scan) → push image to GHCR
  → CD bumps the image tag in the Helm chart (a commit back to Git)
  → ArgoCD detects the change → syncs the cluster → the new version is live

Let's walk through it.

First, we send a message through our own pipeline. You type something into the box, hit Deploy, and the frontend (the website you're looking at) hands that message off to the backend — the little Spring Boot program running on the server.

Here's where it gets interesting. Instead of just saving your message somewhere, the backend commits it straight into the project's code on GitHub (using that secret key we keep locked away). In other words, your message becomes a real change to the codebase — exactly as if a developer had edited a file and run git push. And that one push is what kicks the whole machine into motion.

The second that commit lands, GitHub Actions — GitHub's built-in automation, and our "continuous integration" engine — wakes up and works through the pipeline one stage at a time.

Test: it runs the project's automated tests to confirm the code still behaves the way it should. One failing test and everything stops right here — nothing broken gets to move forward.

Build: it packages the app into a Docker image — think of it as a sealed box holding the app and everything it needs to run, so it behaves identically no matter where you open it.

Scan: Trivy inspects that box for known security flaws (CVEs). Find something serious, and again, the line halts.

Push: only once it's cleared every check does the finished image get uploaded to GHCR (GitHub's storage for images), stamped with the exact code version it came from — so there's never any guessing about what's inside.

Now for the "continuous delivery" half. A second automation grabs that freshly built image and updates a single line in our Helm chart — Helm being the "recipe" that describes how the app should run on the cluster, and that one line being which version of the image to use. Then it commits that change back to Git. Notice what it doesn't do: it never reaches into the cluster and deploys anything itself. It just writes down what should be true.

And that's where ArgoCD comes in — my favorite part. ArgoCD lives inside the cluster and basically does one thing: it constantly compares what's written in Git against what's actually running, and quietly fixes any difference. So the instant it sees that new version written down, it pulls the new image and rolls out a fresh copy of the app, gracefully retiring the old one once the new one is healthy. Nobody logged into a server. Nobody ran a deploy command. Git said "this is the version that should be live," and the cluster made itself match. That idea — Git as the single source of truth, with the cluster reconciling to it — is the whole heart of what's called "GitOps."

This whole time, the page is quietly checking in every couple of seconds, lighting up each stage as it finishes with a plain-English note on what's happening. And when that last piece finishes booting up, it's now serving your message. The page flips to "Live," and there it is — proof that the words you typed traveled through the entire professional release process — tested, packaged, security-scanned, published, and deployed to a live cluster — without a single manual step in between.

That's the loop. A message in, the whole machine turning on its own, and a new version live on the other side. And after building every piece of it by hand, I can finally say I know exactly what happens between git push and "it's live" — which was the entire point.