This commit is contained in:
Graham McIntire 2025-09-06 10:55:13 -05:00
parent f971c35b4e
commit ffe9a59269
No known key found for this signature in database
234 changed files with 7684 additions and 2433 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# Terraform directories
**/.terraform/

View file

@ -12,6 +12,12 @@
name: epel-release
state: present
- name: Clean DNF cache
ansible.builtin.command: dnf clean all
args:
warn: false
changed_when: false
- name: Upgrade all packages
ansible.builtin.dnf:
name: "*"

View file

@ -0,0 +1,7 @@
---
# Alpine-specific service names
ssh_service: sshd
ntp_service: chronyd
# Alpine doesn't have firewalld
manage_firewall: false

View file

@ -0,0 +1,65 @@
---
# Alpine Linux specific tasks
- name: Update apk cache
apk:
update_cache: yes
changed_when: false
- name: Upgrade all packages
apk:
upgrade: yes
update_cache: yes
register: apk_upgrade
changed_when: "'OK:' not in apk_upgrade.stdout"
- name: Install sudo first (required for user management)
apk:
name: sudo
state: present
- name: Install base packages
apk:
name:
- bash
- curl
- git
- htop
- iotop
- nano
- net-tools
- openssh-server
- python3
- py3-pip
- rsync
- shadow # For useradd/usermod commands
- tmux
- vim
- wget
state: present
- name: Ensure sshd is running and enabled
service:
name: sshd
state: started
enabled: yes
- name: Configure sudo for passwordless
lineinfile:
path: /etc/sudoers
regexp: '^%sudo\s'
line: '%sudo ALL=(ALL) NOPASSWD: ALL'
validate: '/usr/sbin/visudo -cf %s'
when: configure_passwordless_sudo | default(true)
- name: Create sudo group if it doesn't exist
group:
name: sudo
state: present
- name: Ensure ansible user is in sudo group
user:
name: ansible
groups: sudo
append: yes
when: ansible_user == "ansible"

View file

@ -35,4 +35,8 @@
- include_role:
name: debian
when: ansible_os_family == "Debian"
when: ansible_os_family == "Debian"
- include_role:
name: alpine
when: ansible_os_family == "Alpine"

View file

@ -1,5 +1,6 @@
---
- name: Install required packages
# Debian/Ubuntu tasks
- name: Install required packages (Debian/Ubuntu)
apt:
name:
- debian-keyring
@ -9,23 +10,35 @@
- gnupg2
state: present
update_cache: yes
when: ansible_os_family == "Debian"
- name: Add Caddy GPG key
- name: Add Caddy GPG key (Debian/Ubuntu)
apt_key:
url: "https://dl.cloudsmith.io/public/caddy/stable/gpg.key"
state: present
when: ansible_os_family == "Debian"
- name: Add Caddy repository
- name: Add Caddy repository (Debian/Ubuntu)
apt_repository:
repo: "deb https://dl.cloudsmith.io/public/caddy/stable/deb/debian any-version main"
state: present
filename: caddy-stable
when: ansible_os_family == "Debian"
- name: Install Caddy
- name: Install Caddy (Debian/Ubuntu)
apt:
name: caddy
state: present
update_cache: yes
when: ansible_os_family == "Debian"
# Alpine Linux tasks
- name: Install Caddy (Alpine)
apk:
name: caddy
state: present
update_cache: yes
when: ansible_os_family == "Alpine"
- name: Ensure Caddy service is enabled and started
service:

View file

@ -0,0 +1,71 @@
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=UTC
# Add Icinga repository
RUN apt-get update && apt-get install -y \
wget \
gnupg \
lsb-release \
ca-certificates \
&& wget -O - https://packages.icinga.com/icinga.key | apt-key add - \
&& echo "deb [signed-by=/usr/share/keyrings/icinga-archive-keyring.gpg] https://packages.icinga.com/ubuntu icinga-$(lsb_release -cs) main" > /etc/apt/sources.list.d/icinga2.list \
&& echo "deb-src [signed-by=/usr/share/keyrings/icinga-archive-keyring.gpg] https://packages.icinga.com/ubuntu icinga-$(lsb_release -cs) main" >> /etc/apt/sources.list.d/icinga2.list \
&& wget -O /usr/share/keyrings/icinga-archive-keyring.gpg https://packages.icinga.com/icinga.key
# Install packages
RUN apt-get update && apt-get install -y \
icinga2 \
icinga2-ido-mysql \
icingaweb2 \
icingacli \
mariadb-client \
apache2 \
libapache2-mod-php \
php-bcmath \
php-curl \
php-gd \
php-intl \
php-ldap \
php-mbstring \
php-mysql \
php-pgsql \
php-soap \
php-xml \
php-zip \
php-imagick \
supervisor \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Enable Apache modules
RUN a2enmod rewrite headers env dir mime
# Configure Apache for IcingaWeb2
COPY icingaweb2.conf /etc/apache2/sites-available/
RUN a2dissite 000-default && a2ensite icingaweb2
# Create directories
RUN mkdir -p /etc/icinga2/pki \
/var/run/icinga2/cmd \
/var/log/icinga2 \
/var/lib/icinga2/api/zones \
/var/cache/icinga2 \
&& chown -R nagios:nagios /etc/icinga2 /var/run/icinga2 /var/log/icinga2 /var/lib/icinga2 /var/cache/icinga2
# Setup volumes
VOLUME ["/etc/icinga2", "/etc/icingaweb2", "/var/lib/icinga2"]
# Expose ports
EXPOSE 80 5665
# Copy supervisord config
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Copy startup script
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View file

@ -0,0 +1,77 @@
#!/bin/bash
set -e
# Wait for database to be ready
if [ ! -z "${MYSQL_HOST}" ]; then
echo "Waiting for database connection..."
for i in {1..30}; do
if mysql -h${MYSQL_HOST} -u${MYSQL_USER} -p${MYSQL_PASSWORD} -e "SELECT 1" >/dev/null 2>&1; then
echo "Database is ready!"
break
fi
echo "Waiting for database... ($i/30)"
sleep 2
done
fi
# Initialize Icinga2 database if needed
if [ ! -z "${MYSQL_HOST}" ] && [ ! -z "${INIT_MYSQL}" ]; then
echo "Checking if Icinga2 database needs initialization..."
if ! mysql -h${MYSQL_HOST} -u${MYSQL_USER} -p${MYSQL_PASSWORD} ${MYSQL_DATABASE} -e "SELECT * FROM icinga_dbversion" >/dev/null 2>&1; then
echo "Initializing Icinga2 database..."
mysql -h${MYSQL_HOST} -u${MYSQL_USER} -p${MYSQL_PASSWORD} ${MYSQL_DATABASE} < /usr/share/icinga2-ido-mysql/schema/mysql.sql
fi
fi
# Configure IDO-MySQL if environment variables are set
if [ ! -z "${MYSQL_HOST}" ]; then
cat > /etc/icinga2/features-available/ido-mysql.conf <<EOF
library "db_ido_mysql"
object IdoMysqlConnection "ido-mysql" {
user = "${MYSQL_USER}"
password = "${MYSQL_PASSWORD}"
host = "${MYSQL_HOST}"
database = "${MYSQL_DATABASE}"
}
EOF
icinga2 feature enable ido-mysql
fi
# Setup API if certificates don't exist
if [ ! -f /var/lib/icinga2/certs/ca.crt ]; then
echo "Setting up Icinga2 API..."
icinga2 api setup
fi
# Enable required features
icinga2 feature enable api command checker mainlog notification
# Configure IcingaWeb2 database settings if provided
if [ ! -z "${ICINGAWEB_MYSQL_HOST}" ]; then
mkdir -p /etc/icingaweb2
cat > /etc/icingaweb2/resources.ini <<EOF
[icingaweb_db]
type = "db"
db = "mysql"
host = "${ICINGAWEB_MYSQL_HOST}"
dbname = "${ICINGAWEB_MYSQL_DATABASE}"
username = "${ICINGAWEB_MYSQL_USER}"
password = "${ICINGAWEB_MYSQL_PASSWORD}"
[icinga_ido]
type = "db"
db = "mysql"
host = "${MYSQL_HOST}"
dbname = "${MYSQL_DATABASE}"
username = "${MYSQL_USER}"
password = "${MYSQL_PASSWORD}"
EOF
fi
# Fix permissions
chown -R nagios:nagios /etc/icinga2 /var/run/icinga2 /var/log/icinga2 /var/lib/icinga2 /var/cache/icinga2
chown -R www-data:icingaweb2 /etc/icingaweb2
# Execute CMD
exec "$@"

View file

@ -0,0 +1,20 @@
<VirtualHost *:80>
DocumentRoot /usr/share/icingaweb2/public
<Directory /usr/share/icingaweb2/public>
Options SymLinksIfOwnerMatch
AllowOverride None
Require all granted
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
RewriteRule ^.*$ index.php [NC,L]
</Directory>
ErrorLog ${APACHE_LOG_DIR}/icingaweb2-error.log
CustomLog ${APACHE_LOG_DIR}/icingaweb2-access.log combined
</VirtualHost>

View file

@ -0,0 +1,18 @@
[supervisord]
nodaemon=true
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
[program:apache2]
command=/usr/sbin/apache2ctl -D FOREGROUND
autostart=true
autorestart=true
stdout_logfile=/var/log/supervisor/apache2.log
stderr_logfile=/var/log/supervisor/apache2_err.log
[program:icinga2]
command=/usr/sbin/icinga2 daemon
autostart=true
autorestart=true
stdout_logfile=/var/log/supervisor/icinga2.log
stderr_logfile=/var/log/supervisor/icinga2_err.log

View file

@ -1,328 +0,0 @@
# 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

@ -1,188 +0,0 @@
# PostgreSQL Auto-Healing Configuration Guide
This guide explains the auto-healing mechanisms implemented to prevent and recover from PostgreSQL connection exhaustion issues.
## Overview
The auto-healing system consists of several components working together:
1. **Enhanced PostgreSQL Configuration**
2. **Improved PgBouncer Connection Pooling**
3. **Health Checks and Automatic Restarts**
4. **Periodic Connection Cleanup**
5. **Resource Limits and Scaling**
## Components
### 1. PostgreSQL Enhancements (`postgis-deployment-enhanced.yaml`)
- **Increased max_connections**: From 100 to 200
- **Connection timeouts**:
- `idle_in_transaction_session_timeout=30s`: Kills idle transactions after 30 seconds
- `statement_timeout=300s`: Kills queries running longer than 5 minutes
- **Readiness probe**: Fails if connection count exceeds 180 (90% of max)
- **Increased resources**: 2Gi memory, 1 CPU
### 2. PgBouncer Enhancements (`pgbouncer-deployment-enhanced.yaml`)
- **Connection pooling settings**:
- Pool mode: `transaction` (releases connections after each transaction)
- Default pool size: 25 connections per database/user pair
- Server idle timeout: 10 minutes
- Query timeout: 5 minutes
- **Retry logic**:
- `server_login_retry=15`: Retries failed logins for 15 seconds
- `server_round_robin=1`: Distributes connections evenly
- **Enhanced logging**: Tracks connections, disconnections, and errors
### 3. APRS Application Enhancements (`aprs-statefulset-enhanced.yaml`)
- **Reduced connection pool**: From 10 to 5 connections per pod
- **Connection timeouts**:
- Pool timeout: 60 seconds
- Connect timeout: 30 seconds
- Idle timeout: 15 minutes
- **Startup probe**: Allows up to 120 seconds for application startup
- **Increased resources**: 1Gi memory, 1 CPU
### 4. Automatic Connection Cleanup (`postgres-cleanup-cronjob.yaml`)
Runs every 15 minutes to:
- Kill idle connections older than 30 minutes
- Kill idle-in-transaction connections older than 5 minutes
- Log connection statistics
- Monitor connection usage percentage
### 5. Pod Disruption Budgets (`postgis-pdb.yaml`)
Ensures at least 1 replica of PostgreSQL and PgBouncer remain available during:
- Cluster upgrades
- Node maintenance
- Voluntary disruptions
## How Auto-Healing Works
### Connection Exhaustion Prevention
1. **Application-level**: Reduced pool size prevents each pod from using too many connections
2. **PgBouncer-level**: Connection pooling multiplexes many client connections over fewer server connections
3. **PostgreSQL-level**: Timeouts automatically close stale connections
### Automatic Recovery
1. **Health checks**: Pods restart automatically when:
- PostgreSQL has too many connections (>180)
- PgBouncer can't connect to PostgreSQL
- Application can't connect to the database
2. **CronJob cleanup**: Every 15 minutes, forcefully closes:
- Idle connections wasting resources
- Stuck transactions blocking others
3. **Connection retry**: PgBouncer retries failed connections for 15 seconds before giving up
## Monitoring
Use the provided monitoring script to check system health:
```bash
./monitor-postgres-health.sh
```
This shows:
- Current connection count and states
- Connection usage percentage
- Recent errors from logs
- Last cleanup job execution
## Manual Intervention
If automatic healing fails, you can manually intervene:
```bash
# Check connection details
kubectl exec -it deployment/postgis -n aprs -- psql -U aprs -d aprs_prod -c "SELECT * FROM pg_stat_activity WHERE datname = 'aprs_prod';"
# Kill all connections (emergency)
kubectl exec -it deployment/postgis -n aprs -- psql -U aprs -d aprs_prod -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'aprs_prod' AND pid <> pg_backend_pid();"
# Restart components
kubectl rollout restart deployment/postgis -n aprs
kubectl rollout restart deployment/pgbouncer -n aprs
kubectl rollout restart statefulset/aprs -n aprs
```
## Applying the Configuration
### ⚠️ WARNING: Data Persistence
The enhanced deployments use different volume configurations. Applying them directly will create new volumes and lose existing data.
### Safe Application (Recommended)
For existing deployments with data:
```bash
cd /Users/graham/dev/infra/clusters/aprs
./apply-auto-healing-safe.sh
```
This applies only non-destructive changes:
- PodDisruptionBudgets
- Cleanup CronJob
- Creates patch files for gradual application
Then apply patches one at a time:
```bash
# PostgreSQL enhancements
kubectl patch deployment postgis -n aprs --patch-file postgis-deployment-patch.yaml
kubectl rollout status deployment/postgis -n aprs
# PgBouncer enhancements
kubectl patch deployment pgbouncer -n aprs --patch-file pgbouncer-deployment-patch.yaml
kubectl rollout status deployment/pgbouncer -n aprs
# APRS enhancements
kubectl patch statefulset aprs -n aprs --patch-file aprs-statefulset-patch.yaml
kubectl rollout status statefulset/aprs -n aprs
```
### Fresh Installation
For new deployments without existing data:
```bash
cd /Users/graham/dev/infra/clusters/aprs
./apply-auto-healing.sh
```
This script:
1. Backs up current configurations
2. Applies all enhanced configurations
3. Waits for rollouts to complete
4. Provides a summary of changes
## Tuning Parameters
You may need to adjust these based on your workload:
1. **PostgreSQL max_connections**: Currently 200, increase if needed
2. **PgBouncer pool sizes**: Currently 25, adjust based on connection patterns
3. **Application POOL_SIZE**: Currently 5, can be increased if connections are available
4. **Cleanup intervals**: Currently 15 minutes, can be more or less frequent
## Troubleshooting
Common issues and solutions:
1. **"too many clients already"**: Cleanup job should fix this within 15 minutes
2. **"server login has been failing"**: PgBouncer is in retry mode, will recover in 15 seconds
3. **Pods in CrashLoopBackOff**: Check logs for specific errors
4. **High memory usage**: Consider increasing resource limits
## Future Improvements
Consider implementing:
1. Prometheus metrics for connection monitoring
2. Alerting when connection usage exceeds 80%
3. Horizontal pod autoscaling based on connection metrics
4. Connection pooling at the application level using a library like `poolboy`

View file

@ -1,148 +0,0 @@
#!/bin/bash
set -e
echo "Applying auto-healing configurations safely..."
# Only apply configurations that don't affect data persistence
echo "Applying PodDisruptionBudgets..."
kubectl apply -f postgis-pdb.yaml
echo "Applying PostgreSQL cleanup CronJob..."
kubectl apply -f postgres-cleanup-cronjob.yaml
echo "Creating enhanced configurations as patches (not applied automatically)..."
# Create patch files instead of replacing deployments
cat > postgis-deployment-patch.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgis
namespace: aprs
spec:
template:
spec:
containers:
- name: postgis
args:
- postgres
- -c
- max_connections=200
- -c
- idle_in_transaction_session_timeout=30s
- -c
- statement_timeout=300s
- -c
- shared_buffers=256MB
resources:
limits:
memory: "2Gi"
cpu: "1000m"
requests:
memory: "1Gi"
cpu: "500m"
livenessProbe:
exec:
command:
- /bin/sh
- -c
- pg_isready -U $POSTGRES_USER -d $POSTGRES_DB
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command:
- /bin/sh
- -c
- pg_isready -U $POSTGRES_USER -d $POSTGRES_DB
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 2
EOF
cat > pgbouncer-deployment-patch.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: pgbouncer
namespace: aprs
spec:
template:
spec:
containers:
- name: pgbouncer
env:
- name: PGBOUNCER_SERVER_IDLE_TIMEOUT
value: "600"
- name: PGBOUNCER_SERVER_LIFETIME
value: "3600"
- name: PGBOUNCER_QUERY_TIMEOUT
value: "300"
- name: PGBOUNCER_QUERY_WAIT_TIMEOUT
value: "120"
- name: PGBOUNCER_CLIENT_IDLE_TIMEOUT
value: "300"
- name: PGBOUNCER_CLIENT_LOGIN_TIMEOUT
value: "60"
- name: PGBOUNCER_SERVER_CONNECT_TIMEOUT
value: "15"
- name: PGBOUNCER_SERVER_LOGIN_RETRY
value: "15"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
EOF
cat > aprs-statefulset-patch.yaml << 'EOF'
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: aprs
namespace: aprs
spec:
template:
spec:
containers:
- name: aprs
env:
- name: POOL_SIZE
value: "5"
- name: DATABASE_POOL_TIMEOUT
value: "60000"
- name: DATABASE_CONNECT_TIMEOUT
value: "30000"
resources:
limits:
memory: "1Gi"
cpu: "1000m"
requests:
memory: "512Mi"
cpu: "500m"
startupProbe:
httpGet:
path: /health
port: 4000
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 10
failureThreshold: 12
EOF
echo ""
echo "Safe auto-healing configurations applied!"
echo ""
echo "To apply the enhanced configurations gradually:"
echo "1. Apply PostgreSQL patch: kubectl patch deployment postgis -n aprs --patch-file postgis-deployment-patch.yaml"
echo "2. Wait for rollout: kubectl rollout status deployment/postgis -n aprs"
echo "3. Apply PgBouncer patch: kubectl patch deployment pgbouncer -n aprs --patch-file pgbouncer-deployment-patch.yaml"
echo "4. Wait for rollout: kubectl rollout status deployment/pgbouncer -n aprs"
echo "5. Apply APRS patch: kubectl patch statefulset aprs -n aprs --patch-file aprs-statefulset-patch.yaml"
echo ""
echo "Monitor with: ./monitor-postgres-health.sh"

View file

@ -1,51 +0,0 @@
#!/bin/bash
set -e
echo "Applying PostgreSQL and PgBouncer auto-healing configurations..."
# Backup current deployments
echo "Creating backups of current deployments..."
kubectl get deployment postgis -n aprs -o yaml > postgis-deployment-backup.yaml || true
kubectl get deployment pgbouncer -n aprs -o yaml > pgbouncer-deployment-backup.yaml || true
kubectl get statefulset aprs -n aprs -o yaml > aprs-statefulset-backup.yaml || true
# Apply enhanced configurations
echo "Applying enhanced PostgreSQL deployment..."
kubectl apply -f postgis-deployment-enhanced.yaml
echo "Applying enhanced PgBouncer deployment..."
kubectl apply -f pgbouncer-deployment-enhanced.yaml
echo "Applying PodDisruptionBudgets..."
kubectl apply -f postgis-pdb.yaml
echo "Applying enhanced APRS StatefulSet..."
kubectl apply -f aprs-statefulset-enhanced.yaml
echo "Applying PostgreSQL cleanup CronJob..."
kubectl apply -f postgres-cleanup-cronjob.yaml
# Wait for rollouts
echo "Waiting for PostgreSQL rollout..."
kubectl rollout status deployment/postgis -n aprs --timeout=300s
echo "Waiting for PgBouncer rollout..."
kubectl rollout status deployment/pgbouncer -n aprs --timeout=300s
echo "Waiting for APRS rollout..."
kubectl rollout status statefulset/aprs -n aprs --timeout=300s
echo "Auto-healing configurations applied successfully!"
echo ""
echo "Summary of changes:"
echo "1. PostgreSQL: Increased max_connections to 200, added connection timeouts"
echo "2. PgBouncer: Enhanced retry logic and connection pooling"
echo "3. APRS pods: Reduced pool size to 5, added startup probes"
echo "4. Added health checks that restart pods when connections are exhausted"
echo "5. CronJob runs every 15 minutes to clean up stale connections"
echo ""
echo "Monitor the deployment with:"
echo " kubectl get pods -n aprs -w"
echo ""
echo "Check PostgreSQL connections with:"
echo " kubectl exec -it deployment/postgis -n aprs -- psql -U aprs -d aprs_prod -c 'SELECT state, count(*) FROM pg_stat_activity GROUP BY state;'"

View file

@ -1,18 +0,0 @@
#!/bin/bash
# Run these commands on your Proxmox node (e.g., lab02)
# Create a Ceph pool for Kubernetes with 128 placement groups
echo "Creating Kubernetes pool..."
ceph osd pool create kubernetes 128
ceph osd pool application enable kubernetes rbd
# Create a Ceph user for Kubernetes with appropriate permissions
echo "Creating Kubernetes user..."
ceph auth get-or-create client.kubernetes \
mon 'profile rbd' \
osd 'profile rbd pool=kubernetes' \
mgr 'profile rbd pool=kubernetes'
# Display the key for configuration
echo -e "\n\nCopy the key value below for your configuration:"
ceph auth get client.kubernetes | grep key

