add Terraform + Ansible infrastructure for GCP deployment

Provisions e2-micro VM (us-east1-b, free tier) with static IP, SSH and
web firewall rules, Docker + Caddy startup script, and IAM bindings for
Firestore and GCS access via ADC. Imports existing drb-calls bucket and
c2-server Firestore database into state. Ansible roles handle first-time
setup (swap, docker group) and all subsequent deploys via rsync + docker
compose, with secrets managed via Ansible Vault. DNS stays on AWS Route 53.
This commit is contained in:
Logan
2026-06-22 02:03:36 -04:00
parent 4295bdf4d2
commit 33700448bf
17 changed files with 461 additions and 25 deletions
+15
View File
@@ -0,0 +1,15 @@
---
# Lightweight update deploy — runs in ~60s.
# Use this for every code push after the initial site.yml run.
#
# Usage:
# ansible-playbook -i inventory.ini deploy.yml --ask-vault-pass
- name: Deploy DRB update
hosts: drb
become: true
vars_files:
- vault.yml
roles:
- deploy
+9
View File
@@ -0,0 +1,9 @@
# Copy to group_vars/all.yml — safe to commit (no secrets here).
domain: example.com # must match Terraform var.domain
app_dir: /opt/drb
ssh_user: drb
# Path to the local repo root on your machine (used for rsync).
# Trailing slash is intentional — rsync copies contents, not the folder itself.
local_repo_path: "/path/to/Version 5C/Server/"
+8
View File
@@ -0,0 +1,8 @@
# Copy to inventory.ini and replace SERVER_IP with the Terraform output.
# Get it with: cd ../terraform && terraform output server_ip
[drb]
SERVER_IP ansible_user=drb ansible_ssh_private_key_file=~/.ssh/id_ed25519
[drb:vars]
ansible_python_interpreter=/usr/bin/python3
+86
View File
@@ -0,0 +1,86 @@
---
# Sync code, write secrets, bring the stack up.
- name: Sync app code from local machine
synchronize:
src: "{{ local_repo_path }}"
dest: "{{ app_dir }}/"
delete: true
recursive: true
rsync_opts:
- "--exclude=.git"
- "--exclude=**/__pycache__"
- "--exclude=**/.env"
- "--exclude=**/gcp-key.json"
- "--exclude=**/node_modules"
- "--exclude=drb-c2-core/gcp-key.json"
- "--exclude=infra/"
become: false # rsync runs as the SSH user, not root
- name: Set ownership of app directory
file:
path: "{{ app_dir }}"
state: directory
owner: "{{ ssh_user }}"
group: "{{ ssh_user }}"
recurse: true
# No gcp-key.json needed — the VM authenticates to GCS/Firestore via ADC
# (GCE metadata server). IAM roles are granted by Terraform.
- name: Template top-level .env (docker-compose MQTT creds)
template:
src: root.env.j2
dest: "{{ app_dir }}/.env"
owner: "{{ ssh_user }}"
group: "{{ ssh_user }}"
mode: "0600"
- name: Template c2-core .env
template:
src: c2-core.env.j2
dest: "{{ app_dir }}/drb-c2-core/.env"
owner: "{{ ssh_user }}"
group: "{{ ssh_user }}"
mode: "0600"
- name: Template discord-bot .env
template:
src: discord-bot.env.j2
dest: "{{ app_dir }}/drb-server-discord-bot/.env"
owner: "{{ ssh_user }}"
group: "{{ ssh_user }}"
mode: "0600"
- name: Template frontend .env
template:
src: frontend.env.j2
dest: "{{ app_dir }}/drb-frontend/.env"
owner: "{{ ssh_user }}"
group: "{{ ssh_user }}"
mode: "0600"
- name: Deploy Caddyfile
template:
src: Caddyfile.j2
dest: /etc/caddy/Caddyfile
owner: root
group: root
mode: "0644"
notify: Reload Caddy
- name: Bring the stack up (builds images if changed)
community.docker.docker_compose_v2:
project_src: "{{ app_dir }}"
build: always
state: present
pull: never
become: true
environment:
DOCKER_BUILDKIT: "1"
- name: Prune unused Docker images
community.docker.docker_prune:
images: true
images_filters:
dangling: true
@@ -0,0 +1,13 @@
# Managed by Ansible — do not edit manually on the server.
api.{{ domain }} {
reverse_proxy localhost:8888 {
header_up X-Forwarded-For {remote_host}
}
}
app.{{ domain }} {
reverse_proxy localhost:3000 {
header_up X-Forwarded-For {remote_host}
}
}
@@ -0,0 +1,20 @@
# drb-c2-core environment — Managed by Ansible. Do not edit manually.
MQTT_BROKER=mosquitto
MQTT_PORT=1883
MQTT_USER={{ vault_mqtt_c2_user }}
MQTT_PASS={{ vault_mqtt_c2_pass }}
# No GCP_CREDENTIALS_PATH — the VM uses Application Default Credentials
# via the GCE metadata server. The Terraform IAM bindings grant the required roles.
FIRESTORE_DATABASE={{ vault_firestore_database }}
GCS_BUCKET={{ vault_gcs_bucket }}
OPENAI_API_KEY={{ vault_openai_api_key }}
GOOGLE_MAPS_API_KEY={{ vault_google_maps_api_key }}
GEMINI_API_KEY={{ vault_gemini_api_key }}
SERVICE_KEY={{ vault_service_key }}
NODE_API_KEY={{ vault_node_api_key }}
CORS_ORIGINS=["https://app.{{ domain }}"]
@@ -0,0 +1,5 @@
# drb-server-discord-bot environment — Managed by Ansible. Do not edit manually.
DISCORD_TOKEN={{ vault_discord_token }}
C2_URL=http://c2-core:8000
C2_SERVICE_KEY={{ vault_service_key }}
@@ -0,0 +1,11 @@
# drb-frontend environment — Managed by Ansible. Do not edit manually.
NEXT_PUBLIC_C2_URL=https://api.{{ domain }}
NEXT_PUBLIC_FIREBASE_API_KEY={{ vault_firebase_api_key }}
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN={{ vault_firebase_auth_domain }}
NEXT_PUBLIC_FIREBASE_PROJECT_ID={{ vault_firebase_project_id }}
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET={{ vault_firebase_storage_bucket }}
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID={{ vault_firebase_messaging_sender_id }}
NEXT_PUBLIC_FIREBASE_APP_ID={{ vault_firebase_app_id }}
NEXT_PUBLIC_FIRESTORE_DATABASE={{ vault_firestore_database }}
@@ -0,0 +1,7 @@
# Top-level docker-compose environment — MQTT credentials for the broker container.
# Managed by Ansible. Do not edit manually.
MQTT_C2_USER={{ vault_mqtt_c2_user }}
MQTT_C2_PASS={{ vault_mqtt_c2_pass }}
MQTT_NODE_USER={{ vault_mqtt_node_user }}
MQTT_NODE_PASS={{ vault_mqtt_node_pass }}
+73
View File
@@ -0,0 +1,73 @@
---
# Full first-time setup: waits for the VM's startup.sh to finish installing
# Docker, then deploys the stack. Safe to re-run — all tasks are idempotent.
#
# Usage:
# ansible-playbook -i inventory.ini site.yml --ask-vault-pass
- name: Bootstrap + deploy DRB server
hosts: drb
become: true
vars_files:
- vault.yml
pre_tasks:
- name: Wait for Docker (startup.sh runs async on first boot)
command: docker info
register: _docker
until: _docker.rc == 0
retries: 30
delay: 10
changed_when: false
- name: Create 2 GB swap file
command: fallocate -l 2G /swapfile
args:
creates: /swapfile
- name: Set swap file permissions
file:
path: /swapfile
mode: "0600"
- name: Format swap file
command: mkswap /swapfile
register: _mkswap
changed_when: _mkswap.rc == 0
- name: Enable swap
command: swapon /swapfile
register: _swapon
failed_when: _swapon.rc != 0 and 'already' not in _swapon.stderr
changed_when: _swapon.rc == 0
- name: Persist swap in fstab
lineinfile:
path: /etc/fstab
line: "/swapfile none swap sw 0 0"
state: present
- name: Set swappiness to 10 (use swap only under pressure)
sysctl:
name: vm.swappiness
value: "10"
sysctl_set: true
state: present
reload: true
- name: Add deploy user to docker group
user:
name: "{{ ssh_user }}"
groups: docker
append: true
- name: Create app directory
file:
path: "{{ app_dir }}"
state: directory
owner: "{{ ssh_user }}"
group: "{{ ssh_user }}"
mode: "0755"
roles:
- deploy
+34
View File
@@ -0,0 +1,34 @@
# Template for your Ansible Vault secrets file.
# Copy to vault.yml, fill in values, then encrypt:
# ansible-vault encrypt vault.yml
# Edit later with:
# ansible-vault edit vault.yml
# ── MQTT ─────────────────────────────────────────────────────────────────────
vault_mqtt_c2_user: drb-c2-core
vault_mqtt_c2_pass: "CHANGE_ME"
vault_mqtt_node_user: drb-node
vault_mqtt_node_pass: "CHANGE_ME"
# ── C2 Core ───────────────────────────────────────────────────────────────────
vault_service_key: "" # openssl rand -hex 32
vault_node_api_key: "" # openssl rand -hex 32
vault_openai_api_key: ""
vault_google_maps_api_key: ""
vault_gemini_api_key: ""
vault_gcs_bucket: "your-gcs-bucket-name"
vault_firestore_database: "c2-server"
# ── Discord Bot ───────────────────────────────────────────────────────────────
vault_discord_token: ""
# ── Frontend (Firebase) ───────────────────────────────────────────────────────
vault_firebase_api_key: ""
vault_firebase_auth_domain: ""
vault_firebase_project_id: ""
vault_firebase_storage_bucket: ""
vault_firebase_messaging_sender_id: ""
vault_firebase_app_id: ""
# No GCP key needed — the VM uses Application Default Credentials via the
# GCE metadata server. Terraform grants the required IAM roles at apply time.