commit 110021726b978b53e1598bd479ce4e88c91f01cf Author: Jason Ross Date: Wed Apr 1 18:20:33 2026 -0500 init diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..83c994e --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +GOOGLE_APPLICATION_CREDENTIALS="secret-path-to-your-service-account-key.json" +TF_VAR_gcp_project="your-gcp-project" +TF_VAR_gcp_region="us-east1" +TF_VAR_gcp_zone="b" \ No newline at end of file diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml new file mode 100644 index 0000000..40a002d --- /dev/null +++ b/.github/workflows/build-images.yml @@ -0,0 +1,41 @@ +name: 01 - Build and Push Images + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - "docker/**" + +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 GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Coturn image + uses: docker/build-push-action@v5 + with: + context: ./docker/coturn + push: true + tags: ghcr.io/${{ github.repository_owner }}/stoat-coturn:4.9.0-trixie + + - name: Build and push Caddy WAF image + uses: docker/build-push-action@v5 + with: + context: ./docker/caddy + push: true + tags: ghcr.io/${{ github.repository_owner }}/stoat-caddy-waf:latest diff --git a/.github/workflows/deploy-stack.yml b/.github/workflows/deploy-stack.yml new file mode 100644 index 0000000..0e7ef99 --- /dev/null +++ b/.github/workflows/deploy-stack.yml @@ -0,0 +1,129 @@ +name: 02 - Deploy Infrastructure and Stack + +on: + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' + AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + TF_VAR_gcp_project: ${{ vars.GCP_PROJECT || 'stoat-burrow' }} + TF_VAR_gcp_region: ${{ vars.GCP_REGION || 'us-west1' }} + TF_VAR_gcp_zone: ${{ vars.GCP_ZONE || 'us-west1-b' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Prepare SSH key for provisioning + shell: bash + run: | + install -m 700 -d "${HOME}/.ssh" + printf '%s\n' "${{ secrets.SSH_PRIVATE_KEY }}" > "${HOME}/.ssh/id_ed25519" + chmod 600 "${HOME}/.ssh/id_ed25519" + { + echo "TF_VAR_admin_ssh_public_key<> "${GITHUB_ENV}" + + - name: Prepare Google credentials file + shell: bash + run: | + credentials_file="${RUNNER_TEMP}/gcp-service-account.json" + printf '%s' "${{ secrets.GCP_SA_KEY }}" > "${credentials_file}" + chmod 600 "${credentials_file}" + echo "GOOGLE_APPLICATION_CREDENTIALS=${credentials_file}" >> "${GITHUB_ENV}" + + - name: Setup OpenTofu + uses: opentofu/setup-opentofu@v1 + + - name: Write OpenTofu backend config + working-directory: ./tofu + shell: bash + run: | + cat > backend.hcl <> "${GITHUB_ENV}" + + - name: Wait for SSH + shell: bash + run: | + for attempt in {1..30}; do + if nc -z -w 5 "${STATIC_IP}" 22; then + exit 0 + fi + sleep 10 + done + echo "SSH did not become reachable on ${STATIC_IP}" >&2 + exit 1 + + - name: Run Ansible Playbook + uses: dawidd6/action-ansible-playbook@v2 + with: + playbook: setup_host.yml + directory: ./ansible + key: ${{ secrets.SSH_PRIVATE_KEY }} + inventory: | + [turn_nodes] + ${{ env.STATIC_IP }} ansible_user=ubuntu + options: --ssh-common-args='-o StrictHostKeyChecking=no' + + - name: SCP compose and config files + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ env.STATIC_IP }} + username: ubuntu + key: ${{ secrets.SSH_PRIVATE_KEY }} + source: "compose/*" + target: "/opt/stoat-turn" + strip_components: 1 + + - name: Deploy Podman compose stack + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ env.STATIC_IP }} + username: ubuntu + key: ${{ secrets.SSH_PRIVATE_KEY }} + script: | + set -eu + cd /opt/stoat-turn + cat > .env <> "${conf_rendered}" + +if [ -n "${TURN_TLS_CERT_FILE:-}" ] && [ -n "${TURN_TLS_KEY_FILE:-}" ]; then + { + echo "tls-listening-port=${TURN_TLS_LISTENING_PORT:-5349}" + echo "cert=${TURN_TLS_CERT_FILE}" + echo "pkey=${TURN_TLS_KEY_FILE}" + } >> "${conf_rendered}" +fi + +set -- turnserver -n -c "${conf_rendered}" + +if [ -n "${EXTERNAL_IP:-}" ]; then + set -- "$@" --external-ip "${EXTERNAL_IP}" +fi + +exec "$@" diff --git a/tofu/backend.tf b/tofu/backend.tf new file mode 100644 index 0000000..51f0215 --- /dev/null +++ b/tofu/backend.tf @@ -0,0 +1,5 @@ +terraform { + required_version = ">= 1.11.5" + + backend "s3" {} +} diff --git a/tofu/main.tf b/tofu/main.tf new file mode 100644 index 0000000..3d18908 --- /dev/null +++ b/tofu/main.tf @@ -0,0 +1,60 @@ +provider "google" { + project = var.gcp_project + region = var.gcp_region +} + +resource "google_compute_address" "turn_static_ip" { + name = "stoat-turn-ip" + network_tier = "PREMIUM" + region = var.gcp_region +} + +resource "google_compute_instance" "turn_outpost" { + name = var.instance_name + machine_type = var.machine_type + zone = var.gcp_zone + + tags = ["webrtc-outpost", "caddy-web"] + + boot_disk { + initialize_params { + image = "projects/ubuntu-os-cloud/global/images/family/ubuntu-2404-lts-amd64" + size = 20 + } + } + + metadata = var.admin_ssh_public_key == "" ? {} : { + ssh-keys = "ubuntu:${var.admin_ssh_public_key}" + } + + network_interface { + network = var.network_name + + access_config { + nat_ip = google_compute_address.turn_static_ip.address + network_tier = "PREMIUM" + } + } +} + +resource "google_compute_firewall" "webrtc_rules" { + name = "allow-webrtc-and-web" + network = var.network_name + + allow { + protocol = "tcp" + ports = ["80", "443", "3478", "5349"] + } + + allow { + protocol = "udp" + ports = ["3478", "5349", "49152-65535"] + } + + target_tags = ["webrtc-outpost", "caddy-web"] +} + +output "relay_ip" { + description = "Public static IP for the relay." + value = google_compute_address.turn_static_ip.address +} diff --git a/tofu/variables.tf b/tofu/variables.tf new file mode 100644 index 0000000..7470f14 --- /dev/null +++ b/tofu/variables.tf @@ -0,0 +1,38 @@ +variable "gcp_project" { + description = "GCP project ID used for provider-scoped resources." + type = string +} + +variable "gcp_region" { + description = "GCP region used for regional resources." + type = string +} + +variable "gcp_zone" { + description = "GCP zone used for the relay instance." + type = string +} + +variable "network_name" { + description = "VPC network name." + type = string + default = "default" +} + +variable "instance_name" { + description = "Name of the relay VM." + type = string + default = "relay-main" +} + +variable "machine_type" { + description = "GCP machine type for the relay." + type = string + default = "e2-micro" +} + +variable "admin_ssh_public_key" { + description = "Optional SSH public key injected for the ubuntu user." + type = string + default = "" +} diff --git a/tofu/versions.tf b/tofu/versions.tf new file mode 100644 index 0000000..e037d41 --- /dev/null +++ b/tofu/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + google = { + source = "hashicorp/google" + version = "~> 7.26" + } + } +}