View file

@ -1,105 +0,0 @@
# Setting up Ceph Storage for PostgreSQL in K3s
## Prerequisites
Before applying these manifests, you need to gather information from your Proxmox Ceph cluster:
1. **Ceph Monitor IPs**: Get the monitor IPs from Proxmox
```bash
# On Proxmox node:
ceph mon dump
```
2. **Ceph Cluster ID**: Get your Ceph cluster ID
```bash
# On Proxmox node:
ceph fsid
```
3. **Create a Ceph pool for Kubernetes** (if not already exists):
```bash
# On Proxmox node:
ceph osd pool create kubernetes 128
ceph osd pool application enable kubernetes rbd
```
4. **Create a Ceph user for Kubernetes**:
```bash
# On Proxmox node:
ceph auth get-or-create client.kubernetes mon 'profile rbd' osd 'profile rbd pool=kubernetes' mgr 'profile rbd pool=kubernetes'
```
## Configuration Steps
1. **Update the Ceph configuration** in `ceph-csi/ceph-config.yaml`:
- Replace `mon_host` with your actual Ceph monitor IPs
- Replace the `userKey` with the key from step 4 above
2. **Update the StorageClass** in `ceph-csi/ceph-storageclass.yaml`:
- Replace `clusterID` with your Ceph cluster ID from step 2
- Ensure the `pool` name matches what you created in step 3
3. **Apply the Ceph CSI driver**:
```bash
kubectl apply -k ceph-csi/
```
4. **Verify the installation**:
```bash
# Check if CSI pods are running
kubectl get pods -n ceph-csi
# Check if StorageClass is created
kubectl get storageclass ceph-rbd
```
## Migrating PostgreSQL to Ceph Storage
1. **Backup existing data** (if any):
```bash
kubectl exec -n aprs deployment/postgis -- pg_dump -U aprs aprs_prod > backup.sql
```
2. **Scale down the current deployment**:
```bash
kubectl scale deployment postgis -n aprs --replicas=0
```
3. **Create the new PVC with Ceph**:
```bash
kubectl apply -f postgis-pvc-ceph.yaml
```
4. **Update the deployment to use Ceph PVC**:
```bash
kubectl apply -f postgis-deployment-ceph.yaml
```
5. **Restore data** (if needed):
```bash
kubectl exec -i -n aprs deployment/postgis -- psql -U aprs aprs_prod < backup.sql
```
## Troubleshooting
If pods fail to mount Ceph volumes:
1. Check CSI driver logs:
```bash
kubectl logs -n ceph-csi deployment/csi-rbdplugin-provisioner
kubectl logs -n ceph-csi daemonset/csi-rbdplugin
```
2. Verify Ceph connectivity from K3s nodes:
```bash
# Install ceph-common on K3s nodes
apt-get install ceph-common
# Test connection
ceph -c /etc/ceph/ceph.conf -n client.kubernetes --keyring /etc/ceph/keyring status
```
3. Check PVC events:
```bash
kubectl describe pvc postgis-pvc-ceph -n aprs
```

View file

@ -1,20 +0,0 @@
#!/bin/bash
# This script creates a Kubernetes secret for pulling images from ghcr.io
echo "Enter your GitHub username:"
read GITHUB_USERNAME
echo "Enter your GitHub Personal Access Token (with read:packages scope):"
read -s GITHUB_TOKEN
# Create the secret
kubectl create secret docker-registry ghcr-pull-secret \
--docker-server=ghcr.io \
--docker-username="$GITHUB_USERNAME" \
--docker-password="$GITHUB_TOKEN" \
--namespace=aprs \
--dry-run=client -o yaml > ghcr-pull-secret.yaml
echo "Secret YAML created in ghcr-pull-secret.yaml"
echo "Apply it with: kubectl apply -f ghcr-pull-secret.yaml"

View file

@ -1,63 +0,0 @@
#!/bin/bash
# Generate Kubernetes secrets from 1Password
# Ensure we're logged in to 1Password
if ! op whoami &>/dev/null; then
echo "Please log in to 1Password first: eval \$(op signin)"
exit 1
fi
# Get PostgreSQL password
POSTGRES_PASSWORD=$(op item get "APRS PostgreSQL Password" --fields password)
# Get APRS application secrets
DATABASE_URL=$(op item get "APRS Application Secrets" --fields database.url)
DATABASE_URL_PGBOUNCER=$(op item get "APRS Application Secrets" --fields database.url.pgbouncer)
DATABASE_PASSWORD=$(op item get "APRS Application Secrets" --fields password)
SECRET_KEY_BASE=$(op item get "APRS Application Secrets" --fields secret.key.base)
ERLANG_COOKIE=$(op item get "APRS Application Secrets" --fields erlang.cookie)
# Get GitHub Container Registry credentials
GHCR_USERNAME=$(op item get "GitHub Container Registry" --fields username)
GHCR_PASSWORD=$(op item get "GitHub Container Registry" --fields password)
GHCR_AUTH=$(echo -n "${GHCR_USERNAME}:${GHCR_PASSWORD}" | base64)
# Create PostgreSQL secret
kubectl create secret generic postgis-secret \
--namespace=aprs \
--from-literal=postgres-password="${POSTGRES_PASSWORD}" \
--dry-run=client -o yaml | kubectl apply -f -
# Create APRS application secret
kubectl create secret generic aprs-secret \
--namespace=aprs \
--from-literal=database-url="${DATABASE_URL}" \
--from-literal=database-url-pgbouncer="${DATABASE_URL_PGBOUNCER}" \
--from-literal=database-password="${DATABASE_PASSWORD}" \
--from-literal=secret-key-base="${SECRET_KEY_BASE}" \
--from-literal=erlang-cookie="${ERLANG_COOKIE}" \
--dry-run=client -o yaml | kubectl apply -f -
# Create Docker registry secret
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: ghcr-pull-secret
namespace: aprs
type: kubernetes.io/dockerconfigjson
stringData:
.dockerconfigjson: |
{
"auths": {
"ghcr.io": {
"username": "${GHCR_USERNAME}",
"password": "${GHCR_PASSWORD}",
"auth": "${GHCR_AUTH}"
}
}
}
EOF
echo "Secrets successfully created/updated from 1Password"

View file

@ -1,37 +0,0 @@
apiVersion: batch/v1
kind: Job
metadata:
name: aprs-migrate
namespace: aprs
spec:
template:
spec:
restartPolicy: Never
imagePullSecrets:
- name: ghcr-pull-secret
containers:
- name: migrate
image: ghcr.io/aprsme/aprs.me:latest
env:
- name: DATABASE_URL
value: "ecto://aprsme:rku23IEO9Xq1lZTl%2BDETeAGRn%2FVxdzXzuidIrHn5Gi4%3D@10.0.19.220:5432/aprsme_prod"
- name: SECRET_KEY_BASE
valueFrom:
secretKeyRef:
name: aprs-secret
key: secret-key-base
- name: MIX_ENV
value: "prod"
- name: POOL_SIZE
value: "2"
- name: PHX_HOST
value: "aprs.me"
- name: PORT
value: "4000"
- name: SKIP_CLUSTERING
value: "true"
- name: CLUSTER_ENABLED
value: "false"
- name: REDIS_URL
value: "redis://redis.aprs.svc.cluster.local:6379"
command: ["/app/bin/migrate"]

View file

@ -1,72 +0,0 @@
#!/bin/bash
set -e
echo "Migrating APRS to external PostgreSQL database"
echo "=============================================="
echo ""
# Test connection to external database first
echo "Testing connection to external PostgreSQL..."
PGPASSWORD="mjBipckSZfNOoSOWLMRsfZEqJ9I5Of21" psql -h 10.0.19.220 -p 6432 -U aprsme -d aprsme_prod -c "SELECT version();" > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "✅ Successfully connected to external database"
else
echo "❌ Failed to connect to external database at 10.0.19.220:6432"
echo "Please ensure the PostgreSQL server is running and accessible"
exit 1
fi
echo ""
echo "Step 1: Updating database connection secret..."
kubectl apply -f aprs-secret-external-db.yaml
echo ""
echo "Step 2: Restarting APRS pods to use new database..."
kubectl rollout restart statefulset/aprs -n aprs
kubectl rollout status statefulset/aprs -n aprs --timeout=300s
echo ""
echo "Step 3: Verifying pods are running with new database..."
sleep 10
kubectl get pods -n aprs -l app=aprs
echo ""
echo "Step 4: Checking application health..."
for i in {1..5}; do
POD=$(kubectl get pods -n aprs -l app=aprs -o jsonpath='{.items[0].metadata.name}')
if kubectl exec -n aprs $POD -- curl -s http://localhost:4000/health > /dev/null; then
echo "✅ Application is healthy"
break
else
echo "Waiting for application to be ready... ($i/5)"
sleep 10
fi
done
echo ""
echo "Step 5: Removing local PostgreSQL resources..."
read -p "Are you sure you want to remove the local PostgreSQL deployment? (y/n): " confirm
if [ "$confirm" = "y" ]; then
echo "Removing PostgreSQL resources..."
kubectl delete deployment postgis -n aprs --ignore-not-found=true
kubectl delete service postgis -n aprs --ignore-not-found=true
kubectl delete deployment pgbouncer -n aprs --ignore-not-found=true
kubectl delete service pgbouncer -n aprs --ignore-not-found=true
kubectl delete cronjob postgres-cleanup -n aprs --ignore-not-found=true
kubectl delete job -l app=postgres-cleanup -n aprs --ignore-not-found=true
echo ""
echo "PostgreSQL PVC will be retained for backup purposes"
echo "To delete it later: kubectl delete pvc postgis-pvc-ceph -n aprs"
fi
echo ""
echo "Migration complete!"
echo ""
echo "Summary:"
echo "- APRS is now using external PostgreSQL at 10.0.19.220"
echo "- Connection is via PgBouncer on port 6432"
echo "- Local PostgreSQL resources have been removed"
echo ""
echo "To verify:"
echo "kubectl logs -n aprs -l app=aprs --tail=50"

View file

@ -1,46 +0,0 @@
#!/bin/bash
echo "=== PostgreSQL Connection Health Check ==="
echo ""
# Check pod status
echo "Pod Status:"
kubectl get pods -n aprs | grep -E "(NAME|postgis|pgbouncer|aprs-)"
echo ""
# Check PostgreSQL connections
echo "PostgreSQL Connection Stats:"
kubectl exec deployment/postgis -n aprs -- psql -U aprs -d aprs_prod -c "
SELECT
state,
COUNT(*) as connections,
ROUND(AVG(EXTRACT(EPOCH FROM (NOW() - state_change))), 2) as avg_duration_seconds
FROM pg_stat_activity
WHERE datname = 'aprs_prod'
GROUP BY state
ORDER BY connections DESC;" 2>/dev/null || echo "Failed to query PostgreSQL"
echo ""
# Check connection percentage
echo "Connection Usage:"
kubectl exec deployment/postgis -n aprs -- psql -U aprs -d aprs_prod -c "
SELECT
(SELECT COUNT(*) FROM pg_stat_activity) as current,
(SELECT setting::int FROM pg_settings WHERE name = 'max_connections') as max,
ROUND(100.0 * COUNT(*) / (SELECT setting::int FROM pg_settings WHERE name = 'max_connections'), 2) || '%' as usage
FROM pg_stat_activity;" 2>/dev/null || echo "Failed to query PostgreSQL"
echo ""
# Check PgBouncer logs for errors
echo "Recent PgBouncer Errors (last 10):"
kubectl logs -n aprs -l app=pgbouncer --tail=50 | grep -i "error\|warning" | tail -10
echo ""
# Check APRS pod logs for connection errors
echo "Recent APRS Connection Errors (last 10):"
kubectl logs -n aprs -l app=aprs --tail=100 | grep -i "postgrex\|connection\|protocol_violation" | tail -10
echo ""
# Show last cleanup job run
echo "Last Cleanup Job Run:"
kubectl get jobs -n aprs | grep postgres-cleanup | tail -1

View file

@ -1,96 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: aprs
labels:
app: redis
spec:
ports:
- port: 6379
targetPort: 6379
name: redis
- port: 9121
targetPort: 9121
name: metrics
selector:
app: redis
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: aprs
labels:
app: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9121"
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
name: redis
command:
- redis-server
- --maxmemory
- "256mb"
- --maxmemory-policy
- allkeys-lru
- --save
- ""
- --appendonly
- "no"
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
tcpSocket:
port: 6379
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 5
periodSeconds: 5
- name: redis-exporter
image: oliver006/redis_exporter:v1.55.0
ports:
- containerPort: 9121
name: metrics
env:
- name: REDIS_ADDR
value: "redis://localhost:6379"
resources:
requests:
memory: 32Mi
cpu: 10m
limits:
memory: 64Mi
cpu: 50m
livenessProbe:
httpGet:
path: /metrics
port: 9121
initialDelaySeconds: 30
periodSeconds: 10

View file

@ -1,32 +0,0 @@
# Setting up GitHub Container Registry Authentication
## Step 1: Create GitHub Personal Access Token
1. Go to https://github.com/settings/tokens/new
2. Name: "k3s-ghcr-pull" (or any name you prefer)
3. Expiration: Choose based on your security requirements
4. Scopes: Select `read:packages`
5. Click "Generate token"
6. Copy the token (starts with `ghp_`)
## Step 2: Create Kubernetes Secret
Replace `YOUR_GITHUB_USERNAME` and `YOUR_GITHUB_PAT` in the command below:
```bash
kubectl create secret docker-registry ghcr-pull-secret \
--docker-server=ghcr.io \
--docker-username=YOUR_GITHUB_USERNAME \
--docker-password=YOUR_GITHUB_PAT \
--namespace=aprs
```
## Step 3: Verify Secret
```bash
kubectl get secret ghcr-pull-secret -n aprs
```
## Step 4: The deployment will be automatically updated to use this secret
The aprs-deployment.yaml already includes the imagePullSecrets configuration.

View file

@ -1,31 +0,0 @@
#!/bin/bash
# Create 1Password items for APRS secrets
# PostgreSQL password
op item create \
--category=password \
--title="APRS PostgreSQL Password" \
--vault="Private" \
password="mjBipckSZfNOoSOWLMRsfZEqJ9I5Of21"
# APRS application secrets
op item create \
--category=login \
--title="APRS Application Secrets" \
--vault="Private" \
username="aprs" \
password="mjBipckSZfNOoSOWLMRsfZEqJ9I5Of21" \
database.url="ecto://aprs:mjBipckSZfNOoSOWLMRsfZEqJ9I5Of21@postgis:5432/aprs" \
database.url.pgbouncer="ecto://aprs:mjBipckSZfNOoSOWLMRsfZEqJ9I5Of21@pgbouncer:5432/aprs" \
secret.key.base="GJxzOUW3YXBAF7xB4mH5VcXY6BN1NYLnoJYadlZyiCxcRUGBw65w4Sco7+sNiW2t" \
erlang.cookie="aprs-cluster-cookie-ksdf8934jfklsdjf983jfklsdf"
# GitHub Container Registry credentials
op item create \
--category=login \
--title="GitHub Container Registry" \
--vault="Private" \
username="gmcintire" \
password="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" \
url="ghcr.io"

View file

@ -1,296 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: connectors.tailscale.com
spec:
group: tailscale.com
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
tags:
type: array
items:
type: string
hostname:
type: string
proxyClass:
type: string
exitNode:
type: boolean
subnetRouter:
type: object
properties:
routes:
type: array
items:
type: string
status:
type: object
properties:
conditions:
type: array
items:
type: object
properties:
type:
type: string
status:
type: string
reason:
type: string
message:
type: string
lastTransitionTime:
type: string
subnetRouter:
type: object
properties:
routes:
type: array
items:
type: string
isExitNode:
type: boolean
subresources:
status: {}
scope: Cluster
names:
plural: connectors
singular: connector
kind: Connector
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: proxyclasses.tailscale.com
spec:
group: tailscale.com
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
metrics:
type: object
properties:
enable:
type: boolean
statefulSet:
type: object
properties:
labels:
type: object
additionalProperties:
type: string
annotations:
type: object
additionalProperties:
type: string
pod:
type: object
properties:
labels:
type: object
additionalProperties:
type: string
annotations:
type: object
additionalProperties:
type: string
tailscaleContainer:
type: object
properties:
env:
type: array
items:
type: object
properties:
name:
type: string
value:
type: string
imagePullPolicy:
type: string
enum: ["Always", "Never", "IfNotPresent"]
resources:
type: object
properties:
requests:
type: object
additionalProperties:
type: string
limits:
type: object
additionalProperties:
type: string
securityContext:
type: object
properties:
privileged:
type: boolean
allowPrivilegeEscalation:
type: boolean
readOnlyRootFilesystem:
type: boolean
runAsNonRoot:
type: boolean
runAsUser:
type: integer
runAsGroup:
type: integer
capabilities:
type: object
properties:
add:
type: array
items:
type: string
drop:
type: array
items:
type: string
tailscaleInitContainer:
type: object
properties:
env:
type: array
items:
type: object
properties:
name:
type: string
value:
type: string
imagePullPolicy:
type: string
enum: ["Always", "Never", "IfNotPresent"]
resources:
type: object
properties:
requests:
type: object
additionalProperties:
type: string
limits:
type: object
additionalProperties:
type: string
securityContext:
type: object
properties:
privileged:
type: boolean
allowPrivilegeEscalation:
type: boolean
readOnlyRootFilesystem:
type: boolean
runAsNonRoot:
type: boolean
runAsUser:
type: integer
runAsGroup:
type: integer
capabilities:
type: object
properties:
add:
type: array
items:
type: string
drop:
type: array
items:
type: string
imagePullSecrets:
type: array
items:
type: object
properties:
name:
type: string
nodeName:
type: string
nodeSelector:
type: object
additionalProperties:
type: string
affinity:
type: object
tolerations:
type: array
items:
type: object
properties:
key:
type: string
operator:
type: string
value:
type: string
effect:
type: string
tolerationSeconds:
type: integer
hostNetwork:
type: boolean
serviceAccountName:
type: string
securityContext:
type: object
properties:
runAsNonRoot:
type: boolean
runAsUser:
type: integer
runAsGroup:
type: integer
fsGroup:
type: integer
service:
type: object
properties:
type:
type: string
enum: ["ClusterIP", "LoadBalancer", "NodePort"]
status:
type: object
properties:
conditions:
type: array
items:
type: object
properties:
type:
type: string
status:
type: string
reason:
type: string
message:
type: string
observedGeneration:
type: integer
lastTransitionTime:
type: string
subresources:
status: {}
scope: Cluster
names:
plural: proxyclasses
singular: proxyclass
kind: ProxyClass

View file

@ -1,29 +0,0 @@
# IMPORTANT: Create your actual secret file as 03-auth-secret.yaml
# Do not commit the actual secret to git!
#
# To create a Tailscale auth key:
# 1. Go to https://login.tailscale.com/admin/settings/keys
# 2. Generate an auth key (reusable, with appropriate expiry)
# 3. Create the secret:
#
# kubectl create secret generic operator-oauth \
# --namespace=tailscale \
# --from-literal=client_id="<your-oauth-client-id>" \
# --from-literal=client_secret="<your-oauth-client-secret>"
#
# OR for auth key:
#
# kubectl create secret generic operator \
# --namespace=tailscale \
# --from-literal=authkey="<your-auth-key>"
#
# Example YAML format:
---
apiVersion: v1
kind: Secret
metadata:
name: operator
namespace: tailscale
type: Opaque
stringData:
authkey: "tskey-auth-YOUR-KEY-HERE"

View file

@ -1,45 +0,0 @@
# Example: Expose a service via Tailscale proxy
# This creates a Tailscale device that forwards traffic to a Kubernetes service
---
apiVersion: v1
kind: Service
metadata:
name: grafana-tailscale
namespace: monitoring
annotations:
tailscale.com/expose: "true"
tailscale.com/hostname: "aprs-grafana"
tailscale.com/tags: "tag:k8s,tag:monitoring"
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: grafana
ports:
- port: 80
targetPort: 3000
protocol: TCP
---
# Alternative: Using Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grafana-tailscale
namespace: monitoring
annotations:
tailscale.com/ingress-class: "tailscale"
spec:
ingressClassName: tailscale
rules:
- host: grafana
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: grafana
port:
number: 80
tls:
- hosts:
- grafana

View file

