Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/terraform-validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Terraform validate

on:
pull_request:
branches:
- dev
paths:
- "terraform/**"
push:
branches:
- dev
paths:
- "terraform/**"

concurrency:
group: terraform-validate-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true

jobs:
validate:
name: Validate ${{ matrix.cloud }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
cloud: [aws, azure, gcp]
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "~> 1.5"

- name: Terraform init
working-directory: terraform/${{ matrix.cloud }}
run: terraform init -backend=false

- name: Terraform validate
working-directory: terraform/${{ matrix.cloud }}
run: terraform validate
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ workspace
*.tfplan
crash.log
terraform/aws/terraform.tfvars
terraform/azure/terraform.tfvars
terraform/gcp/terraform.tfvars

# inst/demo-lps-2025/00_ml_month.ipynb — openEO job downloads
inst/demo-lps-2025/data/output-month/
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,12 @@ docker run -d --name openeocraft --restart unless-stopped \

#### Terraform (recommended)

For repeatable EC2 provisioning, use the Terraform stack in [`terraform/aws/README.md`](terraform/aws/README.md). Defaults target **GPU** (`g4dn.xlarge`, 16 GB RAM, NVIDIA T4) with NVIDIA drivers and `docker run --gpus all`. See the README for GPU vCPU quota, auth, and CPU fallback (`t3.xlarge`).
For repeatable cloud provisioning, use the Terraform stacks in [`terraform/README.md`](terraform/README.md). Defaults target **GPU with at least 32 GB RAM** on AWS (`g4dn.2xlarge`), Azure (`Standard_NC8as_T4_v3`), and GCP (`g2-standard-8`), with NVIDIA drivers and `docker run --gpus all`. See each cloud README for quota, auth, and CPU fallbacks.

```bash
cd terraform/aws
cd terraform/aws # or azure/ or gcp/
cp terraform.tfvars.example terraform.tfvars
# Set key_name, ssh_cidr_blocks, api_cidr_blocks
# Set credentials, key pair / SSH key, ssh_cidr_blocks, api_cidr_blocks
terraform init
terraform apply
```
Expand Down
62 changes: 62 additions & 0 deletions terraform/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# OpenEOcraft Terraform (multi-cloud)

Repeatable provisioning for OpenEOcraft on **AWS**, **Azure**, and **Google Cloud**. Each stack launches a single **x86_64** Ubuntu 22.04 VM, installs Docker via cloud-init, optionally installs the **NVIDIA driver + NVIDIA Container Toolkit**, pulls the OpenEOcraft image, and starts the API on port **8000**.

All defaults target **at least 32 GB instance RAM** (30 GB container limit, leaving headroom for the OS).

| Cloud | Directory | Default GPU instance | Default CPU fallback |
| ----- | --------- | -------------------- | ------------------- |
| AWS | [`aws/`](aws/) | `g4dn.2xlarge` (32 GB, 1× T4) | `m5.2xlarge` (32 GB) |
| Azure | [`azure/`](azure/) | `Standard_NC8as_T4_v3` (56 GB, 1× T4) | `Standard_D8s_v5` (32 GB) |
| GCP | [`gcp/`](gcp/) | `g2-standard-8` (32 GB, 1× L4) | `n2-standard-8` (32 GB) |

Bootstrap scripts live in [`shared/cloud_init.tftpl`](shared/cloud_init.tftpl) and are shared across clouds. Workspace disks are mounted only via stable cloud-specific device paths (never heuristic disk guessing); if the data disk is not ready, bootstrap falls back to a directory on the root filesystem.

## Quick start

Pick a cloud, copy the example variables, edit credentials and CIDR blocks, then apply:

```bash
cd terraform/aws # or azure/ or gcp/
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars (key pair / SSH key, your public IP /32)
terraform init
terraform plan
terraform apply
terraform output api_url
```

Allow **15–20 minutes** on first boot when `enable_gpu = true`.

## Prerequisites (all clouds)

| Requirement | Notes |
| ----------- | ----- |
| [Terraform](https://developer.hashicorp.com/terraform/install) >= 1.5 | |
| Cloud account with billing enabled | GPU instances require quota in most accounts |
| SSH access | AWS: EC2 key pair name; Azure/GCP: SSH public key |
| Your public IP as CIDR | `curl -s ifconfig.me` → `x.x.x.x/32` for SSH and API rules |

## Cloud-specific docs

- [AWS EC2](aws/README.md)
- [Azure VM](azure/README.md)
- [Google Compute Engine](gcp/README.md)

## What each stack creates

| Resource | Purpose |
| -------- | ------- |
| Network rules | SSH (22) and OpenEOcraft API (8000) from your CIDR lists |
| Compute VM | Ubuntu 22.04, Docker, optional NVIDIA stack, OpenEOcraft container |
| Data disk (optional) | Persistent `/var/openeo/workspace` (default 200 GB) |

For production, add a dedicated VPC/VNet, load balancer, and HTTPS termination in front of the API.

## Teardown

```bash
terraform destroy
```

Persistent workspace disks are deleted with the stack unless you snapshot them first.
28 changes: 14 additions & 14 deletions terraform/aws/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This stack provisions a single **x86_64** EC2 instance in the **default VPC**, installs Docker via cloud-init, installs the **NVIDIA driver + NVIDIA Container Toolkit** (GPU mode), pulls the OpenEOcraft image, and starts the API on port **8000**.

**Default target:** `g4dn.xlarge` (4 vCPU, **16 GB RAM**, 1× NVIDIA T4) with `docker run --gpus all`.
**Default target:** `g4dn.2xlarge` (8 vCPU, **32 GB RAM**, 1× NVIDIA T4) with `docker run --gpus all`.

## Prerequisites

Expand Down Expand Up @@ -69,7 +69,7 @@ request a quota increase:

1. AWS Console → **Service Quotas** → region **us-east-1**
2. **Amazon EC2** → **Running On-Demand G and VT instances**
3. **Request increase at account-level** → at least **4 vCPUs** (one `g4dn.xlarge`)
3. **Request increase at account-level** → at least **8 vCPUs** (one `g4dn.2xlarge`)

Or CLI:

Expand All @@ -79,7 +79,7 @@ aws service-quotas request-service-quota-increase \
--region us-east-1 \
--service-code ec2 \
--quota-code L-DB2E81BA \
--desired-value 4
--desired-value 8
```

Check status:
Expand All @@ -93,7 +93,7 @@ aws service-quotas get-service-quota \
--query 'Quota.Value'
```

Wait until the value is **≥ 4** and the request status is **APPROVED**, then `terraform apply` again.
Wait until the value is **≥ 8** and the request status is **APPROVED**, then `terraform apply` again.

## What gets created

Expand All @@ -108,27 +108,27 @@ The stack uses the **default VPC** and the first available subnet. For productio

## Variables

See [`variables.tf`](variables.tf). Defaults target GPU with **16 GB instance RAM**:
See [`variables.tf`](variables.tf). Defaults target GPU with **32 GB instance RAM**:

| Variable | Default | Notes |
| -------- | ------- | ----- |
| `enable_gpu` | `true` | Installs NVIDIA stack; container runs with `--gpus all` |
| `instance_type` | `g4dn.xlarge` | 4 vCPU, **16 GB RAM**, 1× T4; x86_64 only (no Graviton) |
| `docker_cpus` / `docker_memory_gb` | `4` / `14` | Container limits; 14 GB leaves ~2 GB for the OS on g4dn.xlarge |
| `instance_type` | `g4dn.2xlarge` | 8 vCPU, **32 GB RAM**, 1× T4; x86_64 only (no Graviton) |
| `docker_cpus` / `docker_memory_gb` | `8` / `30` | Container limits; 30 GB leaves ~2 GB for the OS on g4dn.2xlarge |
| `nvidia_driver_major` | `535` | Ubuntu driver package during bootstrap |
| `root_volume_size_gb` | `120` | Root disk for OS, drivers, Docker image |
| `workspace_volume_size_gb` | `200` | Set to `0` to skip separate data volume |
| `swap_size_gb` | `0` | Only for very small CPU fallbacks (e.g. `t3.micro`) |
| `swap_size_gb` | `0` | Only for very small CPU fallbacks |
| `enable_ssm` | `true` | `aws ssm start-session --target <instance-id>` |

### CPU fallback (no GPU quota yet)

Use at least **16 GB RAM** for OpenEOcraft (torch + sits). Copy [`terraform.cpu.tfvars.example`](terraform.cpu.tfvars.example):
Use at least **32 GB RAM** for OpenEOcraft (torch + sits). Copy [`terraform.cpu.tfvars.example`](terraform.cpu.tfvars.example):

```hcl
enable_gpu = false
instance_type = "t3.xlarge" # 16 GB RAM
docker_memory_gb = 14
instance_type = "m5.2xlarge" # 32 GB RAM
docker_memory_gb = 30
```

Do **not** use `t3.micro` / `t3.small` — the container will OOM or never become reachable.
Expand Down Expand Up @@ -167,7 +167,7 @@ curl -s -o /dev/null -w "%{http_code}\n" $(terraform output -raw api_url)
| Bootstrap still running | `sudo tail -f /var/log/cloud-init-output.log` — wait 15–20 min |
| Wrong IP in security group | `curl -s ifconfig.me` → update CIDRs in `terraform.tfvars` → `terraform apply` |
| Container crashed | `sudo docker ps -a` and `sudo docker logs openeocraft` |
| Instance too small | Use `g4dn.xlarge` or `t3.xlarge` (16 GB RAM minimum) |
| Instance too small | Use `g4dn.2xlarge` or `m5.2xlarge` (32 GB RAM minimum) |
| GPU quota pending | Wait for G/VT vCPU quota approval |

## Remote state (recommended for teams)
Expand All @@ -178,8 +178,8 @@ Uncomment the `backend "s3"` block in [`versions.tf`](versions.tf) and create an

| Instance | Approx. cost (us-east-1) |
| -------- | -------------------------- |
| `g4dn.xlarge` | ~$0.50+/hr + EBS |
| `t3.xlarge` (CPU fallback) | ~$0.17/hr |
| `g4dn.2xlarge` | ~$0.75+/hr + EBS |
| `m5.2xlarge` (CPU fallback) | ~$0.38/hr |

Stop charges when finished:

Expand Down
34 changes: 19 additions & 15 deletions terraform/aws/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ locals {
subnet_id = sort(data.aws_subnets.default.ids)[0]
}

data "aws_subnet" "selected" {
id = local.subnet_id
}

resource "aws_security_group" "openeocraft" {
name = "${local.name_prefix}-sg"
description = "SSH and OpenEOcraft API access"
Expand Down Expand Up @@ -108,6 +112,19 @@ resource "aws_iam_instance_profile" "ssm" {
role = aws_iam_role.ssm[0].name
}

resource "aws_ebs_volume" "workspace" {
count = var.workspace_volume_size_gb > 0 ? 1 : 0

availability_zone = data.aws_subnet.selected.availability_zone
size = var.workspace_volume_size_gb
type = "gp3"
encrypted = true

tags = merge(local.common_tags, {
Name = "${local.name_prefix}-workspace"
})
}

resource "aws_instance" "openeocraft" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
Expand All @@ -117,13 +134,13 @@ resource "aws_instance" "openeocraft" {
associate_public_ip_address = var.associate_public_ip
iam_instance_profile = var.enable_ssm ? aws_iam_instance_profile.ssm[0].name : null

user_data = templatefile("${path.module}/user_data.tftpl", {
user_data = templatefile("${path.module}/../shared/cloud_init.tftpl", {
docker_image = var.docker_image
docker_cpus = var.docker_cpus
docker_memory_gb = var.docker_memory_gb
api_port = var.api_port
mount_workspace = var.workspace_volume_size_gb > 0
workspace_device = "/dev/sdf"
workspace_device = var.workspace_volume_size_gb > 0 ? "/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_${aws_ebs_volume.workspace[0].id}" : ""
enable_gpu = var.enable_gpu
nvidia_driver_major = var.nvidia_driver_major
swap_size_gb = var.swap_size_gb
Expand Down Expand Up @@ -152,19 +169,6 @@ resource "aws_instance" "openeocraft" {
}
}

resource "aws_ebs_volume" "workspace" {
count = var.workspace_volume_size_gb > 0 ? 1 : 0

availability_zone = aws_instance.openeocraft.availability_zone
size = var.workspace_volume_size_gb
type = "gp3"
encrypted = true

tags = merge(local.common_tags, {
Name = "${local.name_prefix}-workspace"
})
}

resource "aws_volume_attachment" "workspace" {
count = var.workspace_volume_size_gb > 0 ? 1 : 0

Expand Down
8 changes: 4 additions & 4 deletions terraform/aws/terraform.cpu.tfvars.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# CPU-only fallback (no GPU quota required). Minimum 16 GB RAM for OpenEOcraft.
# CPU-only fallback (no GPU quota required). Minimum 32 GB RAM for OpenEOcraft.
# Copy to terraform.tfvars if you cannot launch g4dn instances yet.

aws_region = "us-east-1"
Expand All @@ -10,10 +10,10 @@ ssh_cidr_blocks = ["203.0.113.10/32"]
api_cidr_blocks = ["203.0.113.10/32"]

enable_gpu = false
instance_type = "t3.xlarge" # 4 vCPU, 16 GB RAM
instance_type = "m5.2xlarge" # 8 vCPU, 32 GB RAM

docker_cpus = 4
docker_memory_gb = 14
docker_cpus = 8
docker_memory_gb = 30

swap_size_gb = 0
root_volume_size_gb = 50
Expand Down
6 changes: 3 additions & 3 deletions terraform/aws/terraform.gpu.tfvars.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ ssh_cidr_blocks = ["203.0.113.10/32"]
api_cidr_blocks = ["203.0.113.10/32"]

enable_gpu = true
instance_type = "g4dn.xlarge"
instance_type = "g4dn.2xlarge"
nvidia_driver_major = 535

docker_cpus = 4
docker_memory_gb = 14
docker_cpus = 8
docker_memory_gb = 30

swap_size_gb = 0
root_volume_size_gb = 120
Expand Down
8 changes: 4 additions & 4 deletions terraform/aws/terraform.tfvars.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ key_name = "my-keypair"
ssh_cidr_blocks = ["203.0.113.10/32"]
api_cidr_blocks = ["203.0.113.10/32"]

# GPU defaults: g4dn.xlarge = 4 vCPU, 16 GB RAM, 1x NVIDIA T4
# GPU defaults: g4dn.2xlarge = 8 vCPU, 32 GB RAM, 1x NVIDIA T4
enable_gpu = true
instance_type = "g4dn.xlarge"
instance_type = "g4dn.2xlarge"
nvidia_driver_major = 535

docker_cpus = 4
docker_memory_gb = 14
docker_cpus = 8
docker_memory_gb = 30

swap_size_gb = 0
root_volume_size_gb = 120
Expand Down
10 changes: 5 additions & 5 deletions terraform/aws/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ variable "project_name" {
}

variable "instance_type" {
description = "EC2 instance type. Must be x86_64 (no Graviton) for torch/linux/amd64. Default g4dn.xlarge provides 16 GB RAM and one NVIDIA T4 GPU."
description = "EC2 instance type. Must be x86_64 (no Graviton) for torch/linux/amd64. Default g4dn.2xlarge provides 32 GB RAM and one NVIDIA T4 GPU."
type = string
default = "g4dn.xlarge"
default = "g4dn.2xlarge"
}

variable "key_name" {
Expand Down Expand Up @@ -56,13 +56,13 @@ variable "docker_image" {
variable "docker_cpus" {
description = "CPU limit passed to docker run (--cpus)."
type = number
default = 4
default = 8
}

variable "docker_memory_gb" {
description = "Memory limit passed to docker run (-m), in gigabytes. Set to 0 to omit the limit. Default 14 GB on a 16 GB g4dn.xlarge leaves headroom for the OS."
description = "Memory limit passed to docker run (-m), in gigabytes. Set to 0 to omit the limit. Default 30 GB on a 32 GB g4dn.2xlarge leaves headroom for the OS."
type = number
default = 14
default = 30

validation {
condition = var.docker_memory_gb == 0 || var.docker_memory_gb >= 4
Expand Down
22 changes: 22 additions & 0 deletions terraform/azure/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading