CI / CD
This section is the operational map of how 6grain FrontEnd applications are delivered: where the build servers, registries, and clusters live, what the pipeline does step by step, and which knobs you touch to run it.
Every FrontEnd application follows one path — its git repository is built by TeamCity from a docker-compose.yml, pushed to AWS ECR, rolled onto EKS with kubectl set image, and served through Traefik, with a notification posted to Slack.
The delivery path
The same five stages run for every app on both environments. What differs between apps is values, not the shape of the path — those values are the six parameters described below.
Where things live
Staging and production are fully separate stacks — separate TeamCity servers, separate clusters, separate ECR tag prefixes. When something is missing, this table says which layer to open.
Layer | Staging | Production | Notes |
|---|---|---|---|
CI server |
|
| Runs inside the cluster: namespace |
Build configs | TeamCity project | same project on both servers | Configs e.g. |
Build agents |
|
| EC2 |
App source | git repo per app |
| Compose dir varies: |
Image registry | AWS ECR — account | same | Repos e.g. |
Image tags |
|
| Lifecycle policies keep the last N images per repo; the moving |
Cluster |
|
| Namespace |
Ingress |
|
| Traefik v3. This documentation is served at |
The compose pipeline
Multi-container apps run this shell-script pipeline. Steps 1–2 are optional and usually off; the rest always run. The order is the execution order.
Tests (optional) — runs the app's test suite; commonly disabled.
Restore database from backup (optional) — seeds a fresh database before build/deploy.
Get containers list — parses
docker-compose.ymland pairs eachcontainer_namewith itsimagewithin the same service block, drops the infra services from the exclude list, and writes the survivors intoenv.containers/env.images.List containers — prints the resolved
container → imagepairs so the build is auditable before anything is built.Build containers — builds each resolved container. Working directory is the compose directory; runs under
set -e, so any build failure stops the pipeline immediately.docker-compose %app.compose.buildfiles% build %app.build.nocache% "$container"Refresh ECR token —
aws ecr get-login-password | docker login ….Create ECR repositories if not exist — ensures a repository exists for every image so a brand-new service does not fail the push.
Push containers to ECR — pushes each image, and fails loudly if an expected image is not in the local store. This guard is what stops a non-existent tag from being "deployed".
Remove image from local registry — cleans built images off the agent so the EC2 disk does not fill up across builds.
Deploy to Kubernetes — points kubeconfig at the target cluster, then delivers each container according to
deploy.kind:app(default) — rolls the app by patching its Deployment's image, then gates on readiness: the build stays red until the new pod is actuallyReady(observedGenerationcaught up andreadyReplicasmatches the spec).aws eks --region us-east-1 update-kubeconfig --name %env.cluster% kubectl set image deployment/$c%app.deploy.suffix%-deployment \ $c%app.deploy.suffix%=${images[$i]} -n front-endjob(migrators) — runs the image as a one-shotJobrather than a standing Deployment, streams its log, gates on the migration finishing, and removes the Job. See Migrators run as Jobs below.
Notify deployed (Slack) — posts a service message through the Slack connection
PROJECT_EXT_5to#qaand#k8s, listing the Jira keys found in the build's commits.
The knobs: six parameters
Apps do not differ in pipeline logic — they differ in these six values, set per build config. Two more values are constants pinned at the project/server level.
Parameter | Purpose | Seen values |
|---|---|---|
| Directory holding |
|
| The |
|
| Extra build flags | (empty), |
| Which services to ship — empty = all, one container name = only that one (migrator mode) | (empty), |
| Infra services never deployed |
|
| Infix in the Deployment name: | (empty), |
| How the deploy step delivers the image: |
|
Project-level constants: env.cluster (staging/production) and env.tagprefix (stg_/prod_). A migrator is an ordinary config with app.select=<name> and deploy.kind=job — see Migrators run as Jobs.
Two build templates
No FrontEnd config carries its own copy of the pipeline — each is a TeamCity Build Configuration Template with per-config parameters filled in. There are two templates, split only by how the image is built: a multi-container compose build, or a single Dockerfile. The delivery half — push to ECR, roll onto EKS, notify Slack — is the same idea in both.
FrontEnd_ComposePipeline — Compose Pipeline
Multi-container applications assembled from a docker-compose.yml. Images are built by shell steps that shell out to docker-compose. Each app's compose file declares services as image: ${DOCKER_REGISTRY}/<repo>:${TAG} with a container_name; infra services are present but excluded. 13 configs on staging.
Steps — the nine shell steps described above (plus the optional Tests / Restore DB): Get containers list → List containers → Build containers → Refresh ECR token → Create ECR repositories → Push containers to ECR → Remove local image → Deploy to Kubernetes → Notify (Slack).
Parameters — six, per config (values in The knobs above):
Parameter | Purpose |
|---|---|
| Directory holding |
| The |
| Extra build flags ( |
| Which services to ship — empty = all, one name = migrator mode |
| Infra services never deployed |
| Infix in the Deployment name |
|
|
FrontEnd_NativePipeline — Native Pipeline
Single-image applications built from one Dockerfile by TeamCity's built-in DockerCommand runner — no compose, no shell parsing. A shorter, five-step shape; the deploy step carries a rollout-status gate.
Steps — five: Create ECR repository → Docker build application (DockerCommand) → Docker push (DockerCommand) → Deploy to Kubernetes (+ rollout-status gate) → Notify (Slack).
Parameters — two, per config:
Parameter | Purpose | Seen values |
|---|---|---|
| ECR repository / image for this app |
|
| Path to the |
|
The Deployment and container name are not a separate parameter — they are derived as basename(ImageName) (loan-apply, loan-portfolio, dspspa, code-quality), giving deployment/<name>-deployment in front-end.
Members: LoanApplication, LoanPortfolio, DataSharingPlatform, CodeQuality (plus SixGrainSite on production).
Migrators run as Jobs
A migrator image applies its EF Core migrations on startup and then keeps a web host running — it never exits. Run as a standing Deployment it re-applies migrations on every pod restart, which crashes on non-idempotent migrations. So a migrator config sets deploy.kind=job and the deploy step delivers it as a one-shot Job instead:
the Job runs the freshly built image (
restartPolicy: Never,backoffLimit: 0,ttlSecondsAfterFinished: 600) — one run, no automatic retry, no re-migration on restart;the step streams the pod log into the build log, and the build goes green only when the migration marker appears (the app starts listening) — red on
ImagePullBackOff, a crash, or timeout;the Job and its pod are removed at the end; the migrator's ConfigMap and Secret stay.
Job name / ConfigMap / Secret default to <container>-job/-configmap/-secrets. When those names do not follow the convention (e.g. global-presentation), override job.configmap/job.secret on the config.
RBAC: the teamcity user has a narrow front-end Role (teamcity-migrator) granting jobs create/get/list/watch/delete and pods + pods/log read — scoped to front-end so build-time log access cannot reach other namespaces.
Conventions
Thing | Rule | Example |
|---|---|---|
Build number |
|
|
Image tag |
|
|
Repository |
|
|
Deployment |
|
|
Namespace |
| all FrontEnd workloads, both clusters |
Ingress host |
|
|
Slack notice | connection |
|
Operations
Deploy an app
Push to the app's git repo, then run its FrontEnd_<App> config on the target server. The pipeline builds, pushes the new <prefix><build> tag to ECR, patches the Deployments with kubectl set image, and posts the Jira keys to #qa #k8s when it is live. To ship a migrator only, set app.select to that container name; a migrator config also runs its image as a Job — see Run a migrator.
Run a migrator
A migrator config (app.select=<name>, deploy.kind=job) runs like any other build: trigger FrontEnd_<App>Migrator; it builds the image, then runs it as a one-shot Job and shows the migration log inline, going green only when the migration completes. Off-pipeline, run it straight from a bastion (ssh kube/ssh kube-prod), where kubectl is admin: scripts/teamcity/run-migrator.sh <job-manifest> -i <image> in the kubernetes repo applies the Job, tails its log, and tears it down.
When a build is red — where to look
Symptom | Where it fails | Most likely cause | First move |
|---|---|---|---|
build error |
| Image or app fails to compile | Read the |
"no local image" |
| The build never produced / stored the image — the push guard caught it | Go back to the Build step; the image was not in the local store. |
| pod, after deploy | The tag never reached ECR (or was pruned) | Check the Push step and confirm the tag exists in ECR. |
| pod, after deploy | The app itself crashes at startup — not infra |
|
The build and push guards make the two build-time rows unambiguous: a red build never leaves a phantom tag behind, and a missing tag surfaces at push, not at rollout.
Onboard a new app
Add a
docker-compose.ymlto the app repo. Services useimage: ${DOCKER_REGISTRY}/<repo>:${TAG}with acontainer_name; any infra services go in the exclude list.Create a build config under the FrontEnd project (via UI or REST) and set the six parameters — compose dir, build files, no-cache, select, exclude, deploy suffix.
Ensure the target Deployments exist in the
front-endnamespace, named<container><suffix>-deployment.Add the Traefik ingress host —
<app>.stg.ap.6grain.comon staging,<app>.agroplatform.6grain.comon production.Run the first build. It creates the ECR repositories, pushes the initial tags, and rolls out.