Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1de8771
feat(health): add livez and readyz endpoints with tests
Cybernetic-Ransomware Jul 16, 2026
9f39550
feat(docker): add gunicorn CMD and EXPOSE to app image
Cybernetic-Ransomware Jul 16, 2026
4f12909
refactor(k8s): restructure into base and overlays with sync-waves
Cybernetic-Ransomware Jul 16, 2026
f7b458e
feat(argocd): add applications and deploy-smoke workflowtemplate
Cybernetic-Ransomware Jul 16, 2026
a42cc5e
ci: commit kustomize image bump and gated workflow submit
Cybernetic-Ransomware Jul 16, 2026
ff0834d
docs: add ADR-13 gitops deployment and kubernetes ops readme
Cybernetic-Ransomware Jul 16, 2026
a65bc7c
feat(k8s): add sealed secrets for minikube-argocd overlay
Cybernetic-Ransomware Jul 16, 2026
e8cc2fe
fix(k8s): stamp sync-wave on sealed secrets via kustomize patch
Cybernetic-Ransomware Jul 16, 2026
60e1b6e
fix(k8s): prepare app volumes for uid 1000 with fsGroup and init chown
Cybernetic-Ransomware Jul 16, 2026
c2dac0b
fix(k8s): guard namespace and data pvcs from prune, set backup timezo…
Cybernetic-Ransomware Jul 16, 2026
913e5d9
ci: validate manifests and allow branch image publish via workflow_di…
Cybernetic-Ransomware Jul 16, 2026
426df5a
feat(argocd): sync workflows via application and verify image tag bef…
Cybernetic-Ransomware Jul 16, 2026
d515554
docs: record volume prep, prune guards and workflows wiring in adr an…
Cybernetic-Ransomware Jul 16, 2026
17fd332
fix(workflows): grant executor rbac for workflowtaskresults
Cybernetic-Ransomware Jul 16, 2026
f7f0258
fix(k8s): chown storage unconditionally in prepare-storage init
Cybernetic-Ransomware Jul 16, 2026
c196ae2
docs(k8s): pin minikube-argocd image to branch sha and document the path
Cybernetic-Ransomware Jul 16, 2026
65c5e72
docs: refresh k8s deploy section and add rehearsal learnings to runbook
Cybernetic-Ransomware Jul 16, 2026
4f62f3d
docs: pin argocd version and mark status patch as emergency workaround
Cybernetic-Ransomware Jul 16, 2026
3b3bb6e
build(k8s): AWS deployment
Cybernetic-Ransomware Jul 25, 2026
8bd77c0
fix(terraform): pin eks cluster version to 1.34 and grant creator adm…
Cybernetic-Ransomware Jul 25, 2026
9b313f4
ci: serialize the full pipeline per ref instead of only the bump job
Cybernetic-Ransomware Jul 25, 2026
c406bff
chore(k8s): revert minikube-argocd rehearsal pins before merge
Cybernetic-Ransomware Jul 25, 2026
d182447
ci: validate terraform aws module in the manifests job
Cybernetic-Ransomware Jul 25, 2026
b444e95
fix(terraform): align vpc.tf attribute equals signs for terraform fmt
Cybernetic-Ransomware Jul 25, 2026
0d5c60a
docs(ci): correct concurrency comment about queue ordering guarantees
Cybernetic-Ransomware Jul 25, 2026
5cc268b
fix(terraform): use terraform-aws-modules/iam/aws for IRSA roles
Cybernetic-Ransomware Jul 25, 2026
6d78010
fix(terraform): pin iam module to 5.x, not 6.x
Cybernetic-Ransomware Jul 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 152 additions & 5 deletions .github/workflows/django.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ on:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
workflow_dispatch:

# Serializes the whole test -> lint -> manifests -> publish -> bump chain per
# ref, not just the bump job. Without this, two pushes to main can race:
# if commit A's pipeline starts first but commit B's finishes publish first,
# job-level concurrency on bump alone would still process B's bump before
# A's, letting A's bump overwrite B's newer image tag — an unintended
# GitOps rollback. Keying by ref keeps PRs on different branches from
# blocking each other.
#
# GitHub does not guarantee strict FIFO ordering across every trigger in a
# concurrency group: with cancel-in-progress: false, the currently running
# workflow is protected, but only the single most recently queued run is
# kept — an older *queued* run is cancelled outright when a newer one
# arrives, rather than waiting its turn. For this race that is actually the
# property we want: a stale intermediate push never runs its bump after a
# newer one, it just never runs at all.
concurrency:
group: ahc-ci-${{ github.ref }}
cancel-in-progress: false

jobs:
lint:
Expand Down Expand Up @@ -83,11 +103,67 @@ jobs:
- name: Run tests (pytest)
run: uv run pytest -m "not slow"

