Moving ~15 services in daily campus use onto Kubernetes and GitOps
Every service deployed differently and some were over ten years old. Nothing moved at once: dependencies were assessed first, then services migrated one by one.
- Kubernetes
- Docker
- ArgoCD
- Helm
- GitHub Actions
- Vault
- Grafana
- Nginx
- MySQL
- Linux
Specification
- Organisation
- SKKU Systems Consultant Group (SCG)
- Period
- 2022 – 2026
- Environment
- Self-operated on-premise servers
- Scope
- About 15 web services in real campus use
- Migration
- Dependency assessment, then service-by-service
- Role
- Dev lead → president · server and infrastructure ownership · technical decisions
Context
Every service deployed differently — some under PM2, some under Docker. The projects over ten years old were bound tightly to a local filesystem and to one particular host.
Much of the operating knowledge lived in people's memory rather than in a document: which server ran what was something you had to know.
Approach
Nothing moved at once. For each service I first established whether it was stateless or stateful, how it depended on the local filesystem and the database, where it wrote files, what breaking it would affect, and whether it could be recovered.
Services that could move went to Kubernetes in stages. Services that could not stayed where they were, with the reason written down.
Desired state was declared in Git and reconciled into the cluster by ArgoCD. Helm held per-service differences as values; GitHub Actions built the images.
A Sync status in ArgoCD did not count as a deployment check. After every deploy I confirmed that pods were actually replaced, that containers were healthy, and that the service responded. Declared state and running behaviour are not the same thing.
Vault held the secrets, so credentials never sat in code or plain configuration. Alongside Grafana-based monitoring, I dealt with the gap between a passing health check and a working service.
System
A Sync status is not a deployment check. The response is.
A commit is built into an image by GitHub Actions and the desired state is declared in Git. After ArgoCD reconciles the cluster, pod replacement, container health and service response are each checked directly.
Measured
~15
Web services in real campus use.
10y+
Bound tightly to a local filesystem and a specific host.
Boundaries
- etcd failure, single points of failure, node failure, high availability, stateful workloads and persistent storage, and whether the database belongs inside or outside the cluster — these were reviewed and discussed during operation. I did not build them.
- No infrastructure-as-code tooling (Terraform, Ansible) was used.
- The group's infrastructure was run as a team; I held overall ownership and technical decisions.