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