@ -1,128 +0,0 @@
# Tailscale Operator for K3s
This directory contains the Tailscale operator configuration for the k3s cluster.
## Setup Instructions
1. **Apply the base configuration:**
```bash
kubectl apply -f 00-namespace.yaml
kubectl apply -f 01-rbac.yaml
kubectl apply -f 02-crds.yaml
```
2. **Create the authentication secret:**
Option A - Using OAuth (recommended for production):
```bash
# Get OAuth credentials from https://login.tailscale.com/admin/settings/oauth
kubectl create secret generic operator-oauth \
--namespace=tailscale \
--from-literal=client_id="<your-oauth-client-id>" \
--from-literal=client_secret="<your-oauth-client-secret>"
```
Option B - Using auth key (simpler, good for testing):
```bash
# Get auth key from https://login.tailscale.com/admin/settings/keys
kubectl create secret generic operator \
--namespace=tailscale \
--from-literal=authkey="<your-auth-key>"
```
3. **Deploy the operator:**
```bash
kubectl apply -f 04-operator.yaml
```
4. **Verify the operator is running:**
```bash
kubectl get pods -n tailscale
kubectl logs -n tailscale deployment/operator
```
## Exposing Services
There are several ways to expose services via Tailscale:
### Method 1: Service Annotations
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
annotations:
tailscale.com/expose: "true"
tailscale.com/hostname: "my-app"
tailscale.com/tags: "tag:k8s"
```
### Method 2: Ingress
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
spec:
ingressClassName: tailscale
rules:
- host: my-app
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-service
port:
number: 80
```
### Method 3: Connector (Subnet Router/Exit Node)
```yaml
apiVersion: tailscale.com/v1alpha1
kind: Connector
metadata:
name: subnet-router
spec:
hostname: k3s-subnet
exitNode: false
subnetRouter:
routes:
- "10.0.19.0/24" # Your cluster subnet
tags:
- "tag:k8s"
- "tag:subnet-router"
```
## Accessing the API Server
The operator can expose the Kubernetes API server on your tailnet:
```bash
# This is enabled by default with APISERVER_PROXY=true
# Access via: https://tailscale-operator.tailnet-name.ts.net:443
```
## Troubleshooting
1. Check operator logs:
```bash
kubectl logs -n tailscale deployment/operator
```
2. Check proxy pods:
```bash
kubectl get pods -A | grep ts-
```
3. Verify CRDs are installed:
```bash
kubectl get crds | grep tailscale
```
## Security Notes
- Never commit auth keys or OAuth credentials to git
- Use appropriate ACL tags in your Tailscale admin panel
- Consider using OAuth for production deployments
- Auth keys should be reusable and have appropriate expiry times

View file

@ -1,20 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: grafana-tailscale
namespace: monitoring
annotations:
tailscale.com/expose: "true"
tailscale.com/hostname: "aprs-grafana"
tailscale.com/tags: "tag:k8s"
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: grafana
app.kubernetes.io/part-of: kube-prometheus
ports:
- port: 80
targetPort: 3000
protocol: TCP
name: http

View file

@ -1,76 +0,0 @@
#!/bin/bash
set -e
echo "Tailscale Operator Setup for K3s"
echo "================================"
echo ""
# Check if kubectl is available
if ! command -v kubectl &> /dev/null; then
echo "Error: kubectl is not installed or not in PATH"
exit 1
fi
# Check if we can connect to the cluster
if ! kubectl cluster-info &> /dev/null; then
echo "Error: Cannot connect to Kubernetes cluster"
exit 1
fi
echo "Step 1: Applying base configuration..."
kubectl apply -f 00-namespace.yaml
kubectl apply -f 01-rbac.yaml
kubectl apply -f 02-crds.yaml
echo ""
echo "Step 2: Authentication setup"
echo "You need to create an auth key from: https://login.tailscale.com/admin/settings/keys"
echo ""
echo "Choose one option:"
echo "1) I have an auth key ready"
echo "2) I'll set it up later"
echo ""
read -p "Enter your choice (1 or 2): " choice
if [ "$choice" = "1" ]; then
read -p "Enter your Tailscale auth key: " authkey
kubectl create secret generic operator \
--namespace=tailscale \
--from-literal=authkey="$authkey" \
--dry-run=client -o yaml | kubectl apply -f -
echo "Auth key secret created/updated"
echo ""
echo "Step 3: Deploying operator..."
kubectl apply -f 04-operator-authkey.yaml
echo ""
echo "Waiting for operator to start..."
sleep 5
kubectl wait --for=condition=ready pod -l app=operator -n tailscale --timeout=60s
echo ""
echo "Operator status:"
kubectl get pods -n tailscale
echo ""
echo "Step 4: Would you like to expose Grafana via Tailscale? (y/n)"
read -p "Choice: " expose_grafana
if [ "$expose_grafana" = "y" ]; then
kubectl apply -f grafana-tailscale.yaml
echo "Grafana will be available at: https://aprs-grafana.<your-tailnet-name>.ts.net"
fi
else
echo ""
echo "Skipping auth key setup. To complete the setup later:"
echo "1. Create an auth key at https://login.tailscale.com/admin/settings/keys"
echo "2. Run: kubectl create secret generic operator --namespace=tailscale --from-literal=authkey='<your-key>'"
echo "3. Apply the operator: kubectl apply -f 04-operator-authkey.yaml"
fi
echo ""
echo "Setup complete!"
echo ""
echo "To check the operator status: kubectl logs -n tailscale deployment/operator"
echo "To see Tailscale proxies: kubectl get pods -A | grep ts-"

173
clusters/vntx/DEPLOYMENT.md Normal file
View file

@ -0,0 +1,173 @@
# VNTX Cluster Deployment Guide
This guide walks through deploying the VNTX K3s cluster on Proxmox to consolidate all infrastructure services.
## Prerequisites
- Proxmox cluster with sufficient resources
- OpenTofu/Terraform installed locally
- Ansible installed locally
- SSH access to Proxmox nodes
## Step 1: Deploy Flatcar VMs with OpenTofu
```bash
cd clusters/vntx/terraform
# Copy and edit the example vars
cp terraform.tfvars.example terraform.tfvars
vim terraform.tfvars
# Set Proxmox password
export TF_VAR_proxmox_password="your-proxmox-password"
# Initialize and deploy
tofu init
tofu plan
tofu apply
```
This creates:
- 3 control plane nodes (4 CPU, 8GB RAM, 50GB disk)
- 2 worker nodes (8 CPU, 16GB RAM, 50GB + 200GB disks)
## Step 2: Bootstrap K3s Cluster
```bash
cd ../ansible
# Prepare Flatcar nodes
ansible-playbook -i inventory.yml prepare-flatcar.yml
# Install K3s cluster
ansible-playbook -i inventory.yml k3s-install.yml
# Verify cluster
export KUBECONFIG=./kubeconfig
kubectl get nodes
```
## Step 3: Deploy Core System Services
```bash
cd ..
# Install MetalLB for load balancing
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.12/config/manifests/metallb-native.yaml
kubectl apply -f system/metallb/config.yaml
# Install Longhorn for storage
kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.5.3/deploy/longhorn.yaml
# Install cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.3/cert-manager.yaml
# Create namespaces
kubectl apply -f system/base/namespaces.yaml
```
## Step 4: Deploy Infrastructure Services
### Deploy MariaDB (shared database)
```bash
kubectl apply -f infrastructure/monitoring/mariadb/
# Create databases for services
kubectl exec -it -n monitoring mariadb-0 -- mysql -u root -p
CREATE DATABASE icinga2;
CREATE DATABASE icingaweb2;
CREATE USER 'icinga2'@'%' IDENTIFIED BY 'secure-password';
GRANT ALL ON icinga2.* TO 'icinga2'@'%';
CREATE USER 'icingaweb2'@'%' IDENTIFIED BY 'secure-password';
GRANT ALL ON icingaweb2.* TO 'icingaweb2'@'%';
```
### Deploy Monitoring Stack
```bash
# Create secrets
kubectl create secret generic -n monitoring icinga2-mysql \
--from-literal=password='secure-password'
kubectl create secret generic -n monitoring icingaweb2-mysql \
--from-literal=password='secure-password'
# Deploy Icinga2
kubectl apply -f infrastructure/monitoring/icinga2/
```
### Deploy Network Services
```bash
# Deploy Unbound DNS
kubectl apply -f infrastructure/network-services/unbound/
# Deploy FreeRADIUS
kubectl apply -f infrastructure/network-services/freeradius/
```
## Step 5: Configure Ingress and Access
```bash
# Get MetalLB assigned IPs
kubectl get svc -A | grep LoadBalancer
# Configure DNS entries for services
# icinga.vntx.net -> Icinga2 LoadBalancer IP
# radius.vntx.net -> FreeRADIUS LoadBalancer IP
```
## Service Migration Checklist
### From monitor.vntx.net (Icinga2):
- [ ] Export Icinga2 configuration
- [ ] Export monitoring database
- [ ] Migrate custom check scripts
- [ ] Update monitored hosts to point to new IP
- [ ] Verify all checks working
### From DNS servers:
- [ ] Export zone files
- [ ] Update forwarders configuration
- [ ] Test resolution
- [ ] Update DHCP to use new DNS
### From RADIUS server:
- [ ] Export user database
- [ ] Export clients configuration
- [ ] Test authentication
- [ ] Update network devices to use new RADIUS
## Maintenance
### Cluster Updates
```bash
# Flatcar auto-updates by default
# K3s updates:
curl -sfL https://get.k3s.io | sh -s - upgrade
```
### Backup
```bash
# Install Velero for cluster backup
# Backup to S3-compatible storage
```
### Monitoring
- Access Icinga2 Web: http://icinga.vntx.net
- Kubernetes dashboard: Install via Rancher or Lens
## Troubleshooting
### Check pod logs
```bash
kubectl logs -n monitoring deployment/icinga2
kubectl logs -n network-services deployment/freeradius
```
### Node issues
```bash
# SSH to Flatcar node
ssh core@10.0.0.101
# Check K3s service
sudo systemctl status k3s
sudo journalctl -u k3s -f
```

View file

@ -0,0 +1,98 @@
# Quick Start: Deploy K3s on Proxmox
## 1. Prerequisites
```bash
# Install OpenTofu (or use Terraform)
brew install opentofu
# Install Ansible
pip install ansible
# Get your SSH public key
cat ~/.ssh/id_rsa.pub
```
## 2. Configure for Your Environment
```bash
cd clusters/vntx/terraform
# Edit terraform.tfvars
vim terraform.tfvars
```
**Important settings to update:**
- `proxmox_nodes` - Set to your actual Proxmox node names (check in Proxmox UI)
- `ssh_public_key` - Replace with your actual SSH public key
- `control_plane_ips` and `worker_ips` - Adjust for your network
- `proxmox_storage` - Set to your storage name (usually "local-lvm" or "local")
## 3. Deploy Everything
```bash
cd ../
# Set Proxmox password
export TF_VAR_proxmox_password="your-proxmox-root-password"
# Run the deployment script
./deploy.sh
```
The script will:
1. Create 5 Flatcar Linux VMs on Proxmox
2. Install K3s cluster
3. Configure kubectl access
4. Optionally install MetalLB and Longhorn
## 4. Manual Steps (if not using deploy.sh)
```bash
# Step 1: Deploy VMs
cd terraform
tofu init
tofu plan
tofu apply
# Step 2: Wait for VMs to boot (60 seconds)
# Step 3: Install K3s
cd ../ansible
ansible-playbook -i inventory.yml prepare-flatcar.yml
ansible-playbook -i inventory.yml k3s-install.yml
# Step 4: Get kubeconfig
export KUBECONFIG=./kubeconfig
kubectl get nodes
```
## 5. Access Your Cluster
```bash
# Set kubeconfig
export KUBECONFIG=~/.kube/config-vntx
# Check nodes
kubectl get nodes
# Deploy a test service
kubectl create deployment nginx --image=nginx
kubectl expose deployment nginx --port=80 --type=LoadBalancer
```
## Troubleshooting
### If VMs don't start:
- Check Proxmox has enough resources
- Verify network bridge name in terraform.tfvars
- Check Proxmox storage has space
### If Ansible fails:
- Ensure VMs are accessible: `ping 10.0.0.101`
- Check SSH key is correct
- Wait longer for VMs to fully boot
### If K3s installation fails:
- SSH to node: `ssh core@10.0.0.101`
- Check logs: `sudo journalctl -u k3s -f`

View file

@ -0,0 +1,70 @@
# Deploying K3s on Proxmox with Debian 13
## Prerequisites
1. **Create Debian 13 template on Proxmox:**
```bash
# SSH to your Proxmox host (10.0.0.1 or 10.0.0.2)
ssh root@10.0.0.1
# Run the commands from setup-debian-template.sh
cat setup-debian-template.sh
```
2. **Update Proxmox node names:**
Edit `terraform/terraform.tfvars` and set your actual Proxmox node names
(you can find these in the Proxmox web UI)
3. **Verify your SSH key is set** (already done)
## Deploy the Cluster
```bash
# Set Proxmox password
export TF_VAR_proxmox_password="your-proxmox-password"
# Run deployment
cd clusters/vntx
./deploy.sh
```
## What Gets Deployed
- **5 Debian 13 VMs:**
- 3 control plane nodes (4 CPU, 8GB RAM each)
- 2 worker nodes (8 CPU, 16GB RAM each)
- Worker nodes get extra 200GB disk for container storage
- **K3s Kubernetes cluster** with:
- High availability control plane
- MetalLB for load balancing
- Longhorn for distributed storage
## After Deployment
Your services will be accessible via MetalLB LoadBalancer IPs:
- Icinga2: http://10.0.0.200 (example)
- FreeRADIUS: 10.0.0.201:1812 (example)
- Unbound DNS: Running on all nodes
## Manual Steps Summary
If the script fails, you can run manually:
```bash
# 1. Create VMs
cd terraform
tofu init
tofu apply
# 2. Configure nodes
cd ../ansible
ansible-playbook -i inventory.yml prepare-debian.yml
# 3. Install K3s
ansible-playbook -i inventory.yml k3s-install.yml
# 4. Use cluster
export KUBECONFIG=./kubeconfig
kubectl get nodes
```

36
clusters/vntx/README.md Normal file
View file

@ -0,0 +1,36 @@
# VNTX K3s Cluster
This cluster runs all infrastructure services previously deployed as individual VMs on Proxmox.
## Architecture
- **Platform**: Flatcar Linux on Proxmox VE
- **Kubernetes**: K3s (lightweight Kubernetes)
- **Nodes**: 3 control plane + 2 worker nodes
- **Storage**: Longhorn distributed storage
- **Load Balancer**: MetalLB
## Services
### Infrastructure Services (`infrastructure/`)
- **Monitoring**: Icinga2, Prometheus, Grafana
- **DNS**: Unbound, CoreDNS
- **Authentication**: FreeRADIUS
- **Reverse Proxy**: Caddy
### System Services (`system/`)
- **Storage**: Longhorn
- **Ingress**: Traefik (K3s default)
- **Certificates**: cert-manager
- **Backups**: Velero
### Applications (`apps/`)
- User-facing applications
## Deployment Order
1. Deploy Flatcar VMs via Terraform
2. Bootstrap K3s cluster via Ansible
3. Deploy system services
4. Deploy infrastructure services
5. Migrate data from existing VMs

View file

@ -0,0 +1,31 @@
all:
children:
k3s_cluster:
children:
control_plane:
hosts:
vntx-control-1:
ansible_host: 10.0.0.101
k3s_control_node: true
k3s_primary: true
vntx-control-2:
ansible_host: 10.0.0.102
k3s_control_node: true
vntx-control-3:
ansible_host: 10.0.0.103
k3s_control_node: true
workers:
hosts:
vntx-worker-1:
ansible_host: 10.0.0.111
vntx-worker-2:
ansible_host: 10.0.0.112
vars:
ansible_user: debian
ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
ansible_python_interpreter: /usr/bin/python3
k3s_version: v1.29.0+k3s1
k3s_cluster_domain: cluster.local
k3s_cluster_cidr: 10.42.0.0/16
k3s_service_cidr: 10.43.0.0/16
systemd_dir: /etc/systemd/system

View file

@ -0,0 +1,160 @@
---
- name: Prepare Debian nodes for K3s
hosts: k3s_cluster
become: yes
tasks:
- name: Configure sysctl for K3s
ansible.posix.sysctl:
name: "{{ item }}"
value: '1'
state: present
reload: yes
loop:
- net.bridge.bridge-nf-call-iptables
- net.ipv4.ip_forward
- net.bridge.bridge-nf-call-ip6tables
- name: Load required kernel modules
community.general.modprobe:
name: "{{ item }}"
state: present
loop:
- br_netfilter
- overlay
- name: Ensure kernel modules load on boot
ansible.builtin.copy:
dest: /etc/modules-load.d/k3s.conf
content: |
br_netfilter
overlay
- name: Deploy K3s on primary control node
hosts: control_plane
become: yes
tasks:
- name: Download K3s installer
ansible.builtin.get_url:
url: https://get.k3s.io
dest: /tmp/k3s-install.sh
mode: '0755'
when: k3s_primary | default(false)
- name: Install K3s on primary control node
ansible.builtin.shell: |
INSTALL_K3S_VERSION="{{ k3s_version }}" \
K3S_CLUSTER_INIT=true \
INSTALL_K3S_EXEC="server \
--cluster-cidr={{ k3s_cluster_cidr }} \
--service-cidr={{ k3s_service_cidr }} \
--cluster-domain={{ k3s_cluster_domain }} \
--disable traefik \
--disable servicelb \
--write-kubeconfig-mode=644" \
/tmp/k3s-install.sh
when: k3s_primary | default(false)
register: k3s_primary_install
- name: Get node token
ansible.builtin.slurp:
src: /var/lib/rancher/k3s/server/node-token
register: node_token
when: k3s_primary | default(false)
- name: Set token fact
ansible.builtin.set_fact:
k3s_token: "{{ node_token.content | b64decode | trim }}"
when: k3s_primary | default(false)
- name: Get primary node URL
ansible.builtin.set_fact:
k3s_url: "https://{{ ansible_host }}:6443"
when: k3s_primary | default(false)
- name: Deploy K3s on secondary control nodes
hosts: control_plane:!vntx-control-1
become: yes
tasks:
- name: Download K3s installer
ansible.builtin.get_url:
url: https://get.k3s.io
dest: /tmp/k3s-install.sh
mode: '0755'
- name: Get primary node facts
ansible.builtin.set_fact:
k3s_url: "{{ hostvars['vntx-control-1']['k3s_url'] }}"
k3s_token: "{{ hostvars['vntx-control-1']['k3s_token'] }}"
- name: Install K3s on secondary control nodes
ansible.builtin.shell: |
INSTALL_K3S_VERSION="{{ k3s_version }}" \
K3S_URL="{{ k3s_url }}" \
K3S_TOKEN="{{ k3s_token }}" \
INSTALL_K3S_EXEC="server \
--cluster-cidr={{ k3s_cluster_cidr }} \
--service-cidr={{ k3s_service_cidr }} \
--cluster-domain={{ k3s_cluster_domain }} \
--disable traefik \
--disable servicelb \
--write-kubeconfig-mode=644" \
/tmp/k3s-install.sh
- name: Deploy K3s on worker nodes
hosts: workers
become: yes
tasks:
- name: Download K3s installer
ansible.builtin.get_url:
url: https://get.k3s.io
dest: /tmp/k3s-install.sh
mode: '0755'
- name: Get primary node facts
ansible.builtin.set_fact:
k3s_url: "{{ hostvars['vntx-control-1']['k3s_url'] }}"
k3s_token: "{{ hostvars['vntx-control-1']['k3s_token'] }}"
- name: Install K3s agent on worker nodes
ansible.builtin.shell: |
INSTALL_K3S_VERSION="{{ k3s_version }}" \
K3S_URL="{{ k3s_url }}" \
K3S_TOKEN="{{ k3s_token }}" \
INSTALL_K3S_EXEC="agent" \
/tmp/k3s-install.sh
- name: Configure kubectl access
hosts: vntx-control-1
become: yes
tasks:
- name: Create .kube directory
ansible.builtin.file:
path: /home/debian/.kube
state: directory
owner: debian
group: debian
mode: '0755'
- name: Copy kubeconfig
ansible.builtin.copy:
src: /etc/rancher/k3s/k3s.yaml
dest: /home/debian/.kube/config
owner: debian
group: debian
mode: '0600'
remote_src: yes
- name: Fetch kubeconfig to local machine
ansible.builtin.fetch:
src: /etc/rancher/k3s/k3s.yaml
dest: ./kubeconfig
flat: yes
- name: Display cluster info
ansible.builtin.shell: kubectl get nodes
become_user: debian
register: nodes_output
- name: Show nodes
ansible.builtin.debug:
var: nodes_output.stdout_lines

View file

@ -0,0 +1,19 @@
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTlRZNU1qVTBPRFl3SGhjTk1qVXdPVEF6TVRnMU1USTJXaGNOTXpVd09UQXhNVGcxTVRJMgpXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTlRZNU1qVTBPRFl3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFRWC9KVDdROEV1RktCMWhWUVRHNVVZNXFmVmljcFAybkN5eHE0Tmd1T3AKZCtUSTI4SWgycmVSQ2FsL2d2enJLamxvbTR2ekc5eDJUUnpwMW1LTGJHUlFvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVVAzdG5tcFphN2RIUFluaFBZM2NXCitVNmxnNGd3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUloQVBJWS9jdzBNYXljV3RwNDFDbDZnZEZOY0xBUzNBelAKN21pZWJXcGFWbUNSQWlCaTdaME9mR0ZoeVRoL1BTVG5Qam5weXJzQnprcUlPeEVkU3U2aEdCMnphZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
server: https://127.0.0.1:6443
name: default
contexts:
- context:
cluster: default
user: default
name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrVENDQVRlZ0F3SUJBZ0lJZUwwRzJmMDB3SkF3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOelUyT1RJMU5EZzJNQjRYRFRJMU1Ea3dNekU0TlRFeU5sb1hEVEkyTURrdwpNekU0TlRFeU5sb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJPbGhUT1V3dmVGbjlDTTcKSHZPVk94bVM1TXRsbHM2ZUNOOHN4dTg2NGQzQjBnK0grZnFBelBvbUNWdEp0UmpNdEJhSUltSU9jUWx0NFlqSQpTR1c4a05TalNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCVG9nRjNxOHF5eUllclNuc1Y1dDIxaTQ5MEpCREFLQmdncWhrak9QUVFEQWdOSUFEQkYKQWlFQXFvaDJIR1NYMGlObGo0RWpmVFJXcFBaWFZ1blhUcU5SSmw5UGQxdUc1RWdDSUMvMUk2eGZQZFdnN0d6ZQoyVkl0UjlqNzJFRVpFRTdjUFQwcFRUcExwdld3Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdFkyeHAKWlc1MExXTmhRREUzTlRZNU1qVTBPRFl3SGhjTk1qVXdPVEF6TVRnMU1USTJXaGNOTXpVd09UQXhNVGcxTVRJMgpXakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwWlc1MExXTmhRREUzTlRZNU1qVTBPRFl3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFUc2lmYnZWTEpGYjRHZmpKWHhMZ2gwbndCdW9KdUhUd3NRMWM4R09lQnMKSGtVbGtrOFc0clVVb1IybkMxdVdEeEF5S3N6bE1PRHF1VXVWOXBmT1hRcXFvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVTZJQmQ2dktzc2lIcTBwN0ZlYmR0Cll1UGRDUVF3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUlnU0RuNkh4UU12b2lLNkx3QkdpQW9kRFFQam1CYlUvaWkKQmVCVWFNazRyMXNDSVFEeFpYUTQxWEl3MTRJMWtsdW9wOHNRVVYzTEtWbEtqNTBiaXc5YXpia2xxUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
client-key-data: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU9RdlF2RVpNZ0R3aHVSTUY1NUE4V2J4WUI0VDVQZTFIVW8xVlVnbkVvdDlvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFNldGTTVUQzk0V2YwSXpzZTg1VTdHWkxreTJXV3pwNEkzeXpHN3pyaDNjSFNENGY1K29ETQoraVlKVzBtMUdNeTBGb2dpWWc1eENXM2hpTWhJWmJ5UTFBPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=

