Skip to main content

Overview

The Terraform configurations for Onyx live at deployment/terraform/modules/aws/. They provision a production-grade AWS stack and wire the components together so you can follow up with a Helm install against the resulting EKS cluster. The modules available are:

onyx

High-level composition module. Orchestrates vpc, eks, postgres, redis, and s3 with sane defaults. Start here unless you need granular control.

vpc

Creates a VPC sized for EKS with multiple public and private subnets and an S3 gateway endpoint.

eks

Provisions an EKS cluster, node groups, EBS CSI driver, metrics server, cluster autoscaler, and optional IRSA for S3 access.

postgres

Amazon RDS for PostgreSQL. Returns a ready-to-use connection URL.

redis

ElastiCache for Redis replication group with optional auth token and transit encryption.

s3

S3 bucket for file storage, scoped to the VPC S3 gateway endpoint.

opensearch

Amazon OpenSearch domain inside the VPC. Supports fine-grained access control, encryption, and CloudWatch logging.

waf

AWS WAF with rate limiting, IP allowlisting, geo restriction, and CloudWatch logging.

Prerequisites

  • Terraform CLI 1.x or later
  • AWS credentials configured in your environment (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY, or an IAM role via instance profile / SSO)
  • Sufficient IAM permissions to create EKS clusters, RDS instances, ElastiCache groups, VPCs, S3 buckets, and IAM roles
  • kubectl and helm installed for the post-Terraform application deployment step

Quickstart

The example below provisions a complete Onyx AWS stack using the top-level onyx composition module.
1

Create a root Terraform configuration

Create a file such as main.tf with the following content:
locals {
  region = "us-west-2"
}

provider "aws" {
  region = local.region
}

module "onyx" {
  # Path relative to this file if the onyx repo is checked out alongside your config:
  source = "./modules/aws/onyx"

  region            = local.region
  name              = "onyx"
  postgres_username = "pgusername"
  postgres_password = "your-postgres-password"
}

resource "null_resource" "wait_for_cluster" {
  provisioner "local-exec" {
    command = "aws eks wait cluster-active --name ${module.onyx.cluster_name} --region ${local.region}"
  }
}

data "aws_eks_cluster" "eks" {
  name       = module.onyx.cluster_name
  depends_on = [null_resource.wait_for_cluster]
}

data "aws_eks_cluster_auth" "eks" {
  name       = module.onyx.cluster_name
  depends_on = [null_resource.wait_for_cluster]
}

provider "kubernetes" {
  host                   = data.aws_eks_cluster.eks.endpoint
  cluster_ca_certificate = base64decode(data.aws_eks_cluster.eks.certificate_authority[0].data)
  token                  = data.aws_eks_cluster_auth.eks.token
}

provider "helm" {
  kubernetes {
    host                   = data.aws_eks_cluster.eks.endpoint
    cluster_ca_certificate = base64decode(data.aws_eks_cluster.eks.certificate_authority[0].data)
    token                  = data.aws_eks_cluster_auth.eks.token
  }
}

output "cluster_name" {
  value = module.onyx.cluster_name
}

output "postgres_connection_url" {
  value     = module.onyx.postgres_connection_url
  sensitive = true
}

output "redis_connection_url" {
  value     = module.onyx.redis_connection_url
  sensitive = true
}
2

Initialise and apply

terraform init
terraform apply
The first apply provisions all infrastructure. EKS cluster creation typically takes 10–15 minutes.
3

Configure kubectl

aws eks update-kubeconfig \
  --name $(terraform output -raw cluster_name) \
  --region us-west-2
4

Deploy Onyx via Helm

Once the cluster is active, install the Onyx Helm chart. If the EKS module created an IRSA role for S3 access, disable MinIO and reference the generated service account:
kubectl create namespace onyx --dry-run=client -o yaml | kubectl apply -f -

helm upgrade --install onyx ./deployment/helm/charts/onyx \
  --namespace onyx \
  --set minio.enabled=false \
  --set serviceAccount.create=false \
  --set serviceAccount.name=onyx-s3-access \
  --set auth.opensearch.values.opensearch_admin_password='YourStrongPassword1!'
