Manage immich-sync with Argo CD
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
# Homelab GitOps
|
||||
|
||||
Argo CD is the only component that deploys the resources in this repository. Application CI publishes immutable
|
||||
container images and then commits the selected image tag here; it never receives Kubernetes credentials.
|
||||
|
||||
## Repository layout
|
||||
|
||||
- `apps/immich-sync/` is the desired Kubernetes state for Immich Share Sync.
|
||||
- `bootstrap/` contains the Argo CD project and Application that point back to this repository.
|
||||
- `scripts/update-immich-sync-image.sh` is the single image-promotion interface used by CI.
|
||||
|
||||
The only canonical Immich Share Sync image version is `images[0].newTag` in
|
||||
`apps/immich-sync/kustomization.yaml`. The resource manifest deliberately omits a tag, and Kustomize supplies the
|
||||
immutable full Git SHA while rendering.
|
||||
|
||||
## First-time setup
|
||||
|
||||
The cluster already runs Argo CD in the `argocd` namespace and exposes its UI at <https://cd.brunner.ninja>.
|
||||
|
||||
1. Push this repository so Argo CD can read its public HTTPS URL.
|
||||
2. Configure the `immich-sync` repository's cross-repository token as described below.
|
||||
3. Review the rendered workload:
|
||||
|
||||
```sh
|
||||
kubectl kustomize apps/immich-sync
|
||||
```
|
||||
|
||||
4. Record the existing persistent volume before the first synchronization:
|
||||
|
||||
```sh
|
||||
kubectl --namespace default get pvc immich-sync-stats \
|
||||
-o jsonpath='{.metadata.uid}{"|"}{.spec.volumeName}{"\n"}'
|
||||
```
|
||||
|
||||
5. Bootstrap the Argo CD resources:
|
||||
|
||||
```sh
|
||||
./bootstrap/install.sh
|
||||
```
|
||||
|
||||
6. Inspect the result in the UI or CLI:
|
||||
|
||||
```sh
|
||||
argocd app get immich-sync
|
||||
argocd app diff immich-sync
|
||||
kubectl --namespace default rollout status deployment/immich-sync
|
||||
kubectl --namespace default get deployment immich-sync \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
|
||||
```
|
||||
|
||||
7. Confirm that the PVC UID and volume name are unchanged. Only after Argo CD reports the application `Synced` and
|
||||
`Healthy`, remove the obsolete CI deployer and delete the old `KUBE_CONFIG_BASE64` secret from Gitea:
|
||||
|
||||
```sh
|
||||
kubectl --namespace default delete rolebinding/immich-sync-deployer \
|
||||
role/immich-sync-deployer serviceaccount/immich-sync-deployer \
|
||||
secret/immich-sync-deployer-token
|
||||
```
|
||||
|
||||
The bootstrap Application enables automatic synchronization, pruning, and self-healing. Consequently, direct edits
|
||||
to its managed Kubernetes resources are reverted to Git. Change Git when experimenting, observe `OutOfSync`, and then
|
||||
watch Argo CD return the application to `Synced` and `Healthy`.
|
||||
|
||||
## Give Immich Share Sync CI permission to promote images
|
||||
|
||||
The built-in Gitea Actions job token cannot write to another repository. Create a dedicated bot account, for example
|
||||
`gitops-promoter`, add it to `feedc0de/homelab-gitops` with repository **Write** permission, and create a personal
|
||||
access token for that account with only the `write:repository` scope.
|
||||
|
||||
In `feedc0de/immich-sync`, open **Settings → Actions → Secrets** and create `GITOPS_TOKEN` containing that token. The
|
||||
workflow passes it directly to `actions/checkout` while checking out this repository; the checkout action configures
|
||||
authenticated HTTPS for the final `git push` without placing the token in a committed URL.
|
||||
|
||||
The main-branch workflow now performs this sequence:
|
||||
|
||||
1. Build and push `registry.brunner.ninja/feedc0de/immich-sync:<source SHA>`.
|
||||
2. Verify that the completed build still belongs to the current `main` commit.
|
||||
3. Update `apps/immich-sync/kustomization.yaml` with that SHA.
|
||||
4. Commit and push the promotion to this repository.
|
||||
5. Let Argo CD detect and synchronize the Git commit.
|
||||
|
||||
For a personal token owned by `feedc0de`, `write:repository` also applies to other repositories that account can
|
||||
write. A dedicated bot restricted to this one repository therefore gives the token a meaningfully smaller blast
|
||||
radius.
|
||||
|
||||
## Manual recovery path
|
||||
|
||||
Argo CD is the normal deployment mechanism. If it is unavailable, `apps/immich-sync/install.sh` renders, performs a
|
||||
server-side dry-run, applies the same Kustomize source, and waits for the Deployment rollout. A later Argo CD refresh
|
||||
will see the same desired resources.
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
expected_context=kubernetes-admin@kubernetes
|
||||
current_context=$(kubectl config current-context)
|
||||
if [ "${current_context}" != "${expected_context}" ]; then
|
||||
echo "Refusing to deploy through unexpected context: ${current_context}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
kubectl apply --server-side --dry-run=server -k "${script_dir}"
|
||||
kubectl apply --server-side -k "${script_dir}"
|
||||
kubectl --namespace default rollout status deployment/immich-sync --timeout=5m
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- resources.yaml
|
||||
images:
|
||||
- name: registry.brunner.ninja/feedc0de/immich-sync
|
||||
newTag: a6cb14a675cd12f61719213900df8d9487b079ca
|
||||
@@ -0,0 +1,162 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: immich-sync-stats
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
storageClassName: rook-cephfs
|
||||
resources:
|
||||
requests:
|
||||
storage: 64Mi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: immich-sync
|
||||
spec:
|
||||
replicas: 2
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: immich-sync
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: immich-sync
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
fsGroup: 65532
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app: immich-sync
|
||||
topologyKey: kubernetes.io/hostname
|
||||
containers:
|
||||
- name: immich-sync
|
||||
image: registry.brunner.ninja/feedc0de/immich-sync
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: PORT
|
||||
value: "8090"
|
||||
- name: STATS_FILE
|
||||
value: /data/stats.json
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8090
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 1
|
||||
periodSeconds: 5
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 15
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 32Mi
|
||||
ephemeral-storage: 64Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 128Mi
|
||||
ephemeral-storage: 20Gi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
runAsGroup: 65532
|
||||
volumeMounts:
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: stats
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: tmp
|
||||
emptyDir:
|
||||
sizeLimit: 20Gi
|
||||
- name: stats
|
||||
persistentVolumeClaim:
|
||||
claimName: immich-sync-stats
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: immich-sync
|
||||
labels:
|
||||
app: immich-sync
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
protocol: TCP
|
||||
targetPort: http
|
||||
selector:
|
||||
app: immich-sync
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: immich-sync
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: immich-sync
|
||||
---
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: immich-sync
|
||||
labels:
|
||||
release: prometheus
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: immich-sync
|
||||
namespaceSelector:
|
||||
matchNames:
|
||||
- default
|
||||
endpoints:
|
||||
- port: http
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
scrapeTimeout: 10s
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: immich-sync
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: immich-sync.brunner.ninja
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: immich-sync
|
||||
port:
|
||||
name: http
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: immich-sync
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: homelab
|
||||
source:
|
||||
repoURL: https://code.brunner.ninja/feedc0de/homelab-gitops.git
|
||||
targetRevision: main
|
||||
path: apps/immich-sync
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: default
|
||||
syncPolicy:
|
||||
automated:
|
||||
enabled: true
|
||||
prune: true
|
||||
selfHeal: true
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
expected_context=kubernetes-admin@kubernetes
|
||||
current_context=$(kubectl config current-context)
|
||||
if [ "${current_context}" != "${expected_context}" ]; then
|
||||
echo "Refusing to bootstrap through unexpected context: ${current_context}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
kubectl apply --server-side --dry-run=server -k "${script_dir}"
|
||||
kubectl apply --server-side -k "${script_dir}"
|
||||
kubectl --namespace argocd wait application/immich-sync \
|
||||
--for=jsonpath='{.status.sync.status}'=Synced \
|
||||
--timeout=5m
|
||||
kubectl --namespace argocd wait application/immich-sync \
|
||||
--for=jsonpath='{.status.health.status}'=Healthy \
|
||||
--timeout=5m
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- project.yaml
|
||||
- immich-sync.yaml
|
||||
@@ -0,0 +1,25 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: AppProject
|
||||
metadata:
|
||||
name: homelab
|
||||
namespace: argocd
|
||||
spec:
|
||||
description: Applications managed from the public homelab GitOps repository
|
||||
sourceRepos:
|
||||
- https://code.brunner.ninja/feedc0de/homelab-gitops.git
|
||||
destinations:
|
||||
- namespace: default
|
||||
server: https://kubernetes.default.svc
|
||||
namespaceResourceWhitelist:
|
||||
- group: ""
|
||||
kind: PersistentVolumeClaim
|
||||
- group: ""
|
||||
kind: Service
|
||||
- group: apps
|
||||
kind: Deployment
|
||||
- group: networking.k8s.io
|
||||
kind: Ingress
|
||||
- group: policy
|
||||
kind: PodDisruptionBudget
|
||||
- group: monitoring.coreos.com
|
||||
kind: ServiceMonitor
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -ne 1 ] || ! printf '%s\n' "$1" | grep -Eq '^[0-9a-f]{40}$'; then
|
||||
echo "usage: $0 <full-lowercase-git-sha>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
repository_dir=$(dirname -- "${script_dir}")
|
||||
kustomization="${repository_dir}/apps/immich-sync/kustomization.yaml"
|
||||
temporary="${kustomization}.tmp"
|
||||
|
||||
awk -v tag="$1" '
|
||||
/^ newTag: / {
|
||||
if (updated) {
|
||||
print "multiple newTag fields found" > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
print " newTag: " tag
|
||||
updated = 1
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (!updated) {
|
||||
print "newTag field not found" > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
' "${kustomization}" > "${temporary}"
|
||||
mv "${temporary}" "${kustomization}"
|
||||
Reference in New Issue
Block a user