View file

@ -0,0 +1,167 @@
---
- name: Prepare Debian 13 nodes for K3s
hosts: k3s_cluster
become: yes
tasks:
- name: Wait for cloud-init to complete
ansible.builtin.wait_for:
path: /var/lib/cloud/instance/boot-finished
timeout: 300
- name: Wait for apt locks to be released
ansible.builtin.shell: |
while fuser /var/lib/apt/lists/lock >/dev/null 2>&1; do
echo "Waiting for apt lock..."
sleep 5
done
changed_when: false
- name: Create systemd-resolved config directory
ansible.builtin.file:
path: /etc/systemd/resolved.conf.d
state: directory
mode: '0755'
- name: Configure DNS for internet access
ansible.builtin.copy:
dest: /etc/systemd/resolved.conf.d/dns.conf
content: |
[Resolve]
DNS=9.9.9.9
FallbackDNS=149.112.112.112
Domains=~.
mode: '0644'
notify: restart systemd-resolved
- name: Restart systemd-resolved immediately
ansible.builtin.systemd:
name: systemd-resolved
state: restarted
daemon_reload: yes
- name: Update apt cache
ansible.builtin.apt:
update_cache: yes
- name: Install required packages
ansible.builtin.apt:
name:
- curl
- apt-transport-https
- ca-certificates
- gnupg
- lsb-release
- iptables
- open-iscsi
- nfs-common
state: present
- name: Load required kernel modules
community.general.modprobe:
name: "{{ item }}"
state: present
loop:
- br_netfilter
- overlay
- name: Configure sysctl for K3s
ansible.posix.sysctl:
name: "{{ item }}"
value: '1'
state: present
reload: yes
loop:
- net.bridge.bridge-nf-call-iptables
- net.ipv4.ip_forward
- net.bridge.bridge-nf-call-ip6tables
- name: Ensure kernel modules load on boot
ansible.builtin.copy:
dest: /etc/modules-load.d/k3s.conf
content: |
br_netfilter
overlay
mode: '0644'
- name: Format and mount additional storage disk (workers only)
when: inventory_hostname in groups['workers']
block:
- name: Find additional storage disk
ansible.builtin.shell: |
# Find the 200G disk which is our storage disk
for disk in /dev/sda /dev/sdb /dev/sdc; do
if [ -b "$disk" ]; then
size=$(lsblk -d -n -o SIZE "$disk" | tr -d ' ')
if [ "$size" = "200G" ]; then
echo "$disk"
exit 0
fi
fi
done
exit 1
register: storage_disk_path
failed_when: storage_disk_path.rc != 0
- name: Check if storage disk has a filesystem
ansible.builtin.command: "blkid {{ storage_disk_path.stdout }}"
register: blkid_result
failed_when: false
changed_when: false
- name: Create filesystem on storage disk
ansible.builtin.filesystem:
fstype: ext4
dev: "{{ storage_disk_path.stdout }}"
force: no
when: blkid_result.rc != 0
- name: Create mount point for container storage
ansible.builtin.file:
path: /var/lib/rancher
state: directory
mode: '0755'
- name: Get UUID of storage disk
ansible.builtin.command: "blkid -s UUID -o value {{ storage_disk_path.stdout }}"
register: storage_uuid
changed_when: false
- name: Mount storage disk by UUID
ansible.posix.mount:
path: /var/lib/rancher
src: "UUID={{ storage_uuid.stdout }}"
fstype: ext4
opts: defaults
state: mounted
- name: Configure systemd-resolved for cluster DNS
ansible.builtin.file:
path: /etc/systemd/resolved.conf.d
state: directory
mode: '0755'
- name: Configure cluster DNS
ansible.builtin.copy:
dest: /etc/systemd/resolved.conf.d/k3s.conf
content: |
[Resolve]
DNS=10.43.0.10
Domains=~cluster.local
mode: '0644'
notify: restart systemd-resolved
- name: Configure container runtime limits
ansible.builtin.copy:
dest: /etc/security/limits.d/k3s.conf
content: |
* soft nofile 65536
* hard nofile 65536
* soft nproc 65536
* hard nproc 65536
mode: '0644'
handlers:
- name: restart systemd-resolved
ansible.builtin.systemd:
name: systemd-resolved
state: restarted

View file

@ -0,0 +1,66 @@
#!/bin/bash
# Script to create K3s VMs directly on Proxmox
# Run this on your Proxmox host (10.0.0.2)
echo "Creating K3s cluster VMs on Proxmox..."
# Your SSH public key
SSH_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHLyVSEEAEbGZZqwPwEup0XrlTpu3x3iF9ExvvQPA4xN graham@mcintire.me"
# Create a temporary file with the SSH key
echo "$SSH_KEY" > /tmp/authorized_keys
# Start with higher VM IDs to avoid conflicts
BASE_ID=130
echo "Using VM IDs starting from $BASE_ID"
# Control plane nodes
for i in 1 2 3; do
VMID=$((BASE_ID + i - 1))
echo "Creating vntx-control-$i (VM $VMID)..."
qm clone 9000 $VMID --name vntx-control-$i
qm set $VMID \
--cores 4 \
--memory 8192 \
--net0 virtio,bridge=vmbr0 \
--ipconfig0 ip=10.0.0.$((100 + i))/24,gw=10.0.0.1 \
--sshkey /tmp/authorized_keys
qm resize $VMID scsi0 50G
qm start $VMID
sleep 2
done
# Worker nodes
for i in 1 2; do
VMID=$((BASE_ID + 3 + i - 1))
echo "Creating vntx-worker-$i (VM $VMID)..."
qm clone 9000 $VMID --name vntx-worker-$i
qm set $VMID \
--cores 8 \
--memory 16384 \
--net0 virtio,bridge=vmbr0 \
--ipconfig0 ip=10.0.0.$((110 + i))/24,gw=10.0.0.1 \
--sshkey /tmp/authorized_keys
qm resize $VMID scsi0 50G
# Add additional disk for storage
qm set $VMID --scsi1 local-lvm:200
qm start $VMID
sleep 2
done
# Clean up
rm -f /tmp/authorized_keys
echo
echo "All VMs created and started!"
echo
echo "VMs created:"
echo " vntx-control-1: 10.0.0.101 (VM ID: 130)"
echo " vntx-control-2: 10.0.0.102 (VM ID: 131)"
echo " vntx-control-3: 10.0.0.103 (VM ID: 132)"
echo " vntx-worker-1: 10.0.0.111 (VM ID: 133)"
echo " vntx-worker-2: 10.0.0.112 (VM ID: 134)"
echo
echo "Wait 60 seconds for cloud-init to complete, then run Ansible"

177
clusters/vntx/deploy.sh Executable file
View file

@ -0,0 +1,177 @@
#!/bin/bash
set -e
echo "=== VNTX K3s Cluster Deployment Script ==="
echo
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Check prerequisites
echo "Checking prerequisites..."
if ! command -v tofu &> /dev/null && ! command -v terraform &> /dev/null; then
echo -e "${RED}ERROR: OpenTofu or Terraform not found${NC}"
echo "Install OpenTofu: https://opentofu.org/docs/intro/install/"
exit 1
fi
if ! command -v ansible &> /dev/null; then
echo -e "${RED}ERROR: Ansible not found${NC}"
echo "Install Ansible: pip install ansible"
exit 1
fi
if ! command -v kubectl &> /dev/null; then
echo -e "${YELLOW}WARNING: kubectl not found${NC}"
echo "Install kubectl to manage the cluster after deployment"
fi
# Use tofu if available, otherwise terraform
TF_CMD="tofu"
if ! command -v tofu &> /dev/null; then
TF_CMD="terraform"
fi
echo -e "${GREEN}Prerequisites OK${NC}"
echo
# Step 1: Configure Terraform
echo "=== Step 1: Configure Terraform ==="
cd terraform
if [ ! -f terraform.tfvars ]; then
echo -e "${RED}ERROR: terraform.tfvars not found${NC}"
echo "Please edit terraform/terraform.tfvars with your settings"
exit 1
fi
# Check for SSH key
if grep -q "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC..." terraform.tfvars; then
echo -e "${RED}ERROR: SSH public key not configured${NC}"
echo "Please add your SSH public key to terraform.tfvars"
echo "You can find it in ~/.ssh/id_rsa.pub or ~/.ssh/id_ed25519.pub"
exit 1
fi
# Verify SSH key is present
if ! grep -E "ssh-(rsa|ed25519|ecdsa)" terraform.tfvars > /dev/null; then
echo -e "${RED}ERROR: No valid SSH key found in terraform.tfvars${NC}"
echo "Please add your SSH public key to terraform.tfvars"
exit 1
fi
# Get Proxmox password
if [ -z "$TF_VAR_proxmox_password" ]; then
echo -n "Enter Proxmox password: "
read -s TF_VAR_proxmox_password
export TF_VAR_proxmox_password
echo
fi
# Initialize Terraform
echo "Initializing Terraform..."
$TF_CMD init
# Plan deployment
echo
echo "Planning infrastructure..."
$TF_CMD plan -out=tfplan
echo
read -p "Deploy infrastructure? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
# Deploy VMs
echo "Deploying Flatcar VMs on Proxmox..."
$TF_CMD apply tfplan
echo -e "${GREEN}Infrastructure deployed!${NC}"
# Step 2: Wait for VMs to be ready
echo
echo "=== Step 2: Waiting for VMs to be ready ==="
cd ../ansible
echo "Waiting 60 seconds for VMs to boot..."
sleep 60
# Test connectivity
echo "Testing connectivity to nodes..."
ansible -i inventory.yml all -m ping --one-line
# Step 3: Prepare Debian nodes
echo
echo "=== Step 3: Preparing Debian nodes ==="
ansible-playbook -i inventory.yml prepare-debian.yml
# Step 4: Install K3s
echo
echo "=== Step 4: Installing K3s cluster ==="
ansible-playbook -i inventory.yml k3s-install.yml
# Step 5: Setup kubeconfig
echo
echo "=== Step 5: Setting up kubectl access ==="
if [ -f kubeconfig ]; then
mkdir -p ~/.kube
cp kubeconfig ~/.kube/config-vntx
export KUBECONFIG=~/.kube/config-vntx
echo "Cluster nodes:"
kubectl get nodes
echo
echo -e "${GREEN}K3s cluster deployed successfully!${NC}"
echo
echo "To use this cluster:"
echo " export KUBECONFIG=~/.kube/config-vntx"
echo " kubectl get nodes"
else
echo -e "${RED}ERROR: kubeconfig not found${NC}"
exit 1
fi
# Step 6: Deploy core services
echo
read -p "Deploy core services (MetalLB, Longhorn)? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
cd ..
echo "Creating namespaces..."
kubectl apply -f system/base/namespaces.yaml
echo "Installing MetalLB..."
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.12/config/manifests/metallb-native.yaml
echo "Waiting for MetalLB to be ready..."
kubectl wait --namespace metallb-system \
--for=condition=ready pod \
--selector=app=metallb \
--timeout=90s
echo "Configuring MetalLB..."
kubectl apply -f system/metallb/config.yaml
echo "Installing Longhorn..."
kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.5.3/deploy/longhorn.yaml
echo -e "${GREEN}Core services deployed!${NC}"
fi
echo
echo "=== Deployment Complete ==="
echo
echo "Cluster endpoints:"
echo " Control plane: https://10.0.0.101:6443"
echo
echo "Next steps:"
echo "1. Deploy your services: kubectl apply -f infrastructure/"
echo "2. Check the deployment guide: cat DEPLOYMENT.md"

View file

@ -0,0 +1,159 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: icinga2-config
namespace: monitoring
data:
icinga2.conf: |
include "constants.conf"
include "zones.conf"
include <itl>
include <plugins>
include <plugins-contrib>
include <manubulon>
include <windows-plugins>
include <nscp>
include "features-enabled/*.conf"
include_recursive "conf.d"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: icinga2-data
namespace: monitoring
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: icinga2
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: icinga2
template:
metadata:
labels:
app: icinga2
spec:
containers:
- name: icinga2
image: icinga/icinga2:2.14.0
ports:
- containerPort: 5665
name: api
env:
- name: ICINGA2_MASTER
value: "1"
- name: ICINGA2_CN
value: "icinga2.monitoring.svc.cluster.local"
- name: MYSQL_HOST
value: "mariadb.monitoring.svc.cluster.local"
- name: MYSQL_DATABASE
value: "icinga2"
- name: MYSQL_USER
value: "icinga2"
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: icinga2-mysql
key: password
volumeMounts:
- name: config
mountPath: /etc/icinga2/icinga2.conf
subPath: icinga2.conf
- name: data
mountPath: /var/lib/icinga2
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
volumes:
- name: config
configMap:
name: icinga2-config
- name: data
persistentVolumeClaim:
claimName: icinga2-data
---
apiVersion: v1
kind: Service
metadata:
name: icinga2
namespace: monitoring
spec:
selector:
app: icinga2
ports:
- port: 5665
targetPort: 5665
name: api
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: icingaweb2
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: icingaweb2
template:
metadata:
labels:
app: icingaweb2
spec:
containers:
- name: icingaweb2
image: icinga/icingaweb2:2.12.1
ports:
- containerPort: 80
name: http
env:
- name: MYSQL_HOST
value: "mariadb.monitoring.svc.cluster.local"
- name: MYSQL_DATABASE
value: "icingaweb2"
- name: MYSQL_USER
value: "icingaweb2"
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: icingaweb2-mysql
key: password
- name: ICINGA2_API_HOST
value: "icinga2.monitoring.svc.cluster.local"
- name: ICINGA2_API_PORT
value: "5665"
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: icingaweb2
namespace: monitoring
spec:
selector:
app: icingaweb2
ports:
- port: 80
targetPort: 80
name: http

View file

@ -0,0 +1,133 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: freeradius-config
namespace: network-services
data:
radiusd.conf: |
prefix = /usr
exec_prefix = /usr
sysconfdir = /etc
localstatedir = /var
sbindir = ${exec_prefix}/sbin
logdir = /var/log/freeradius
raddbdir = /etc/freeradius
radacctdir = ${logdir}/radacct
name = radiusd
confdir = ${raddbdir}
modconfdir = ${confdir}/mods-config
certdir = ${confdir}/certs
cadir = ${confdir}/certs
run_dir = ${localstatedir}/run/${name}
db_dir = ${localstatedir}/lib/${name}
libdir = ${exec_prefix}/lib/freeradius
pidfile = ${run_dir}/${name}.pid
max_request_time = 30
cleanup_delay = 5
max_requests = 1024
listen {
type = auth
ipaddr = *
port = 1812
}
listen {
type = acct
ipaddr = *
port = 1813
}
log {
destination = stdout
auth = yes
auth_badpass = no
auth_goodpass = no
}
$INCLUDE mods-enabled/
$INCLUDE sites-enabled/
---
apiVersion: v1
kind: Secret
metadata:
name: freeradius-secrets
namespace: network-services
type: Opaque
data:
# Base64 encoded secrets
clients.conf: |
Y2xpZW50IGxvY2FsaG9zdCB7CiAgICBpcGFkZHIgPSAxMjcuMC4wLjEKICAgIHNlY3JldCA9IHRl
c3RpbmcxMjMKfQo=
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: freeradius
namespace: network-services
spec:
replicas: 2
selector:
matchLabels:
app: freeradius
template:
metadata:
labels:
app: freeradius
spec:
containers:
- name: freeradius
image: freeradius/freeradius-server:latest
ports:
- containerPort: 1812
protocol: UDP
name: auth
- containerPort: 1813
protocol: UDP
name: acct
volumeMounts:
- name: config
mountPath: /etc/freeradius/radiusd.conf
subPath: radiusd.conf
- name: secrets
mountPath: /etc/freeradius/clients.conf
subPath: clients.conf
- name: data
mountPath: /var/lib/freeradius
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: config
configMap:
name: freeradius-config
- name: secrets
secret:
secretName: freeradius-secrets
- name: data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: freeradius
namespace: network-services
spec:
selector:
app: freeradius
type: LoadBalancer
ports:
- port: 1812
targetPort: 1812
protocol: UDP
name: auth
- port: 1813
targetPort: 1813
protocol: UDP
name: acct

View file

@ -0,0 +1,117 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: unbound-config
namespace: network-services
data:
unbound.conf: |
server:
interface: 0.0.0.0
access-control: 10.0.0.0/8 allow
access-control: 172.16.0.0/12 allow
access-control: 192.168.0.0/16 allow
verbosity: 1
# Performance tuning
num-threads: 4
msg-cache-slabs: 8
rrset-cache-slabs: 8
infra-cache-slabs: 8
key-cache-slabs: 8
# Cache sizes
rrset-cache-size: 256m
msg-cache-size: 128m
# Security
hide-identity: yes
hide-version: yes
# Forward zones
include: /etc/unbound/forward-zones.conf
remote-control:
control-enable: yes
control-interface: 127.0.0.1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: unbound-forward-zones
namespace: network-services
data:
forward-zones.conf: |
forward-zone:
name: "."
forward-addr: 8.8.8.8
forward-addr: 8.8.4.4
forward-addr: 1.1.1.1
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: unbound
namespace: network-services
spec:
selector:
matchLabels:
app: unbound
template:
metadata:
labels:
app: unbound
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
containers:
- name: unbound
image: mvance/unbound:latest
ports:
- containerPort: 53
protocol: UDP
hostPort: 53
- containerPort: 53
protocol: TCP
hostPort: 53
volumeMounts:
- name: config
mountPath: /opt/unbound/etc/unbound/unbound.conf
subPath: unbound.conf
- name: forward-zones
mountPath: /etc/unbound/forward-zones.conf
subPath: forward-zones.conf
securityContext:
capabilities:
add: ["NET_BIND_SERVICE"]
resources:
requests:
memory: "512Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "500m"
volumes:
- name: config
configMap:
name: unbound-config
- name: forward-zones
configMap:
name: unbound-forward-zones
---
apiVersion: v1
kind: Service
metadata:
name: unbound
namespace: network-services
spec:
selector:
app: unbound
clusterIP: None
ports:
- port: 53
protocol: UDP
name: dns-udp
- port: 53
protocol: TCP
name: dns-tcp

View file

@ -0,0 +1,40 @@
#!/bin/bash
# Script to create Debian 13 cloud-init template on Proxmox
echo "This script will create a Debian 13 (Bookworm) cloud-init template on your Proxmox host"
echo "Run this on your Proxmox host (10.0.0.1 or 10.0.0.2)"
echo
echo "Commands to run:"
echo
cat << 'EOF'
# Download Debian 13 cloud image
wget https://cloud.debian.org/images/cloud/bookworm/latest/debian-13-genericcloud-amd64.qcow2
# Create a new VM
qm create 9000 --name debian13-cloud --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0
# Import the downloaded disk to the VM
qm importdisk 9000 debian-13-genericcloud-amd64.qcow2 local-lvm
# Attach the disk to the VM
qm set 9000 --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-9000-disk-0
# Set boot disk
qm set 9000 --boot c --bootdisk scsi0
# Add cloud-init drive
qm set 9000 --ide2 local-lvm:cloudinit
# Configure serial console
qm set 9000 --serial0 socket --vga serial0
# Enable QEMU agent
qm set 9000 --agent enabled=1
# Convert to template
qm template 9000
echo "Template created! VM ID 9000 named 'debian13-cloud'"
EOF

View file

@ -0,0 +1,40 @@
#!/bin/bash
# Script to create Ubuntu cloud-init template on Proxmox
echo "This script will create an Ubuntu 22.04 cloud-init template on your Proxmox host"
echo "Run this on your Proxmox host (10.0.0.1)"
echo
echo "Commands to run:"
echo
cat << 'EOF'
# Download Ubuntu 22.04 cloud image
wget https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img
# Create a new VM
qm create 9000 --name ubuntu-cloud --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0
# Import the downloaded disk to the VM
qm importdisk 9000 jammy-server-cloudimg-amd64.img local-lvm
# Attach the disk to the VM
qm set 9000 --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-9000-disk-0
# Set boot disk
qm set 9000 --boot c --bootdisk scsi0
# Add cloud-init drive
qm set 9000 --ide2 local-lvm:cloudinit
# Configure serial console
qm set 9000 --serial0 socket --vga serial0
# Enable QEMU agent
qm set 9000 --agent enabled=1
# Convert to template
qm template 9000
echo "Template created! VM ID 9000 named 'ubuntu-cloud'"
EOF

View file

