From c2ab63fdeff783f78d2fe15ed3fe5f6428ee1e73 Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:09:49 +1000 Subject: [PATCH] feat: basic terraform templates --- docs/terraform/bastion-private-subnet/main.tf | 378 +++++++++++++++ docs/terraform/nginx-webserver/main.tf | 282 +++++++++++ docs/terraform/s3-webapp/main.tf | 441 ++++++++++++++++++ .../s3-webapp/terraform.tfvars.example | 14 + 4 files changed, 1115 insertions(+) create mode 100644 docs/terraform/bastion-private-subnet/main.tf create mode 100644 docs/terraform/nginx-webserver/main.tf create mode 100644 docs/terraform/s3-webapp/main.tf create mode 100644 docs/terraform/s3-webapp/terraform.tfvars.example diff --git a/docs/terraform/bastion-private-subnet/main.tf b/docs/terraform/bastion-private-subnet/main.tf new file mode 100644 index 000000000..21bb7fc94 --- /dev/null +++ b/docs/terraform/bastion-private-subnet/main.tf @@ -0,0 +1,378 @@ +# Example 2: Bastion Host with Private Subnet +# +# Deploys a VPC with both public and private subnets. A bastion host in the +# public subnet provides SSH access to instances in the private subnet. +# A NAT gateway gives private instances outbound internet access. +# +# Architecture: +# +# WAN ──SSH──▶ Bastion (public subnet) +# │ +# ▼ SSH (private IP) +# App Server (private subnet) +# │ +# ▼ outbound via NAT GW +# Internet (apt-get, etc.) +# +# Usage: +# cd spinifex/scripts/iac/aws/examples/02-bastion-private-subnet +# export AWS_PROFILE=spinifex +# tofu init && tofu apply +# +# After apply: +# # SSH to bastion +# ssh -i bastion-demo.pem ec2-user@ +# +# # From bastion, SSH to private instance +# ssh -i /tmp/key.pem ec2-user@ +# +# # Or use SSH proxy jump (single command from your workstation) +# ssh -i bastion-demo.pem -J ec2-user@ ec2-user@ + +terraform { + required_version = ">= 1.6.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + tls = { + source = "hashicorp/tls" + version = ">= 4.0" + } + local = { + source = "hashicorp/local" + version = ">= 2.0" + } + } +} + +# --------------------------------------------------------------------------- +# Variables +# --------------------------------------------------------------------------- + +variable "region" { + type = string + default = "ap-southeast-2" +} + +variable "spinifex_endpoint" { + type = string + default = "https://localhost:9999" + description = "Spinifex AWS gateway endpoint" +} + +# --------------------------------------------------------------------------- +# Provider +# --------------------------------------------------------------------------- + +provider "aws" { + region = var.region + + endpoints { + ec2 = var.spinifex_endpoint + iam = var.spinifex_endpoint + sts = var.spinifex_endpoint + } + + skip_credentials_validation = true + skip_metadata_api_check = true + skip_requesting_account_id = true + skip_region_validation = true +} + +# --------------------------------------------------------------------------- +# Data sources +# --------------------------------------------------------------------------- + +data "aws_availability_zones" "available" { + state = "available" +} + +data "aws_ami" "debian12" { + most_recent = true + owners = ["000000000000"] + + filter { + name = "name" + values = ["*debian-12*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + filter { + name = "root-device-type" + values = ["ebs"] + } +} + +# --------------------------------------------------------------------------- +# SSH Key Pair (shared by bastion and private instances) +# --------------------------------------------------------------------------- + +resource "tls_private_key" "bastion" { + algorithm = "ED25519" +} + +resource "aws_key_pair" "bastion" { + key_name = "bastion-demo" + public_key = tls_private_key.bastion.public_key_openssh +} + +resource "local_file" "bastion_pem" { + filename = "${path.module}/bastion-demo.pem" + content = tls_private_key.bastion.private_key_openssh + file_permission = "0600" +} + +# --------------------------------------------------------------------------- +# VPC +# --------------------------------------------------------------------------- + +resource "aws_vpc" "main" { + cidr_block = "10.20.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = "bastion-demo-vpc" + } +} + +# --------------------------------------------------------------------------- +# Internet Gateway +# --------------------------------------------------------------------------- + +resource "aws_internet_gateway" "igw" { + vpc_id = aws_vpc.main.id + + tags = { + Name = "bastion-demo-igw" + } +} + +# --------------------------------------------------------------------------- +# Public Subnet — bastion host lives here +# --------------------------------------------------------------------------- + +resource "aws_subnet" "public" { + vpc_id = aws_vpc.main.id + cidr_block = "10.20.1.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + map_public_ip_on_launch = true + + tags = { + Name = "bastion-demo-public" + } +} + +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.igw.id + } + + tags = { + Name = "bastion-demo-public-rt" + } +} + +resource "aws_route_table_association" "public" { + subnet_id = aws_subnet.public.id + route_table_id = aws_route_table.public.id +} + +# --------------------------------------------------------------------------- +# Private Subnet — application instances live here (no public IPs) +# --------------------------------------------------------------------------- + +resource "aws_subnet" "private" { + vpc_id = aws_vpc.main.id + cidr_block = "10.20.2.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + map_public_ip_on_launch = false + + tags = { + Name = "bastion-demo-private" + } +} + +# --------------------------------------------------------------------------- +# NAT Gateway — gives private subnet outbound internet access +# --------------------------------------------------------------------------- + +resource "aws_eip" "nat" { + domain = "vpc" + + tags = { + Name = "bastion-demo-nat-eip" + } +} + +resource "aws_nat_gateway" "nat" { + allocation_id = aws_eip.nat.id + subnet_id = aws_subnet.public.id + + tags = { + Name = "bastion-demo-nat-gw" + } + + depends_on = [aws_internet_gateway.igw] +} + +resource "aws_route_table" "private" { + vpc_id = aws_vpc.main.id + + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.nat.id + } + + tags = { + Name = "bastion-demo-private-rt" + } +} + +resource "aws_route_table_association" "private" { + subnet_id = aws_subnet.private.id + route_table_id = aws_route_table.private.id +} + +# --------------------------------------------------------------------------- +# Security Groups +# --------------------------------------------------------------------------- + +# Bastion: SSH from anywhere +resource "aws_security_group" "bastion" { + name = "bastion-demo-bastion-sg" + description = "Bastion: SSH from WAN" + vpc_id = aws_vpc.main.id + + ingress { + description = "SSH" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + description = "All outbound" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "bastion-demo-bastion-sg" + } +} + +# Private instances: SSH only from the bastion security group +resource "aws_security_group" "private" { + name = "bastion-demo-private-sg" + description = "Private: SSH from bastion only" + vpc_id = aws_vpc.main.id + + ingress { + description = "SSH from bastion" + from_port = 22 + to_port = 22 + protocol = "tcp" + security_groups = [aws_security_group.bastion.id] + } + + egress { + description = "All outbound" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "bastion-demo-private-sg" + } +} + +# --------------------------------------------------------------------------- +# Bastion Host (public subnet) +# --------------------------------------------------------------------------- + +resource "aws_instance" "bastion" { + ami = data.aws_ami.debian12.id + instance_type = "t3.small" + + subnet_id = aws_subnet.public.id + vpc_security_group_ids = [aws_security_group.bastion.id] + key_name = aws_key_pair.bastion.key_name + + associate_public_ip_address = true + + # Copy the SSH private key onto the bastion so you can hop to private instances + user_data_base64 = base64encode(<<-USERDATA + #!/bin/bash + set -euo pipefail + mkdir -p /home/ec2-user/.ssh + cat > /home/ec2-user/.ssh/private-key.pem <<'KEY' + ${tls_private_key.bastion.private_key_openssh} + KEY + chmod 600 /home/ec2-user/.ssh/private-key.pem + chown -R ec2-user:ec2-user /home/ec2-user/.ssh + USERDATA + ) + + tags = { + Name = "bastion-demo-bastion" + } +} + +# --------------------------------------------------------------------------- +# Private Instance (private subnet — no public IP) +# --------------------------------------------------------------------------- + +resource "aws_instance" "private" { + ami = data.aws_ami.debian12.id + instance_type = "t3.small" + + subnet_id = aws_subnet.private.id + vpc_security_group_ids = [aws_security_group.private.id] + key_name = aws_key_pair.bastion.key_name + + tags = { + Name = "bastion-demo-private-app" + } +} + +# --------------------------------------------------------------------------- +# Outputs +# --------------------------------------------------------------------------- + +output "bastion_public_ip" { + value = aws_instance.bastion.public_ip +} + +output "private_instance_ip" { + value = aws_instance.private.private_ip +} + +output "ssh_to_bastion" { + value = "ssh -i bastion-demo.pem ec2-user@${aws_instance.bastion.public_ip}" +} + +output "ssh_to_private_via_bastion" { + description = "From your workstation, jump through the bastion to the private instance" + value = "ssh -i bastion-demo.pem -J ec2-user@${aws_instance.bastion.public_ip} ec2-user@${aws_instance.private.private_ip}" +} + +output "ssh_from_bastion_to_private" { + description = "Once on the bastion, SSH to the private instance" + value = "ssh -i ~/.ssh/private-key.pem ec2-user@${aws_instance.private.private_ip}" +} diff --git a/docs/terraform/nginx-webserver/main.tf b/docs/terraform/nginx-webserver/main.tf new file mode 100644 index 000000000..9289081f8 --- /dev/null +++ b/docs/terraform/nginx-webserver/main.tf @@ -0,0 +1,282 @@ +# Example 1: Nginx Web Server on Spinifex +# +# Deploys a VPC with a public subnet and an EC2 instance running Nginx. +# Demonstrates: VPC, subnet, internet gateway, route table, security group, +# key pair, cloud-init user-data, and EC2 instance provisioning. +# +# Usage: +# cd spinifex/scripts/iac/aws/examples/01-nginx-webserver +# export AWS_PROFILE=spinifex +# tofu init && tofu apply +# +# After apply: +# curl http:// # Nginx welcome page +# ssh -i nginx-demo.pem ec2-user@ + +terraform { + required_version = ">= 1.6.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + tls = { + source = "hashicorp/tls" + version = ">= 4.0" + } + local = { + source = "hashicorp/local" + version = ">= 2.0" + } + } +} + +# --------------------------------------------------------------------------- +# Variables +# --------------------------------------------------------------------------- + +variable "region" { + type = string + default = "ap-southeast-2" +} + +variable "spinifex_endpoint" { + type = string + default = "https://localhost:9999" + description = "Spinifex AWS gateway endpoint" +} + +# --------------------------------------------------------------------------- +# Provider — point the AWS provider at Spinifex +# --------------------------------------------------------------------------- + +provider "aws" { + region = var.region + + endpoints { + ec2 = var.spinifex_endpoint + iam = var.spinifex_endpoint + sts = var.spinifex_endpoint + } + + skip_credentials_validation = true + skip_metadata_api_check = true + skip_requesting_account_id = true + skip_region_validation = true +} + +# --------------------------------------------------------------------------- +# Data sources +# --------------------------------------------------------------------------- + +data "aws_availability_zones" "available" { + state = "available" +} + +data "aws_ami" "debian12" { + most_recent = true + owners = ["000000000000"] # Spinifex system images + + filter { + name = "name" + values = ["*debian-12*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + filter { + name = "root-device-type" + values = ["ebs"] + } +} + +# --------------------------------------------------------------------------- +# SSH Key Pair +# --------------------------------------------------------------------------- + +resource "tls_private_key" "nginx" { + algorithm = "ED25519" +} + +resource "aws_key_pair" "nginx" { + key_name = "nginx-demo" + public_key = tls_private_key.nginx.public_key_openssh +} + +resource "local_file" "nginx_pem" { + filename = "${path.module}/nginx-demo.pem" + content = tls_private_key.nginx.private_key_openssh + file_permission = "0600" +} + +# --------------------------------------------------------------------------- +# VPC +# --------------------------------------------------------------------------- + +resource "aws_vpc" "main" { + cidr_block = "10.10.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = "nginx-demo-vpc" + } +} + +# --------------------------------------------------------------------------- +# Internet Gateway — gives the public subnet a route to the internet +# --------------------------------------------------------------------------- + +resource "aws_internet_gateway" "igw" { + vpc_id = aws_vpc.main.id + + tags = { + Name = "nginx-demo-igw" + } +} + +# --------------------------------------------------------------------------- +# Public Subnet +# --------------------------------------------------------------------------- + +resource "aws_subnet" "public" { + vpc_id = aws_vpc.main.id + cidr_block = "10.10.1.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + map_public_ip_on_launch = true + + tags = { + Name = "nginx-demo-public" + } +} + +# --------------------------------------------------------------------------- +# Route Table — send 0.0.0.0/0 through the internet gateway +# --------------------------------------------------------------------------- + +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.igw.id + } + + tags = { + Name = "nginx-demo-public-rt" + } +} + +resource "aws_route_table_association" "public" { + subnet_id = aws_subnet.public.id + route_table_id = aws_route_table.public.id +} + +# --------------------------------------------------------------------------- +# Security Group — SSH + HTTP inbound, all outbound +# --------------------------------------------------------------------------- + +resource "aws_security_group" "web" { + name = "nginx-demo-sg" + description = "Allow SSH and HTTP inbound" + vpc_id = aws_vpc.main.id + + ingress { + description = "SSH" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + description = "HTTP" + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + description = "All outbound" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "nginx-demo-sg" + } +} + +# --------------------------------------------------------------------------- +# EC2 Instance — Debian 12 with Nginx installed via cloud-init +# --------------------------------------------------------------------------- + +resource "aws_instance" "nginx" { + ami = data.aws_ami.debian12.id + instance_type = "t3.small" + + subnet_id = aws_subnet.public.id + vpc_security_group_ids = [aws_security_group.web.id] + key_name = aws_key_pair.nginx.key_name + + associate_public_ip_address = true + + user_data_base64 = base64encode(<<-USERDATA + #!/bin/bash + set -euo pipefail + + # Install Nginx + apt-get update -y + apt-get install -y nginx + + # Write a custom landing page + cat > /var/www/html/index.html <<'HTML' + + + Spinifex Demo + +

