diff --git a/ansible/k3s-tailscale.yml b/ansible/k3s-tailscale.yml new file mode 100644 index 0000000..0025ef6 --- /dev/null +++ b/ansible/k3s-tailscale.yml @@ -0,0 +1,48 @@ +--- +- name: Install Tailscale on K3s control plane + hosts: k3s_server + become: true + roles: + - role: artis3n.tailscale + vars: + # Pull Tailscale auth key from environment variable + tailscale_authkey: "{{ lookup('ansible.builtin.env', 'TAILSCALE_KEY') }}" + tailscale_args: "--ssh" + pre_tasks: + - name: Debug - Check Tailscale key (will be partially redacted) + debug: + msg: + - "Key exists: {{ 'TAILSCALE_KEY' in ansible_env }}" + - "Key length: {{ lookup('ansible.builtin.env', 'TAILSCALE_KEY') | length }}" + - "Key prefix: {{ lookup('ansible.builtin.env', 'TAILSCALE_KEY')[0:10] }}..." + when: lookup('ansible.builtin.env', 'TAILSCALE_KEY') | length > 0 + + - name: Fail if no Tailscale key + fail: + msg: "TAILSCALE_KEY environment variable is not set!" + when: lookup('ansible.builtin.env', 'TAILSCALE_KEY') | length == 0 + + - name: Ensure artis3n.tailscale role is installed + delegate_to: localhost + become: false + ansible.builtin.command: + cmd: ansible-galaxy install artis3n.tailscale + run_once: true + + post_tasks: + - name: Wait for Tailscale to be connected + ansible.builtin.command: + cmd: tailscale status + register: tailscale_status + until: '"100.64" in tailscale_status.stdout' + retries: 10 + delay: 5 + + - name: Get Tailscale IP + ansible.builtin.command: + cmd: tailscale ip -4 + register: tailscale_ip + + - name: Display Tailscale IP + debug: + msg: "{{ inventory_hostname }} Tailscale IP: {{ tailscale_ip.stdout }}" \ No newline at end of file diff --git a/clusters/aprs/APRS_REPO_SETUP.md b/clusters/aprs/APRS_REPO_SETUP.md new file mode 100644 index 0000000..211d4cc --- /dev/null +++ b/clusters/aprs/APRS_REPO_SETUP.md @@ -0,0 +1,328 @@ +# APRS Repository Setup for Kubernetes Deployment + +This document provides instructions for setting up your Elixir/Phoenix APRS repository to build Docker images and deploy to your K3s cluster on GitHub Actions push to main branch. + +## Prerequisites + +1. GitHub repository for your APRS application +2. GitHub Container Registry (ghcr.io) access +3. Tailscale installed on your K3s nodes +4. Tailscale OAuth client for GitHub Actions + +## Steps to Configure Your APRS Repository + +### 1. Create Dockerfile + +Add this `Dockerfile` to the root of your APRS repository: + +```dockerfile +# Build stage +FROM elixir:1.15-alpine AS build + +# Install build dependencies +RUN apk add --no-cache build-base git python3 + +# Set build env +ENV MIX_ENV=prod + +# Install hex + rebar +RUN mix local.hex --force && \ + mix local.rebar --force + +# Set build directory +WORKDIR /app + +# Copy mix files +COPY mix.exs mix.lock ./ +COPY config config + +# Install dependencies +RUN mix deps.get --only $MIX_ENV +RUN mix deps.compile + +# Copy app files +COPY priv priv +COPY lib lib +COPY assets assets + +# Compile assets +RUN mix assets.deploy + +# Compile the release +RUN mix compile + +# Build release +RUN mix release + +# Runtime stage +FROM alpine:3.18 AS runtime + +# Install runtime dependencies +RUN apk add --no-cache libstdc++ openssl ncurses-libs + +WORKDIR /app + +# Create non-root user +RUN addgroup -g 1000 -S aprs && \ + adduser -u 1000 -S aprs -G aprs + +# Copy release from build stage +COPY --from=build --chown=aprs:aprs /app/_build/prod/rel/aprs ./ + +# Add health check endpoint if not already present +USER aprs + +ENV HOME=/app +ENV MIX_ENV=prod +ENV PORT=4000 +ENV PHX_SERVER=true + +EXPOSE 4000 + +CMD ["bin/aprs", "start"] +``` + +### 2. Create GitHub Actions Workflow + +Create `.github/workflows/deploy.yml` in your APRS repository: + +```yaml +name: Build and Deploy + +on: + push: + branches: [ main ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=sha,prefix={{branch}}- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + deploy: + needs: build-and-push + runs-on: ubuntu-latest + steps: + - name: Setup Tailscale + uses: tailscale/github-action@v2 + with: + oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} + oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} + tags: tag:ci + + - name: Set kubeconfig + run: | + mkdir -p ~/.kube + echo "${{ secrets.TAILSCALE_KUBECONFIG }}" | base64 -d > ~/.kube/config + # Replace the K3s master IP with its Tailscale IP + sed -i 's/server: https:\/\/[0-9.]*:6443/server: https:\/\/${{ secrets.K3S_TAILSCALE_IP }}:6443/g' ~/.kube/config + + - name: Deploy to K3s + run: | + kubectl set image deployment/aprs aprs=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} -n aprs + + - name: Wait for rollout + run: | + kubectl rollout status deployment/aprs -n aprs +``` + +### 3. Configure Health Check Endpoint + +Add a health check endpoint to your Phoenix application if not already present: + +```elixir +# In your router (lib/aprs_web/router.ex) +scope "/", AprsWeb do + pipe_through :browser + + get "/health", HealthController, :index + # ... other routes +end +``` + +Create the health controller: + +```elixir +# lib/aprs_web/controllers/health_controller.ex +defmodule AprsWeb.HealthController do + use AprsWeb, :controller + + def index(conn, _params) do + json(conn, %{status: "ok"}) + end +end +``` + +### 4. Update Production Configuration + +Ensure your `config/runtime.exs` includes: + +```elixir +import Config + +if config_env() == :prod do + database_url = + System.get_env("DATABASE_URL") || + raise """ + environment variable DATABASE_URL is missing. + """ + + config :aprs, Aprs.Repo, + url: database_url, + pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), + socket_options: [:inet6] + + secret_key_base = + System.get_env("SECRET_KEY_BASE") || + raise """ + environment variable SECRET_KEY_BASE is missing. + """ + + host = System.get_env("PHX_HOST") || "example.com" + port = String.to_integer(System.get_env("PORT") || "4000") + + config :aprs, AprsWeb.Endpoint, + url: [host: host, port: 443, scheme: "https"], + http: [ + ip: {0, 0, 0, 0}, + port: port + ], + secret_key_base: secret_key_base + + config :aprs, AprsWeb.Endpoint, server: true +end +``` + +### 5. Setup Tailscale OAuth Client + +1. Go to https://login.tailscale.com/admin/settings/oauth +2. Create a new OAuth client with these settings: + - Description: "GitHub Actions CI" + - Tags: `tag:ci` + - Capabilities: Read-only access +3. Save the Client ID and Secret + +### 6. Setup GitHub Secrets + +In your APRS GitHub repository settings, add these secrets: + +1. `TS_OAUTH_CLIENT_ID`: Your Tailscale OAuth client ID +2. `TS_OAUTH_SECRET`: Your Tailscale OAuth client secret +3. `K3S_TAILSCALE_IP`: The Tailscale IP of your K3s master node (get it with `tailscale ip -4` on the master) +4. `TAILSCALE_KUBECONFIG`: Your K3s cluster kubeconfig file content (base64 encoded) + ```bash + # On your K3s server: + sudo cat /etc/rancher/k3s/k3s.yaml | base64 -w0 + ``` + +### 7. Update Kubernetes Deployment Image + +After your first GitHub Actions run, update the image in the Kubernetes deployment to use your actual GitHub username: + +```bash +# In the infra repository +sed -i 's/YOUR_GITHUB_USERNAME/your-actual-username/g' clusters/aprs/aprs-deployment.yaml +``` + +### 8. Generate Secrets + +Before deploying to Kubernetes, generate proper secrets: + +```bash +# Generate a 64-character secret key base +mix phx.gen.secret + +# Generate a strong PostgreSQL password +openssl rand -base64 32 +``` + +Update the `secrets.yaml` file with these values before applying to your cluster. + +### 9. Install Tailscale on K3s Nodes + +Before deploying, ensure Tailscale is installed on your K3s nodes: + +```bash +# From the ansible directory +export TAILSCALE_KEY="your-tailscale-auth-key" +ansible-playbook k3s-tailscale.yml +``` + +### 10. Apply Initial Deployment + +```bash +# From the infra repository +kubectl apply -k clusters/aprs/ +``` + +## SSL Proxy Configuration + +Since the APRS service is only accessible from the 10.0.16.0/22 network, you'll need to set up an external SSL proxy. Here's an example nginx configuration: + +```nginx +server { + listen 443 ssl http2; + server_name aprs.yourdomain.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://10.0.16.x:4000; # Replace with actual service IP + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +## Monitoring and Troubleshooting + +Check deployment status: +```bash +kubectl get all -n aprs +kubectl logs -n aprs deployment/aprs +kubectl logs -n aprs deployment/postgis +``` + +## Notes + +- The PostGIS database will persist data in the PersistentVolume +- The APRS application runs with 2 replicas for high availability +- Resource limits are set conservatively; adjust based on your needs +- Remember to backup your PostGIS data regularly \ No newline at end of file diff --git a/clusters/aprs/aprs-deployment.yaml b/clusters/aprs/aprs-deployment.yaml new file mode 100644 index 0000000..1e97a3d --- /dev/null +++ b/clusters/aprs/aprs-deployment.yaml @@ -0,0 +1,58 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: aprs + namespace: aprs +spec: + replicas: 2 + selector: + matchLabels: + app: aprs + template: + metadata: + labels: + app: aprs + spec: + containers: + - name: aprs + image: ghcr.io/gmcintire/aprs:latest + ports: + - containerPort: 4000 + env: + - name: PHX_HOST + value: "aprs.me" + - name: PORT + value: "4000" + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: aprs-secret + key: database-url + - name: SECRET_KEY_BASE + valueFrom: + secretKeyRef: + name: aprs-secret + key: secret-key-base + - name: PHX_SERVER + value: "true" + - name: MIX_ENV + value: "prod" + resources: + limits: + memory: "512Mi" + cpu: "500m" + requests: + memory: "256Mi" + cpu: "250m" + livenessProbe: + httpGet: + path: /health + port: 4000 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: 4000 + initialDelaySeconds: 5 + periodSeconds: 5 diff --git a/clusters/aprs/aprs-service.yaml b/clusters/aprs/aprs-service.yaml new file mode 100644 index 0000000..ccb1fc0 --- /dev/null +++ b/clusters/aprs/aprs-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: aprs + namespace: aprs +spec: + selector: + app: aprs + ports: + - port: 4000 + targetPort: 4000 + type: ClusterIP \ No newline at end of file diff --git a/clusters/aprs/kustomization.yaml b/clusters/aprs/kustomization.yaml new file mode 100644 index 0000000..bf15a54 --- /dev/null +++ b/clusters/aprs/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: aprs + +resources: + - namespace.yaml + - secrets.yaml + - postgis-pvc.yaml + - postgis-deployment.yaml + - postgis-service.yaml + - aprs-deployment.yaml + - aprs-service.yaml \ No newline at end of file diff --git a/clusters/aprs/namespace.yaml b/clusters/aprs/namespace.yaml new file mode 100644 index 0000000..65e1f5f --- /dev/null +++ b/clusters/aprs/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: aprs \ No newline at end of file diff --git a/clusters/aprs/postgis-deployment.yaml b/clusters/aprs/postgis-deployment.yaml new file mode 100644 index 0000000..b114287 --- /dev/null +++ b/clusters/aprs/postgis-deployment.yaml @@ -0,0 +1,46 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgis + namespace: aprs +spec: + replicas: 1 + selector: + matchLabels: + app: postgis + template: + metadata: + labels: + app: postgis + spec: + containers: + - name: postgis + image: postgis/postgis:17-3.4 + ports: + - containerPort: 5432 + env: + - name: POSTGRES_DB + value: aprs_prod + - name: POSTGRES_USER + value: aprs + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: postgis-secret + key: postgres-password + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: postgis-storage + mountPath: /var/lib/postgresql/data + resources: + limits: + memory: "1Gi" + cpu: "500m" + requests: + memory: "512Mi" + cpu: "250m" + volumes: + - name: postgis-storage + persistentVolumeClaim: + claimName: postgis-pvc \ No newline at end of file diff --git a/clusters/aprs/postgis-pvc.yaml b/clusters/aprs/postgis-pvc.yaml new file mode 100644 index 0000000..162d10a --- /dev/null +++ b/clusters/aprs/postgis-pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgis-pvc + namespace: aprs +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + storageClassName: local-path \ No newline at end of file diff --git a/clusters/aprs/postgis-service.yaml b/clusters/aprs/postgis-service.yaml new file mode 100644 index 0000000..49d0f8b --- /dev/null +++ b/clusters/aprs/postgis-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgis + namespace: aprs +spec: + selector: + app: postgis + ports: + - port: 5432 + targetPort: 5432 + type: ClusterIP \ No newline at end of file diff --git a/clusters/aprs/secrets.yaml b/clusters/aprs/secrets.yaml new file mode 100644 index 0000000..df43ec8 --- /dev/null +++ b/clusters/aprs/secrets.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Secret +metadata: + name: postgis-secret + namespace: aprs +type: Opaque +stringData: + postgres-password: "mjBipckSZfNOoSOWLMRsfZEqJ9I5Of21" +--- +apiVersion: v1 +kind: Secret +metadata: + name: aprs-secret + namespace: aprs +type: Opaque +stringData: + database-url: "ecto://aprs:mjBipckSZfNOoSOWLMRsfZEqJ9I5Of21@postgis:5432/aprs" + secret-key-base: "GJxzOUW3YXBAF7xB4mH5VcXY6BN1NYLnoJYadlZyiCxcRUGBw65w4Sco7+sNiW2t"