@ -0,0 +1,29 @@
apiVersion: v1
kind: Namespace
metadata:
name: metallb-system
---
apiVersion: v1
kind: Namespace
metadata:
name: longhorn-system
---
apiVersion: v1
kind: Namespace
metadata:
name: cert-manager
---
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
---
apiVersion: v1
kind: Namespace
metadata:
name: network-services
---
apiVersion: v1
kind: Namespace
metadata:
name: ingress-system

View file

@ -0,0 +1,17 @@
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default-pool
namespace: metallb-system
spec:
addresses:
- 10.0.0.200-10.0.0.250 # Adjust based on your network
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default-advertisement
namespace: metallb-system
spec:
ipAddressPools:
- default-pool

View file

@ -0,0 +1 @@
{"version":4,"terraform_version":"1.10.5","serial":1,"lineage":"22c48096-8818-3e9f-61ec-3007015f4917","outputs":{},"resources":[],"check_results":null}

View file

@ -0,0 +1,24 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/telmate/proxmox" {
version = "2.9.14"
constraints = "~> 2.9"
hashes = [
"h1:asZa5VKbWeCpLNv1JAutt5CdD27HaGFjxxcr6mvn8Ps=",
"zh:0d049d33f705e5b814d30028770c084151218439424e99684ce31d7e26a720b5",
"zh:20b1c64ed56d81de95f3f37b82b45b4654c0de26670c0e87a474c5cce13cd015",
"zh:2946058abd1d8e50e475b9ec39781eb02576b40dbd80f4653fade4493a4514c6",
"zh:29e50a25c456f040ce072f23ac57b5b82ebd3b916ca5ae6688332b5ec62adc4a",
"zh:3612932306ce5f08db94868f526cbb8c56d0d3c6ebe1c11a83f92bbf94354296",
"zh:42d1699b0abebaac82ea5a19f4393541d8bb2741bde204a8ac1028cdc29d1b14",
"zh:5ffd5dc567262eb8aafdf2f6eac63f7f21361da9c5d75a3c36b479638a0001b0",
"zh:6692ef323e3b89de99934ad731f6a1850525bf8142916ae28ea4e4048d73a787",
"zh:a5afc98e9a4038516bb58e788cb77dea67a60dce780dfcd206d7373c5a56b776",
"zh:bf902cded709d84fa27fbf91b589c241f2238a6c4924e4e479eebd74320b93a5",
"zh:cab0e1e72c9cebcf669fc6f35ec28cb8ab2dffb0237afc8860aa40d23bf8a49f",
"zh:e523b99a48beec83d9bc04b2d336266044f9f53514cefb652fe6768611847196",
"zh:f593915e8a24829d322d2eaeedcb153328cf9042f0d84f66040dde1be70ede04",
"zh:fba1aff541133e2129dfda0160369635ab48503d5c44b8407ce5922ecc15d0bd",
]
}

View file

@ -0,0 +1,108 @@
terraform {
required_providers {
proxmox = {
source = "Telmate/proxmox"
version = "2.9.11"
}
}
}
provider "proxmox" {
pm_api_url = var.proxmox_api_url
pm_user = var.proxmox_user
pm_password = var.proxmox_password
pm_tls_insecure = var.proxmox_tls_insecure
}
# Control plane nodes
resource "proxmox_vm_qemu" "k3s_control" {
count = var.control_plane_count
name = "vntx-control-${count.index + 1}"
target_node = var.proxmox_nodes[count.index % length(var.proxmox_nodes)]
# Use Debian 13 cloud image
clone = var.debian_template_name
cores = var.control_plane_specs.cores
memory = var.control_plane_specs.memory
boot = "order=scsi0"
agent = 1
disk {
size = var.control_plane_specs.disk_size
storage = var.proxmox_storage
type = "scsi"
format = "qcow2"
}
network {
model = "virtio"
bridge = var.network_bridge
}
# Cloud-init
os_type = "cloud-init"
ipconfig0 = "ip=${var.control_plane_ips[count.index]}/24,gw=${var.gateway}"
sshkeys = var.ssh_public_key
lifecycle {
ignore_changes = [
network,
disk,
]
}
}
# Worker nodes
resource "proxmox_vm_qemu" "k3s_worker" {
count = var.worker_count
name = "vntx-worker-${count.index + 1}"
target_node = var.proxmox_nodes[count.index % length(var.proxmox_nodes)]
# Use Debian 13 cloud image
clone = var.debian_template_name
cores = var.worker_specs.cores
memory = var.worker_specs.memory
boot = "order=scsi0"
agent = 1
disk {
size = var.worker_specs.disk_size
storage = var.proxmox_storage
type = "scsi"
format = "qcow2"
}
# Additional disk for container storage
disk {
size = var.worker_specs.storage_disk_size
storage = var.proxmox_storage
type = "scsi"
format = "qcow2"
slot = 1
}
network {
model = "virtio"
bridge = var.network_bridge
}
# Cloud-init
os_type = "cloud-init"
ipconfig0 = "ip=${var.worker_ips[count.index]}/24,gw=${var.gateway}"
sshkeys = var.ssh_public_key
lifecycle {
ignore_changes = [
network,
disk,
]
}
}

View file

@ -0,0 +1,27 @@
output "control_plane_ips" {
value = proxmox_vm_qemu.k3s_control[*].default_ipv4_address
description = "IP addresses of control plane nodes"
}
output "worker_ips" {
value = proxmox_vm_qemu.k3s_worker[*].default_ipv4_address
description = "IP addresses of worker nodes"
}
output "cluster_config" {
value = {
control_nodes = [
for i, vm in proxmox_vm_qemu.k3s_control : {
name = vm.name
ip = var.control_plane_ips[i]
}
]
worker_nodes = [
for i, vm in proxmox_vm_qemu.k3s_worker : {
name = vm.name
ip = var.worker_ips[i]
}
]
}
description = "Cluster configuration for Ansible"
}

View file

@ -0,0 +1 @@
{"version":4,"terraform_version":"1.10.5","serial":6,"lineage":"dec2afcf-a221-0f9b-d460-39a11719c258","outputs":{"cluster_config":{"value":{"control_nodes":[{"ip":"10.0.0.101","name":"vntx-control-1"},{"ip":"10.0.0.102","name":"vntx-control-2"},{"ip":"10.0.0.103","name":"vntx-control-3"}],"worker_nodes":[{"ip":"10.0.0.111","name":"vntx-worker-1"},{"ip":"10.0.0.112","name":"vntx-worker-2"}]},"type":["object",{"control_nodes":["tuple",[["object",{"ip":"string","name":"string"}],["object",{"ip":"string","name":"string"}],["object",{"ip":"string","name":"string"}]]],"worker_nodes":["tuple",[["object",{"ip":"string","name":"string"}],["object",{"ip":"string","name":"string"}]]]}]}},"resources":[],"check_results":null}

View file

@ -0,0 +1 @@
{"version":4,"terraform_version":"1.10.5","serial":5,"lineage":"dec2afcf-a221-0f9b-d460-39a11719c258","outputs":{"cluster_config":{"value":{"control_nodes":[{"ip":"10.0.0.101","name":"vntx-control-1"},{"ip":"10.0.0.102","name":"vntx-control-2"},{"ip":"10.0.0.103","name":"vntx-control-3"}],"worker_nodes":[{"ip":"10.0.0.111","name":"vntx-worker-1"},{"ip":"10.0.0.112","name":"vntx-worker-2"}]},"type":["object",{"control_nodes":["tuple",[["object",{"ip":"string","name":"string"}],["object",{"ip":"string","name":"string"}],["object",{"ip":"string","name":"string"}]]],"worker_nodes":["tuple",[["object",{"ip":"string","name":"string"}],["object",{"ip":"string","name":"string"}]]]}]}},"resources":[{"mode":"managed","type":"proxmox_vm_qemu","name":"k3s_worker","provider":"provider[\"registry.opentofu.org/telmate/proxmox\"]","instances":[]}],"check_results":null}

View file

@ -0,0 +1,38 @@
# Proxmox connection
proxmox_api_url = "https://10.0.0.1:8006/api2/json"
proxmox_host = "10.0.0.1"
proxmox_user = "root@pam"
# proxmox_password = "set via TF_VAR_proxmox_password environment variable"
# Proxmox cluster nodes - deploying all on vm2-380
proxmox_nodes = ["vm2-380"]
# Debian template name (you'll need to create this)
debian_template_name = "debian13-cloud"
# Storage
proxmox_storage = "local-lvm" # Adjust to your storage pool name
# Network configuration
network_bridge = "vmbr0"
vlan_tag = -1 # Set to VLAN ID if needed, -1 for no VLAN
gateway = "10.0.0.1"
# Reduce node count for 2-host setup
control_plane_count = 3 # Can run on 2 hosts
worker_count = 2
# Node IPs - adjust for your network
control_plane_ips = [
"10.0.0.101",
"10.0.0.102",
"10.0.0.103"
]
worker_ips = [
"10.0.0.111",
"10.0.0.112"
]
# SSH key for node access
ssh_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHLyVSEEAEbGZZqwPwEup0XrlTpu3x3iF9ExvvQPA4xN graham@mcintire.me"

View file

@ -0,0 +1,31 @@
# Proxmox connection
proxmox_api_url = "https://pve1.vntx.net:8006/api2/json"
proxmox_host = "pve1.vntx.net"
proxmox_user = "root@pam"
# proxmox_password = "set via TF_VAR_proxmox_password"
# Proxmox cluster nodes
proxmox_nodes = ["pve1", "pve2", "pve3"]
# Storage
proxmox_storage = "local-lvm"
# Network configuration
network_bridge = "vmbr0"
vlan_tag = -1 # Set to VLAN ID if needed, -1 for no VLAN
gateway = "10.0.0.1"
# Node IPs
control_plane_ips = [
"10.0.0.101",
"10.0.0.102",
"10.0.0.103"
]
worker_ips = [
"10.0.0.111",
"10.0.0.112"
]
# SSH key for node access
ssh_public_key = "ssh-rsa AAAAB3..."

Binary file not shown.

View file

@ -0,0 +1,129 @@
variable "proxmox_api_url" {
description = "Proxmox API URL"
type = string
}
variable "proxmox_user" {
description = "Proxmox user"
type = string
default = "root@pam"
}
variable "proxmox_password" {
description = "Proxmox password"
type = string
sensitive = true
}
variable "proxmox_tls_insecure" {
description = "Allow insecure TLS for Proxmox API"
type = bool
default = true
}
variable "proxmox_host" {
description = "Proxmox host for SSH commands"
type = string
}
variable "proxmox_nodes" {
description = "List of Proxmox nodes to distribute VMs across"
type = list(string)
default = ["pve1", "pve2", "pve3"]
}
variable "proxmox_storage" {
description = "Proxmox storage pool"
type = string
default = "local-lvm"
}
variable "debian_template_name" {
description = "Name of Debian cloud-init template VM"
type = string
default = "debian13-cloud"
}
variable "network_bridge" {
description = "Network bridge"
type = string
default = "vmbr0"
}
variable "vlan_tag" {
description = "VLAN tag for cluster network"
type = number
default = -1
}
variable "gateway" {
description = "Network gateway"
type = string
default = "10.0.0.1"
}
variable "control_plane_count" {
description = "Number of control plane nodes"
type = number
default = 3
}
variable "worker_count" {
description = "Number of worker nodes"
type = number
default = 2
}
variable "control_plane_specs" {
description = "Specifications for control plane nodes"
type = object({
cores = number
memory = number
disk_size = string
})
default = {
cores = 4
memory = 8192
disk_size = "50G"
}
}
variable "worker_specs" {
description = "Specifications for worker nodes"
type = object({
cores = number
memory = number
disk_size = string
storage_disk_size = string
})
default = {
cores = 8
memory = 16384
disk_size = "50G"
storage_disk_size = "200G"
}
}
variable "control_plane_ips" {
description = "Static IPs for control plane nodes"
type = list(string)
default = [
"10.0.0.101",
"10.0.0.102",
"10.0.0.103"
]
}
variable "worker_ips" {
description = "Static IPs for worker nodes"
type = list(string)
default = [
"10.0.0.111",
"10.0.0.112"
]
}
variable "ssh_public_key" {
description = "SSH public key for node access"
type = string
}

View file

@ -0,0 +1,162 @@
---
- name: Configure Caddy reverse proxy with Tailscale on Debian 13
hosts: vultr
become: yes
vars:
tailscale_key: "{{ lookup('env', 'TAILSCALE_KEY') }}"
tasks:
- name: Wait for cloud-init to complete
wait_for:
path: /var/lib/cloud/instance/boot-finished
timeout: 300
- name: Check cloud-init status
command: cloud-init status --wait
register: cloud_init_status
changed_when: false
failed_when: false
- name: Show cloud-init status
debug:
var: cloud_init_status.stdout
- name: Check if Caddy is installed
command: which caddy
register: caddy_installed
changed_when: false
failed_when: false
- name: Install Caddy if not present
apt:
name: caddy
state: present
update_cache: yes
when: caddy_installed.rc != 0
- name: Verify Caddy is installed and running
service:
name: caddy
state: started
enabled: yes
- name: Check if Tailscale is installed
command: which tailscale
register: tailscale_check
changed_when: false
failed_when: false
- name: Install Tailscale on Debian
block:
- name: Install Tailscale repository key
get_url:
url: https://pkgs.tailscale.com/stable/debian/bookworm.noarmor.gpg
dest: /usr/share/keyrings/tailscale-archive-keyring.gpg
- name: Add Tailscale repository
apt_repository:
repo: "deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/debian bookworm main"
state: present
filename: tailscale
- name: Update apt cache
apt:
update_cache: yes
- name: Install Tailscale
apt:
name: tailscale
state: present
- name: Enable and start Tailscale daemon
service:
name: tailscaled
state: started
enabled: yes
when:
- ansible_os_family == "Debian"
- tailscale_check.rc != 0
- name: Verify Tailscale is installed
command: tailscale version
register: tailscale_version
changed_when: false
- name: Show Tailscale version
debug:
var: tailscale_version.stdout
- name: Check if Tailscale is already authenticated
command: tailscale status --json
register: tailscale_status_check
changed_when: false
failed_when: false
- name: Restart tailscaled service
service:
name: tailscaled
state: restarted
register: restart_result
- name: Wait for tailscaled to start
pause:
seconds: 5
when: restart_result is changed
- name: Try to authenticate with explicit socket path
command: tailscale --socket=/var/run/tailscale/tailscaled.sock up --authkey="{{ tailscale_key }}" --accept-routes --hostname=caddy
when:
- tailscale_key is defined
- tailscale_key != ""
environment:
TAILSCALE_SOCKET: /var/run/tailscale/tailscaled.sock
register: tailscale_auth
failed_when: false
ignore_errors: yes
- name: Debug authentication result
debug:
msg: |
RC: {{ tailscale_auth.rc | default('N/A') }}
STDOUT: {{ tailscale_auth.stdout | default('') }}
STDERR: {{ tailscale_auth.stderr | default('') }}
when: tailscale_auth is defined and tailscale_auth is not skipped
- name: Get Tailscale status
command: tailscale --socket=/var/run/tailscale/tailscaled.sock status
register: tailscale_final_status
changed_when: false
failed_when: false
- name: Show final Tailscale status
debug:
var: tailscale_final_status.stdout_lines
when: tailscale_final_status is defined and tailscale_final_status.stdout_lines is defined
- name: Show Tailscale setup issue
debug:
msg: |
WARNING: Tailscale authentication failed.
You may need to manually authenticate Tailscale on this host.
To debug further, SSH to the host and try:
tailscale up --authkey=YOUR_KEY
when: tailscale_auth is defined and tailscale_auth is not skipped and tailscale_auth.rc != 0
- name: Update Caddyfile with any custom configurations
template:
src: Caddyfile.j2
dest: /etc/caddy/Caddyfile
owner: caddy
group: caddy
mode: "0644"
backup: yes
notify: reload caddy
when: false # Disable for now - can be enabled when you have custom configs
handlers:
- name: reload caddy
service:
name: caddy
state: reloaded

View file

@ -0,0 +1,40 @@
---
- name: Check Ceph status on Proxmox
hosts: proxmox[0]
become: true
tasks:
- name: Check Ceph status
command: ceph status
register: ceph_status
changed_when: false
- name: Display Ceph status
debug:
var: ceph_status.stdout_lines
- name: List Ceph pools
command: ceph osd pool ls
register: ceph_pools
changed_when: false
- name: Display Ceph pools
debug:
msg: "Available pools: {{ ceph_pools.stdout_lines }}"
- name: List Ceph auth users
command: ceph auth ls
register: ceph_users
changed_when: false
- name: Check if kubernetes user exists
debug:
msg: "Kubernetes user exists: {{ 'client.kubernetes' in ceph_users.stdout }}"
- name: Get Ceph config
command: ceph config generate-minimal-conf
register: ceph_conf
changed_when: false
- name: Display Ceph config
debug:
var: ceph_conf.stdout_lines

View file

@ -0,0 +1,29 @@
---
- name: Check PostgreSQL configuration
hosts: proxmox
become: yes
gather_facts: no
vars:
ct_id: 300
tasks:
- name: Find PostgreSQL version and config location
shell: |
pct exec {{ ct_id }} -- bash -c "
# Check if PostgreSQL is installed
which psql || echo 'psql not found'
# Find PostgreSQL version
dpkg -l | grep postgresql-[0-9] | grep -v client || echo 'PostgreSQL package not found'
# Find pg_hba.conf location
find /etc -name pg_hba.conf 2>/dev/null || echo 'pg_hba.conf not found'
# Check if PostgreSQL is running
systemctl status postgresql --no-pager || echo 'PostgreSQL service not found'
"
register: postgres_info
- name: Display PostgreSQL information
debug:
msg: "{{ postgres_info.stdout_lines }}"

View file

@ -0,0 +1,10 @@
---
- name: Create Ceph keyfile for mounting
hosts: proxmox[0]
become: true
tasks:
- name: Create kubernetes key file
copy:
content: "AQAq14Jo/g4ZORAAwxpPOmqj1PYgmih6s6b4vA=="
dest: /etc/ceph/client.kubernetes.key
mode: '0600'

View file

@ -0,0 +1,56 @@
---
- name: Setup CephFS for Kubernetes shared storage
hosts: proxmox[0]
become: true
vars:
cephfs_name: kubernetes-shared
tasks:
- name: Check if CephFS already exists
command: ceph fs ls
register: cephfs_list
changed_when: false
- name: Display existing CephFS
debug:
var: cephfs_list.stdout_lines
- name: Create CephFS data pool if needed
command: ceph osd pool create cephfs_kubernetes_data 32
register: data_pool
failed_when:
- data_pool.rc != 0
- "'already exists' not in data_pool.stderr"
changed_when: data_pool.rc == 0
- name: Create CephFS metadata pool if needed
command: ceph osd pool create cephfs_kubernetes_metadata 16
register: metadata_pool
failed_when:
- metadata_pool.rc != 0
- "'already exists' not in metadata_pool.stderr"
changed_when: metadata_pool.rc == 0
- name: Create CephFS if needed
command: ceph fs new {{ cephfs_name }} cephfs_kubernetes_metadata cephfs_kubernetes_data
register: cephfs_create
failed_when:
- cephfs_create.rc != 0
- "'already exists' not in cephfs_create.stderr"
changed_when: cephfs_create.rc == 0
- name: Get CephFS status
command: ceph fs status {{ cephfs_name }}
register: cephfs_status
changed_when: false
failed_when: false
- name: Display CephFS status
debug:
var: cephfs_status.stdout_lines
when: cephfs_status.rc == 0
- name: Ensure kubernetes user has CephFS permissions
command: |
ceph auth caps client.kubernetes mon 'allow r' mds 'allow rw' osd 'allow rw pool=cephfs_kubernetes_data, allow rw pool=cephfs_kubernetes_metadata, allow rw pool=kubernetes'
changed_when: false

View file

@ -0,0 +1,65 @@
---
- name: Create Debian 13 Cloud-Init Template on Proxmox
hosts: lab02 # Run on first Proxmox host
gather_facts: no
vars:
template_vmid: 9000
template_name: "debian-13-cloudinit"
storage: "ceph"
tasks:
- name: Check if template already exists
command: qm status {{ template_vmid }}
register: vm_check
failed_when: false
changed_when: false
- name: Template creation block
when: vm_check.rc != 0
block:
- name: Download Debian 13 cloud image
get_url:
url: https://cloud.debian.org/images/cloud/trixie/latest/debian-13-generic-amd64.qcow2
dest: /var/lib/vz/template/iso/debian-13-generic-amd64.qcow2
mode: '0644'
- name: Create VM for template
command: qm create {{ template_vmid }} --name {{ template_name }} --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0 --scsihw virtio-scsi-single
- name: Import disk image
command: qm importdisk {{ template_vmid }} /var/lib/vz/template/iso/debian-13-generic-amd64.qcow2 {{ storage }}
register: import_result
- name: Get imported disk name
set_fact:
imported_disk: "{{ storage }}:vm-{{ template_vmid }}-disk-0"
- name: Attach imported disk
command: qm set {{ template_vmid }} --scsi0 {{ imported_disk }}
- name: Add cloud-init drive
command: qm set {{ template_vmid }} --ide2 {{ storage }}:cloudinit
- name: Configure boot and display settings
command: qm set {{ template_vmid }} --boot c --bootdisk scsi0 --serial0 socket --vga serial0
- name: Enable QEMU guest agent
command: qm set {{ template_vmid }} --agent enabled=1
- name: Set cloud-init defaults
command: qm set {{ template_vmid }} --ipconfig0 ip=dhcp --ciuser debian
- name: Resize disk to 20G
command: qm resize {{ template_vmid }} scsi0 20G
- name: Convert to template
command: qm template {{ template_vmid }}
- name: Clean up downloaded image
file:
path: /var/lib/vz/template/iso/debian-13-generic-amd64.qcow2
state: absent
- name: Template ready
debug:
msg: "Template '{{ template_name }}' (ID: {{ template_vmid }}) is ready on {{ inventory_hostname }}"

