Initial infra attempt
This commit is contained in:
@@ -0,0 +1,46 @@
|
|||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
SERVER_IP: ${{ secrets.SERVER_IP }}
|
||||||
|
SSH_USER: drb
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy to VM
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Write SSH key
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.SSH_PRIVATE_KEY }}" > /tmp/deploy_key
|
||||||
|
chmod 600 /tmp/deploy_key
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
run: |
|
||||||
|
ssh -o StrictHostKeyChecking=no -i /tmp/deploy_key \
|
||||||
|
${{ env.SSH_USER }}@${{ env.SERVER_IP }} << 'ENDSSH'
|
||||||
|
set -e
|
||||||
|
cd /opt/drb
|
||||||
|
|
||||||
|
# Pull latest code
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# Rebuild and restart changed services
|
||||||
|
docker compose up -d --build --remove-orphans
|
||||||
|
|
||||||
|
# Reload Caddy if Caddyfile changed
|
||||||
|
sudo systemctl reload caddy
|
||||||
|
|
||||||
|
# Clean up old images
|
||||||
|
docker image prune -f
|
||||||
|
ENDSSH
|
||||||
|
|
||||||
|
- name: Verify health
|
||||||
|
run: |
|
||||||
|
sleep 15
|
||||||
|
curl -f https://api.${{ secrets.DRB_DOMAIN }}/health || \
|
||||||
|
(echo "Health check failed" && exit 1)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Production overrides — used on the VM.
|
||||||
|
# Run with: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
||||||
|
#
|
||||||
|
# Differences from dev:
|
||||||
|
# - MQTT port 1883 is NOT published to the host (stays on the Docker bridge).
|
||||||
|
# Edge nodes reach it via WireGuard tunnel to the Docker bridge IP.
|
||||||
|
# - c2-core and frontend ports are only bound to localhost (Caddy proxies them).
|
||||||
|
# - restart: always (instead of unless-stopped) for hard reboots.
|
||||||
|
|
||||||
|
services:
|
||||||
|
mosquitto:
|
||||||
|
restart: always
|
||||||
|
ports: !reset [] # Remove the dev 1883:1883 mapping — internal only
|
||||||
|
|
||||||
|
c2-core:
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8888:8000" # Caddy proxies, not exposed publicly
|
||||||
|
|
||||||
|
discord-bot:
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3000:3000" # Caddy proxies, not exposed publicly
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Managed by CI — deployed to /etc/caddy/Caddyfile on the server.
|
||||||
|
# Caddy handles TLS automatically via Let's Encrypt.
|
||||||
|
|
||||||
|
api.{$DRB_DOMAIN} {
|
||||||
|
reverse_proxy localhost:8888 {
|
||||||
|
header_up X-Forwarded-For {remote_host}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.{$DRB_DOMAIN} {
|
||||||
|
reverse_proxy localhost:3000 {
|
||||||
|
header_up X-Forwarded-For {remote_host}
|
||||||
|
}
|
||||||
|
}
|
||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.6"
|
||||||
|
required_providers {
|
||||||
|
google = {
|
||||||
|
source = "hashicorp/google"
|
||||||
|
version = "~> 5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Store state in GCS — create the bucket manually once before first apply
|
||||||
|
backend "gcs" {
|
||||||
|
bucket = "drb-tf-state"
|
||||||
|
prefix = "drb/state"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "google" {
|
||||||
|
project = var.project_id
|
||||||
|
region = var.region
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Static external IP
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
resource "google_compute_address" "drb" {
|
||||||
|
name = "drb-server-ip"
|
||||||
|
region = var.region
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Firewall rules
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
resource "google_compute_firewall" "allow_web" {
|
||||||
|
name = "drb-allow-web"
|
||||||
|
network = "default"
|
||||||
|
|
||||||
|
allow {
|
||||||
|
protocol = "tcp"
|
||||||
|
ports = ["80", "443"]
|
||||||
|
}
|
||||||
|
|
||||||
|
source_ranges = ["0.0.0.0/0"]
|
||||||
|
target_tags = ["drb-server"]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_compute_firewall" "allow_ssh" {
|
||||||
|
name = "drb-allow-ssh"
|
||||||
|
network = "default"
|
||||||
|
|
||||||
|
allow {
|
||||||
|
protocol = "tcp"
|
||||||
|
ports = ["22"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Restrict SSH to your IP(s) and Gitea runner IP
|
||||||
|
source_ranges = var.allowed_ssh_cidrs
|
||||||
|
target_tags = ["drb-server"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# MQTT is NOT exposed externally — edge nodes connect via WireGuard (see below)
|
||||||
|
# If you need to temporarily allow direct MQTT access for testing, uncomment and
|
||||||
|
# restrict source_ranges to your node IPs.
|
||||||
|
#
|
||||||
|
# resource "google_compute_firewall" "allow_mqtt" {
|
||||||
|
# name = "drb-allow-mqtt"
|
||||||
|
# network = "default"
|
||||||
|
# allow {
|
||||||
|
# protocol = "tcp"
|
||||||
|
# ports = ["8883"] # TLS MQTT, not 1883
|
||||||
|
# }
|
||||||
|
# source_ranges = ["YOUR_NODE_CIDR"]
|
||||||
|
# target_tags = ["drb-server"]
|
||||||
|
# }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Compute Engine VM
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
resource "google_compute_instance" "drb_server" {
|
||||||
|
name = "drb-server"
|
||||||
|
machine_type = var.machine_type
|
||||||
|
zone = var.zone
|
||||||
|
tags = ["drb-server"]
|
||||||
|
|
||||||
|
boot_disk {
|
||||||
|
initialize_params {
|
||||||
|
image = "debian-cloud/debian-12"
|
||||||
|
size = 30 # GB — enough for Docker images + mosquitto data
|
||||||
|
type = "pd-balanced"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
network_interface {
|
||||||
|
network = "default"
|
||||||
|
access_config {
|
||||||
|
nat_ip = google_compute_address.drb.address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
ssh-keys = "${var.ssh_user}:${var.ssh_public_key}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Startup script runs once on first boot to install Docker + Caddy
|
||||||
|
metadata_startup_script = file("${path.module}/startup.sh")
|
||||||
|
|
||||||
|
# Allow the VM to pull from Artifact Registry using its service account
|
||||||
|
service_account {
|
||||||
|
scopes = ["cloud-platform"]
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
# Prevent Terraform from destroying + recreating on metadata changes
|
||||||
|
ignore_changes = [metadata_startup_script]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cloud DNS records
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
resource "google_dns_record_set" "app" {
|
||||||
|
name = "app.${var.domain}."
|
||||||
|
type = "A"
|
||||||
|
ttl = 300
|
||||||
|
managed_zone = var.dns_zone_name
|
||||||
|
rrdatas = [google_compute_address.drb.address]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_dns_record_set" "api" {
|
||||||
|
name = "api.${var.domain}."
|
||||||
|
type = "A"
|
||||||
|
ttl = 300
|
||||||
|
managed_zone = var.dns_zone_name
|
||||||
|
rrdatas = [google_compute_address.drb.address]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
output "server_ip" {
|
||||||
|
value = google_compute_address.drb.address
|
||||||
|
description = "Static external IP of the DRB server VM"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_url" {
|
||||||
|
value = "https://app.${var.domain}"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "api_url" {
|
||||||
|
value = "https://api.${var.domain}"
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Runs once on first VM boot. Installs Docker, Docker Compose, and Caddy.
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
# ── Docker ────────────────────────────────────────────────────────────────────
|
||||||
|
apt-get update -y
|
||||||
|
apt-get install -y ca-certificates curl gnupg lsb-release
|
||||||
|
|
||||||
|
install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||||
|
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||||
|
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
||||||
|
https://download.docker.com/linux/debian $(lsb_release -cs) stable" \
|
||||||
|
> /etc/apt/sources.list.d/docker.list
|
||||||
|
|
||||||
|
apt-get update -y
|
||||||
|
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
|
||||||
|
systemctl enable docker
|
||||||
|
systemctl start docker
|
||||||
|
|
||||||
|
# Allow drb user to run docker
|
||||||
|
usermod -aG docker drb 2>/dev/null || true
|
||||||
|
|
||||||
|
# ── Caddy (reverse proxy + auto TLS) ─────────────────────────────────────────
|
||||||
|
apt-get install -y debian-keyring debian-archive-keyring apt-transport-https
|
||||||
|
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
|
||||||
|
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||||
|
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
|
||||||
|
> /etc/apt/sources.list.d/caddy-stable.list
|
||||||
|
apt-get update -y
|
||||||
|
apt-get install -y caddy
|
||||||
|
|
||||||
|
# ── App directory — clone repo so CI can git pull + docker compose up ─────────
|
||||||
|
apt-get install -y git
|
||||||
|
mkdir -p /opt/drb
|
||||||
|
# Repo is cloned here by initial setup; CI just git pulls and rebuilds.
|
||||||
|
# Set safe directory for the drb user
|
||||||
|
git config --global --add safe.directory /opt/drb
|
||||||
|
chown -R drb:drb /opt/drb 2>/dev/null || true
|
||||||
|
|
||||||
|
# ── Caddyfile placeholder (CI will write the real one on first deploy) ────────
|
||||||
|
cat > /etc/caddy/Caddyfile <<'CADDY'
|
||||||
|
# This file is managed by CI. Do not edit manually.
|
||||||
|
# It will be replaced on the first deployment.
|
||||||
|
:80 {
|
||||||
|
respond "DRB server — waiting for deployment" 200
|
||||||
|
}
|
||||||
|
CADDY
|
||||||
|
|
||||||
|
systemctl enable caddy
|
||||||
|
systemctl reload caddy
|
||||||
|
|
||||||
|
echo "Startup complete."
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
variable "project_id" {
|
||||||
|
description = "GCP project ID"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "region" {
|
||||||
|
description = "GCP region"
|
||||||
|
type = string
|
||||||
|
default = "us-central1"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "zone" {
|
||||||
|
description = "GCP zone"
|
||||||
|
type = string
|
||||||
|
default = "us-central1-a"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "domain" {
|
||||||
|
description = "Base domain (e.g. example.com)"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "dns_zone_name" {
|
||||||
|
description = "Cloud DNS managed zone name"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "machine_type" {
|
||||||
|
description = "Compute Engine machine type"
|
||||||
|
type = string
|
||||||
|
default = "e2-small"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_user" {
|
||||||
|
description = "SSH username for the VM"
|
||||||
|
type = string
|
||||||
|
default = "drb"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_public_key" {
|
||||||
|
description = "SSH public key to authorize on the VM"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "allowed_ssh_cidrs" {
|
||||||
|
description = "CIDR ranges allowed to SSH to the VM (your IP + Gitea runner)"
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user