feat: add Terraform config for GCP deployment
- GCE e2-small with Ubuntu 24.04 + Docker - Static IP, firewall rules, SSD boot disk - Startup script: installs Docker, clones repo, creates .env, starts compose - Outputs: IP, SSH command, API URL, bootstrap command, CLI setup - ~7$/month for always-on server
This commit is contained in:
parent
7b0a161d3d
commit
b6a94add67
5 changed files with 280 additions and 0 deletions
6
infra/.gitignore
vendored
Normal file
6
infra/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
*.tfstate
|
||||
*.tfstate.backup
|
||||
.terraform/
|
||||
.terraform.lock.hcl
|
||||
terraform.tfvars
|
||||
*.auto.tfvars
|
||||
137
infra/main.tf
Normal file
137
infra/main.tf
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
terraform {
|
||||
required_version = ">= 1.5"
|
||||
|
||||
required_providers {
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "google" {
|
||||
project = var.project_id
|
||||
region = var.region
|
||||
zone = var.zone
|
||||
}
|
||||
|
||||
# --- Network ---
|
||||
|
||||
resource "google_compute_firewall" "data_analyst" {
|
||||
name = "${var.instance_name}-allow-web"
|
||||
network = "default"
|
||||
|
||||
allow {
|
||||
protocol = "tcp"
|
||||
ports = ["22", "80", "443", "8000"]
|
||||
}
|
||||
|
||||
source_ranges = ["0.0.0.0/0"]
|
||||
target_tags = [var.instance_name]
|
||||
}
|
||||
|
||||
# --- Static IP ---
|
||||
|
||||
resource "google_compute_address" "data_analyst" {
|
||||
name = "${var.instance_name}-ip"
|
||||
region = var.region
|
||||
}
|
||||
|
||||
# --- Startup script ---
|
||||
|
||||
locals {
|
||||
startup_script = <<-SCRIPT
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
exec > /var/log/startup.log 2>&1
|
||||
|
||||
echo "=== Installing Docker ==="
|
||||
if ! command -v docker &> /dev/null; then
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
usermod -aG docker ${var.ssh_user}
|
||||
fi
|
||||
|
||||
# Install docker compose plugin
|
||||
if ! docker compose version &> /dev/null; then
|
||||
apt-get update && apt-get install -y docker-compose-plugin
|
||||
fi
|
||||
|
||||
echo "=== Cloning repository ==="
|
||||
APP_DIR="/opt/data-analyst"
|
||||
if [ ! -d "$APP_DIR" ]; then
|
||||
git clone https://github.com/padak/tmp_oss.git "$APP_DIR"
|
||||
cd "$APP_DIR"
|
||||
git checkout feature/v2-fastapi-duckdb-docker-cli
|
||||
else
|
||||
cd "$APP_DIR"
|
||||
git pull origin feature/v2-fastapi-duckdb-docker-cli || true
|
||||
fi
|
||||
|
||||
echo "=== Creating .env ==="
|
||||
cat > "$APP_DIR/.env" << 'ENVEOF'
|
||||
JWT_SECRET_KEY=${var.jwt_secret}
|
||||
DATA_DIR=/data
|
||||
DATA_SOURCE=${var.keboola_token != "" ? "keboola" : "local"}
|
||||
KEBOOLA_STORAGE_TOKEN=${var.keboola_token}
|
||||
KEBOOLA_STACK_URL=${var.keboola_stack_url}
|
||||
KEBOOLA_PROJECT_ID=${var.keboola_project_id}
|
||||
LOG_LEVEL=info
|
||||
ENVEOF
|
||||
# Strip leading whitespace from heredoc
|
||||
sed -i 's/^ //' "$APP_DIR/.env"
|
||||
chmod 600 "$APP_DIR/.env"
|
||||
|
||||
echo "=== Creating data directory ==="
|
||||
mkdir -p /data/state /data/analytics /data/src_data/parquet
|
||||
chown -R 1000:1000 /data
|
||||
|
||||
echo "=== Starting Docker Compose ==="
|
||||
cd "$APP_DIR"
|
||||
docker compose pull 2>/dev/null || true
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
|
||||
echo "=== Startup complete ==="
|
||||
docker compose ps
|
||||
SCRIPT
|
||||
}
|
||||
|
||||
# --- VM Instance ---
|
||||
|
||||
resource "google_compute_instance" "data_analyst" {
|
||||
name = var.instance_name
|
||||
machine_type = var.machine_type
|
||||
zone = var.zone
|
||||
|
||||
tags = [var.instance_name]
|
||||
|
||||
boot_disk {
|
||||
initialize_params {
|
||||
image = "ubuntu-os-cloud/ubuntu-2404-lts-amd64"
|
||||
size = var.disk_size_gb
|
||||
type = "pd-ssd"
|
||||
}
|
||||
}
|
||||
|
||||
network_interface {
|
||||
network = "default"
|
||||
access_config {
|
||||
nat_ip = google_compute_address.data_analyst.address
|
||||
}
|
||||
}
|
||||
|
||||
metadata = {
|
||||
ssh-keys = "${var.ssh_user}:${file(pathexpand(var.ssh_public_key_path))}"
|
||||
}
|
||||
|
||||
metadata_startup_script = local.startup_script
|
||||
|
||||
service_account {
|
||||
scopes = ["cloud-platform"]
|
||||
}
|
||||
|
||||
labels = {
|
||||
app = "data-analyst"
|
||||
managed = "terraform"
|
||||
}
|
||||
}
|
||||
39
infra/outputs.tf
Normal file
39
infra/outputs.tf
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
output "instance_ip" {
|
||||
description = "Public IP address of the server"
|
||||
value = google_compute_address.data_analyst.address
|
||||
}
|
||||
|
||||
output "ssh_command" {
|
||||
description = "SSH command to connect"
|
||||
value = "ssh ${var.ssh_user}@${google_compute_address.data_analyst.address}"
|
||||
}
|
||||
|
||||
output "api_url" {
|
||||
description = "API URL"
|
||||
value = "http://${google_compute_address.data_analyst.address}:8000"
|
||||
}
|
||||
|
||||
output "web_url" {
|
||||
description = "Web UI URL"
|
||||
value = var.domain != "" ? "https://${var.domain}" : "http://${google_compute_address.data_analyst.address}:8000"
|
||||
}
|
||||
|
||||
output "swagger_url" {
|
||||
description = "Swagger API docs URL"
|
||||
value = "http://${google_compute_address.data_analyst.address}:8000/docs"
|
||||
}
|
||||
|
||||
output "bootstrap_command" {
|
||||
description = "Command to bootstrap first admin user"
|
||||
value = "curl -X POST http://${google_compute_address.data_analyst.address}:8000/auth/bootstrap -H 'Content-Type: application/json' -d '{\"email\":\"admin@keboola.com\",\"name\":\"Admin\"}'"
|
||||
}
|
||||
|
||||
output "cli_setup_commands" {
|
||||
description = "Commands to set up local CLI"
|
||||
value = <<-EOT
|
||||
da setup init --server http://${google_compute_address.data_analyst.address}:8000
|
||||
da setup bootstrap admin@keboola.com
|
||||
da setup test-connection
|
||||
da sync
|
||||
EOT
|
||||
}
|
||||
20
infra/terraform.tfvars.example
Normal file
20
infra/terraform.tfvars.example
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Copy to terraform.tfvars and fill in values
|
||||
project_id = "your-gcp-project-id"
|
||||
region = "europe-west1"
|
||||
zone = "europe-west1-b"
|
||||
machine_type = "e2-small" # 2 vCPU, 2GB RAM, ~$7/mo
|
||||
disk_size_gb = 30
|
||||
instance_name = "data-analyst"
|
||||
ssh_user = "deploy"
|
||||
ssh_public_key_path = "~/.ssh/id_ed25519.pub"
|
||||
|
||||
# App secrets
|
||||
jwt_secret = "" # Generate: python3 -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
|
||||
# Keboola (optional — leave empty for sample data)
|
||||
keboola_token = ""
|
||||
keboola_stack_url = "https://connection.keboola.com"
|
||||
keboola_project_id = ""
|
||||
|
||||
# Domain (optional — leave empty for IP-only access)
|
||||
domain = ""
|
||||
78
infra/variables.tf
Normal file
78
infra/variables.tf
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
variable "project_id" {
|
||||
description = "GCP project ID"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "GCP region"
|
||||
type = string
|
||||
default = "europe-west1"
|
||||
}
|
||||
|
||||
variable "zone" {
|
||||
description = "GCP zone"
|
||||
type = string
|
||||
default = "europe-west1-b"
|
||||
}
|
||||
|
||||
variable "machine_type" {
|
||||
description = "VM machine type"
|
||||
type = string
|
||||
default = "e2-small"
|
||||
}
|
||||
|
||||
variable "disk_size_gb" {
|
||||
description = "Boot disk size in GB"
|
||||
type = number
|
||||
default = 30
|
||||
}
|
||||
|
||||
variable "instance_name" {
|
||||
description = "Name for the VM instance"
|
||||
type = string
|
||||
default = "data-analyst"
|
||||
}
|
||||
|
||||
variable "ssh_user" {
|
||||
description = "SSH username"
|
||||
type = string
|
||||
default = "deploy"
|
||||
}
|
||||
|
||||
variable "ssh_public_key_path" {
|
||||
description = "Path to SSH public key file"
|
||||
type = string
|
||||
default = "~/.ssh/id_ed25519.pub"
|
||||
}
|
||||
|
||||
# App config
|
||||
variable "jwt_secret" {
|
||||
description = "JWT secret key (min 32 chars)"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "keboola_token" {
|
||||
description = "Keboola Storage API token"
|
||||
type = string
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "keboola_stack_url" {
|
||||
description = "Keboola Stack URL"
|
||||
type = string
|
||||
default = "https://connection.keboola.com"
|
||||
}
|
||||
|
||||
variable "keboola_project_id" {
|
||||
description = "Keboola project ID"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "domain" {
|
||||
description = "Domain name for SSL (optional, empty = IP only)"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
Loading…
Reference in a new issue