View file

@ -0,0 +1,10 @@
---
- name: Create Forgejo storage directory
hosts: proxmox[0]
become: true
tasks:
- name: Create Forgejo directory on CephFS
file:
path: /mnt/cephfs-kubernetes/volumes/forgejo
state: directory
mode: '0777'

View file

@ -0,0 +1,82 @@
---
- name: Create K3s VMs on Proxmox
hosts: lab02
gather_facts: no
vars:
template_vmid: 9000
cluster_name: "home-k3s"
network_gateway: "10.0.19.254"
ssh_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHLyVSEEAEbGZZqwPwEup0XrlTpu3x3iF9ExvvQPA4xN graham@mcintire.me"
control_nodes:
- { vmid: 1100, name: "{{ cluster_name }}-control-1", ip: "10.0.19.100/22", cores: 2, memory: 8192, disk: "32G", node: "lab02" }
- { vmid: 1101, name: "{{ cluster_name }}-control-2", ip: "10.0.19.101/22", cores: 2, memory: 8192, disk: "32G", node: "lab03" }
- { vmid: 1102, name: "{{ cluster_name }}-control-3", ip: "10.0.19.102/22", cores: 2, memory: 8192, disk: "32G", node: "lab04" }
worker_nodes:
- { vmid: 1200, name: "{{ cluster_name }}-worker-1", ip: "10.0.19.110/22", cores: 4, memory: 8192, disk: "64G", node: "lab02" }
- { vmid: 1201, name: "{{ cluster_name }}-worker-2", ip: "10.0.19.111/22", cores: 4, memory: 8192, disk: "64G", node: "lab03" }
- { vmid: 1202, name: "{{ cluster_name }}-worker-3", ip: "10.0.19.112/22", cores: 4, memory: 8192, disk: "64G", node: "lab04" }
all_nodes: "{{ control_nodes + worker_nodes }}"
tasks:
- name: Check which VMs already exist
command: qm status {{ item.vmid }}
loop: "{{ all_nodes }}"
register: vm_checks
failed_when: false
changed_when: false
- name: Create VMs that don't exist
command: |
qm clone {{ template_vmid }} {{ item.item.vmid }} \
--name {{ item.item.name }} \
--full true
loop: "{{ vm_checks.results }}"
when: item.rc != 0
loop_control:
label: "{{ item.item.name }}"
- name: Configure VMs
command: |
qm set {{ item.vmid }} \
--cores {{ item.cores }} \
--memory {{ item.memory }} \
--ipconfig0 ip={{ item.ip }},gw={{ network_gateway }} \
--sshkeys /root/.ssh/authorized_keys \
--onboot 1
loop: "{{ all_nodes }}"
loop_control:
label: "{{ item.name }}"
- name: Resize disks
command: qm resize {{ item.vmid }} scsi0 {{ item.disk }}
loop: "{{ all_nodes }}"
loop_control:
label: "{{ item.name }}"
register: resize_result
failed_when:
- resize_result.rc != 0
- "'shrinking disks is not supported' not in resize_result.stderr"
- name: Start VMs
command: qm start {{ item.vmid }}
loop: "{{ all_nodes }}"
loop_control:
label: "{{ item.name }}"
register: start_result
failed_when:
- start_result.rc != 0
- "'already running' not in start_result.stderr"
- name: Migrate VMs to their target nodes
command: qm migrate {{ item.vmid }} {{ item.node }} --online
loop: "{{ all_nodes }}"
when: item.node != 'lab02'
loop_control:
label: "{{ item.name }} -> {{ item.node }}"
- name: VM creation complete
debug:
msg: "All K3s VMs have been created and distributed across the cluster"

View file

@ -0,0 +1,63 @@
---
- name: Create PostgreSQL LXC Container on Proxmox
hosts: proxmox[0]
become: true
vars:
ct_id: 300
ct_name: postgres
ct_template: "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
ct_memory: 8192 # 8GB RAM
ct_disk_size: 1024 # 1TB storage
ct_cores: 4
ct_ip: "10.0.19.5/22"
ct_gateway: "10.0.19.254"
ct_storage: "ceph" # Use Ceph storage
tasks:
- name: Check if container already exists
command: pct status {{ ct_id }}
register: ct_exists
failed_when: false
changed_when: false
- name: Create PostgreSQL LXC container
command: |
pct create {{ ct_id }} {{ ct_template }} \
--hostname {{ ct_name }} \
--memory {{ ct_memory }} \
--cores {{ ct_cores }} \
--net0 name=eth0,bridge=vmbr0,ip={{ ct_ip }},gw={{ ct_gateway }} \
--storage {{ ct_storage }} \
--rootfs {{ ct_storage }}:{{ ct_disk_size }} \
--unprivileged 1 \
--features nesting=1 \
--onboot 1 \
--startup order=10
when: ct_exists.rc != 0
- name: Start container
command: pct start {{ ct_id }}
when: ct_exists.rc != 0
- name: Wait for container to be ready
wait_for:
host: 10.0.19.5
port: 22
timeout: 60
delay: 10
when: ct_exists.rc != 0
- name: Update container packages
shell: pct exec {{ ct_id }} -- apt update && pct exec {{ ct_id }} -- apt upgrade -y
when: ct_exists.rc != 0
- name: Display container info
debug:
msg: |
PostgreSQL LXC Container created:
- ID: {{ ct_id }}
- Name: {{ ct_name }}
- IP: 10.0.19.5
- RAM: {{ ct_memory }}MB
- Storage: {{ ct_disk_size }}GB
- Cores: {{ ct_cores }}

View file

@ -0,0 +1,12 @@
---
- name: Create PostgreSQL storage directory
hosts: proxmox[0]
become: true
tasks:
- name: Create PostgreSQL directory on CephFS
file:
path: /mnt/cephfs-kubernetes/volumes/postgresql
state: directory
mode: '0700'
owner: '999' # PostgreSQL container user
group: '999'

View file

@ -0,0 +1,44 @@
---
- name: Create RBD volumes on Proxmox Ceph
hosts: proxmox[0]
become: true
vars:
volumes:
- name: adguard-config
size: 2G
- name: adguard-data
size: 10G
- name: nodered-data
size: 5G
tasks:
- name: Create RBD volumes
shell: |
rbd create kubernetes/{{ item.name }} --size {{ item.size }} --image-feature layering
loop: "{{ volumes }}"
register: rbd_create
failed_when:
- rbd_create.rc != 0
- "'already exists' not in rbd_create.stderr"
changed_when: rbd_create.rc == 0
- name: List created volumes
command: rbd ls kubernetes
register: rbd_list
changed_when: false
- name: Display volumes
debug:
msg: "Created volumes in kubernetes pool: {{ rbd_list.stdout_lines }}"
- name: Get volume info
shell: |
rbd info kubernetes/{{ item.name }} --format json
loop: "{{ volumes }}"
register: volume_info
changed_when: false
- name: Display volume details
debug:
msg: "Volume {{ item.item.name }}: {{ (item.stdout | from_json).size | int / 1024 / 1024 / 1024 }}GB"
loop: "{{ volume_info.results }}"

View file

@ -0,0 +1,138 @@
---
- name: Deploy Ceph CSI for direct RBD access
hosts: localhost
connection: local
vars:
kubeconfig: "{{ playbook_dir }}/kubeconfig"
ceph_csi_dir: "../cluster/ceph-csi"
tasks:
- name: Create temporary directory for fixed configs
tempfile:
state: directory
register: temp_dir
- name: Copy original kustomization.yaml
copy:
src: "{{ ceph_csi_dir }}/kustomization.yaml"
dest: "{{ temp_dir.path }}/kustomization.yaml"
- name: Update kustomization to use fixed files
replace:
path: "{{ temp_dir.path }}/kustomization.yaml"
regexp: "{{ item.old }}"
replace: "{{ item.new }}"
loop:
- { old: "csi-rbdplugin.yaml", new: "csi-rbdplugin-fixed.yaml" }
- { old: "csi-rbdplugin-provisioner.yaml", new: "csi-rbdplugin-provisioner-fixed.yaml" }
- { old: "ceph-config.yaml", new: "ceph-config-fixed.yaml" }
- name: Copy all required files to temp directory
copy:
src: "{{ ceph_csi_dir }}/{{ item }}"
dest: "{{ temp_dir.path }}/{{ item }}"
loop:
- namespace.yaml
- ceph-csi-configmap.yaml
- ceph-csi-secret.yaml
- ceph-csi-rbd-driver.yaml
- csi-driver.yaml
- ceph-storageclass.yaml
- csi-rbdplugin-fixed.yaml
- csi-rbdplugin-provisioner-fixed.yaml
- ceph-config-fixed.yaml
ignore_errors: yes
- name: Check if namespace exists
shell: |
export KUBECONFIG={{ kubeconfig }}
kubectl get namespace ceph-csi
register: ns_check
failed_when: false
changed_when: false
- name: Delete existing Ceph CSI deployment if exists
shell: |
export KUBECONFIG={{ kubeconfig }}
kubectl delete daemonset,deployment,service -n ceph-csi -l app in (csi-rbdplugin,csi-rbdplugin-provisioner,csi-metrics) --ignore-not-found
when: ns_check.rc == 0
- name: Wait for pods to terminate
shell: |
export KUBECONFIG={{ kubeconfig }}
kubectl wait --for=delete pod -n ceph-csi -l app in (csi-rbdplugin,csi-rbdplugin-provisioner) --timeout=60s || true
when: ns_check.rc == 0
- name: Apply Ceph CSI configuration
shell: |
export KUBECONFIG={{ kubeconfig }}
kubectl apply -k {{ temp_dir.path }}
register: apply_result
- name: Display apply result
debug:
var: apply_result.stdout_lines
- name: Wait for CSI pods to be ready
shell: |
export KUBECONFIG={{ kubeconfig }}
kubectl wait --for=condition=ready pod -n ceph-csi -l app=csi-rbdplugin-provisioner --timeout=300s || true
kubectl wait --for=condition=ready pod -n ceph-csi -l app=csi-rbdplugin --timeout=300s || true
register: wait_result
ignore_errors: yes
- name: Check CSI pod status
shell: |
export KUBECONFIG={{ kubeconfig }}
echo "=== CSI Provisioner Pods ==="
kubectl get pods -n ceph-csi -l app=csi-rbdplugin-provisioner
echo -e "\n=== CSI Node Plugin Pods ==="
kubectl get pods -n ceph-csi -l app=csi-rbdplugin
echo -e "\n=== Storage Class ==="
kubectl get storageclass ceph-rbd
register: status_check
- name: Display CSI status
debug:
var: status_check.stdout_lines
- name: Clean up temporary directory
file:
path: "{{ temp_dir.path }}"
state: absent
- name: Test Ceph CSI with a sample PVC
shell: |
export KUBECONFIG={{ kubeconfig }}
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-ceph-pvc
namespace: default
spec:
accessModes:
- ReadWriteOnce
storageClassName: ceph-rbd
resources:
requests:
storage: 1Gi
EOF
register: test_pvc
- name: Check test PVC status
shell: |
export KUBECONFIG={{ kubeconfig }}
sleep 5
kubectl get pvc test-ceph-pvc
register: pvc_status
- name: Display test PVC status
debug:
var: pvc_status.stdout_lines
- name: Clean up test PVC
shell: |
export KUBECONFIG={{ kubeconfig }}
kubectl delete pvc test-ceph-pvc --ignore-not-found
when: test_pvc is succeeded

416
home/ansible/deploy-k3s.yml Normal file
View file

@ -0,0 +1,416 @@
---
- name: Prepare K3s nodes
hosts: k3s_cluster
become: true
tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Install curl
package:
name: curl
state: present
- name: Configure DNS
copy:
content: |
nameserver 9.9.9.9
nameserver 149.112.112.112
nameserver 1.1.1.1
dest: /etc/resolv.conf
backup: yes
- name: Test DNS resolution
command: nslookup github.com
changed_when: false
register: dns_test
failed_when: false
- name: Show DNS test result
debug:
var: dns_test.stdout_lines
when: dns_test.rc != 0
- name: Load required kernel modules
modprobe:
name: "{{ item }}"
state: present
loop:
- br_netfilter
- overlay
- wireguard
ignore_errors: yes # Wireguard module may not exist in Debian 13
- name: Persist kernel modules
copy:
content: |
br_netfilter
overlay
wireguard
dest: /etc/modules-load.d/k3s.conf
- name: Set sysctl parameters
sysctl:
name: "{{ item.name }}"
value: "{{ item.value }}"
state: present
sysctl_file: /etc/sysctl.d/k3s.conf
reload: yes
loop:
- { name: 'net.bridge.bridge-nf-call-iptables', value: '1' }
- { name: 'net.bridge.bridge-nf-call-ip6tables', value: '1' }
- { name: 'net.ipv4.ip_forward', value: '1' }
- { name: 'net.ipv6.conf.all.forwarding', value: '1' }
- name: Download k3s binary
hosts: k3s_cluster
become: true
tasks:
- name: Download k3s binary
get_url:
url: https://github.com/k3s-io/k3s/releases/download/{{ k3s_version }}/k3s
dest: /usr/local/bin/k3s
mode: '0755'
- name: Deploy first control plane node
hosts: home-k3s-control-1
become: true
tasks:
- name: Create k3s config directory
file:
path: /etc/rancher/k3s
state: directory
mode: '0755'
- name: Create k3s server config
copy:
content: |
cluster-init: true
disable:
- servicelb
- traefik
cluster-cidr: 10.42.0.0/16
service-cidr: 10.43.0.0/16
cluster-dns: 10.43.0.10
flannel-backend: vxlan
write-kubeconfig-mode: "0644"
tls-san:
- "{{ ansible_host }}"
- "{{ ansible_hostname }}"
dest: /etc/rancher/k3s/config.yaml
- name: Create k3s systemd service
copy:
content: |
[Unit]
Description=Lightweight Kubernetes
Documentation=https://k3s.io
Wants=network-online.target
After=network-online.target
[Service]
Type=notify
EnvironmentFile=-/etc/default/%N
EnvironmentFile=-/etc/sysconfig/%N
KillMode=process
Delegate=yes
LimitNOFILE=1048576
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
TimeoutStartSec=0
Restart=always
RestartSec=5s
ExecStartPre=/bin/sh -xc '! /usr/bin/systemctl is-enabled --quiet nm-cloud-setup.service'
ExecStartPre=-/sbin/modprobe br_netfilter
ExecStartPre=-/sbin/modprobe overlay
ExecStart=/usr/local/bin/k3s server
[Install]
WantedBy=multi-user.target
dest: /etc/systemd/system/k3s.service
- name: Start k3s on first control plane
systemd:
name: k3s
daemon_reload: yes
enabled: yes
state: started
- name: Wait for node token
wait_for:
path: /var/lib/rancher/k3s/server/node-token
timeout: 120
- name: Read node token
slurp:
src: /var/lib/rancher/k3s/server/node-token
register: node_token
- name: Set token fact
set_fact:
k3s_token: "{{ node_token.content | b64decode | trim }}"
- name: Wait for k3s to be ready
command: /usr/local/bin/k3s kubectl get nodes
register: nodes_result
until: nodes_result.rc == 0
retries: 30
delay: 10
changed_when: false
environment:
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
- name: Check k3s service status
command: systemctl status k3s
register: k3s_status
changed_when: false
failed_when: false
- name: Show k3s logs if not ready
command: journalctl -u k3s -n 50 --no-pager
register: k3s_logs
when: nodes_result.rc != 0
changed_when: false
- name: Display logs
debug:
var: k3s_logs.stdout_lines
when: k3s_logs is defined
- name: Deploy additional control plane nodes
hosts: k3s_control:!home-k3s-control-1
become: true
serial: 1
tasks:
- name: Create k3s config directory
file:
path: /etc/rancher/k3s
state: directory
mode: '0755'
- name: Create k3s server config for additional control planes
copy:
content: |
server: https://{{ hostvars['home-k3s-control-1']['ansible_host'] }}:6443
token: {{ hostvars['home-k3s-control-1']['k3s_token'] }}
disable:
- servicelb
- traefik
cluster-cidr: 10.42.0.0/16
service-cidr: 10.43.0.0/16
cluster-dns: 10.43.0.10
flannel-backend: vxlan
write-kubeconfig-mode: "0644"
tls-san:
- "{{ ansible_host }}"
- "{{ ansible_hostname }}"
dest: /etc/rancher/k3s/config.yaml
- name: Create k3s systemd service
copy:
content: |
[Unit]
Description=Lightweight Kubernetes
Documentation=https://k3s.io
Wants=network-online.target
After=network-online.target
[Service]
Type=notify
EnvironmentFile=-/etc/default/%N
EnvironmentFile=-/etc/sysconfig/%N
KillMode=process
Delegate=yes
LimitNOFILE=1048576
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
TimeoutStartSec=0
Restart=always
RestartSec=5s
ExecStartPre=/bin/sh -xc '! /usr/bin/systemctl is-enabled --quiet nm-cloud-setup.service'
ExecStartPre=-/sbin/modprobe br_netfilter
ExecStartPre=-/sbin/modprobe overlay
ExecStart=/usr/local/bin/k3s server
[Install]
WantedBy=multi-user.target
dest: /etc/systemd/system/k3s.service
- name: Start k3s on additional control planes
systemd:
name: k3s
daemon_reload: yes
enabled: yes
state: started
- name: Deploy worker nodes
hosts: k3s_workers
become: true
tasks:
- name: Create k3s config directory
file:
path: /etc/rancher/k3s
state: directory
mode: '0755'
- name: Create k3s agent config
copy:
content: |
server: https://{{ hostvars['home-k3s-control-1']['ansible_host'] }}:6443
token: {{ hostvars['home-k3s-control-1']['k3s_token'] }}
dest: /etc/rancher/k3s/config.yaml
- name: Create k3s-agent systemd service
copy:
content: |
[Unit]
Description=Lightweight Kubernetes
Documentation=https://k3s.io
Wants=network-online.target
After=network-online.target
[Service]
Type=notify
EnvironmentFile=-/etc/default/%N
EnvironmentFile=-/etc/sysconfig/%N
KillMode=process
Delegate=yes
LimitNOFILE=1048576
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
TimeoutStartSec=0
Restart=always
RestartSec=5s
ExecStartPre=/bin/sh -xc '! /usr/bin/systemctl is-enabled --quiet nm-cloud-setup.service'
ExecStartPre=-/sbin/modprobe br_netfilter
ExecStartPre=-/sbin/modprobe overlay
ExecStart=/usr/local/bin/k3s agent
[Install]
WantedBy=multi-user.target
dest: /etc/systemd/system/k3s-agent.service
- name: Start k3s-agent on workers
systemd:
name: k3s-agent
daemon_reload: yes
enabled: yes
state: started
- name: Configure kubectl access
hosts: home-k3s-control-1
become: true
tasks:
- name: Create kubectl symlink
file:
src: /usr/local/bin/k3s
dest: /usr/local/bin/kubectl
state: link
- name: Create crictl symlink
file:
src: /usr/local/bin/k3s
dest: /usr/local/bin/crictl
state: link
- name: Create .kube directory for user
file:
path: "/home/{{ ansible_user }}/.kube"
state: directory
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0755'
- name: Copy kubeconfig to user
copy:
src: /etc/rancher/k3s/k3s.yaml
dest: "/home/{{ ansible_user }}/.kube/config"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0600'
remote_src: yes
- name: Fetch kubeconfig
fetch:
src: /etc/rancher/k3s/k3s.yaml
dest: "{{ playbook_dir }}/kubeconfig"
flat: yes
- name: Update kubeconfig server URL
replace:
path: "{{ playbook_dir }}/kubeconfig"
regexp: 'server: https://127.0.0.1:6443'
replace: 'server: https://{{ ansible_host }}:6443'
delegate_to: localhost
become: false
- name: Apply MetalLB configuration
hosts: home-k3s-control-1
become: true
tasks:
- name: Wait for MetalLB namespace
command: /usr/local/bin/k3s kubectl get namespace metallb-system
register: metallb_ns
until: metallb_ns.rc == 0
retries: 60
delay: 5
changed_when: false
environment:
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
- name: Wait for MetalLB controller deployment
command: /usr/local/bin/k3s kubectl -n metallb-system wait --for=condition=available --timeout=300s deployment/controller
changed_when: false
environment:
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
- name: Create MetalLB IP pool manifest
copy:
content: |
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: home-pool
namespace: metallb-system
spec:
addresses:
- 10.0.19.1-10.0.19.99
dest: /tmp/metallb-pool.yaml
- name: Apply MetalLB IP pool
command: /usr/local/bin/k3s kubectl apply -f /tmp/metallb-pool.yaml
environment:
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
- name: Create MetalLB L2 advertisement manifest
copy:
content: |
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: home-l2
namespace: metallb-system
spec:
ipAddressPools:
- home-pool
dest: /tmp/metallb-l2.yaml
- name: Apply MetalLB L2 advertisement
command: /usr/local/bin/k3s kubectl apply -f /tmp/metallb-l2.yaml
environment:
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
- name: Clean up manifests
file:
path: "{{ item }}"
state: absent
loop:
- /tmp/metallb-pool.yaml
- /tmp/metallb-l2.yaml

View file

@ -0,0 +1,25 @@
---
- name: Download Debian 13 LXC template
hosts: proxmox[0]
become: true
tasks:
- name: Update template list
command: pveam update
changed_when: false
- name: Search for Debian 13 template
command: pveam available | grep -i "debian-13"
register: debian13_search
changed_when: false
failed_when: false
- name: Show available Debian 13 templates
debug:
var: debian13_search.stdout_lines
when: debian13_search.rc == 0
- name: Download Debian 12 template (if 13 not available)
command: pveam download local debian-12-standard_12.7-1_amd64.tar.zst
register: download_result
failed_when: false
changed_when: "'already exists' not in download_result.stderr"

