Sync the self-hosting stack
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
# First, alone. Everything below is namespaced, and a sync that applied them in parallel
|
||||
# would race the namespace's own creation — including the Pod Security labels above, which
|
||||
# decide whether any of these pods are admitted at all.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "-3"
|
||||
name: jarvis
|
||||
labels:
|
||||
# Both images run unprivileged and drop every capability, so the strictest profile applies
|
||||
|
||||
@@ -31,12 +31,21 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: jarvis-secret-minter
|
||||
namespace: jarvis
|
||||
# The identity the minting Job below authenticates as, one wave ahead of it. Ordinary
|
||||
# resources, not hooks: a hook ServiceAccount would be deleted and recreated on every sync,
|
||||
# invalidating its token for no reason.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "-2"
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: jarvis-secret-minter
|
||||
namespace: jarvis
|
||||
# The permission that identity carries, in the same wave as the identity itself: a Role that
|
||||
# lands after the Job has already authenticated is a Job that got a 403.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "-2"
|
||||
rules:
|
||||
# `create` AND NOTHING ELSE. No `get`, no `list`, no `update`.
|
||||
#
|
||||
@@ -57,6 +66,9 @@ kind: RoleBinding
|
||||
metadata:
|
||||
name: jarvis-secret-minter
|
||||
namespace: jarvis
|
||||
# And the binding between the two, in the same wave for the same reason.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "-2"
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
@@ -71,6 +83,22 @@ kind: Job
|
||||
metadata:
|
||||
name: jarvis-mint-secrets
|
||||
namespace: jarvis
|
||||
annotations:
|
||||
# A sync hook for the same reason the migration is one — `spec.template` is immutable, so an
|
||||
# ordinary Job whose image digest moved is a sync that fails on "field is immutable" — plus one
|
||||
# of its own: `ttlSecondsAfterFinished` deletes this Job an hour after it succeeds. As an
|
||||
# ordinary tracked resource that is a controller reporting OutOfSync forever, on an object whose
|
||||
# whole purpose is to be gone.
|
||||
#
|
||||
# Running it again is safe BY CONSTRUCTION rather than by care: the identity below can `create`
|
||||
# and nothing else, so a second run cannot overwrite the secret it wrote the first time. It gets
|
||||
# a 409 and treats it as success. That is what makes it safe to hand to a controller that will
|
||||
# reconcile it on a schedule nobody is watching.
|
||||
argocd.argoproj.io/hook: Sync
|
||||
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
|
||||
# Before the migration (wave 0), because the migration reads DATABASE_URL out of the secrets;
|
||||
# after the ServiceAccount and Role above (wave -2), which this Job authenticates as.
|
||||
argocd.argoproj.io/sync-wave: "-1"
|
||||
spec:
|
||||
# Re-running is safe (409 → left untouched), so a retry costs nothing. A high limit would only
|
||||
# mean a broken RBAC took longer to become visible.
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
# Before anything reads it: the migration Job (wave 0) and both Deployments (wave 1) mount
|
||||
# these as envFrom, and a pod that starts without them starts with the wrong configuration
|
||||
# rather than with none — envFrom of a missing ConfigMap is a pod that never becomes ready.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "-2"
|
||||
name: jarvis-api-config
|
||||
namespace: jarvis
|
||||
data:
|
||||
@@ -65,6 +70,9 @@ data:
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
# With the ConfigMap above, for the same reason.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "-2"
|
||||
name: jarvis-web-config
|
||||
namespace: jarvis
|
||||
data:
|
||||
|
||||
@@ -10,8 +10,17 @@
|
||||
# observable status, a bounded number of retries, and a failure that stops the rollout instead of
|
||||
# letting it proceed against a schema that was never applied.
|
||||
#
|
||||
# With Helm this becomes a `pre-install,pre-upgrade` hook; with Argo, a sync-wave. Applied by hand,
|
||||
# the ordering is yours to keep — see the README.
|
||||
# UNDER ARGO CD IT IS A SYNC HOOK, and that is not decoration — it is what makes this directory
|
||||
# reconcilable at all. A Job's `spec.template` is immutable, so an ordinary Job whose image digest
|
||||
# changed is a patch the API server REFUSES: the sync fails on "field is immutable" and the release
|
||||
# stops, every time, from the second release onwards. `BeforeHookCreation` deletes the previous Job
|
||||
# before creating this one, which is the supported way of saying "run it again, with new contents".
|
||||
#
|
||||
# It costs one thing worth knowing: the previous run's Job object, and its logs, are gone by the
|
||||
# time the next one starts. Ship the logs elsewhere if you need them after the fact.
|
||||
#
|
||||
# With Helm this is a `pre-install,pre-upgrade` hook instead. Applied by hand, the ordering is yours
|
||||
# to keep — see the README.
|
||||
#
|
||||
# It is safe to run twice: every pass is idempotent and the whole phase is wrapped in a Postgres
|
||||
# advisory lock, so a second copy waits rather than racing.
|
||||
@@ -20,6 +29,17 @@ kind: Job
|
||||
metadata:
|
||||
name: jarvis-migrate
|
||||
namespace: jarvis
|
||||
annotations:
|
||||
# The SYNC phase, not PreSync. A PreSync hook runs in a phase of its own, before any ordinary
|
||||
# resource is applied — which on a first sync means before the namespace, the ConfigMap and the
|
||||
# Secret this Job reads exist. In the Sync phase it is ordered by the same waves as everything
|
||||
# else, so "after the config, before the api" is one number rather than a second mechanism.
|
||||
argocd.argoproj.io/hook: Sync
|
||||
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
|
||||
# After the config and the minting (waves -2 and -1), before any api pod (wave 1). Argo does not
|
||||
# begin the next wave until this Job reports complete, which is the ordering the README's
|
||||
# `kubectl wait` keeps by hand.
|
||||
argocd.argoproj.io/sync-wave: "0"
|
||||
spec:
|
||||
# One writer. The lock makes a second one wait rather than corrupt, but there is no reason to
|
||||
# have one waiting.
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
# After the schema. This is the wave the whole ordering exists for: an api pod that starts
|
||||
# against a schema the migration has not applied is the failure every other number here is
|
||||
# arranged to prevent.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "1"
|
||||
name: jarvis-api
|
||||
namespace: jarvis
|
||||
spec:
|
||||
@@ -176,6 +181,9 @@ spec:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
# With the Deployment it fronts.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "1"
|
||||
name: jarvis-api
|
||||
namespace: jarvis
|
||||
spec:
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
# Beside the api, in the same wave, and NOT because it is independent of it — measured on a real
|
||||
# cluster, this pod's `/readyz` is nginx proxying the api's `/api/health/live`, so it answers 502
|
||||
# and stays unready for as long as there is no api behind it.
|
||||
#
|
||||
# The same wave is still right. Argo waits for a wave as a whole, so the api coming up is what
|
||||
# makes this ready and there is nothing for a later wave to add; putting it earlier would only
|
||||
# mean it sat unready for longer. What it does mean is that a stuck api presents as TWO unhealthy
|
||||
# Deployments rather than one, and the api is the one to look at.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "1"
|
||||
name: jarvis-web
|
||||
namespace: jarvis
|
||||
spec:
|
||||
@@ -112,6 +122,9 @@ spec:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
# With the Deployment it belongs to.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "1"
|
||||
name: jarvis-web
|
||||
namespace: jarvis
|
||||
spec:
|
||||
@@ -134,6 +147,9 @@ spec:
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
# With the Deployment it belongs to.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "1"
|
||||
name: jarvis-web
|
||||
namespace: jarvis
|
||||
spec:
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
# Last, so that the first traffic admitted from outside meets Services with endpoints behind
|
||||
# them. An Ingress applied early is a route to a 503.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "2"
|
||||
name: jarvis
|
||||
namespace: jarvis
|
||||
annotations:
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
# With the Ingress, after the Deployments they protect. A PodDisruptionBudget naming a
|
||||
# selector that matches nothing is not an error and not a warning — it is simply not
|
||||
# protecting anything, which is the one state it must never be in quietly.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "2"
|
||||
name: jarvis-api
|
||||
namespace: jarvis
|
||||
spec:
|
||||
@@ -18,6 +23,9 @@ spec:
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
# Same wave, same reason.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "2"
|
||||
name: jarvis-web
|
||||
namespace: jarvis
|
||||
spec:
|
||||
|
||||
@@ -23,6 +23,11 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
# Beside the config, well before the api. The api reaches Redis on its readiness probe, so a
|
||||
# keyspace that is not there yet is a Deployment that rolls and then reports unready for as
|
||||
# long as it takes — which Argo would read as a failed sync rather than as a wait.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "-2"
|
||||
name: redis-scripts
|
||||
namespace: jarvis
|
||||
data:
|
||||
@@ -79,6 +84,9 @@ data:
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
# With the rest of Redis, in the wave before anything reads it.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "-2"
|
||||
name: redis-config
|
||||
namespace: jarvis
|
||||
data:
|
||||
@@ -112,6 +120,9 @@ data:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
# With the rest of Redis, in the wave before anything reads it.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "-2"
|
||||
name: redis
|
||||
namespace: jarvis
|
||||
spec:
|
||||
@@ -129,6 +140,9 @@ spec:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
# With the rest of Redis, in the wave before anything reads it.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "-2"
|
||||
name: redis-sentinel
|
||||
namespace: jarvis
|
||||
spec:
|
||||
@@ -144,6 +158,9 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
# With the rest of Redis, in the wave before anything reads it.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "-2"
|
||||
name: redis
|
||||
namespace: jarvis
|
||||
spec:
|
||||
@@ -232,6 +249,9 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
# With the rest of Redis, in the wave before anything reads it.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "-2"
|
||||
name: redis-sentinel
|
||||
namespace: jarvis
|
||||
spec:
|
||||
@@ -321,6 +341,9 @@ spec:
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
# With the rest of Redis, in the wave before anything reads it.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "-2"
|
||||
name: redis-sentinel
|
||||
namespace: jarvis
|
||||
spec:
|
||||
@@ -332,6 +355,9 @@ spec:
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
# With the rest of Redis, in the wave before anything reads it.
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "-2"
|
||||
name: redis
|
||||
namespace: jarvis
|
||||
spec:
|
||||
|
||||
@@ -97,6 +97,90 @@ about the install screen applies unchanged — including that **whoever reaches
|
||||
instance first owns it**, with no deadline. Point a public hostname at this after you have
|
||||
installed, not before.
|
||||
|
||||
## GitOps: Argo CD, Flux, or anything that reconciles
|
||||
|
||||
`kustomization.yaml` is the entry point. Point a controller at this directory and it renders every
|
||||
manifest here except one — and `argocd-application.example.yaml` is a worked Application to copy.
|
||||
|
||||
```sh
|
||||
kubectl apply -k . # the same thing, by hand
|
||||
```
|
||||
|
||||
**The exception is `01-secret.example.yaml`, and it is left out on purpose.** It is a template full
|
||||
of placeholders. A controller applies what it is pointed at, continuously, without asking — so a
|
||||
Secret full of `REPLACE_ME` inside the source of truth is a controller that overwrites your real
|
||||
credentials with placeholders on its next reconcile and puts them back every time you fix them. The
|
||||
api cannot reach its database, the vault key is wrong, and nothing about the sync looks unhealthy
|
||||
while it happens. Write `jarvis-secrets` by hand, or through SealedSecrets, ExternalSecrets or
|
||||
Vault, whose ciphertext is safe to commit. `jarvis-generated` is minted in-cluster and never exists
|
||||
in git at all.
|
||||
|
||||
### The order is in the manifests, not in the filenames
|
||||
|
||||
A controller reads this directory as a set; `00-`, `01-` and `02-` mean nothing to it. The order
|
||||
that the `kubectl apply` sequence above keeps by hand is carried by
|
||||
`argocd.argoproj.io/sync-wave` annotations on the resources themselves, so that it lives beside the
|
||||
thing being ordered:
|
||||
|
||||
| wave | what | why it cannot be later |
|
||||
| --- | --- | --- |
|
||||
| `-3` | Namespace | it carries the Pod Security labels that decide whether any of these pods are admitted |
|
||||
| `-2` | ConfigMaps, the minter's RBAC, Redis | everything below reads the config; the api's readiness probe reaches Redis |
|
||||
| `-1` | `jarvis-mint-secrets` **(hook)** | the migration reads `DATABASE_URL` out of what it writes |
|
||||
| `0` | `jarvis-migrate` **(hook)** | an api pod against an unmigrated schema is the failure all of this prevents |
|
||||
| `1` | api and web Deployments, Services | the web pod's `/readyz` proxies the api's health endpoint, so it cannot be ready before the api is |
|
||||
| `2` | Ingress, PodDisruptionBudgets | an Ingress applied early is a route to a 503 |
|
||||
|
||||
Argo does not begin a wave until the previous one reports healthy, which is what the `kubectl wait`
|
||||
lines are doing by hand.
|
||||
|
||||
### Why the two Jobs are hooks
|
||||
|
||||
Without this, the directory works exactly once. **A Job's `spec.template` is immutable**, so the
|
||||
second release — the first one with a different image digest — is a patch the API server refuses
|
||||
with `field is immutable`, and the sync fails there and stays failed. `hook: Sync` with
|
||||
`hook-delete-policy: BeforeHookCreation` deletes the previous Job before creating the new one, which
|
||||
is the supported way to say "run it again, with new contents".
|
||||
|
||||
The Sync phase rather than PreSync, deliberately: a PreSync hook runs before any ordinary resource
|
||||
is applied, which on a first sync is before the namespace, the ConfigMap and the Secret these Jobs
|
||||
read exist. In the Sync phase they are ordered by the same waves as everything else.
|
||||
|
||||
Two consequences worth knowing:
|
||||
|
||||
- **The previous run's Job object, and its logs, are gone** by the time the next one starts. Ship
|
||||
the logs if you need them after the fact.
|
||||
- **Every sync runs the migration.** That is safe — the phase is idempotent and holds a Postgres
|
||||
advisory lock — and it is why the example Application leaves `automated` sync OFF. A commit that
|
||||
applies a schema change with nobody watching turns the one thing this directory works hardest to
|
||||
make visible, a migration that REFUSES because it would drop a column, into a notification.
|
||||
|
||||
### Pin the images in one place
|
||||
|
||||
`kustomization.yaml` carries an `images:` block, so the three digests are set once rather than in
|
||||
three files. Three, not two: `jarvis-agent-dist` is an initContainer in `04-api.yaml` and is easy to
|
||||
miss, and missing it renders cleanly while the api pod never starts.
|
||||
|
||||
A version bump is then those lines and nothing else.
|
||||
|
||||
### What most estates should actually do
|
||||
|
||||
Point the Application at **your** repository, not at this one, with this directory as a remote base:
|
||||
|
||||
```yaml
|
||||
# your-repo/jarvis/kustomization.yaml
|
||||
resources:
|
||||
- https://git.luxit.be/Luxit/jarvis-selfhost//kubernetes?ref=v1.2.3
|
||||
images:
|
||||
- name: git.luxit.be/luxit/jarvis-api
|
||||
digest: sha256:...
|
||||
patches:
|
||||
- path: replicas.yaml # your node count, your ingress host, your storage class
|
||||
```
|
||||
|
||||
Then the digests you run are in a commit you control, upgrading is a diff you can review and revert,
|
||||
and `?ref=` pins the base to a tag rather than a branch that moves under you. Pin it.
|
||||
|
||||
## The three things most likely to bite
|
||||
|
||||
**Never mint secrets per pod.** On Compose a one-shot generates the vault key and the JWT secrets
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# An Argo CD Application pointing at this directory. EXAMPLE — read it, do not apply it unedited.
|
||||
#
|
||||
# It is `.example.` for the same reason `01-secret.example.yaml` is: it names a repository that is
|
||||
# not yours. Copy it into whatever repository holds your Argo Applications, change the three values
|
||||
# marked below, and apply it there.
|
||||
#
|
||||
# ─── WHAT TO POINT IT AT ──────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# You have two honest choices, and the second is the one to grow into.
|
||||
#
|
||||
# DIRECTLY AT THIS DIRECTORY, as written below. Simplest, and it works — but the image digests live
|
||||
# in `kustomization.yaml` here, so pinning a version means committing to a repository you do not
|
||||
# own. Fine for reading, for a first cluster, and for tracking `main` on a lab estate.
|
||||
#
|
||||
# OR AS A BASE UNDER YOUR OWN OVERLAY, which is what most estates end up wanting:
|
||||
#
|
||||
# # your-repo/jarvis/kustomization.yaml
|
||||
# apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
# kind: Kustomization
|
||||
# resources:
|
||||
# - https://git.luxit.be/Luxit/jarvis-selfhost//kubernetes?ref=main
|
||||
# images:
|
||||
# - name: git.luxit.be/luxit/jarvis-api
|
||||
# digest: sha256:<the one you are running>
|
||||
# - name: git.luxit.be/luxit/jarvis-web
|
||||
# digest: sha256:<the one you are running>
|
||||
# patches:
|
||||
# - path: replicas.yaml # your node count, your ingress host, your storage class
|
||||
#
|
||||
# Then the Application points at YOUR repository, your digests are in a commit you control, and
|
||||
# upgrading is a one-line diff you can review and revert. `?ref=` pins the base to a tag or a commit
|
||||
# rather than tracking a branch that moves under you — pin it.
|
||||
#
|
||||
# ─── WHAT IS NOT IN GIT, AND MUST NOT BE ──────────────────────────────────────────────────────
|
||||
#
|
||||
# `jarvis-secrets`. See the note at the top of `kustomization.yaml`: a controller that reconciles a
|
||||
# Secret full of placeholders will overwrite your real credentials with them, repeatedly, and
|
||||
# nothing about the sync will look unhealthy while it does. Write it by hand or through
|
||||
# SealedSecrets / ExternalSecrets / Vault, whose ciphertext IS safe to commit.
|
||||
#
|
||||
# `jarvis-generated` is minted in-cluster by the hook in `01-secret-job.yaml` and never exists in
|
||||
# git at all. Back the vault key up out of the cluster the first time it is minted — see that file.
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: jarvis
|
||||
namespace: argocd
|
||||
# Argo deletes an Application's resources when the Application is deleted. On something holding a
|
||||
# database's credentials and a vault key, that should be a deliberate act rather than a
|
||||
# consequence of `kubectl delete app jarvis` — so the finalizer is left OFF and pruning of the
|
||||
# namespace is not automatic. Add it when you have decided that is what you want:
|
||||
#
|
||||
# finalizers:
|
||||
# - resources-finalizer.argocd.argoproj.io
|
||||
spec:
|
||||
project: default
|
||||
|
||||
source:
|
||||
# CHANGE ME — your repository, or the public one if you are only reading.
|
||||
repoURL: https://git.luxit.be/Luxit/jarvis-selfhost.git
|
||||
# CHANGE ME — a tag or a commit, not a branch. A branch moves; a cluster that follows one is a
|
||||
# cluster whose version is decided by somebody else's merge.
|
||||
targetRevision: main
|
||||
path: kubernetes
|
||||
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
# Matches the namespace the manifests declare. They carry it explicitly, so this is agreement
|
||||
# rather than instruction.
|
||||
namespace: jarvis
|
||||
|
||||
syncPolicy:
|
||||
# DELIBERATELY MANUAL, and this is the one setting to think hardest about before changing.
|
||||
#
|
||||
# Every sync runs the migration hook against your production schema. Automated, that means a
|
||||
# commit to the tracked revision applies a schema change with nobody watching — and the one
|
||||
# thing this whole directory is arranged to make visible (a migration that REFUSES because it
|
||||
# would drop a column) becomes a failed sync notification instead of a person deciding.
|
||||
#
|
||||
# Turn it on once you trust the pipeline in front of it, not before:
|
||||
#
|
||||
# automated:
|
||||
# prune: true
|
||||
# selfHeal: true
|
||||
|
||||
# NOTHING HERE, and the absence is deliberate in one specific place.
|
||||
#
|
||||
# `CreateNamespace=true` is the option most Applications carry, and this one must not. The
|
||||
# namespace is `00-namespace.yaml` at wave -3, and it carries the Pod Security labels that
|
||||
# decide whether any of these pods are admitted at all. A namespace Argo creates for you has no
|
||||
# labels — so the first sync would admit pods under no policy and enforce `restricted` a moment
|
||||
# later, which is the one ordering this directory exists to get right.
|
||||
#
|
||||
# syncOptions: []
|
||||
|
||||
# The HorizontalPodAutoscaler in `05-web.yaml` targets the web Deployment, which also declares
|
||||
# `replicas: 2`. Once the autoscaler acts, git and the cluster disagree about that field forever —
|
||||
# and it is the autoscaler working, not drift. Without this, a `selfHeal` policy and the HPA push
|
||||
# the replica count back and forth at each other indefinitely.
|
||||
#
|
||||
# The api Deployment is deliberately NOT here: nothing autoscales it, so git owns its replica
|
||||
# count and a difference there is a real one worth showing.
|
||||
ignoreDifferences:
|
||||
- group: apps
|
||||
kind: Deployment
|
||||
name: jarvis-web
|
||||
jsonPointers:
|
||||
- /spec/replicas
|
||||
@@ -0,0 +1,75 @@
|
||||
# The entry point for `kubectl apply -k` and for Argo CD.
|
||||
#
|
||||
# It exists so that a GitOps tool has ONE path to point at, and so that the two images are pinned in
|
||||
# ONE place instead of three. The manifests beside it are unchanged and still apply individually
|
||||
# with `kubectl apply -f` — this aggregates them, it does not replace them.
|
||||
#
|
||||
# ─── WHAT IS DELIBERATELY NOT HERE ────────────────────────────────────────────────────────────
|
||||
#
|
||||
# `01-secret.example.yaml`, and its absence is the most important line in this file.
|
||||
#
|
||||
# It is a TEMPLATE carrying placeholder values. A GitOps controller applies what it is pointed at,
|
||||
# continuously, without asking — so a Secret full of `REPLACE_ME` inside the source of truth is a
|
||||
# controller that will overwrite your real credentials with placeholders on its next reconcile, and
|
||||
# then keep putting them back every time you fix them. The api would fail to reach its database, the
|
||||
# vault key would be wrong, and nothing in the sync would look unhealthy.
|
||||
#
|
||||
# Write `jarvis-secrets` into the cluster by another route entirely: by hand, or through
|
||||
# SealedSecrets, ExternalSecrets or Vault — anything whose ciphertext is safe to commit. The
|
||||
# generated half (`jarvis-generated`) is minted in-cluster by the hook in `01-secret-job.yaml` and
|
||||
# is likewise never in git.
|
||||
#
|
||||
# ─── ORDER ────────────────────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# Filenames do not order anything: `kubectl apply -k` and Argo both read this list as a set. The
|
||||
# order is carried by `argocd.argoproj.io/sync-wave` annotations on the resources themselves, so
|
||||
# that it lives beside the thing being ordered rather than in a list somebody has to keep in step.
|
||||
# See the table in README.md.
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- 00-namespace.yaml
|
||||
# The identity that mints the generated secrets, and the Job that uses it. The Job is a sync hook;
|
||||
# its ServiceAccount, Role and RoleBinding are ordinary resources in an earlier wave.
|
||||
- 01-secret-job.yaml
|
||||
- 02-config.yaml
|
||||
- 03-migrate-job.yaml
|
||||
- 04-api.yaml
|
||||
- 05-web.yaml
|
||||
- 06-ingress.yaml
|
||||
- 07-disruption.yaml
|
||||
# Remove this line if you point REDIS_URL at a managed Redis. It deploys three Redis processes and
|
||||
# three sentinels; see the README on what that costs on three nodes.
|
||||
- 08-redis-sentinel.yaml
|
||||
|
||||
# ─── THE THREE IMAGES ───────────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# By DIGEST, not by tag, and the manifests carry `@sha256:REPLACE_ME` so that an unedited copy fails
|
||||
# loudly rather than quietly pulling something.
|
||||
#
|
||||
# A tag is a name that can be repointed; a digest is the bytes. It matters more here than in most
|
||||
# places because the migration Job and the api Deployment must be the SAME BUILD — a schema applied
|
||||
# by one version and served by another is the failure this whole ordering exists to prevent, and two
|
||||
# copies of a mutable tag pulled a minute apart can be two different images.
|
||||
#
|
||||
# `dev` and `stable` are channels, so they are exactly the kind of name not to pin to. Resolve one
|
||||
# to its digest and put that here:
|
||||
#
|
||||
# docker buildx imagetools inspect git.luxit.be/luxit/jarvis-api:stable --format '{{.Manifest.Digest}}'
|
||||
#
|
||||
# This is also the whole of a version bump: change these lines, commit, and let the controller do
|
||||
# the rest.
|
||||
#
|
||||
# THREE, not two. `jarvis-agent-dist` is easy to miss — it is an initContainer in 04-api.yaml rather
|
||||
# than a container of its own — and missing it is the worst possible half-fix: two images pinned
|
||||
# from here and one left carrying `REPLACE_ME` in the file, so the render succeeds and the api pod
|
||||
# never starts. It is also the one that may legitimately be absent: leaving that initContainer out
|
||||
# is supported, and the installer then answers 503 saying no agent build is published.
|
||||
images:
|
||||
- name: git.luxit.be/luxit/jarvis-api
|
||||
digest: sha256:REPLACE_ME
|
||||
- name: git.luxit.be/luxit/jarvis-web
|
||||
digest: sha256:REPLACE_ME
|
||||
- name: git.luxit.be/luxit/jarvis-agent-dist
|
||||
digest: sha256:REPLACE_ME
|
||||
Reference in New Issue
Block a user