This commit is contained in:
Graham McIntire 2025-07-24 17:30:42 -05:00
parent 928a06659f
commit bfdecc805b
No known key found for this signature in database
10 changed files with 551 additions and 0 deletions

48
ansible/k3s-tailscale.yml Normal file
View file

@ -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 }}"

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: aprs
namespace: aprs
spec:
selector:
app: aprs
ports:
- port: 4000
targetPort: 4000
type: ClusterIP

View file

@ -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

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: aprs

View file

@ -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

View file

@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgis-pvc
namespace: aprs
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: local-path

View file

@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: postgis
namespace: aprs
spec:
selector:
app: postgis
ports:
- port: 5432
targetPort: 5432
type: ClusterIP

View file

@ -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"