CI/CD & DevOps

When I joined the company, deploying software was a surprisingly manual process. The deployment instructions were literally stored in a TXT file.
A developer had to connect to three different production servers, manually update the code, run a sequence of commands, and repeat the process on each server.
The servers were behind a load balancer, but the deployment itself wasn’t coordinated.
It was slow, error-prone and stressful.
A typical deployment took around 50 minutes, and it wasn’t unusual for something to go wrong along the way — sometimes leaving the system partially updated or temporarily unavailable.
We needed to change that.
The first thing I changed wasn’t Jenkins.
It was Git.
The entire codebase was still in Subversion. Moving to Git gave us much more flexibility around branching and feature development, and it provided the foundation for everything that followed.
We initially adopted a Git Flow-inspired branching model and then adapted it to our own development process as we learned what worked and what didn’t.
This was an important pattern throughout the transformation:
Don’t adopt a process because it’s popular. Adapt it to the way your team actually works.
Once the source code was in Git, the next problem became obvious.
We didn’t want developers manually executing deployment instructions anymore.
I evaluated and experimented with several CI/CD platforms.
They all had strengths and weaknesses. Some had better user experiences, others were easier to get started with, and some offered more built-in functionality.
But I kept running into the same limitation: I wanted the deployment process to be ours.
In the end, I chose Jenkins.
It wasn’t the easiest option.
The UI wasn’t great. Running Jenkins ourselves meant managing controllers and workers, and upgrades could be painful. A managed CI/CD platform would have removed some of that operational overhead.
But Jenkins gave us something I valued more at the time: flexibility.
We could build exactly the deployment workflows we needed and extend them with our own code.
The initial setup was relatively simple:
Git → Jenkins → tests → build → deploy
Jenkins handled the orchestration, while shell scripts and Groovy handled the implementation details.
But the system quickly grew beyond a collection of pipelines.
We introduced shared Jenkins libraries and our own Groovy-based DSL so that common deployment logic didn’t have to be duplicated across projects.
Instead of every project implementing its own version of the deployment process, we started building reusable capabilities into the platform itself.
The deployment process became something closer to:
Git push → tests → build → image → deployment
And eventually Docker and Kubernetes became part of the picture.
As the infrastructure evolved, so did the pipeline. We introduced:
- Docker-based builds
- Kubernetes deployments
- Parallel builds & deployments
- Shared Jenkins libraries
- Groovy deployment abstractions
- Automated testing
- Multi-stage builds
- Environment-specific logic
- Blue-green deployments
- Canary-style deployments
The important part wasn’t any individual technology.
It was that the deployment process became software itself.
Instead of maintaining a document describing how humans should deploy an application, we built a system that knew how to do it.
One of the biggest problems with the original process was that deploying the application meant modifying the running production environment directly.
There was very little room for error.
As we moved towards containerized Kubernetes deployments, we could change that.
Blue-green deployments allowed us to prepare a new version separately and switch traffic when it was ready. Later, we also introduced canary-style deployments where appropriate.
This fundamentally changed the risk profile of a deployment. A release was no longer:
Connect to server → update files → hope everything works
It became:
Build → test → deploy → verify → shift traffic
That distinction matters.
The deployment process went from roughly 50 minutes to around 10 minutes.
But speed wasn’t actually the most important improvement.
The bigger change was repeatability. Developers no longer had to remember a sequence of commands or connect to production servers manually. Deployments became:
- Automated
- Repeatable
- Faster
- Safer
- Easier to audit
- Less dependent on individual knowledge
And because the deployment logic itself was version-controlled, the process could evolve alongside the applications.
Three lessons stuck with me long after the pipeline was running.
“The best deployment is the one nobody has to think about.”
The most satisfying part of this project wasn’t going from 50 minutes to 10.
It was seeing deployment become boring.
No one needed to SSH into three production servers. No one needed to open a TXT file and follow a list of commands. No one had to wonder whether someone forgot step seven.
The system did the work.
When the deployment process becomes boring, predictable and almost invisible, you’ve probably done something right.