View file

@ -0,0 +1,77 @@
---
- name: Fix NFS permissions for all users write access
hosts: proxmox[0]
become: true
vars:
nfs_export_path: /mnt/cephfs-kubernetes
k8s_network: "10.0.19.0/24"
tasks:
- name: Update NFS export configuration for better access
lineinfile:
path: /etc/exports
line: "{{ nfs_export_path }} {{ k8s_network }}(rw,sync,no_subtree_check,no_root_squash,no_all_squash)"
regexp: "^{{ nfs_export_path | regex_escape() }}\\s+"
create: yes
backup: yes
notify:
- restart nfs
- export nfs shares
- name: Ensure base directories have proper permissions
file:
path: "{{ nfs_export_path }}/{{ item }}"
state: directory
mode: '0777'
loop:
- volumes
- shared
- name: Set world-writable permissions on general storage directories
file:
path: "{{ nfs_export_path }}/{{ item }}"
state: directory
mode: '0777'
loop:
- volumes/adguard
- volumes/nodered
- volumes/forgejo
- shared
ignore_errors: yes # In case some don't exist yet
- name: Preserve PostgreSQL specific permissions
file:
path: "{{ nfs_export_path }}/volumes/postgresql"
state: directory
owner: '999'
group: '999'
mode: '0700'
recurse: yes
when: ansible_check_mode or (nfs_export_path + "/volumes/postgresql") is directory
ignore_errors: yes
- name: Set sticky bit on shared directories
command: chmod +t {{ nfs_export_path }}/{{ item }}
loop:
- volumes
- shared
changed_when: false
- name: Display current exports
command: exportfs -v
register: exports_output
changed_when: false
- name: Show current exports
debug:
var: exports_output.stdout_lines
handlers:
- name: restart nfs
service:
name: nfs-kernel-server
state: restarted
- name: export nfs shares
command: exportfs -ra
changed_when: false

View file

@ -0,0 +1,33 @@
---
- name: Fix PostgreSQL pg_hba.conf for K3s access
hosts: proxmox
become: yes
gather_facts: no
vars:
ct_id: 300
tasks:
- name: Update pg_hba.conf to allow K3s nodes
shell: |
pct exec {{ ct_id }} -- bash -c "
# Backup current pg_hba.conf
cp /etc/postgresql/16/main/pg_hba.conf /etc/postgresql/16/main/pg_hba.conf.backup
# Check if K3s network entry exists
if ! grep -q '10.0.19.0/24' /etc/postgresql/16/main/pg_hba.conf; then
# Add K3s network access
echo 'host all all 10.0.19.0/24 md5' >> /etc/postgresql/16/main/pg_hba.conf
fi
# Reload PostgreSQL
systemctl reload postgresql
"
- name: Verify pg_hba.conf entries
shell: |
pct exec {{ ct_id }} -- grep '10.0.19' /etc/postgresql/16/main/pg_hba.conf
register: hba_entries
- name: Display pg_hba.conf entries
debug:
msg: "{{ hba_entries.stdout_lines }}"

View file

@ -0,0 +1,17 @@
---
- name: Fix PostgreSQL directory permissions
hosts: proxmox[0]
become: true
tasks:
- name: Fix PostgreSQL directory ownership
file:
path: /mnt/cephfs-kubernetes/volumes/postgresql
state: directory
owner: '999'
group: '999'
mode: '0700'
recurse: yes
- name: Ensure write permissions
command: chmod -R 700 /mnt/cephfs-kubernetes/volumes/postgresql
changed_when: false

View file

@ -0,0 +1,22 @@
---
- name: Get Ceph kubernetes user key
hosts: proxmox[0]
become: true
tasks:
- name: Get kubernetes user auth
command: ceph auth get client.kubernetes
register: k8s_auth
changed_when: false
- name: Display kubernetes auth
debug:
var: k8s_auth.stdout_lines
- name: Extract just the key
command: ceph auth print-key client.kubernetes
register: k8s_key
changed_when: false
- name: Display key
debug:
msg: "Kubernetes key: {{ k8s_key.stdout }}"

View file

@ -0,0 +1,44 @@
---
- name: Install Ceph client packages on K3s nodes
hosts: k3s_cluster
become: true
tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Install ceph-common package
package:
name: ceph-common
state: present
register: ceph_install
ignore_errors: yes
- name: Check if ceph-common is installed
command: which ceph
register: ceph_check
failed_when: false
changed_when: false
- name: Display ceph installation status
debug:
msg: "Ceph client installed: {{ ceph_check.rc == 0 }}"
- name: Create ceph configuration directory
file:
path: /etc/ceph
state: directory
mode: '0755'
- name: Load RBD kernel module
modprobe:
name: rbd
state: present
- name: Persist RBD module
lineinfile:
path: /etc/modules-load.d/ceph.conf
line: rbd
create: yes

View file

@ -0,0 +1,9 @@
---
- name: Install NFS client on K3s nodes
hosts: k3s_cluster
become: true
tasks:
- name: Install NFS client packages
package:
name: nfs-common
state: present

View file

@ -0,0 +1,54 @@
all:
children:
proxmox:
hosts:
lab02:
ansible_host: 10.0.16.231
lab03:
ansible_host: 10.0.16.232
lab04:
ansible_host: 10.0.16.233
vars:
ansible_user: root
ansible_python_interpreter: /usr/bin/python3
k3s_cluster:
children:
k3s_control:
hosts:
home-k3s-control-1:
ansible_host: 10.0.19.100
k3s_control_node: true
home-k3s-control-2:
ansible_host: 10.0.19.101
k3s_control_node: true
home-k3s-control-3:
ansible_host: 10.0.19.102
k3s_control_node: true
k3s_workers:
hosts:
home-k3s-worker-1:
ansible_host: 10.0.19.110
home-k3s-worker-2:
ansible_host: 10.0.19.111
home-k3s-worker-3:
ansible_host: 10.0.19.112
vars:
ansible_user: debian
ansible_ssh_private_key_file: "~/.ssh/id_rsa"
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
k3s_version: v1.28.5+k3s1
vultr:
hosts:
caddy:
ansible_host: "caddy.w5isp.com"
vars:
ansible_user: root # Initial connection as root to create users
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
caddy_servers:
hosts:
caddy:
vars:
ansible_user: root # Will switch to ansible user after creation
ansible_python_interpreter: /usr/bin/python3

19
home/ansible/kubeconfig Normal file
View file

@ -0,0 +1,19 @@
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkakNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTlRjd09EVTRORGd3SGhjTk1qVXdPVEExTVRVeU5EQTRXaGNOTXpVd09UQXpNVFV5TkRBNApXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTlRjd09EVTRORGd3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFUQm5YbGtOQlhZN0hrRTRyczlycEtCL0NhTnh1ekZUSUpTUjdpdWQyMEUKb2ZSNWZYVlFrTWp5R0tBZTBQaWFWT1ZFZXo0OEdVeUl1T21pMTZIZnp3czdvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVVF0d2RJaDlkWVlpTkZzcDBvRmZBCk5lYXNMem93Q2dZSUtvWkl6ajBFQXdJRFJ3QXdSQUlnVk5acVBQMDBOR09xYnF3SFkxdVM4YjRLeG9MWkpXekUKbmNmNUJHazNqQVlDSUNNMlZwd3E5VmZtTTRFMTZ0VEFqeStHOGprWW1PMEFCdHdST1NnM293cDgKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
server: https://10.0.19.100:6443
name: default
contexts:
- context:
cluster: default
user: default
name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrRENDQVRlZ0F3SUJBZ0lJWHliZTEvMDh1N2N3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOelUzTURnMU9EUTRNQjRYRFRJMU1Ea3dOVEUxTWpRd09Gb1hEVEkyTURrdwpOVEUxTWpRd09Gb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJOWEs5VndoYWNaT1BjNVQKVGp2bVdCdDJaZmxiWmkzM1pjZk5VWVRTSGUxbzZaMEhvUVhHdDN5V2lrOVdmUGF6OTRYSHZVZXdLSmpOTnJBWgpsYzk2U21HalNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCVE5CbDhSUGJnRTBFVXRUeGJyUXhyekE0Lzg2VEFLQmdncWhrak9QUVFEQWdOSEFEQkUKQWlBcmUySmNPa0lDbmhZWXptODZCVThEampNdFJZUTE1SWRoamJuenZPZ09WUUlnREtsaHIxL1FKWkMwellaLwoveUt5OEEzTWN4aXRQMEJkdXBvQzhnRDdrQkU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdFkyeHAKWlc1MExXTmhRREUzTlRjd09EVTRORGd3SGhjTk1qVXdPVEExTVRVeU5EQTRXaGNOTXpVd09UQXpNVFV5TkRBNApXakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwWlc1MExXTmhRREUzTlRjd09EVTRORGd3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFUNlZCdUI4UStFblNXOUFZT0N3b0ZsUFcwdzdqUjZlZC9qdTArbVIySDgKVkdnTk1qdXFUalU0a3o0RWhrYmJsaHloVVc5ZE9FWVRJSDM2bVlGUHltaVBvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVXpRWmZFVDI0Qk5CRkxVOFc2ME1hCjh3T1AvT2t3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUloQVBja200cHA2anI1aStWcFRLbDNFTU5EWmNoQTZ5eXgKVDA4UCt5eHlieGxyQWlCZWJ6cDFqSDBYbUZKVjcxWjd2dDZFMzlsVTcyTTJJdlM0YW1zUncvd1lqdz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
client-key-data: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUFPNjA5Q2tBNEl1NXQyRFdMVkhBMHRDTFBZSHlBZ21nQlBrU3V2OElXOUVvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFMWNyMVhDRnB4azQ5emxOT08rWllHM1psK1Z0bUxmZGx4ODFSaE5JZDdXanBuUWVoQmNhMwpmSmFLVDFaODlyUDNoY2U5UjdBb21NMDJzQm1WejNwS1lRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=

View file

@ -0,0 +1,13 @@
---
- name: List available LXC templates
hosts: proxmox[0]
become: true
tasks:
- name: List templates in local storage
command: pveam list local
register: templates
changed_when: false
- name: Show Debian templates
debug:
msg: "{{ templates.stdout_lines | select('search', 'debian') | list }}"

View file

@ -0,0 +1,28 @@
---
- name: Migrate template to all Proxmox nodes
hosts: lab02
gather_facts: no
vars:
template_vmid: 9000
tasks:
- name: Migrate template to lab03
command: qm migrate {{ template_vmid }} lab03
register: migrate_lab03
failed_when:
- migrate_lab03.rc != 0
- "'already on node' not in migrate_lab03.stderr"
- name: Migrate template to lab04
command: qm migrate {{ template_vmid }} lab04
register: migrate_lab04
failed_when:
- migrate_lab04.rc != 0
- "'already on node' not in migrate_lab04.stderr"
- name: Migrate template back to lab02
command: qm migrate {{ template_vmid }} lab02
register: migrate_back
failed_when:
- migrate_back.rc != 0
- "'already on node' not in migrate_back.stderr"

View file

@ -0,0 +1,40 @@
---
- name: Configure Ceph RBD access on K3s nodes
hosts: k3s_cluster
become: true
vars:
ceph_monitors: "10.0.16.231:6789,10.0.16.232:6789,10.0.16.233:6789"
ceph_user: "kubernetes"
ceph_key: "AQAq14Jo/g4ZORAAwxpPOmqj1PYgmih6s6b4vA=="
tasks:
- name: Create Ceph configuration file
copy:
content: |
[global]
fsid = 6e5e8932-3613-471d-afa0-410fade4d4ff
mon_host = 10.0.16.231,10.0.16.232,10.0.16.233
auth_cluster_required = cephx
auth_service_required = cephx
auth_client_required = cephx
dest: /etc/ceph/ceph.conf
mode: '0644'
- name: Create Ceph keyring for kubernetes user
copy:
content: |
[client.kubernetes]
key = {{ ceph_key }}
dest: /etc/ceph/ceph.client.kubernetes.keyring
mode: '0600'
- name: Test Ceph connectivity
shell: |
rbd ls kubernetes --id kubernetes
register: rbd_test
failed_when: false
changed_when: false
- name: Show RBD test result
debug:
var: rbd_test

View file

@ -0,0 +1,35 @@
---
- name: Configure CephFS access on K3s nodes
hosts: k3s_cluster
become: true
tasks:
- name: Install ceph-fuse package for CephFS
package:
name:
- ceph-fuse
- ceph-common
state: present
ignore_errors: yes
register: ceph_install
- name: Create mount point for testing
file:
path: /mnt/cephfs-test
state: directory
mode: '0755'
- name: Test CephFS mount
shell: |
timeout 5 ceph-fuse -n client.kubernetes --keyring=/etc/ceph/ceph.client.kubernetes.keyring -m 10.0.16.231:6789,10.0.16.232:6789,10.0.16.233:6789 /mnt/cephfs-test
register: mount_test
failed_when: false
changed_when: false
- name: Unmount test
shell: fusermount -u /mnt/cephfs-test
failed_when: false
changed_when: false
- name: Show mount test result
debug:
msg: "CephFS mount test: {{ 'SUCCESS' if mount_test.rc == 124 else 'FAILED' }}"

View file

@ -0,0 +1,27 @@
---
- name: Replicate Debian 13 template to all Proxmox nodes
hosts: proxmox
gather_facts: no
vars:
template_vmid: 9000
storage: "ceph"
source_node: "lab02"
tasks:
- name: Check if template exists on this node
command: qm status {{ template_vmid }}
register: vm_check
failed_when: false
changed_when: false
- name: Clone template from source node
when:
- vm_check.rc != 0
- inventory_hostname != source_node
command: |
qm clone {{ template_vmid }} {{ template_vmid }} --name debian-13-cloudinit --target {{ inventory_hostname }}
delegate_to: "{{ source_node }}"
- name: Template status
debug:
msg: "Template {{ template_vmid }} exists on {{ inventory_hostname }}"

View file

@ -0,0 +1,82 @@
---
- name: Setup Ceph client tools on K3s nodes
hosts: k3s_cluster
become: true
tasks:
- name: Install Ceph repository key
apt_key:
url: https://download.ceph.com/keys/release.asc
state: present
- name: Get Debian codename
command: lsb_release -cs
register: debian_codename
changed_when: false
- name: Add Ceph repository
apt_repository:
repo: "deb https://download.ceph.com/debian-reef/ {{ debian_codename.stdout }} main"
state: present
filename: ceph
- name: Update apt cache
apt:
update_cache: yes
- name: Install Ceph client packages
apt:
name:
- ceph-common
- python3-rbd
- python3-rados
state: present
- name: Create Ceph configuration directory
file:
path: /etc/ceph
state: directory
mode: '0755'
- name: Copy Ceph configuration
copy:
content: |
[global]
fsid = 6e5e8932-3613-471d-afa0-410fade4d4ff
mon_initial_members = lab02,lab03,lab04
mon_host = 10.0.16.231,10.0.16.232,10.0.16.233
auth_cluster_required = cephx
auth_service_required = cephx
auth_client_required = cephx
dest: /etc/ceph/ceph.conf
mode: '0644'
- name: Copy Ceph kubernetes keyring
copy:
content: |
[client.kubernetes]
key = AQAq14Jo/g4ZORAAwxpPOmqj1PYgmih6s6b4vA==
dest: /etc/ceph/ceph.client.kubernetes.keyring
mode: '0600'
- name: Test Ceph connectivity
command: ceph -s --id kubernetes
register: ceph_status
changed_when: false
ignore_errors: yes
- name: Display Ceph status
debug:
var: ceph_status.stdout_lines
when: ceph_status.rc == 0
- name: Check RBD module
modprobe:
name: rbd
state: present
- name: Ensure RBD module loads on boot
lineinfile:
path: /etc/modules
line: rbd
create: yes

View file

@ -0,0 +1,155 @@
---
# This playbook sets up direct Ceph RBD access for Kubernetes
# It replaces NFS-based access with native Ceph RBD CSI
- name: Ensure Ceph kubernetes pool exists
hosts: proxmox[0]
become: true
tasks:
- name: Check if kubernetes pool exists
command: ceph osd pool ls
register: ceph_pools
changed_when: false
- name: Create kubernetes pool if not exists
command: ceph osd pool create kubernetes 128 128
when: "'kubernetes' not in ceph_pools.stdout"
- name: Initialize pool for RBD use
command: rbd pool init kubernetes
when: "'kubernetes' not in ceph_pools.stdout"
- name: Check if kubernetes user exists
command: ceph auth list
register: ceph_users
changed_when: false
- name: Create kubernetes user if not exists
shell: |
ceph auth get-or-create client.kubernetes \
mon 'profile rbd' \
osd 'profile rbd pool=kubernetes' \
mgr 'profile rbd pool=kubernetes'
when: "'client.kubernetes' not in ceph_users.stdout"
- name: Get kubernetes user key
command: ceph auth print-key client.kubernetes
register: k8s_key
changed_when: false
- name: Display kubernetes key
debug:
msg: "Kubernetes key: {{ k8s_key.stdout }}"
- name: Set fact for kubernetes key
set_fact:
ceph_kubernetes_key: "{{ k8s_key.stdout }}"
- name: Setup Ceph client on K3s nodes
import_playbook: setup-ceph-clients-k3s.yml
- name: Deploy Ceph CSI
import_playbook: deploy-ceph-csi.yml
- name: Verify Ceph RBD setup
hosts: localhost
connection: local
vars:
kubeconfig: "{{ playbook_dir }}/kubeconfig"
tasks:
- name: Check CSI driver status
shell: |
export KUBECONFIG={{ kubeconfig }}
echo "=== CSI Drivers ==="
kubectl get csidrivers
echo -e "\n=== CSI Nodes ==="
kubectl get csinodes
echo -e "\n=== Storage Classes ==="
kubectl get storageclass
echo -e "\n=== CSI Pods Status ==="
kubectl get pods -n ceph-csi
register: csi_status
- name: Display CSI status
debug:
var: csi_status.stdout_lines
- name: Create test application with Ceph RBD
shell: |
export KUBECONFIG={{ kubeconfig }}
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Namespace
metadata:
name: ceph-test
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-rbd-pvc
namespace: ceph-test
spec:
accessModes:
- ReadWriteOnce
storageClassName: ceph-rbd
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: Pod
metadata:
name: test-rbd-pod
namespace: ceph-test
spec:
containers:
- name: test
image: nginx:alpine
volumeMounts:
- name: test-volume
mountPath: /data
volumes:
- name: test-volume
persistentVolumeClaim:
claimName: test-rbd-pvc
EOF
register: test_app
- name: Wait for test pod to be ready
shell: |
export KUBECONFIG={{ kubeconfig }}
kubectl wait --for=condition=ready pod -n ceph-test test-rbd-pod --timeout=120s
register: wait_result
ignore_errors: yes
- name: Check test application status
shell: |
export KUBECONFIG={{ kubeconfig }}
echo "=== Test PVC Status ==="
kubectl get pvc -n ceph-test
echo -e "\n=== Test Pod Status ==="
kubectl get pod -n ceph-test
echo -e "\n=== RBD Image List ==="
kubectl exec -n ceph-test test-rbd-pod -- df -h /data
register: test_status
when: wait_result is succeeded
- name: Display test results
debug:
var: test_status.stdout_lines
when: wait_result is succeeded
- name: Cleanup test resources
shell: |
export KUBECONFIG={{ kubeconfig }}
kubectl delete namespace ceph-test --ignore-not-found
when: test_app is succeeded
- name: Summary
debug:
msg:
- "Ceph RBD CSI setup complete!"
- "Storage class 'ceph-rbd' is now available for direct Ceph RBD access"
- "You can now create PVCs with storageClassName: ceph-rbd"
- "This provides better performance than NFS-based access"

View file

@ -0,0 +1,66 @@
---
- name: Setup NFS export for CephFS
hosts: proxmox[0]
become: true
vars:
nfs_export_path: /mnt/cephfs-kubernetes
k8s_network: "10.0.19.0/24"
tasks:
- name: Install NFS server
apt:
name:
- nfs-kernel-server
- ceph-fuse
state: present
update_cache: yes
- name: Create CephFS mount point
file:
path: "{{ nfs_export_path }}"
state: directory
mode: '0755'
- name: Mount CephFS
mount:
path: "{{ nfs_export_path }}"
src: "10.0.16.231,10.0.16.232,10.0.16.233:/"
fstype: ceph
opts: "name=kubernetes,secretfile=/etc/ceph/client.kubernetes.key"
state: mounted
- name: Create Kubernetes storage directories
file:
path: "{{ nfs_export_path }}/{{ item }}"
state: directory
mode: '0777'
owner: nobody
group: nogroup
loop:
- volumes
- volumes/adguard
- volumes/nodered
- shared
- name: Ensure world-writable permissions on CephFS mount
file:
path: "{{ nfs_export_path }}"
mode: '0777'
recurse: yes
- name: Configure NFS exports
lineinfile:
path: /etc/exports
line: "{{ nfs_export_path }} {{ k8s_network }}(rw,sync,no_subtree_check,no_root_squash,no_all_squash)"
create: yes
notify: restart nfs
- name: Export NFS shares
command: exportfs -ra
changed_when: false
handlers:
- name: restart nfs
service:
name: nfs-kernel-server
state: restarted

View file