manifests:
name: Kubernetes manifests
runs-on: ubuntu-latest
env:
KUSTOMIZE_VERSION: "5.8.1"
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install pinned kustomize
run: |
curl -fsSL "https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${KUSTOMIZE_VERSION}/kustomize_v${KUSTOMIZE_VERSION}_linux_amd64.tar.gz" \
| tar xz -C /usr/local/bin kustomize
kustomize version

- name: Render base
run: kustomize build kubernetes/base > /tmp/base.yaml

- name: Render ArgoCD test overlay
run: kustomize build kubernetes/overlays/minikube-argocd > /tmp/minikube-argocd.yaml

# overlays/home joins once its sealed/ files exist (requires the home
# cluster's sealed-secrets controller key).

- name: Assert sync-wave present on SealedSecrets
run: |
python3 - <<'EOF'
import sys, yaml
docs = list(yaml.safe_load_all(open("/tmp/minikube-argocd.yaml")))
sealed = [d for d in docs if d and d.get("kind") == "SealedSecret"]
assert sealed, "no SealedSecrets rendered"
for d in sealed:
wave = d["metadata"].get("annotations", {}).get("argocd.argoproj.io/sync-wave")
assert wave == "-3", f"{d['metadata']['name']}: sync-wave={wave!r}, expected '-3'"
print(f"OK: {len(sealed)} SealedSecrets at wave -3")
EOF

# overlays/aws joins the render step above once its sealed/ files exist
# (same reasoning as overlays/home). Terraform is validated separately
# below — it needs neither AWS credentials nor sealed secrets.
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.9.8"

- name: Terraform fmt check
run: terraform -chdir=terraform/aws fmt -check -recursive

- name: Terraform init (no backend, no cloud credentials needed)
run: terraform -chdir=terraform/aws init -backend=false

- name: Terraform validate
run: terraform -chdir=terraform/aws validate

publish:
name: Publish app image to GHCR
runs-on: ubuntu-latest
needs: [test, lint]
if: github.event_name == 'push'
needs: [test, lint, manifests]
# workflow_dispatch allows publishing a sha- tag from a feature branch
# (e.g. to test the branch on minikube before merge).
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
permissions:
contents: read
packages: write
Expand All @@ -105,12 +181,83 @@ jobs:
- name: Set lowercase owner
run: echo "OWNER=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_ENV

- name: Set image tags
# The prod tag moves ONLY on main: Watchtower deploys whatever prod
# points at, so a branch dispatch must never touch it.
run: |
TAGS="ghcr.io/${OWNER}/ahc-app:sha-${{ github.sha }}"
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
TAGS="${TAGS},ghcr.io/${OWNER}/ahc-app:prod"
fi
echo "TAGS=${TAGS}" >> $GITHUB_ENV

- name: Build and push ahc-app
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile-app
push: true
tags: |
ghcr.io/${{ env.OWNER }}/ahc-app:prod
ghcr.io/${{ env.OWNER }}/ahc-app:sha-${{ github.sha }}
tags: ${{ env.TAGS }}

bump:
name: Bump image tag in home overlay
runs-on: ubuntu-latest
needs: publish
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: write
env:
KUSTOMIZE_VERSION: "5.8.1"
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: main

- name: Install pinned kustomize
run: |
curl -fsSL "https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${KUSTOMIZE_VERSION}/kustomize_v${KUSTOMIZE_VERSION}_linux_amd64.tar.gz" \
| tar xz -C /usr/local/bin kustomize
kustomize version

- name: Set lowercase owner
run: echo "OWNER=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_ENV

- name: Set image tag in home overlay
run: |
cd kubernetes/overlays/home
kustomize edit set image "ghcr.io/${OWNER}/ahc-app=ghcr.io/${OWNER}/ahc-app:sha-${{ github.sha }}"

- name: Commit and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add kubernetes/overlays/home/kustomization.yaml
if git diff --cached --quiet; then
echo "No image tag change to commit."
exit 0
fi
git commit -m "chore(deploy): bump ahc-app to sha-${{ github.sha }} [skip ci]"
git push

