Aahil13/terraform-upcloud-fullstack

★ 0Forks 0HCLGitHub ↗Compare

README

terraform-upcloud-fullstack

An OpenTofu module that provisions a complete, production-oriented application environment on UpCloud: private networking with NAT, a load balancer, multiple application servers, managed PostgreSQL with PgBouncer, managed Valkey, S3-compatible object storage, encrypted shared file storage, and a peered management network.

Only the load balancer is public. Every server and managed service sits on a private network with no public IP.

Architecture

flowchart TB
    user([Users]) -->|HTTP| lb[Load balancer<br/>the only public resource]

    subgraph private["Private network · 10.0.10.0/24"]
        app1[App server 1]
        app2[App server 2]
        pg[(PostgreSQL + PgBouncer)]
        vk[(Valkey)]
        obj[Object storage]
        fs[File storage]
        app1 & app2 --> pg
        app1 & app2 --> vk
        app1 & app2 --> obj
        app1 & app2 --> fs
    end

    lb --> app1
    lb --> app2
    private -->|NAT gateway, outbound only| internet([Internet])
    private <-->|Peering| mgmt[Management network<br/>10.0.20.0/24]
Loading

Requirements

  • OpenTofu 1.6 or later (Terraform should also work, but this module is tested with OpenTofu)
  • UpCloud provider UpCloudLtd/upcloud ~> 5.0
  • An UpCloud account with a payment method. Trial accounts allow only one managed database and can't modify firewalls, so this module won't apply on a trial.
  • An UpCloud API sub-account with credentials exported as UPCLOUD_USERNAME and UPCLOUD_PASSWORD

Usage

provider "upcloud" {}

module "fullstack" {
  source      = "github.com/Aahil13/terraform-upcloud-fullstack?ref=v1.0.0"
  name_prefix = "myapp"
  db_password = var.db_password
}
export UPCLOUD_USERNAME="your-api-username"
export UPCLOUD_PASSWORD="your-api-password"
export TF_VAR_db_password='something-long-and-random'

tofu init
tofu apply

A full build takes around 25 minutes. Managed databases and object storage are the slow parts, and there's no progress output while they provision. Don't interrupt the apply. An interrupted apply leaves resources that exist in UpCloud but not in your state.

A complete working configuration is in examples/basic.

Inputs

Name Description Type Default Required
name_prefix Prefix for every resource name. Also used as the database, pool, bucket, and share name. string yes
db_password Password for the application database user string yes
zone UpCloud zone for all zonal resources string "fi-hel1" no
object_storage_region Object storage region. Must include zone in its zone list. string "europe-1" no
cidr Address range for the application network string "10.0.10.0/24" no
mgmt_cidr Address range for the management network. Must not overlap cidr. string "10.0.20.0/24" no
server_count Number of application servers number 2 no
server_plan UpCloud plan for the application servers string "1xCPU-2GB" no
db_plan Managed PostgreSQL plan string "1x1xCPU-2GB-25GB" no
valkey_plan Managed Valkey plan string "1x1xCPU-2GB" no
pool_mode PgBouncer pool mode string "transaction" no
pool_size PgBouncer pool size number 10 no

Outputs

Name Description
lb_dns Public DNS name of the load balancer
app_private_ips Private IPs of the application servers
network_id UUID of the application network
mgmt_network_id UUID of the peered management network
pool_uri PgBouncer connection URI (sensitive). Use this rather than db_host and db_port.
db_host PostgreSQL hostname, direct connection
db_port PostgreSQL port, direct connection
db_name Application database name
valkey_host Valkey hostname
valkey_port Valkey port
bucket_name Object storage bucket name
s3_endpoint Private S3-compatible endpoint
file_share NFS share name
nfs_host NFS server address on the private network

What's on each server

Each application server boots with cloud-init, which:

  • Writes every connection detail to /etc/app.env, readable only by root: DATABASE_URL (through PgBouncer), VALKEY_HOST, VALKEY_PORT, VALKEY_PASSWORD, S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY, S3_SECRET_KEY, and SHARED_PATH.
  • Mounts the shared file storage at /mnt/shared through /etc/fstab, so it survives reboots.
  • Installs nginx and serves a placeholder page, so the load balancer's health checks have something to hit.

Replace the nginx placeholder in modules/compute/main.tf with your application's install.

Submodules

Each part of the architecture is its own module under modules/ and can be used on its own:

Module Creates
network Private network, router, NAT gateway
compute Application servers with cloud-init
loadbalancer Load balancer, backend, members, frontend, health checks
database PostgreSQL, logical database, user, PgBouncer pool, Valkey
storage Object storage, bucket, user, access key, scoped policy
filestorage Encrypted file storage, NFS share, access list
peering Management network and bidirectional peering

Things that will catch you out

  • A NAT gateway alone doesn't give private servers internet access. The network also needs dhcp_default_route = true, which this module sets.
  • Object storage takes a region, not a zone. The region's zone list must include your zone.
  • Managed databases get assigned ports, not 5432 or 6379. Always use the outputs.
  • Connecting with db_host and db_port bypasses PgBouncer without any error. Use pool_uri.
  • Managed databases on a private network can't migrate zones. Choose your zone before the first apply.
  • File storage encryption can only be set at creation.
  • Peering must exist in both directions before traffic flows. This module creates both.
  • The object storage bucket can't be emptied by OpenTofu. If tofu destroy fails on the bucket, delete its objects first.

Security

  • No application server has a public interface. The load balancer is the only ingress.
  • Every managed service has public access disabled.
  • Credentials marked sensitive are hidden from terminal output but stored in plain text in state, in the server metadata service, and in /etc/app.env. Use an encrypted remote backend for state. OpenTofu supports native state encryption from 1.7.
  • Access to servers is through the UpCloud web console, using the root password emailed at creation.

Limitations

  • The load balancer listens on HTTP only. Add a certificate bundle and a 443 frontend before production use.
  • The health check hits / and expects a 200.
  • File storage is fixed at 250 GB.
  • Replacing the servers, for example after changing user_data, replaces them all at once, with a short outage.

Cost

Most of these services bill hourly. Run tofu destroy when you're not using the environment. One tofu apply brings it back.

License

MIT. See LICENSE.

Contributors

Aahil13

Issues