diff --git a/.gitignore b/.gitignore index 536d051..269081d 100644 --- a/.gitignore +++ b/.gitignore @@ -139,3 +139,7 @@ repopack.txt # Environment files .envrc + +# Docker local data +**/docker/**/.data/ +**/docker/**/.env diff --git a/Makefile b/Makefile index fc77484..e50f6e3 100644 --- a/Makefile +++ b/Makefile @@ -77,6 +77,12 @@ ssh-key: # Save SSH private key to file @echo "SSH key saved to uptime-kuma-key.pem" .PHONY: ssh-key +ssh-key-peekaping: # Save Peekaping SSH private key to file + terraform -chdir=$(TF_DIR) output -raw module.peekaping.ssh_private_key > peekaping-key.pem + chmod 600 peekaping-key.pem + @echo "SSH key saved to peekaping-key.pem" +.PHONY: ssh-key-peekaping + deploy-uptime: init-vendor # Deploy or update Uptime Kuma using Ansible @echo "Deploying Uptime Kuma..." @if [ ! -f ansible/inventory-uptime-kuma.ini ]; then \ @@ -86,6 +92,15 @@ deploy-uptime: init-vendor # Deploy or update Uptime Kuma using Ansible cd ansible && ansible-playbook -i inventory-uptime-kuma.ini playbooks/uptime-kuma.yaml .PHONY: deploy-uptime +deploy-peekaping: init-vendor # Deploy or update Peekaping using Ansible + @echo "Deploying Peekaping..." + @if [ ! -f ansible/inventory-peekaping.ini ]; then \ + echo "Error: Ansible inventory file not found. Run 'make apply' first to generate it."; \ + exit 1; \ + fi + cd ansible && ansible-playbook -i inventory-peekaping.ini playbooks/peekaping.yaml +.PHONY: deploy-peekaping + todo: # Show to-do items per file $(Q) grep \ --exclude=Makefile.util \ diff --git a/ansible/inventory-peekaping.ini b/ansible/inventory-peekaping.ini new file mode 100644 index 0000000..cef2e01 --- /dev/null +++ b/ansible/inventory-peekaping.ini @@ -0,0 +1,8 @@ +# Auto-generated by Terraform - DO NOT EDIT MANUALLY + +[peekaping] +uptime.ainsley.dev ansible_host=91.98.66.40 ansible_user=root + +[peekaping:vars] +domain=uptime.ainsley.dev +admin_email=hello@ainsley.dev diff --git a/ansible/playbooks/peekaping.yaml b/ansible/playbooks/peekaping.yaml new file mode 100644 index 0000000..b96a4e2 --- /dev/null +++ b/ansible/playbooks/peekaping.yaml @@ -0,0 +1,224 @@ +--- +- name: Deploy Peekaping with Docker and Nginx + hosts: all + become: true + vars: + # Variables to be passed via Terraform/cloud-init + # domain: uptime.ainsley.dev + # admin_email: hello@ainsley.dev + docker_port: 8383 + service_name: peekaping + data_mount_point: /mnt/peekaping + enable_https: true + # Pin to specific version for production stability + # Check releases: https://github.com/0xfurai/peekaping/releases + peekaping_version: latest + + pre_tasks: + - name: Update apt package cache + apt: + update_cache: yes + cache_valid_time: 3600 + + - name: Upgrade all packages + apt: + upgrade: dist + register: upgrade_result + + - name: Ensure python3-pip is installed + apt: + name: python3-pip + state: present + + - name: Reboot if kernel updated + reboot: + msg: 'Reboot initiated by Ansible due to package upgrade' + reboot_timeout: 600 + when: + - upgrade_result.changed + - skip_reboot is not defined or not skip_reboot + + roles: + - fail2ban + - docker + - nginx + - ufw + + tasks: + - name: Check if Hetzner volume is attached + stat: + path: "{{ data_mount_point }}" + register: volume_mount + + - name: Create data mount point directory + file: + path: "{{ data_mount_point }}" + state: directory + mode: '0755' + when: not volume_mount.stat.exists + + - name: Find Hetzner volume device + shell: | + lsblk -o NAME,SERIAL,MOUNTPOINT | grep -E "HC_Volume" | head -1 | awk '{print "/dev/" $1}' + register: volume_device + changed_when: false + failed_when: false + + - name: Format Hetzner volume if not already formatted + filesystem: + fstype: ext4 + dev: "{{ volume_device.stdout }}" + when: + - volume_device.stdout != "" + - not volume_mount.stat.exists + ignore_errors: yes + + - name: Mount Hetzner volume + mount: + path: "{{ data_mount_point }}" + src: "{{ volume_device.stdout }}" + fstype: ext4 + state: mounted + opts: defaults,nofail + when: volume_device.stdout != "" + ignore_errors: yes + + - name: Ensure mount point has correct permissions + file: + path: "{{ data_mount_point }}" + state: directory + mode: '0755' + owner: root + group: root + + - name: Create Peekaping deployment directory + file: + path: /opt/peekaping + state: directory + mode: '0755' + + - name: Copy docker-compose.yml to server + copy: + src: "{{ playbook_dir }}/../../docker/peekaping/docker-compose.yml" + dest: /opt/peekaping/docker-compose.yml + mode: '0644' + + - name: Replace DATA_PATH variable in docker-compose.yml + replace: + path: /opt/peekaping/docker-compose.yml + regexp: '\$\{DATA_PATH:-\.\/.data\/sqlite\}' + replace: '{{ data_mount_point }}' + + - name: Copy nginx.conf to server + copy: + src: "{{ playbook_dir }}/../../docker/peekaping/nginx.conf" + dest: /opt/peekaping/nginx.conf + mode: '0644' + + - name: Create .env file for Peekaping + copy: + content: | + # Peekaping Version + PEEKAPING_VERSION={{ peekaping_version }} + + # Database Configuration + DB_USER=root + DB_PASS={{ lookup('password', '/tmp/peekaping_db_pass chars=ascii_letters,digits length=32') }} + DB_NAME=/app/data/peekaping.db + DB_TYPE=sqlite + + # Server Configuration + SERVER_PORT=8034 + CLIENT_URL="https://{{ domain }}" + + # Application Settings + MODE=prod + TZ="Europe/London" + dest: /opt/peekaping/.env + mode: '0600' + + - name: Start Peekaping services + shell: | + cd /opt/peekaping + docker compose up -d + args: + executable: /bin/bash + + - name: Wait for Peekaping API to start + uri: + url: "http://localhost:{{ docker_port }}/api/v1/health" + method: GET + status_code: 200 + register: peekaping_health + retries: 30 + delay: 10 + until: peekaping_health.status == 200 + + - name: Disable default Nginx site + file: + path: /etc/nginx/sites-enabled/default + state: absent + + - name: Copy Nginx site configuration to server + copy: + src: "{{ playbook_dir }}/../../docker/peekaping/nginx-site.conf" + dest: /etc/nginx/sites-available/{{ domain }} + mode: '0644' + + - name: Replace domain placeholder in Nginx config + replace: + path: /etc/nginx/sites-available/{{ domain }} + regexp: 'DOMAIN_PLACEHOLDER' + replace: '{{ domain }}' + + - name: Enable Nginx site + file: + src: /etc/nginx/sites-available/{{ domain }} + dest: /etc/nginx/sites-enabled/{{ domain }} + state: link + + - name: Test Nginx configuration + command: nginx -t + changed_when: false + + - name: Reload Nginx + service: + name: nginx + state: reloaded + + - name: Run Certbot to get HTTPS certificate + include_role: + name: certbot + when: enable_https | default(true) | bool + + - name: Deploy SSL Nginx configuration after certificates are obtained + include_role: + name: nginx + vars: + skip_install: true + when: enable_https | default(true) | bool + + - name: Display success message + debug: + msg: | + Peekaping has been successfully deployed! + + Service Details: + ---------------- + Domain: {{ domain }} + External Port: {{ docker_port }} + Data Location: {{ data_mount_point }} + + Services Running: + - Redis (message broker) + - API (REST API server) + - Producer (job scheduler) + - Worker (monitoring executor) + - Ingester (result processor) + - Web (frontend SPA) + - Gateway (Nginx reverse proxy) + + Access your Peekaping instance at: + https://{{ domain }} + + Note: Ensure DNS A record for {{ domain }} points to this server's IP. diff --git a/docker/peekaping/.env.example b/docker/peekaping/.env.example new file mode 100644 index 0000000..3ae3a91 --- /dev/null +++ b/docker/peekaping/.env.example @@ -0,0 +1,30 @@ +# Peekaping Version +# Pin to a specific version for production stability, or use 'latest' for development +# Check releases: https://github.com/0xfurai/peekaping/releases +# Example: PEEKAPING_VERSION=v1.2.3 +PEEKAPING_VERSION=latest + +# Database Configuration +DB_USER=root +DB_PASS=your-secure-password-here +DB_NAME=/app/data/peekaping.db +DB_TYPE=sqlite + +# Server Configuration +SERVER_PORT=8034 +CLIENT_URL="http://localhost:8383" + +# Application Settings +MODE=prod +TZ="Europe/London" + +# Data Path (for production VM) +# Local dev: ./.data/sqlite (default in docker-compose) +# Production VM: /mnt/peekaping (set in Ansible) +DATA_PATH=./.data/sqlite + +# JWT settings are automatically managed in the database +# Default settings are initialized on first startup: +# - Access token expiration: 15 minutes +# - Refresh token expiration: 720 hours (30 days) +# - Secret keys are automatically generated securely diff --git a/docker/peekaping/README.md b/docker/peekaping/README.md new file mode 100644 index 0000000..4e8376a --- /dev/null +++ b/docker/peekaping/README.md @@ -0,0 +1,103 @@ +# Peekaping Local Development + +This directory contains Docker Compose configuration for running Peekaping locally. + +## Quick Start + +```bash +# Copy environment variables +cp .env.example .env + +# Start all services +docker compose up -d + +# Check status +docker compose ps + +# View logs +docker compose logs -f + +# Stop services +docker compose down +``` + +## Access + +Once running, Peekaping will be available at: +- **Web Interface**: http://localhost:8383 +- **API**: http://localhost:8383/api/ + +## Services + +- **redis**: Message broker for microservices +- **migrate**: Database migrations (runs once) +- **api**: REST API server (port 8034 internal) +- **producer**: Job scheduler for monitoring tasks +- **worker**: Executes monitoring checks +- **ingester**: Processes monitoring results +- **web**: Frontend SPA +- **gateway**: Nginx reverse proxy (port 8383 external) + +## Data Persistence + +Data is stored in `./.data/sqlite` directory. To reset: + +```bash +docker compose down -v +rm -rf .data +docker compose up -d +``` + +## Configuration + +Edit `.env` file to customize: +- **Version pinning** (`PEEKAPING_VERSION`) - Pin to specific version for stability +- Database settings +- Timezone +- Client URL +- Server port + +### Version Management + +```bash +# Production (recommended) +PEEKAPING_VERSION=v1.2.3 + +# Development +PEEKAPING_VERSION=latest +``` + +**Upgrading**: +```bash +# Update version in .env +echo "PEEKAPING_VERSION=v1.2.3" >> .env + +# Pull new images and restart +docker compose pull && docker compose up -d +``` + +Check releases: https://github.com/0xfurai/peekaping/releases + +## Troubleshooting + +### Check service health +```bash +docker compose ps +``` + +### View specific service logs +```bash +docker compose logs -f api +docker compose logs -f worker +``` + +### Restart a service +```bash +docker compose restart api +``` + +### Clean restart +```bash +docker compose down +docker compose up -d +``` diff --git a/docker/peekaping/docker-compose.yml b/docker/peekaping/docker-compose.yml new file mode 100644 index 0000000..202af4f --- /dev/null +++ b/docker/peekaping/docker-compose.yml @@ -0,0 +1,126 @@ +version: '3.8' + +networks: + appnet: + driver: bridge + +services: + redis: + image: redis:7 + restart: unless-stopped + networks: + - appnet + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 2s + retries: 5 + start_period: 5s + + migrate: + image: 0xfurai/peekaping-migrate:${PEEKAPING_VERSION:-latest} + restart: "no" + env_file: + - .env + volumes: + - ${DATA_PATH:-./.data/sqlite}:/app/data + + api: + image: 0xfurai/peekaping-api:${PEEKAPING_VERSION:-latest} + restart: unless-stopped + env_file: + - .env + volumes: + - ${DATA_PATH:-./.data/sqlite}:/app/data + depends_on: + redis: + condition: service_started + migrate: + condition: service_completed_successfully + networks: + - appnet + healthcheck: + test: + [ + "CMD-SHELL", + "wget -qO - http://localhost:8034/api/v1/health || exit 1", + ] + interval: 30s + timeout: 2s + retries: 5 + start_period: 5s + + producer: + image: 0xfurai/peekaping-producer:${PEEKAPING_VERSION:-latest} + restart: unless-stopped + env_file: + - .env + volumes: + - ${DATA_PATH:-./.data/sqlite}:/app/data + depends_on: + redis: + condition: service_healthy + migrate: + condition: service_completed_successfully + networks: + - appnet + + worker: + image: 0xfurai/peekaping-worker:${PEEKAPING_VERSION:-latest} + restart: unless-stopped + env_file: + - .env + depends_on: + redis: + condition: service_healthy + networks: + - appnet + + ingester: + image: 0xfurai/peekaping-ingester:${PEEKAPING_VERSION:-latest} + restart: unless-stopped + env_file: + - .env + volumes: + - ${DATA_PATH:-./.data/sqlite}:/app/data + depends_on: + redis: + condition: service_started + migrate: + condition: service_completed_successfully + networks: + - appnet + + web: + image: 0xfurai/peekaping-web:${PEEKAPING_VERSION:-latest} + depends_on: + api: + condition: service_healthy + networks: + - appnet + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:80 || exit 1"] + interval: 30s + timeout: 2s + retries: 5 + start_period: 5s + + gateway: + image: nginx:latest + ports: + - "8383:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + api: + condition: service_healthy + web: + condition: service_healthy + networks: + - appnet + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:80 || exit 1"] + interval: 30s + timeout: 2s + retries: 5 + start_period: 5s diff --git a/docker/peekaping/nginx-site.conf b/docker/peekaping/nginx-site.conf new file mode 100644 index 0000000..e14fa55 --- /dev/null +++ b/docker/peekaping/nginx-site.conf @@ -0,0 +1,26 @@ +server { + listen 80; + listen [::]:80; + server_name DOMAIN_PLACEHOLDER; + + # Proxy to Docker gateway + location / { + proxy_pass http://localhost:8383; + 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; + } + + # WebSocket support for Socket.io + location /socket.io/ { + proxy_pass http://localhost:8383; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + 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; + } +} diff --git a/docker/peekaping/nginx.conf b/docker/peekaping/nginx.conf new file mode 100644 index 0000000..e57a3ee --- /dev/null +++ b/docker/peekaping/nginx.conf @@ -0,0 +1,30 @@ +events {} +http { + upstream api { server api:8034; } + upstream web { server web:80; } + + server { + listen 80; + + # Pure API calls + location /api/ { + proxy_pass http://api; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + # socket.io + location /socket.io/ { + proxy_pass http://api; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Everything else → static SPA + location / { + proxy_pass http://web; + } + } +} diff --git a/terraform.tfvars.example b/terraform.tfvars.example index 898ebf7..3474c87 100644 --- a/terraform.tfvars.example +++ b/terraform.tfvars.example @@ -21,3 +21,13 @@ environment = "production" # Project name (used for resource naming) project_name = "ainsley-dev-platform" + +# ===== Service Domains ===== +# Configure domains for each monitoring service +# Make sure DNS A records point to the server IPs after deployment + +# Uptime Kuma monitoring service +uptime_kuma_domain = "old.ainsley.dev" + +# Peekaping monitoring service +peekaping_domain = "uptime.ainsley.dev" diff --git a/terraform/base/main.tf b/terraform/base/main.tf index 4866943..a962eca 100644 --- a/terraform/base/main.tf +++ b/terraform/base/main.tf @@ -5,6 +5,16 @@ module "uptime_kuma" { source = "../services/uptime-kuma" hetzner_token = var.hetzner_token + domain = var.uptime_kuma_domain + admin_email = var.admin_email + environment = var.environment +} + +module "peekaping" { + source = "../services/peekaping" + + hetzner_token = var.hetzner_token + domain = var.peekaping_domain admin_email = var.admin_email environment = var.environment } diff --git a/terraform/base/variables.tf b/terraform/base/variables.tf index a91a80a..201749c 100644 --- a/terraform/base/variables.tf +++ b/terraform/base/variables.tf @@ -26,3 +26,15 @@ variable "admin_email" { description = "Admin email for Let's Encrypt certificates" default = "hello@ainsley.dev" } + +# Service-specific domains +variable "uptime_kuma_domain" { + type = string + description = "Domain for Uptime Kuma monitoring service" + default = "uptime.ainsley.dev" +} + +variable "peekaping_domain" { + type = string + description = "Domain for Peekaping monitoring service" +} diff --git a/terraform/services/peekaping/README.md b/terraform/services/peekaping/README.md new file mode 100644 index 0000000..be6d307 --- /dev/null +++ b/terraform/services/peekaping/README.md @@ -0,0 +1,316 @@ +# Peekaping Service + +This Terraform module provisions infrastructure for Peekaping, a modern open-source uptime monitoring and status page service. + +## Overview + +Peekaping is deployed in microservice mode with the following components: +- **Redis**: Message broker for inter-service communication +- **API**: REST API server for backend operations +- **Producer**: Job scheduler for monitoring tasks +- **Worker**: Executes monitoring checks +- **Ingester**: Processes and stores monitoring results +- **Web**: Frontend SPA (Single Page Application) +- **Gateway**: Nginx reverse proxy for routing + +## Infrastructure + +### Hetzner Resources + +- **Server**: CX23 (2 vCPU, 4GB RAM, ~€5.83/month) +- **Location**: Nuremberg, Germany (nbg1) +- **Volume**: 10GB persistent storage for SQLite database +- **OS**: Ubuntu with Docker + +### Components + +1. **Hetzner VM**: Provisioned using WebKit's server module +2. **Persistent Volume**: 10GB ext4 volume for database storage +3. **Ansible Inventory**: Auto-generated for deployment + +## Configuration + +### Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `hetzner_token` | Hetzner Cloud API token | (required) | +| `service_name` | Name of the service | `peekaping` | +| `server_type` | Hetzner server type | `cx23` | +| `location` | Hetzner datacenter location | `nbg1` | +| `volume_size` | Data volume size in GB | `10` | +| `domain` | Domain for the instance | `peekaping.ainsley.dev` | +| `admin_email` | Admin email for SSL certs | `hello@ainsley.dev` | +| `environment` | Environment name | `production` | +| `tags` | Resource tags | `["peekaping", "monitoring"]` | + +### Customization + +Configure your domain and settings in `terraform.tfvars`: + +```hcl +# Required +hetzner_token = "your-token-here" + +# Service domains (customize these!) +peekaping_domain = "monitoring.yourdomain.com" + +# Optional overrides +admin_email = "admin@yourdomain.com" +environment = "production" +``` + +**Important**: After deployment, create a DNS A record pointing your domain to the server IP. + +## Deployment Workflow + +### 1. Initialize Terraform + +```bash +make init +``` + +This initializes Terraform with the Backblaze B2 backend. + +### 2. Plan Infrastructure Changes + +```bash +make plan +``` + +Review the planned changes before applying. + +### 3. Apply Infrastructure + +```bash +make apply +``` + +This will: +- Create the Hetzner VM +- Attach the persistent volume +- Generate Ansible inventory file + +### 4. Deploy Peekaping + +```bash +make deploy-peekaping +``` + +This runs the Ansible playbook which: +- Installs Docker and dependencies +- Configures firewall (UFW) +- Sets up fail2ban +- Mounts the Hetzner volume +- Deploys Peekaping via Docker Compose +- Configures Nginx reverse proxy +- Obtains SSL certificate (Let's Encrypt) + +### 5. Access Peekaping + +Once deployed, access your instance at: +``` +https://uptime.ainsley.dev +``` + +## Outputs + +| Output | Description | +|--------|-------------| +| `server_id` | Hetzner server ID | +| `ip_address` | Public IP address | +| `ssh_private_key` | SSH private key (sensitive) | +| `ssh_public_key` | SSH public key | +| `volume_id` | Hetzner volume ID | +| `domain` | Configured domain | +| `server_user` | SSH username | + +### Export SSH Key + +To SSH into the server: + +```bash +make ssh-key-peekaping +ssh -i peekaping-key.pem root@ +``` + +## Maintenance + +### Version Management + +Peekaping versions are controlled by the `PEEKAPING_VERSION` environment variable. + +**Check releases**: https://github.com/0xfurai/peekaping/releases + +**Best Practices**: +- ✅ Production: Pin to specific versions (e.g., `v1.2.3`) +- ✅ Development: Use `latest` to stay current +- ✅ Always backup database before upgrades +- ✅ Test new versions locally first + +### Upgrading Peekaping + +**Method 1: Via Ansible (Recommended)** + +```bash +# 1. Backup database first +ssh -i peekaping-key.pem root@ +cp /mnt/peekaping/peekaping.db /mnt/peekaping/backup-$(date +%Y%m%d).db + +# 2. Update version in ansible/playbooks/peekaping.yaml +peekaping_version: v1.2.3 + +# 3. Re-run deployment +make deploy-peekaping +``` + +**Method 2: Manual SSH Update** + +```bash +# SSH into the server +ssh -i peekaping-key.pem root@ + +# Update version in .env +cd /opt/peekaping +sed -i 's/PEEKAPING_VERSION=.*/PEEKAPING_VERSION=v1.2.3/' .env + +# Pull and restart +docker compose pull && docker compose up -d + +# Clean up old images +docker image prune -f +``` + +**Rollback**: Change version back to previous in `.env` and restart + +### View Logs + +```bash +# All services +docker compose logs -f + +# Specific service +docker compose logs -f api +docker compose logs -f worker +``` + +### Check Service Status + +```bash +docker compose ps +``` + +### Backup Database + +The SQLite database is stored on the Hetzner volume at `/mnt/peekaping/peekaping.db`. + +To backup: + +```bash +# SSH into server +ssh -i peekaping-key.pem root@ + +# Create backup +cp /mnt/peekaping/peekaping.db /mnt/peekaping/peekaping.db.backup-$(date +%Y%m%d) + +# Or download locally +scp -i peekaping-key.pem root@:/mnt/peekaping/peekaping.db ./peekaping-backup.db +``` + +## Troubleshooting + +### Services Not Starting + +Check logs for specific services: + +```bash +docker compose logs api +docker compose logs worker +``` + +### Health Check Failed + +Verify API is responding: + +```bash +curl http://localhost:8383/api/v1/health +``` + +### SSL Certificate Issues + +Re-run Certbot: + +```bash +certbot --nginx -d uptime.ainsley.dev +``` + +### Volume Not Mounted + +Check if volume is attached: + +```bash +lsblk +mount | grep peekaping +``` + +## Architecture Diagram + +``` + Internet + | + [HTTPS] + | + +----------v----------+ + | Nginx (Host) | + | SSL/TLS | + +----------+----------+ + | + +----------v----------+ + | Gateway Container | + | (Nginx) | + +----------+----------+ + | + +------------------+------------------+ + | | + +-------v--------+ +-------v--------+ + | API | | Web | + | (Port 8034) | | (SPA) | + +-------+--------+ +----------------+ + | + +-----------+------------+ + | | | ++-------v---+ +-----v-----+ +----v------+ +| Producer | | Ingester | | Worker | ++-----------+ +-----------+ +-----------+ + | | | + +-----------|------------+ + | + +-------v--------+ + | Redis | + +----------------+ + | + +-------v--------+ + | SQLite DB | + | /mnt/peekaping | + +----------------+ +``` + +## Cost Estimation + +- **Server (CX23)**: ~€5.83/month +- **Volume (10GB)**: ~€0.50/month +- **Total**: ~€6.33/month + +## Security + +- Firewall (UFW) configured to allow only necessary ports +- fail2ban installed for intrusion prevention +- SSL/TLS encryption via Let's Encrypt +- SSH key-based authentication +- Automated security updates + +## References + +- [Peekaping Documentation](https://docs.peekaping.com/) +- [Hetzner Cloud](https://www.hetzner.com/cloud) +- [WebKit Infrastructure](https://github.com/ainsleydev/webkit) diff --git a/terraform/services/peekaping/main.tf b/terraform/services/peekaping/main.tf new file mode 100644 index 0000000..92c2362 --- /dev/null +++ b/terraform/services/peekaping/main.tf @@ -0,0 +1,70 @@ +# Peekaping Service Module +# +# This module creates a Hetzner VM for Peekaping monitoring +# service using WebKit's server/volume modules. + +terraform { + required_providers { + hcloud = { + source = "hetznercloud/hcloud" + version = "~> 1.0" + } + local = { + source = "hashicorp/local" + version = "~> 2.0" + } + } +} + +provider "hcloud" { + token = var.hetzner_token +} + +# Hetzner Server (WebKit) +# +# This creates the VM, SSH keys, firewall, and installs Ansible via cloud-init. +module "server" { + source = "github.com/ainsleydev/webkit//platform/terraform/providers/hetzner/server?ref=main" + + name = var.service_name + server_type = var.server_type + location = var.location + tags = var.tags + ssh_key_ids = ["hello@ainsley.dev", "ainsley.clark@GGYN90GJW7"] +} + +# Hetzner Volume (Webkit) +# +# Uses persistent data to save the Peekaping SQLite database. +# Note: lifecycle blocks cannot be used on modules, only resources. +# Data protection is provided by state backups and approval gates in CI/CD. +module "volume" { + source = "github.com/ainsleydev/webkit//platform/terraform/providers/hetzner/volume?ref=main" + + name = "${var.service_name}-data" + size = var.volume_size + location = var.location + server_id = module.server.id + format = "ext4" + automount = true + tags = concat(var.tags, [var.environment]) +} + +# Ansible Config +# +# Generate Ansible inventory file automatically +resource "local_file" "ansible_inventory" { + filename = "${path.root}/../../ansible/inventory-peekaping.ini" + content = <<-EOT + # Auto-generated by Terraform - DO NOT EDIT MANUALLY + + [peekaping] + ${var.domain} ansible_host=${module.server.ip_address} ansible_user=${module.server.server_user} + + [peekaping:vars] + domain=${var.domain} + admin_email=${var.admin_email} + EOT + + file_permission = "0644" +} diff --git a/terraform/services/peekaping/outputs.tf b/terraform/services/peekaping/outputs.tf new file mode 100644 index 0000000..c87653c --- /dev/null +++ b/terraform/services/peekaping/outputs.tf @@ -0,0 +1,35 @@ +output "server_id" { + description = "The ID of the Hetzner server" + value = module.server.id +} + +output "ip_address" { + description = "The public IP address of the server" + value = module.server.ip_address +} + +output "ssh_private_key" { + description = "SSH private key for server access" + value = module.server.ssh_private_key + sensitive = true +} + +output "ssh_public_key" { + description = "SSH public key for server access" + value = module.server.ssh_public_key +} + +output "volume_id" { + description = "The ID of the Hetzner volume" + value = module.volume.id +} + +output "domain" { + description = "The domain for the Peekaping instance" + value = var.domain +} + +output "server_user" { + description = "SSH user for the server" + value = module.server.server_user +} diff --git a/terraform/services/peekaping/variables.tf b/terraform/services/peekaping/variables.tf new file mode 100644 index 0000000..6c98a5d --- /dev/null +++ b/terraform/services/peekaping/variables.tf @@ -0,0 +1,52 @@ +variable "hetzner_token" { + type = string + description = "Hetzner Cloud API token" + sensitive = true +} + +variable "service_name" { + type = string + description = "Name of the service" + default = "peekaping" +} + +variable "server_type" { + type = string + description = "Hetzner server type (CX23 = 2 vCPU, 4GB RAM, ~€5.83/month)" + default = "cx23" +} + +variable "location" { + type = string + description = "Hetzner datacenter location" + default = "nbg1" # Nuremberg, Germany +} + +variable "volume_size" { + type = number + description = "Size of the data volume in GB" + default = 10 +} + +variable "domain" { + type = string + description = "Domain for the Peekaping instance" +} + +variable "admin_email" { + type = string + description = "Admin email for Let's Encrypt certificates" + default = "hello@ainsley.dev" +} + +variable "environment" { + type = string + description = "Environment name" + default = "production" +} + +variable "tags" { + type = list(string) + description = "Tags to apply to resources" + default = ["peekaping", "monitoring"] +} diff --git a/terraform/services/uptime-kuma/outputs.tf b/terraform/services/uptime-kuma/outputs.tf index 5e88380..e69de29 100644 --- a/terraform/services/uptime-kuma/outputs.tf +++ b/terraform/services/uptime-kuma/outputs.tf @@ -1,71 +0,0 @@ -output "server_id" { - description = "Hetzner server ID" - value = module.server.id -} - -output "server_ip" { - description = "Public IPv4 address of the Uptime Kuma server" - value = module.server.ip_address -} - -output "ssh_private_key" { - description = "SSH private key for server access (generated by Terraform)" - value = module.server.ssh_private_key - sensitive = true -} - -output "ssh_public_key" { - description = "SSH public key" - value = module.server.ssh_public_key -} - -output "ssh_user" { - description = "SSH username for server access" - value = module.server.server_user -} - -output "volume_id" { - description = "Hetzner volume ID" - value = module.volume.id -} - -output "volume_device" { - description = "Volume device path on the server" - value = module.volume.linux_device -} - -output "domain" { - description = "Domain for Uptime Kuma instance" - value = var.domain -} - -output "ansible_inventory_path" { - description = "Path to the auto-generated Ansible inventory file" - value = local_file.ansible_inventory.filename -} - -output "access_instructions" { - description = "Instructions for accessing the server" - value = <<-EOT - Uptime Kuma Server Details: - --------------------------- - IP Address: ${module.server.ip_address} - Domain: ${var.domain} - SSH User: ${module.server.server_user} - - Ansible inventory file generated at: ${local_file.ansible_inventory.filename} - - To SSH into the server: - 1. Save the SSH private key: make ssh-key - 2. Connect: ssh -i uptime-kuma-key.pem ${module.server.server_user}@${module.server.ip_address} - - DNS Configuration Required: - --------------------------- - Add an A record for ${var.domain} pointing to ${module.server.ip_address} - - To deploy or update Uptime Kuma with Ansible: - make deploy-uptime - - Then access at: https://${var.domain} - EOT -}