deploy-workflow:
name: Submit deploy-smoke Argo Workflow
runs-on: ubuntu-latest
needs: bump
# Self-disabled until an Argo Server is reachable from CI (self-hosted
# runner or tunnel). ArgoCD auto-sync remains the primary deploy path.
if: github.event_name == 'push' && vars.ARGO_SERVER_URL != ''
steps:
- name: Submit workflow via Argo Server API
run: |
curl -fsS -X POST \
-H "Authorization: Bearer ${{ secrets.ARGO_TOKEN }}" \
-H "Content-Type: application/json" \
"${{ vars.ARGO_SERVER_URL }}/api/v1/workflows/argo/submit" \
-d '{
"resourceKind": "WorkflowTemplate",
"resourceName": "ahc-deploy-smoke",
"submitOptions": {
"parameters": ["image-tag=sha-${{ github.sha }}"]
}
}'
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ CLAUDE.md
/tars/
/kubernetes/**/*.tar
secret.yaml
# Filled plain Secrets (templates stay tracked; SealedSecrets in sealed/ stay tracked)
kubernetes/overlays/*/secrets/*.yaml
# Fetched sealed-secrets controller certificates
*.pem

# Volumes
/db/
Expand Down
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,20 @@ The stack exposes: Django app on `:8000`, Flower (Celery monitor) on `:5555`.
uv run python manage.py runserver
```

### Kubernetes Deploy (alternative)

An alternative to Docker Compose for production-like environments.
See [`kubernetes/`](kubernetes/) for kustomization files and secret templates.
Build and load images, then apply with `kubectl apply -k kubernetes/`.
### Kubernetes Deploy (GitOps via ArgoCD)

Kubernetes manifests live in `kubernetes/` as a kustomize base with four overlays
(`minikube-local` for a plain `kubectl apply -k` dev loop, `minikube-argocd` for the GitOps
rehearsal, `home` for the production k3s cluster synced by ArgoCD, `aws` for an EKS demo target
provisioned by Terraform). Images are pulled from GHCR; the deployed tag for `home` is committed
to git by CI, other overlays are bumped manually.

- Operational runbook (local run, sealing secrets, rehearsal, home bootstrap, backups):
[`kubernetes/README.md`](kubernetes/README.md)
- AWS EKS runbook (Terraform bootstrap, cost, **mandatory teardown**):
[`kubernetes/README-aws.md`](kubernetes/README-aws.md)
- Architecture decision records: [`doc/13_adr_gitops_argocd.md`](doc/13_adr_gitops_argocd.md),
[`doc/14_adr_aws_eks_deployment.md`](doc/14_adr_aws_eks_deployment.md)

## Testing

Expand Down
22 changes: 22 additions & 0 deletions argocd/ahc-aws.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ahc-aws
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/Cybernetic-Ransomware/Animals_Healthcare_Application.git
targetRevision: main
path: kubernetes/overlays/aws
destination:
server: https://kubernetes.default.svc
namespace: ahc
# Deliberately no automated sync policy: this is a new cluster type (EKS +
# ALB Controller + EBS CSI) with its own untested failure modes. Syncs are
# manual until a full drift/prune/sealed-secret-recovery pass has been
# done here, same bar ahc-home was held to before it got automation
# (see argocd/ahc-minikube-test.yaml for the precedent).
syncPolicy:
syncOptions:
- CreateNamespace=true
26 changes: 26 additions & 0 deletions argocd/ahc-home.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ahc-home
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/Cybernetic-Ransomware/Animals_Healthcare_Application.git
targetRevision: main
path: kubernetes/overlays/home
destination:
server: https://kubernetes.default.svc
namespace: ahc
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
retry:
limit: 3
backoff:
duration: 30s
factor: 2
maxDuration: 5m
20 changes: 20 additions & 0 deletions argocd/ahc-minikube-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ahc-minikube-test
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/Cybernetic-Ransomware/Animals_Healthcare_Application.git
targetRevision: main
path: kubernetes/overlays/minikube-argocd
destination:
server: https://kubernetes.default.svc
namespace: ahc
# Deliberately no automated sync policy: syncs are manual, without prune
# and without selfHeal, until drift/prune/sealed-secret-recovery tests
# pass on minikube. Automation belongs to ahc-home after the merge.
syncPolicy:
syncOptions:
- CreateNamespace=true
20 changes: 20 additions & 0 deletions argocd/ahc-workflows.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ahc-workflows
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/Cybernetic-Ransomware/Animals_Healthcare_Application.git
targetRevision: main
path: workflows
destination:
server: https://kubernetes.default.svc
namespace: argo
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
26 changes: 26 additions & 0 deletions argocd/argo-workflows.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: argo-workflows
namespace: argocd
spec:
project: default
source:
repoURL: https://argoproj.github.io/argo-helm
chart: argo-workflows
targetRevision: 0.45.11
helm:
values: |
server:
# Homelab-grade auth: the server acts with its own service account.
authModes:
- server
destination:
server: https://kubernetes.default.svc
namespace: argo
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
22 changes: 22 additions & 0 deletions argocd/sealed-secrets.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: sealed-secrets
namespace: argocd
spec:
project: default
source:
repoURL: https://bitnami-labs.github.io/sealed-secrets
chart: sealed-secrets
targetRevision: 2.17.3
helm:
values: |
# Default name expected by the kubeseal CLI discovery.
fullnameOverride: sealed-secrets-controller
destination:
server: https://kubernetes.default.svc
namespace: kube-system
syncPolicy:
automated:
prune: true
selfHeal: true
Loading
Loading