Hello from Spinifex!

+

This Nginx server was deployed with Terraform on Spinifex infrastructure.

+
+

Instance provisioned via cloud-init user-data.

+ + + HTML + + # Ensure Nginx is running + systemctl enable nginx + systemctl restart nginx + USERDATA + ) + + tags = { + Name = "nginx-demo" + } +} + +# --------------------------------------------------------------------------- +# Outputs +# --------------------------------------------------------------------------- + +output "instance_id" { + value = aws_instance.nginx.id +} + +output "public_ip" { + value = aws_instance.nginx.public_ip +} + +output "ssh_command" { + value = "ssh -i nginx-demo.pem ec2-user@${aws_instance.nginx.public_ip}" +} + +output "web_url" { + value = "http://${aws_instance.nginx.public_ip}" +} diff --git a/docs/terraform/s3-webapp/main.tf b/docs/terraform/s3-webapp/main.tf new file mode 100644 index 000000000..2a8b80c30 --- /dev/null +++ b/docs/terraform/s3-webapp/main.tf @@ -0,0 +1,441 @@ +# Example 3: S3-Backed Web Application +# +# Deploys an EC2 instance running a simple file-sharing webapp backed by S3 +# (Predastore). Users can upload files through a web form and browse uploaded +# content — demonstrating Terraform managing both compute (Spinifex) and +# object storage (Predastore) resources together. +# +# Architecture: +# +# Browser ──HTTP──▶ EC2 Instance (Flask webapp, port 80) +# │ +# ▼ S3 API (boto3) +# Predastore (port 8443) +# +# Prerequisites: +# - Spinifex services running (gateway on port 9999) +# - Predastore running (S3 API on port 8443) +# - The EC2 instance must be able to reach the Predastore endpoint. +# Set `predastore_host` to the IP reachable from inside the VPC +# (e.g. the host's br-wan IP, NOT localhost). +# +# Usage: +# cd spinifex/scripts/iac/aws/examples/03-s3-webapp +# export AWS_PROFILE=spinifex +# tofu init && tofu apply +# +# After apply: +# curl http:// # File browser UI +# ssh -i s3-webapp-demo.pem ec2-user@ + +terraform { + required_version = ">= 1.6.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + tls = { + source = "hashicorp/tls" + version = ">= 4.0" + } + local = { + source = "hashicorp/local" + version = ">= 2.0" + } + } +} + +# --------------------------------------------------------------------------- +# Variables +# --------------------------------------------------------------------------- + +variable "region" { + type = string + default = "ap-southeast-2" +} + +variable "spinifex_endpoint" { + type = string + default = "https://localhost:9999" + description = "Spinifex AWS gateway endpoint (EC2/IAM)" +} + +variable "predastore_endpoint" { + type = string + default = "https://localhost:8443" + description = "Predastore S3 endpoint (for Terraform to create buckets)" +} + +variable "predastore_host" { + type = string + description = "Predastore host:port reachable from inside the VPC (e.g. 192.168.1.10:8443)" +} + +variable "s3_access_key" { + type = string + description = "S3 access key for Predastore" +} + +variable "s3_secret_key" { + type = string + sensitive = true + description = "S3 secret key for Predastore" +} + +variable "bucket_name" { + type = string + default = "webapp-uploads" +} + +# --------------------------------------------------------------------------- +# Provider — EC2 via Spinifex gateway, S3 via Predastore +# --------------------------------------------------------------------------- + +provider "aws" { + region = var.region + access_key = var.s3_access_key + secret_key = var.s3_secret_key + + endpoints { + ec2 = var.spinifex_endpoint + s3 = var.predastore_endpoint + iam = var.spinifex_endpoint + sts = var.spinifex_endpoint + } + + s3_use_path_style = true + + skip_credentials_validation = true + skip_metadata_api_check = true + skip_requesting_account_id = true + skip_region_validation = true +} + +# --------------------------------------------------------------------------- +# Data sources +# --------------------------------------------------------------------------- + +data "aws_availability_zones" "available" { + state = "available" +} + +data "aws_ami" "debian12" { + most_recent = true + owners = ["000000000000"] + + filter { + name = "name" + values = ["*debian-12*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + filter { + name = "root-device-type" + values = ["ebs"] + } +} + +# --------------------------------------------------------------------------- +# S3 Bucket (created on Predastore) +# --------------------------------------------------------------------------- + +resource "aws_s3_bucket" "uploads" { + bucket = var.bucket_name +} + +# --------------------------------------------------------------------------- +# SSH Key Pair +# --------------------------------------------------------------------------- + +resource "tls_private_key" "webapp" { + algorithm = "ED25519" +} + +resource "aws_key_pair" "webapp" { + key_name = "s3-webapp-demo" + public_key = tls_private_key.webapp.public_key_openssh +} + +resource "local_file" "webapp_pem" { + filename = "${path.module}/s3-webapp-demo.pem" + content = tls_private_key.webapp.private_key_openssh + file_permission = "0600" +} + +# --------------------------------------------------------------------------- +# VPC + Public Subnet +# --------------------------------------------------------------------------- + +resource "aws_vpc" "main" { + cidr_block = "10.30.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = "s3-webapp-demo-vpc" + } +} + +resource "aws_internet_gateway" "igw" { + vpc_id = aws_vpc.main.id + + tags = { + Name = "s3-webapp-demo-igw" + } +} + +resource "aws_subnet" "public" { + vpc_id = aws_vpc.main.id + cidr_block = "10.30.1.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + map_public_ip_on_launch = true + + tags = { + Name = "s3-webapp-demo-public" + } +} + +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.igw.id + } + + tags = { + Name = "s3-webapp-demo-public-rt" + } +} + +resource "aws_route_table_association" "public" { + subnet_id = aws_subnet.public.id + route_table_id = aws_route_table.public.id +} + +# --------------------------------------------------------------------------- +# Security Group — SSH + HTTP inbound, all outbound +# --------------------------------------------------------------------------- + +resource "aws_security_group" "webapp" { + name = "s3-webapp-demo-sg" + description = "Allow SSH and HTTP inbound" + vpc_id = aws_vpc.main.id + + ingress { + description = "SSH" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + description = "HTTP" + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + description = "All outbound" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "s3-webapp-demo-sg" + } +} + +# --------------------------------------------------------------------------- +# EC2 Instance — Flask webapp that talks to Predastore S3 +# --------------------------------------------------------------------------- + +resource "aws_instance" "webapp" { + ami = data.aws_ami.debian12.id + instance_type = "t3.small" + + subnet_id = aws_subnet.public.id + vpc_security_group_ids = [aws_security_group.webapp.id] + key_name = aws_key_pair.webapp.key_name + + associate_public_ip_address = true + + user_data_base64 = base64encode(<<-USERDATA + #!/bin/bash + set -euo pipefail + + # Install dependencies + apt-get update -y + apt-get install -y python3-pip python3-venv + + # Create app directory and virtualenv + mkdir -p /opt/webapp + python3 -m venv /opt/webapp/venv + /opt/webapp/venv/bin/pip install flask boto3 + + # Write S3 credentials config + cat > /opt/webapp/.env <<'ENVFILE' + S3_ENDPOINT=https://${var.predastore_host} + S3_BUCKET=${var.bucket_name} + S3_ACCESS_KEY=${var.s3_access_key} + S3_SECRET_KEY=${var.s3_secret_key} + S3_REGION=${var.region} + ENVFILE + + # Write the Flask application + cat > /opt/webapp/app.py <<'PYEOF' + import os, io, urllib3 + from flask import Flask, request, redirect, url_for, Response + + # Suppress TLS warnings for self-signed certs + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + # Load env + env = {} + with open("/opt/webapp/.env") as f: + for line in f: + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + env[k] = v + + import boto3 + from botocore.config import Config + + s3 = boto3.client( + "s3", + endpoint_url=env["S3_ENDPOINT"], + aws_access_key_id=env["S3_ACCESS_KEY"], + aws_secret_access_key=env["S3_SECRET_KEY"], + region_name=env["S3_REGION"], + verify=False, + config=Config(s3={"addressing_style": "path"}), + ) + + BUCKET = env["S3_BUCKET"] + app = Flask(__name__) + + @app.route("/") + def index(): + # List objects in the bucket + try: + resp = s3.list_objects_v2(Bucket=BUCKET) + objects = resp.get("Contents", []) + except Exception as e: + objects = [] + + rows = "" + for obj in objects: + key = obj["Key"] + size = obj["Size"] + rows += f'{key}{size} bytes\n' + + return f""" + + Spinifex S3 File Browser + +