See Kubernetes for full Helm configuration options.

Using an existing VPC

If you already have a VPC, disable VPC creation and provide your existing resource IDs:
module "onyx" {
  source = "./modules/aws/onyx"

  region            = local.region
  name              = "onyx"
  postgres_username = "pgusername"
  postgres_password = "your-postgres-password"

  create_vpc         = false
  vpc_id             = "vpc-xxxxxxxx"
  private_subnets    = ["subnet-aaaa", "subnet-bbbb", "subnet-cccc"]
  public_subnets     = ["subnet-dddd", "subnet-eeee", "subnet-ffff"]
  vpc_cidr_block     = "10.0.0.0/16"
  s3_vpc_endpoint_id = "vpce-xxxxxxxxxxxxxxxxx"
}

Key variables

onyx module

VariableTypeDefaultDescription
namestring"onyx"Prefix for all resource names. Combined with the Terraform workspace.
regionstring"us-west-2"AWS region for all resources.
postgres_usernamestring"postgres"RDS superuser username.
postgres_passwordstringRDS superuser password. Required.
create_vpcbooltrueCreate a new VPC. Set to false to reuse an existing one.
public_cluster_enabledbooltrueEnable public EKS API endpoint.
private_cluster_enabledboolfalseEnable private EKS API endpoint. Set to true for production.
cluster_endpoint_public_access_cidrslist(string)[]Restrict public API access to these CIDR blocks.
redis_auth_tokenstringnullAuth token for the ElastiCache Redis cluster.
enable_iam_authboolfalseUse IAM authentication for RDS instead of a password.
tagsmap(string){"project":"onyx"}Tags applied to all resources.

WAF variables

VariableDefaultDescription
waf_rate_limit_requests_per_5_minutes2000Requests per 5 min per IP before blocking.
waf_api_rate_limit_requests_per_5_minutes1000API-specific rate limit per IP.
waf_allowed_ip_cidrs[]IPv4 CIDRs allowed through WAF. Empty = no allowlist.
waf_geo_restriction_countries[]Country codes to block. Empty = no restriction.
waf_enable_loggingtrueLog WAF decisions to CloudWatch.
waf_log_retention_days90Days to retain WAF logs.

OpenSearch variables

OpenSearch is opt-in for the AWS stack (the onyx module deploys Vespa on EKS by default):
VariableDefaultDescription
enable_opensearchfalseCreate a managed OpenSearch domain.
opensearch_engine_version"3.3"OpenSearch engine version.
opensearch_instance_type"r8g.large.search"Data node instance type.
opensearch_instance_count3Number of data nodes.
opensearch_ebs_volume_size512EBS volume size (GiB) per node.
opensearch_master_user_namenullMaster username for fine-grained access control.
opensearch_master_user_passwordnullMaster password.

Module outputs

OutputSensitiveDescription
cluster_nameNoEKS cluster name
postgres_connection_urlYesFull postgres://... connection string
redis_connection_urlYesElastiCache endpoint and port
The postgres_connection_url and redis_connection_url outputs are marked sensitive. Do not print them to CI logs. Retrieve them with terraform output -raw postgres_connection_url only in secure contexts.

Terraform workspaces

The onyx module uses the current Terraform workspace name as a suffix for resource names. Use workspaces to create isolated staging and production environments from a single configuration:
terraform workspace new staging
terraform apply

terraform workspace new production
terraform apply

IRSA for S3 access

If you supply s3_bucket_names to the EKS module, it creates an IRSA role and a Kubernetes ServiceAccount named onyx-s3-access in the onyx namespace. Use this to avoid storing long-lived S3 credentials:
module "onyx" {
  source          = "./modules/aws/onyx"
  # ... other vars ...
}

# The EKS module accepts s3_bucket_names via the onyx module's passthrough.
# Refer to modules/aws/eks/variables.tf for the full list of EKS inputs.
In the Helm chart, reference the generated service account:
serviceAccount:
  create: false
  name: onyx-s3-access

Build docs developers (and LLMs) love