@ -0,0 +1,175 @@
---
- name: Setup PostgreSQL with PostGIS in LXC
hosts: proxmox[0]
become: true
vars:
ct_id: 300
postgres_version: 16
tasks:
- name: Check if container is running
command: pct status {{ ct_id }}
register: ct_status
failed_when: false
changed_when: false
- name: Start container if stopped
command: pct start {{ ct_id }}
when: ct_status.rc == 0 and 'stopped' in ct_status.stdout
- name: Wait for container to be ready
wait_for:
host: 10.0.19.5
port: 22
timeout: 60
delay: 5
when: ct_status.rc == 0
- name: Check if PostgreSQL is already installed
shell: pct exec {{ ct_id }} -- which psql || echo "not installed"
register: psql_check
changed_when: false
when: ct_status.rc == 0
- name: Update package list
shell: pct exec {{ ct_id }} -- apt-get update -y
when: ct_status.rc == 0 and "'not installed' in psql_check.stdout"
- name: Install PostgreSQL
shell: pct exec {{ ct_id }} -- bash -c "export DEBIAN_FRONTEND=noninteractive && apt-get install -y postgresql postgresql-contrib"
when: ct_status.rc == 0 and "'not installed' in psql_check.stdout"
- name: Start PostgreSQL service
shell: |
pct exec {{ ct_id }} -- bash -c "
systemctl enable postgresql
systemctl start postgresql
systemctl status postgresql
"
register: pg_status
when: ct_status.rc == 0
- name: Show PostgreSQL status
debug:
var: pg_status.stdout_lines
when: pg_status is defined
- name: Get PostgreSQL version
shell: pct exec {{ ct_id }} -- ls /etc/postgresql/
register: pg_dir
when: ct_status.rc == 0
- name: Configure PostgreSQL for network access
shell: |
pct exec {{ ct_id }} -- bash -c "
# Get actual PostgreSQL version
PG_VERSION=$(ls /etc/postgresql/ 2>/dev/null | head -1)
# If no PostgreSQL directory found, exit
if [ -z \"\$PG_VERSION\" ]; then
echo 'PostgreSQL not found'
exit 1
fi
# Configure postgresql.conf
sed -i \"s/#listen_addresses = 'localhost'/listen_addresses = '*'/g\" /etc/postgresql/\$PG_VERSION/main/postgresql.conf
# Configure pg_hba.conf properly
echo '# TYPE DATABASE USER ADDRESS METHOD' > /etc/postgresql/\$PG_VERSION/main/pg_hba.conf
echo 'local all postgres peer' >> /etc/postgresql/\$PG_VERSION/main/pg_hba.conf
echo 'local all all peer' >> /etc/postgresql/\$PG_VERSION/main/pg_hba.conf
echo 'host all all 127.0.0.1/32 scram-sha-256' >> /etc/postgresql/\$PG_VERSION/main/pg_hba.conf
echo 'host all all ::1/128 scram-sha-256' >> /etc/postgresql/\$PG_VERSION/main/pg_hba.conf
echo 'host all all 10.0.19.0/24 md5' >> /etc/postgresql/\$PG_VERSION/main/pg_hba.conf
echo 'host all all 10.0.16.0/22 md5' >> /etc/postgresql/\$PG_VERSION/main/pg_hba.conf
echo 'host all all 10.42.0.0/16 md5' >> /etc/postgresql/\$PG_VERSION/main/pg_hba.conf
# Restart PostgreSQL
systemctl restart postgresql
"
register: pg_config_result
- name: Verify pg_hba.conf was updated
shell: |
pct exec {{ ct_id }} -- bash -c "
PG_VERSION=$(ls /etc/postgresql/ 2>/dev/null | head -1)
if [ -n \"\$PG_VERSION\" ]; then
echo '=== PostgreSQL pg_hba.conf ==='
cat /etc/postgresql/\$PG_VERSION/main/pg_hba.conf | grep -E '10.0.19|10.42|10.0.16'
else
echo 'PostgreSQL not found'
fi
"
register: pg_hba_verify
- name: Display pg_hba.conf entries
debug:
var: pg_hba_verify.stdout_lines
when: pg_hba_verify is defined
- name: Install sudo if missing
shell: pct exec {{ ct_id }} -- apt-get install -y sudo
when: ct_status.rc == 0
- name: Create databases and users
shell: |
pct exec {{ ct_id }} -- su - postgres -c "psql << 'EOF'
-- Drop existing if needed and recreate
DROP DATABASE IF EXISTS forgejo;
DROP USER IF EXISTS forgejo;
CREATE USER forgejo WITH PASSWORD 'fj8K3n2Qp9Lx5mW7';
CREATE DATABASE forgejo OWNER forgejo;
\c forgejo
CREATE EXTENSION IF NOT EXISTS postgis;
GRANT ALL PRIVILEGES ON DATABASE forgejo TO forgejo;
-- Create other databases as needed
DROP DATABASE IF EXISTS apps;
DROP USER IF EXISTS appuser;
CREATE USER appuser WITH PASSWORD 'AppPass2024!';
CREATE DATABASE apps OWNER appuser;
\c apps
CREATE EXTENSION IF NOT EXISTS postgis;
GRANT ALL PRIVILEGES ON DATABASE apps TO appuser;
EOF"
when: ct_status.rc == 0
- name: Configure firewall
shell: |
pct exec {{ ct_id }} -- bash -c "
apt-get install -y ufw
ufw allow 22/tcp
ufw allow 5432/tcp
ufw --force enable
"
when: ct_status.rc == 0
- name: Wait for PostgreSQL to be ready
wait_for:
host: 10.0.19.5
port: 5432
timeout: 30
delay: 2
- name: Test PostgreSQL connectivity
shell: |
pct exec {{ ct_id }} -- sudo -u postgres psql -c "SELECT version();" || echo "PostgreSQL query failed"
register: pg_test
when: ct_status.rc == 0
- name: Display connection info
debug:
msg: |
PostgreSQL with PostGIS installed successfully!
Connection details:
- Host: 10.0.19.5
- Port: 5432
- Version: PostgreSQL {{ postgres_version }} with PostGIS 3
Databases:
- forgejo (user: forgejo, pass: fj8K3n2Qp9Lx5mW7)
- apps (user: appuser, pass: AppPass2024!)
From Kubernetes pods, connect using:
postgresql://forgejo:fj8K3n2Qp9Lx5mW7@10.0.19.5:5432/forgejo

View file

@ -0,0 +1,22 @@
---
- name: Check CephFS capacity
hosts: proxmox[0]
become: true
tasks:
- name: Check Ceph cluster capacity
command: ceph df
register: ceph_df
changed_when: false
- name: Display Ceph capacity
debug:
var: ceph_df.stdout_lines
- name: Get CephFS status
command: ceph fs status kubernetes-shared
register: cephfs_status
changed_when: false
- name: Display CephFS status
debug:
var: cephfs_status.stdout_lines

View file

@ -0,0 +1,233 @@
#cloud-config
# Minimal cloud-init configuration for Debian 13 base template
# Update and upgrade packages on first boot
package_update: true
package_upgrade: true
# Configure timezone
timezone: America/Chicago
# Install essential packages
packages:
# System utilities
- sudo
- curl
- wget
- git
- vim
- nano
- tmux
- htop
- iotop
- rsync
- net-tools
- dnsutils
- traceroute
- mtr-tiny
- tcpdump
- nmap
- netcat-openbsd
# Development tools
- build-essential
- python3
- python3-pip
- python3-venv
- jq
- fzf
- zsh
# System monitoring
- sysstat
- smartmontools
- lm-sensors
# Time synchronization
- chrony
# Security
- fail2ban
- ufw
# Container runtime (optional)
- docker.io
- docker-compose
# Other useful tools
- apt-transport-https
- ca-certificates
- gnupg
- lsb-release
- software-properties-common
# Create users with sudo access
users:
- name: graham
groups: sudo,docker
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_import_id:
- gh:gmcintire
- name: andy
groups: sudo,docker
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_import_id:
- gh:nsnw
- name: ansible
uid: 10001
groups: sudo,docker
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC5aTtW3lgF9PfV8sTIxZfbvo6dUBMq5C5zJrXy6mwdb5UK9xvqty5Vlh6F1FqYlZgvIyR7NqLbXnKqsvLVGdkTm9VEXGlNcbPJ3ilbhc3OJPX0k63CwokU6nSztXguE1v3KB7JjXiM0dj7ujuDzAGfPZNClR6u06NjIamzeWcQnsJV3P8rLxa7ppvFgBp0K1N1pMb3AUqXOutsOUjSvs5C4H+GMmKYCYNpPGbfKNDLfB9vYLPYv/ynF1lDDzBDL6gy0PnDX6lXC9jvfPWSKNu5lgIUc6p9J++I/ERGktbGPvEaLPeJjgIJPbVLwvnmCLDj2VBmvu2cVE+oXWjLJLF7y8T1cZA0d7aH1TWHcaFPNd3nsPbWGOA3PtMoNRLN/6b7mA7QAKCEBVcR1cZ8c5wQ7ilcLETrNBmJLPpBQa7tLvCx8Q8KPNmtZX6dMKR1o7yj5mXq7Slzo6RTdKLpDqODPVPmGboChMQWFEvGCKxVYLDuvS8iXHdkRE+C9xwqNis= ansible
# Configure SSH
ssh_pwauth: false
disable_root: true
# SSH configuration
write_files:
- path: /etc/ssh/sshd_config.d/99-custom.conf
content: |
PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding yes
PrintMotd no
AcceptEnv LANG LC_*
Banner /etc/issue.net
- path: /etc/issue.net
content: |
******************************************************************
* *
* This system is for authorized users only. All activity is *
* monitored and reported. Unauthorized access is prohibited. *
* *
******************************************************************
- path: /etc/sudoers.d/90-cloud-init-users
content: |
# Created by cloud-init
graham ALL=(ALL) NOPASSWD:ALL
andy ALL=(ALL) NOPASSWD:ALL
ansible ALL=(ALL) NOPASSWD:ALL
permissions: '0440'
- path: /etc/motd
content: |
Welcome to Debian 13 (Trixie)
System Information:
* Documentation: https://www.debian.org/doc/
* Support: https://www.debian.org/support
- path: /etc/chrony/sources.d/custom.sources
content: |
pool 0.us.pool.ntp.org iburst
pool 1.us.pool.ntp.org iburst
pool 2.us.pool.ntp.org iburst
pool 3.us.pool.ntp.org iburst
- path: /etc/sysctl.d/99-custom.conf
content: |
# IP forwarding for containers/k8s
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
# Increase inotify limits for containers
fs.inotify.max_user_watches = 524288
fs.inotify.max_user_instances = 512
# Network performance tuning
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
# Security hardening
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
- path: /etc/security/limits.d/99-custom.conf
content: |
# Increase limits for containers
* soft nofile 65536
* hard nofile 65536
* soft nproc 32768
* hard nproc 32768
# Configure firewall (ufw)
ufw:
enabled: true
rules:
- port: 22
protocol: tcp
rule: allow
comment: SSH
- port: 80
protocol: tcp
rule: allow
comment: HTTP
- port: 443
protocol: tcp
rule: allow
comment: HTTPS
# Run commands on first boot
runcmd:
# Configure fail2ban
- systemctl enable fail2ban
- systemctl start fail2ban
# Enable and configure Docker
- systemctl enable docker
- systemctl start docker
- usermod -aG docker graham
- usermod -aG docker andy
- usermod -aG docker ansible
# Apply sysctl settings
- sysctl -p /etc/sysctl.d/99-custom.conf
# Configure chrony
- systemctl restart chrony
# Secure shared memory
- echo "tmpfs /run/shm tmpfs defaults,noexec,nosuid 0 0" >> /etc/fstab
- mount -o remount /run/shm
# Create common directories
- mkdir -p /opt/scripts /opt/configs
# Set proper permissions
- chmod 644 /etc/issue.net
- chmod 440 /etc/sudoers.d/90-cloud-init-users
# Update locate database
- updatedb
# Configure automatic updates
apt:
preserve_sources_list: true
conf: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";
# Power state change timeout
power_state:
delay: now
mode: reboot
message: Initial configuration complete, rebooting...
timeout: 30
condition: true

View file

@ -0,0 +1,225 @@
#cloud-config
# Cloud-init configuration for Debian 13 Vultr server with Caddy proxy and Tailscale
# Update and upgrade packages on first boot
package_update: true
package_upgrade: true
# Configure hostname
hostname: caddy
# Configure timezone
timezone: America/Chicago
# Install packages
packages:
# Include all base packages
- sudo
- curl
- wget
- git
- vim
- tmux
- htop
- rsync
- net-tools
- dnsutils
- build-essential
- python3
- python3-pip
- apt-transport-https
- ca-certificates
- gnupg
- lsb-release
- software-properties-common
- fail2ban
- ufw
# Caddy and web server tools
- debian-keyring
- debian-archive-keyring
# Create users with sudo access
users:
- name: graham
groups: sudo
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_import_id:
- gh:gmcintire
- name: andy
groups: sudo
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_import_id:
- gh:nsnw
- name: ansible
uid: 10001
groups: sudo
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC5aTtW3lgF9PfV8sTIxZfbvo6dUBMq5C5zJrXy6mwdb5UK9xvqty5Vlh6F1FqYlZgvIyR7NqLbXnKqsvLVGdkTm9VEXGlNcbPJ3ilbhc3OJPX0k63CwokU6nSztXguE1v3KB7JjXiM0dj7ujuDzAGfPZNClR6u06NjIamzeWcQnsJV3P8rLxa7ppvFgBp0K1N1pMb3AUqXOutsOUjSvs5C4H+GMmKYCYNpPGbfKNDLfB9vYLPYv/ynF1lDDzBDL6gy0PnDX6lXC9jvfPWSKNu5lgIUc6p9J++I/ERGktbGPvEaLPeJjgIJPbVLwvnmCLDj2VBmvu2cVE+oXWjLJLF7y8T1cZA0d7aH1TWHcaFPNd3nsPbWGOA3PtMoNRLN/6b7mA7QAKCEBVcR1cZ8c5wQ7ilcLETrNBmJLPpBQa7tLvCx8Q8KPNmtZX6dMKR1o7yj5mXq7Slzo6RTdKLpDqODPVPmGboChMQWFEvGCKxVYLDuvS8iXHdkRE+C9xwqNis= ansible
# Configure SSH
ssh_pwauth: false
disable_root: true
# Write configuration files
write_files:
- path: /etc/ssh/sshd_config.d/99-custom.conf
content: |
PermitRootLogin no
PasswordAuthentication no
Banner /etc/issue.net
- path: /etc/issue.net
content: |
******************************************************************
* Authorized access only. All activity is monitored. *
******************************************************************
- path: /etc/apt/sources.list.d/caddy-stable.list
content: |
deb [signed-by=/usr/share/keyrings/caddy-stable-archive-keyring.gpg] https://dl.cloudsmith.io/public/caddy/stable/deb/debian any-version main
- path: /etc/caddy/Caddyfile
content: |
# Global options
{
# Email for Let's Encrypt
email admin@example.com
# Enable the admin endpoint
admin off
}
# Default site - return 404 for undefined hosts
:80 {
respond 404
}
# Example reverse proxy configuration
# example.com {
# reverse_proxy localhost:8080
# }
permissions: '0644'
owner: root:root
- path: /etc/sysctl.d/99-tailscale.conf
content: |
# Tailscale UDP GRO fix
net.ipv4.udp_rmem_min = 131072
net.ipv4.udp_wmem_min = 131072
net.core.rmem_default = 131072
net.core.wmem_default = 131072
net.core.netdev_max_backlog = 5000
- path: /opt/scripts/install-tailscale.sh
content: |
#!/bin/bash
# Install Tailscale
curl -fsSL https://pkgs.tailscale.com/stable/debian/bookworm.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null
curl -fsSL https://pkgs.tailscale.com/stable/debian/bookworm.tailscale-keyring.list | tee /etc/apt/sources.list.d/tailscale.list
apt-get update
apt-get install -y tailscale
# Enable and start tailscaled
systemctl enable tailscaled
systemctl start tailscaled
# If TAILSCALE_KEY is provided, authenticate
if [ -n "$TAILSCALE_KEY" ]; then
tailscale up --authkey="$TAILSCALE_KEY" --accept-routes --hostname=$(hostname)
fi
permissions: '0755'
- path: /opt/scripts/setup-caddy.sh
content: |
#!/bin/bash
# Ensure Caddy starts after network is ready
mkdir -p /etc/systemd/system/caddy.service.d
cat > /etc/systemd/system/caddy.service.d/override.conf <<EOF
[Unit]
After=network-online.target
Wants=network-online.target
[Service]
# Restart on failure
Restart=on-failure
RestartSec=5s
EOF
systemctl daemon-reload
systemctl enable caddy
systemctl start caddy
permissions: '0755'
# Configure firewall
ufw:
enabled: true
rules:
- port: 22
protocol: tcp
rule: allow
comment: SSH
- port: 80
protocol: tcp
rule: allow
comment: HTTP
- port: 443
protocol: tcp
rule: allow
comment: HTTPS
- port: 41641
protocol: udp
rule: allow
from_any: true
comment: Tailscale
# Run commands on first boot
runcmd:
# Add Caddy GPG key and install
- curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
- apt-get update
- apt-get install -y caddy
# Apply sysctl settings
- sysctl -p /etc/sysctl.d/99-tailscale.conf
# Create Caddy directories
- mkdir -p /etc/caddy/sites-enabled
- chown -R caddy:caddy /etc/caddy
# Install Tailscale
- /opt/scripts/install-tailscale.sh
# Setup Caddy service
- /opt/scripts/setup-caddy.sh
# Configure fail2ban for Caddy
- |
cat > /etc/fail2ban/jail.d/caddy.conf <<EOF
[caddy-status]
enabled = true
filter = caddy-status
logpath = /var/log/caddy/access.log
maxretry = 5
bantime = 600
EOF
# Enable services
- systemctl enable fail2ban
- systemctl start fail2ban
# Final message
final_message: "Debian 13 Caddy proxy setup complete! System ready after $UPTIME seconds"
# Power state - don't reboot automatically for proxy servers
power_state:
delay: now
mode: poweroff
message: Initial configuration complete
condition: false

View file

@ -0,0 +1,38 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: adguard
namespace: adguard
spec:
replicas: 1
selector:
matchLabels:
app: adguard
template:
metadata:
labels:
app: adguard
spec:
containers:
- name: adguard
image: adguard/adguardhome:latest
ports:
- containerPort: 3000 # Web UI
name: web
- containerPort: 53 # DNS
name: dns
protocol: UDP
- containerPort: 53 # DNS TCP
name: dns-tcp
protocol: TCP
- containerPort: 80 # HTTP
name: http
- containerPort: 443 # HTTPS
name: https
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"

View file

@ -0,0 +1,50 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: adguard
namespace: adguard
spec:
replicas: 1
selector:
matchLabels:
app: adguard
template:
metadata:
labels:
app: adguard
spec:
containers:
- name: adguard
image: adguard/adguardhome:latest
ports:
- containerPort: 3000 # Web UI
name: web
- containerPort: 53 # DNS
name: dns
protocol: UDP
- containerPort: 53 # DNS TCP
name: dns-tcp
protocol: TCP
- containerPort: 80 # HTTP
name: http
- containerPort: 443 # HTTPS
name: https
volumeMounts:
- name: config
mountPath: /opt/adguardhome/conf
- name: data
mountPath: /opt/adguardhome/work
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: config
persistentVolumeClaim:
claimName: adguard-config-rbd
- name: data
persistentVolumeClaim:
claimName: adguard-data-rbd

View file

@ -0,0 +1,36 @@
apiVersion: batch/v1
kind: Job
metadata:
name: adguard-data-migration
namespace: adguard
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: busybox:1.36
command:
- sh
- -c
- |
echo "Starting AdGuard data migration..."
if [ -d /old-data/AdGuardHome ]; then
echo "Copying AdGuard data..."
cp -av /old-data/* /new-data/
else
echo "No data to migrate"
fi
echo "Migration complete!"
volumeMounts:
- name: old-data
mountPath: /old-data
- name: new-data
mountPath: /new-data
volumes:
- name: old-data
persistentVolumeClaim:
claimName: adguard-data
- name: new-data
persistentVolumeClaim:
claimName: adguard-data-rbd

View file

@ -1,5 +1,4 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: tailscale
name: adguard

View file

@ -1,28 +1,25 @@
apiVersion: v1
kind: PersistentVolume
kind: PersistentVolumeClaim
metadata:
name: portal-pv
namespace: portal
labels:
type: local
name: adguard-config
namespace: adguard
spec:
storageClassName: local-path
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/var/www/data/storage"
storageClassName: local-path
resources:
requests:
storage: 2Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: portal-pv-claim
namespace: portal
name: adguard-data
namespace: adguard
spec:
storageClassName: local-path
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 1Gi
storage: 10Gi

View file

@ -0,0 +1,25 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: adguard-config-rbd
namespace: adguard
spec:
accessModes:
- ReadWriteOnce
storageClassName: ceph-rbd
resources:
requests:
storage: 2Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: adguard-data-rbd
namespace: adguard
spec:
accessModes:
- ReadWriteOnce
storageClassName: ceph-rbd
resources:
requests:
storage: 10Gi

View file

@ -0,0 +1,25 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: adguard-config
namespace: adguard
spec:
accessModes:
- ReadWriteOnce
storageClassName: ceph-rbd
resources:
requests:
storage: 2Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: adguard-data
namespace: adguard
spec:
accessModes:
- ReadWriteOnce
storageClassName: ceph-rbd
resources:
requests:
storage: 10Gi

View file

@ -0,0 +1,22 @@
apiVersion: v1
kind: Service
metadata:
name: adguard
namespace: adguard
spec:
type: LoadBalancer
selector:
app: adguard
ports:
- port: 80
targetPort: 3000
protocol: TCP
name: web
- port: 53
targetPort: 53
protocol: UDP
name: dns-udp
- port: 53
targetPort: 53
protocol: TCP
name: dns-tcp

Some files were not shown because too many files have changed in this diff Show more