Spinifex S3 File Browser

+

Bucket: {BUCKET}

+ +

Upload a File

+
+ + +
+ +

Files

+ + + {rows if rows else ''} +
KeySize
No files yet
+ +
+

Powered by Spinifex + Predastore

+ + """ + + @app.route("/upload", methods=["POST"]) + def upload(): + f = request.files.get("file") + if not f or not f.filename: + return redirect("/") + s3.put_object(Bucket=BUCKET, Key=f.filename, Body=f.read()) + return redirect("/") + + @app.route("/files/") + def download(key): + try: + obj = s3.get_object(Bucket=BUCKET, Key=key) + return Response( + obj["Body"].read(), + headers={"Content-Disposition": f'inline; filename="{key}"'}, + ) + except Exception: + return "Not found", 404 + + if __name__ == "__main__": + app.run(host="0.0.0.0", port=80) + PYEOF + + # Create a systemd service so the webapp starts on boot + cat > /etc/systemd/system/s3-webapp.service <<'SVCEOF' + [Unit] + Description=S3 File Browser Webapp + After=network.target + + [Service] + Type=simple + ExecStart=/opt/webapp/venv/bin/python /opt/webapp/app.py + WorkingDirectory=/opt/webapp + Restart=always + RestartSec=3 + + [Install] + WantedBy=multi-user.target + SVCEOF + + systemctl daemon-reload + systemctl enable s3-webapp + systemctl start s3-webapp + USERDATA + ) + + tags = { + Name = "s3-webapp-demo" + } +} + +# --------------------------------------------------------------------------- +# Outputs +# --------------------------------------------------------------------------- + +output "instance_id" { + value = aws_instance.webapp.id +} + +output "public_ip" { + value = aws_instance.webapp.public_ip +} + +output "bucket_name" { + value = aws_s3_bucket.uploads.id +} + +output "ssh_command" { + value = "ssh -i s3-webapp-demo.pem ec2-user@${aws_instance.webapp.public_ip}" +} + +output "web_url" { + value = "http://${aws_instance.webapp.public_ip}" +} diff --git a/docs/terraform/s3-webapp/terraform.tfvars.example b/docs/terraform/s3-webapp/terraform.tfvars.example new file mode 100644 index 000000000..e07e373fa --- /dev/null +++ b/docs/terraform/s3-webapp/terraform.tfvars.example @@ -0,0 +1,14 @@ +# Copy this to terraform.tfvars and fill in your values. +# +# The predastore_host must be reachable from INSIDE the VPC guest — not +# localhost. Use the host's br-wan or LAN IP, e.g. "192.168.1.10:8443". + +predastore_host = "192.168.1.10:8443" +s3_access_key = "AKIAIOSFODNN7EXAMPLE" +s3_secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + +# Optional overrides: +# spinifex_endpoint = "https://localhost:9999" +# predastore_endpoint = "https://localhost:8443" +# bucket_name = "webapp-uploads" +# region = "ap-southeast-2"