feat: own production deployment manifests

This commit is contained in:
2026-05-05 04:14:12 +00:00
parent 4c6b1dc752
commit 70c2dd4b32
11 changed files with 297 additions and 4 deletions
+33 -1
View File
@@ -4,9 +4,11 @@ on:
push:
branches:
- main
paths-ignore:
- deploy/prod/**
permissions:
contents: read
contents: write
packages: write
env:
@@ -46,3 +48,33 @@ jobs:
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:main
update-deploy:
name: Update production image tags
runs-on: ubuntu-latest
needs: publish
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Update deploy/prod image tags
run: |
set -euo pipefail
sed -i -E "/name: ghcr\.io\/openstaticfish\/microservices\/site-analyzer/{n;s/newTag: .*/newTag: ${GITHUB_SHA}/;}" deploy/prod/kustomization.yaml
sed -i -E "/name: ghcr\.io\/openstaticfish\/microservices\/scraper/{n;s/newTag: .*/newTag: ${GITHUB_SHA}/;}" deploy/prod/kustomization.yaml
- name: Commit production image tag update
run: |
set -euo pipefail
if git diff --quiet -- deploy/prod/kustomization.yaml; then
echo "deploy/prod image tags already match ${GITHUB_SHA}"
exit 0
fi
git config user.name github-actions[bot]
git config user.email github-actions[bot]@users.noreply.github.com
git add deploy/prod/kustomization.yaml
git commit -m "chore: update production image tags [skip ci]"
git push
+10 -3
View File
@@ -281,11 +281,11 @@ doppler run -- your-command
Tilt uses Doppler to create the local Kubernetes `webshare-api` secret for the scraper service. Do not commit local `.env` files; `.env` and `.env.*` are ignored by Git.
## Production Images
## Production Deployment
Tilt is local-development only in this repo. It continues to build local images named `site-analyzer` and `scraper` for the Kind/Tilt workflow.
Production deployment is owned by the HetznerTerra GitOps repo. This repo publishes container images only; it is not the source of truth for production Kubernetes deployment manifests.
Production Kubernetes manifests live in `deploy/prod` in this repo. HetznerTerra owns the cluster and subscribes Flux to this repository; adding or changing MicroServices workloads should happen here, not in the infrastructure repo.
On pushes to `main`, GitHub Actions builds and pushes these GHCR images:
@@ -294,7 +294,9 @@ On pushes to `main`, GitHub Actions builds and pushes these GHCR images:
- `ghcr.io/openstaticfish/microservices/scraper:main`
- `ghcr.io/openstaticfish/microservices/scraper:<git-sha>`
HetznerTerra should reference immutable `<git-sha>` tags for production deployments. Runtime secrets, including the scraper Webshare configuration, must be supplied by the deployment environment and are not baked into images.
After pushing images, CI updates `deploy/prod/kustomization.yaml` to the immutable `<git-sha>` tag and commits that change back to `main`. Flux then applies the production manifests from this repo.
Runtime secrets, including the scraper Webshare configuration, stay in Doppler project `openstaticfish-microservices` config `dev`. They are exposed to the cluster through External Secrets and are not baked into images or stored in HetznerTerra.
## Layout
@@ -303,6 +305,11 @@ HetznerTerra should reference immutable `<git-sha>` tags for production deployme
├── flake.nix
├── doppler.yaml
├── Tiltfile
├── deploy/
│ └── prod/
│ ├── kustomization.yaml
│ ├── scraper-deployment.yaml
│ └── site-analyzer-deployment.yaml
├── k8s/
│ ├── scraper.yaml
│ └── site-analyzer.yaml
@@ -0,0 +1,23 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: microservices
namespace: microservices
spec:
entryPoints:
- web
routes:
- match: Host(`apps.silverside-gopher.ts.net`) && PathPrefix(`/site-analyzer`)
kind: Rule
middlewares:
- name: microservices-strip-prefix
services:
- name: site-analyzer
port: 8090
- match: Host(`apps.silverside-gopher.ts.net`) && PathPrefix(`/scraper`)
kind: Rule
middlewares:
- name: microservices-strip-prefix
services:
- name: scraper
port: 8080
+16
View File
@@ -0,0 +1,16 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- webshare-api-externalsecret.yaml
- site-analyzer-deployment.yaml
- site-analyzer-service.yaml
- scraper-deployment.yaml
- scraper-service.yaml
- traefik-middleware-strip-prefix.yaml
- ingressroute-microservices.yaml
images:
- name: ghcr.io/openstaticfish/microservices/site-analyzer
newTag: 4c6b1dc752feab5ef4c52f40d5c97a85ec59bcba
- name: ghcr.io/openstaticfish/microservices/scraper
newTag: 4c6b1dc752feab5ef4c52f40d5c97a85ec59bcba
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: microservices
+64
View File
@@ -0,0 +1,64 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: scraper
namespace: microservices
labels:
app: scraper
spec:
replicas: 1
selector:
matchLabels:
app: scraper
template:
metadata:
labels:
app: scraper
spec:
imagePullSecrets:
- name: ghcr-pull-secret
containers:
- name: scraper
image: ghcr.io/openstaticfish/microservices/scraper:4c6b1dc752feab5ef4c52f40d5c97a85ec59bcba
ports:
- containerPort: 8080
env:
- name: PORT
value: "8080"
- name: WEBSHARE_API_KEY
valueFrom:
secretKeyRef:
name: webshare-api
key: api-key
optional: true
- name: WEBSHARE_PROXY_USERNAME
valueFrom:
secretKeyRef:
name: webshare-api
key: proxy-username
optional: true
- name: WEBSHARE_PROXY_PASSWORD
valueFrom:
secretKeyRef:
name: webshare-api
key: proxy-password
optional: true
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 2
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 2
periodSeconds: 5
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: scraper
namespace: microservices
spec:
type: ClusterIP
selector:
app: scraper
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080
+80
View File
@@ -0,0 +1,80 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: site-analyzer
namespace: microservices
labels:
app: site-analyzer
spec:
replicas: 1
selector:
matchLabels:
app: site-analyzer
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: site-analyzer
spec:
imagePullSecrets:
- name: ghcr-pull-secret
terminationGracePeriodSeconds: 30
containers:
- name: site-analyzer
image: ghcr.io/openstaticfish/microservices/site-analyzer:4c6b1dc752feab5ef4c52f40d5c97a85ec59bcba
ports:
- containerPort: 8090
env:
- name: PORT
value: "8090"
- name: MAX_CONCURRENT_ANALYSES
value: "20"
- name: ANALYSIS_TIMEOUT
value: 15s
- name: FETCH_TIMEOUT
value: 10s
- name: MAX_REQUEST_BYTES
value: "4096"
- name: MAX_RESPONSE_BYTES
value: "2097152"
- name: READ_HEADER_TIMEOUT
value: 2s
- name: READ_TIMEOUT
value: 5s
- name: WRITE_TIMEOUT
value: 20s
- name: IDLE_TIMEOUT
value: 60s
- name: SHUTDOWN_TIMEOUT
value: 25s
- name: MAX_IDLE_CONNS
value: "200"
- name: MAX_IDLE_CONNS_PER_HOST
value: "20"
resources:
requests:
cpu: 250m
memory: 128Mi
limits:
cpu: "1"
memory: 512Mi
livenessProbe:
httpGet:
path: /health
port: 8090
initialDelaySeconds: 2
periodSeconds: 10
timeoutSeconds: 1
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8090
initialDelaySeconds: 2
periodSeconds: 3
timeoutSeconds: 1
failureThreshold: 2
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: site-analyzer
namespace: microservices
spec:
type: ClusterIP
selector:
app: site-analyzer
ports:
- name: http
protocol: TCP
port: 8090
targetPort: 8090
@@ -0,0 +1,10 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: microservices-strip-prefix
namespace: microservices
spec:
stripPrefix:
prefixes:
- /site-analyzer
- /scraper
@@ -0,0 +1,29 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: webshare-api
namespace: microservices
spec:
refreshInterval: 1h
secretStoreRef:
name: doppler-openstaticfish-microservices
kind: ClusterSecretStore
target:
name: webshare-api
creationPolicy: Owner
template:
type: Opaque
data:
api-key: "{{ .webshareApiKey }}"
proxy-username: "{{ .webshareProxyUsername }}"
proxy-password: "{{ .webshareProxyPassword }}"
data:
- secretKey: webshareApiKey
remoteRef:
key: WEBSHARE_API_KEY
- secretKey: webshareProxyUsername
remoteRef:
key: WEBSHARE_PROXY_USERNAME
- secretKey: webshareProxyPassword
remoteRef:
key: WEBSHARE_PROXY_PASSWORD