add blog
This commit is contained in:
1 parent
0af896ab02
commit
6752379d80
7 files changed
+300
No files matched your search
@@ -0,0 +1,26 @@
|
|||||||
|
# CI publishes the static site image; this Application makes Git, rather than
|
||||||
|
# a runner's kubeconfig, the source of truth for which image Kubernetes runs.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: blog-ahmza-com
|
||||||
|
namespace: argocd
|
||||||
|
# No cascading-deletion finalizer during initial migration: removing this
|
||||||
|
# Application must not unexpectedly delete the public site's resources.
|
||||||
|
spec:
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
repoURL: https://tea.ahmza.com/ahmza.com/k8s-starter-pack.git
|
||||||
|
targetRevision: main
|
||||||
|
path: apps/blog-ahmza-com
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: blog-ahmza-com
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
# Review the initial sync before allowing Git deletions to remove live
|
||||||
|
# resources. Enabling prune later is separate from cascading app deletion.
|
||||||
|
prune: false
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Hugo generates the complete site at image-build time. SWS serves only those
|
||||||
|
# immutable files, with no writable content volume or Kubernetes API access.
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: blog
|
||||||
|
namespace: blog-ahmza-com
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
strategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
rollingUpdate:
|
||||||
|
# Keep the existing site available while replacement Pods become ready.
|
||||||
|
maxUnavailable: 0
|
||||||
|
maxSurge: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: blog
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: blog
|
||||||
|
spec:
|
||||||
|
automountServiceAccountToken: false
|
||||||
|
topologySpreadConstraints:
|
||||||
|
- maxSkew: 1
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
# Ignore tainted control-plane nodes when balancing the two workers;
|
||||||
|
# otherwise a surge Pod could be blocked by an unusable third node.
|
||||||
|
nodeTaintsPolicy: Honor
|
||||||
|
whenUnsatisfiable: DoNotSchedule
|
||||||
|
labelSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: blog
|
||||||
|
securityContext:
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: blog
|
||||||
|
# Bootstrap with the verified published amd64/arm64 index. CI updates
|
||||||
|
# this line to each build's digest, never a mutable latest tag.
|
||||||
|
# yamllint disable-line rule:line-length
|
||||||
|
image: tea.ahmza.com/ahmza/blog-ahmza-com@sha256:25dce7a45c8869879bbb6f056b3e4d18971c7da542255867ad83f1db26ffefc3
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 80
|
||||||
|
protocol: TCP
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
runAsNonRoot: true
|
||||||
|
# SWS does not declare USER in the image. An explicit unprivileged
|
||||||
|
# identity lets Kubernetes enforce runAsNonRoot safely.
|
||||||
|
runAsUser: 10001
|
||||||
|
runAsGroup: 10001
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 2
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 30
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 16Mi
|
||||||
|
limits:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 128Mi
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Share ingress-nginx at 10.10.60.12. DNS and any external Caddy/NetBird
|
||||||
|
# hostname routing must also be pointed at this ingress before public cutover.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: blog
|
||||||
|
namespace: blog-ahmza-com
|
||||||
|
annotations:
|
||||||
|
# Reuse the production DNS-01 issuer already used by the cluster's sites.
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- blog.ahmza.com
|
||||||
|
secretName: blog-tls
|
||||||
|
rules:
|
||||||
|
- host: blog.ahmza.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: blog
|
||||||
|
port:
|
||||||
|
name: http
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Isolate the blog's lifecycle and access controls from unrelated applications.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: blog-ahmza-com
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# A stable in-cluster address for ingress-nginx; no extra MetalLB IP is needed.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: blog
|
||||||
|
namespace: blog-ahmza-com
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: blog
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# 13 — Deploy the Hugo blog through Argo CD
|
||||||
|
|
||||||
|
The blog source lives in `ahmza/blog-ahmza-com`. This GitOps repository owns
|
||||||
|
**desired state**: the Kubernetes resources and exact image that should run.
|
||||||
|
Argo CD compares those files with the cluster and applies changes. CI no longer
|
||||||
|
needs a kubeconfig, Kubernetes credentials, or a per-job NetBird connection.
|
||||||
|
The runner still needs working HTTPS access to Gitea and its container registry.
|
||||||
|
|
||||||
|
## What owns what
|
||||||
|
|
||||||
|
```text
|
||||||
|
blog source push to main
|
||||||
|
-> Gitea Actions builds amd64 + arm64 images
|
||||||
|
-> registry returns the multi-architecture image digest
|
||||||
|
-> CI commits that digest to this repository
|
||||||
|
-> app-of-apps discovers apps/blog-ahmza-com.yaml
|
||||||
|
-> Argo CD applies apps/blog-ahmza-com/ to the cluster
|
||||||
|
```
|
||||||
|
|
||||||
|
A **digest** (`sha256:...`) identifies exact image contents, unlike the mutable
|
||||||
|
`latest` tag. Both architecture variants are referenced by the same index digest,
|
||||||
|
so the Pi and x86 workers select their matching image automatically.
|
||||||
|
|
||||||
|
The new files are:
|
||||||
|
|
||||||
|
- `apps/blog-ahmza-com.yaml`: the Argo CD Application, meaning which Git path to
|
||||||
|
watch and where to apply it.
|
||||||
|
- `apps/blog-ahmza-com/namespace.yaml`: the namespace, an isolation boundary for
|
||||||
|
this application's resources.
|
||||||
|
- `apps/blog-ahmza-com/deployment.yaml`: two Pods (running container instances),
|
||||||
|
with zero-unavailable rolling updates and the SWS `/health` probes.
|
||||||
|
- `apps/blog-ahmza-com/service.yaml`: a ClusterIP Service, a stable internal
|
||||||
|
destination for those replaceable Pods.
|
||||||
|
- `apps/blog-ahmza-com/ingress.yaml`: routes HTTPS `blog.ahmza.com` to that Service
|
||||||
|
through the already-running ingress-nginx at `10.10.60.12`.
|
||||||
|
|
||||||
|
There is no PVC or extra Caddy Deployment. The image includes the full generated
|
||||||
|
site, and TLS is handled by the existing ingress/cert-manager infrastructure.
|
||||||
|
The Application starts with automatic pruning disabled and no cascading-delete
|
||||||
|
finalizer. Review deletion behavior before changing either: removing the
|
||||||
|
Application initially leaves its workloads running rather than deleting them.
|
||||||
|
|
||||||
|
## Prepare credentials before enabling the new workflow
|
||||||
|
|
||||||
|
In the **blog source repository**, configure these Gitea Actions secrets:
|
||||||
|
|
||||||
|
| Secret | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `GITEATOKEN` | Existing credential that can push `ahmza/blog-ahmza-com` registry packages |
|
||||||
|
| `GITOPS_TOKEN` | Separate bot PAT permitted to read/write `ahmza.com/k8s-starter-pack` |
|
||||||
|
|
||||||
|
The bot must be allowed to push to the GitOps repository's `main` branch. Scope
|
||||||
|
its token and membership as narrowly as Gitea supports; it does not need
|
||||||
|
Kubernetes credentials or administrator access. Do not commit either token.
|
||||||
|
|
||||||
|
At preparation time, the published blog image was readable with an anonymous
|
||||||
|
registry bearer-token exchange. Therefore no image pull secret is shipped in
|
||||||
|
these manifests. If package visibility is changed to private, manage a read-only
|
||||||
|
registry credential in this namespace through the existing SOPS mechanism
|
||||||
|
(`docs/10-secrets.md`) and reference it using `imagePullSecrets`. The source
|
||||||
|
repository and registry package have separate visibility settings.
|
||||||
|
|
||||||
|
## First deployment: GitOps first, workflow second
|
||||||
|
|
||||||
|
1. Review and commit the Application, manifests, and documentation in this
|
||||||
|
repository, then push to `main`. They must exist remotely before the new CI
|
||||||
|
workflow can update the Deployment file. Nothing local is visible to Argo CD.
|
||||||
|
|
||||||
|
2. The existing app-of-apps watches `apps/` non-recursively. It should create the
|
||||||
|
child Application without another manual `kubectl apply`. Initial deployment
|
||||||
|
uses the already-published image index pinned in the manifest. Check:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl -n argocd get application blog-ahmza-com
|
||||||
|
kubectl -n blog-ahmza-com get deployment,pods,service,ingress,certificate
|
||||||
|
kubectl -n blog-ahmza-com rollout status deployment/blog --timeout=5m
|
||||||
|
```
|
||||||
|
|
||||||
|
These commands are diagnostics, not the deployment mechanism. If the child
|
||||||
|
Application does not appear, inspect `app-of-apps` sync status and conditions.
|
||||||
|
Do not solve this by letting CI apply resources directly again.
|
||||||
|
|
||||||
|
3. Verify the hostname without changing public routing first:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl --fail --show-error --resolve blog.ahmza.com:443:10.10.60.12 \
|
||||||
|
https://blog.ahmza.com/
|
||||||
|
```
|
||||||
|
|
||||||
|
TLS verification remains enabled. If certificate issuance is still pending,
|
||||||
|
inspect `certificate/blog-tls` and cert-manager events; do not bypass TLS.
|
||||||
|
Reuse of the ready production DNS-01 issuer follows the other cluster sites.
|
||||||
|
|
||||||
|
4. Update external routing only after the new site is healthy. For the existing
|
||||||
|
NetBird -> ingress1 Caddy -> Kubernetes topology, add `blog.ahmza.com` to the
|
||||||
|
Caddy matcher forwarding to `10.10.60.12:443`, preserving the hostname for TLS
|
||||||
|
SNI and HTTP Host. Check that the public NetBird service sends that hostname
|
||||||
|
to Caddy. Set the LAN's Technitium override to `10.10.60.12` if LAN clients
|
||||||
|
should use the internal ingress directly. These settings are outside this
|
||||||
|
repository and have not been changed by adding an Ingress manifest.
|
||||||
|
|
||||||
|
Keep public TLS termination, compression and security headers owned by the
|
||||||
|
intended proxy layer; SWS deliberately has compression/security headers off.
|
||||||
|
Cloudflare Full/Full (strict), if used, requires HTTPS to Cloudflare's actual
|
||||||
|
origin. An encrypted NetBird hop later in the path is not a substitute for
|
||||||
|
that TLS connection.
|
||||||
|
|
||||||
|
5. With `GITOPS_TOKEN` configured, commit/push the updated workflow and helper
|
||||||
|
files in the blog source repository. That push builds the site and updates
|
||||||
|
only `apps/blog-ahmza-com/deployment.yaml` here to the new digest.
|
||||||
|
|
||||||
|
6. Check both the Gitea job and Argo CD. A successful CI job means the image and
|
||||||
|
desired-state commit were published, NOT that Kubernetes is already healthy.
|
||||||
|
Argo CD applies the commit asynchronously; inspect its sync/health status and
|
||||||
|
the Deployment's rollout before declaring the release successful.
|
||||||
|
|
||||||
|
7. After a successful cutover, remove obsolete blog repository secrets
|
||||||
|
`KUBE_CONFIG`, `NETBIRD_SETUP_KEY`, and `NETBIRD_URL` if no other workflow uses
|
||||||
|
them. If the kubeconfig had broad cluster access, revoke/rotate its credential
|
||||||
|
separately; deleting a CI secret does not revoke the underlying credential.
|
||||||
|
Remove no account-wide NetBird policies/keys without checking other users.
|
||||||
|
|
||||||
|
## Concurrent publishing and rollback
|
||||||
|
|
||||||
|
The blog workflow serializes its publishes. Other applications can still push to
|
||||||
|
this GitOps repository, so it retries non-fast-forward pushes with a rebase.
|
||||||
|
Conflicting changes fail visibly instead of force-pushing over someone else's
|
||||||
|
work. The digest updater refuses missing/duplicate image lines or a different
|
||||||
|
image repository.
|
||||||
|
|
||||||
|
To roll back, revert the GitOps commit changing the blog's image digest, then
|
||||||
|
push that revert. Keep the old digest in the registry so it remains pullable.
|
||||||
|
Do not use `kubectl set image`: Argo CD's self-healing would put the Git version
|
||||||
|
back. A future source build will deliberately publish another desired-state
|
||||||
|
commit.
|
||||||
|
|
||||||
|
> Checkpoint: if an image build succeeds but the GitOps push fails, should Argo
|
||||||
|
> deploy that image? No: the desired-state commit did not land, so it should keep
|
||||||
|
> the previous image. This separation is why CI no longer needs cluster access.
|
||||||
@@ -94,6 +94,16 @@ Add a row here every time you point a new hostname at this cluster (see
|
|||||||
[`docs/11-ingress.md`](11-ingress.md)). The DNS record itself lives in
|
[`docs/11-ingress.md`](11-ingress.md)). The DNS record itself lives in
|
||||||
Cloudflare, not in this repo — this table is just the log of what's real.
|
Cloudflare, not in this repo — this table is just the log of what's real.
|
||||||
|
|
||||||
|
## Planned hostname migrations (not yet verified live)
|
||||||
|
|
||||||
|
- `https://blog.ahmza.com` → the `blog` Service in `blog-ahmza-com`, through
|
||||||
|
the existing ingress-nginx address `https://10.10.60.12`. Defined in
|
||||||
|
[`apps/blog-ahmza-com/ingress.yaml`](../apps/blog-ahmza-com/ingress.yaml).
|
||||||
|
Uses `letsencrypt-prod`; verify certificate issuance and update internal
|
||||||
|
DNS/external Caddy routing before cutover. No new MetalLB address is allocated.
|
||||||
|
Move this entry into the live hostname table only after verification. See
|
||||||
|
[`docs/13-blog-gitops.md`](13-blog-gitops.md) for the migration checklist.
|
||||||
|
|
||||||
## Free in the pool
|
## Free in the pool
|
||||||
|
|
||||||
Addresses from the range above that aren't spoken for yet. Update this
|
Addresses from the range above that aren't spoken for yet. Update this
|
||||||
|
|||||||
Reference in new issue
Block a user