---
title: How an application is deployed
description: One HelmRelease, a chart from helmforge, a floating image tag, and the pipeline that builds the image.
weight: 10
icons: [flux, helm, tekton, harbor]
tags: [flux, helm, kaniko, tekton, harbor]
---

Every application reaches this cluster the same way: a `HelmRelease` in git,
reconciled by Flux, pointing at a chart and an image tag that automation keeps
current. I do not run `helm upgrade`, and nobody edits an image tag by hand.

## The repository layout

| path                          | holds                                                                 |
| ----------------------------- | --------------------------------------------------------------------- |
| `clusters/default/`           | the Kustomizations Flux applies, and what depends on what             |
| `infrastructure/controllers/` | controllers: Cilium, Flux, cert-manager, ESO, Tekton                  |
| `apps/base/`                  | applications, one directory each                                      |
| `apps/namespaces/`            | the namespaces themselves                                             |
| `terraform/environments/`     | declarative state the cluster cannot own: DNS, OIDC apps, node config |
| `.tekton/`                    | Pipelines-as-Code build pipelines                                     |

A Flux `Kustomization` per tree, applied by a root Kustomization, so the whole cluster
is four documents deep from the entry point.

## The release

This site's own release is the smallest complete example:

{{< include apps/kubexa-site/release.yaml lang=yaml >}}

Three things in there matter. `chart.spec` names a chart and a version, so a chart
upgrade is a diff I can read. `image.tag` carries a `$imagepolicy` marker, which is how
the image automation knows it may rewrite that field. `imagePullSecrets` names a
Secret that External Secrets fills from OpenBao, so no credential is ever in git.

## The build

The image is built by Pipelines-as-Code on a push that touches `apps/kubexa-site/**`,
using kaniko inside the cluster with no Docker daemon and no privileged pod:

{{< include .tekton/kubexa-site-build.yaml lang=yaml >}}

The pipeline pushes two tags: `latest` and the commit SHA. The `ImagePolicy` selects
`latest`, `ImageUpdateAutomation` writes the resolved digest back into the release,
and Flux applies the result. A rebuild therefore reaches production without a human
touching a manifest.

## What happens when I change something

1. `git push` to `main`.
2. Flux's source controller fetches within its interval, the `apps-site` Kustomization
   applies the manifests it owns.
3. If the image changed, the pipeline built it first; if the pipeline failed, the old
   image stays and the cluster does not move.
4. Anything I delete from git is pruned from the cluster on the same reconcile.

The failure mode to watch is a Kustomization stuck on `DependencyNotReady`, which
means an upstream tree never became ready and nothing downstream applied. That is what
`kubectl get kustomization -n flux-system` is for.
