Ensemble Docs
Self-Hosting

Deploy on AWS (EKS)

End-to-end walkthrough to deploy Ensemble on Amazon EKS with Terraform, the Helm chart, ALB ingress, and ACM TLS.

This walkthrough deploys the platform on Amazon EKS. It assumes you have read Prerequisites and have a PostgreSQL database and a Temporal endpoint ready. Commands use a profile named ensemble; adjust to your setup.

1. Set your environment

export AWS_PROFILE=ensemble
export AWS_REGION=us-west-2
export CLUSTER_NAME=workflows-prod
export K8S_VERSION=1.31
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export IMAGE_REGISTRY=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
export DOMAIN=workflows.example.com

2. Create the EKS cluster

Create the cluster with an OIDC provider (required for IRSA) and nodes in private subnets with a single NAT gateway for a stable outbound IP.

eksctl create cluster \
  --name $CLUSTER_NAME \
  --region $AWS_REGION \
  --version $K8S_VERSION \
  --nodegroup-name workflows-nodes \
  --node-type t3.large \
  --nodes 2 --nodes-min 1 --nodes-max 4 \
  --managed --with-oidc \
  --node-private-networking \
  --vpc-nat-mode Single

aws eks update-kubeconfig --name $CLUSTER_NAME --region $AWS_REGION
kubectl get nodes

Capture the OIDC provider ARN for Terraform:

OIDC_URL=$(aws eks describe-cluster --name $CLUSTER_NAME --region $AWS_REGION \
  --query "cluster.identity.oidc.issuer" --output text)
export OIDC_PROVIDER_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/${OIDC_URL#https://}"
echo $OIDC_PROVIDER_ARN

kubectl and helm act on your kubeconfig's current-context, which persists across shells. If it is on a GKE cluster, AWS commands fail trying to use gcloud auth. Run aws eks update-kubeconfig ... (above) to point at EKS, and verify with kubectl config current-context.

3. Install the cluster add-ons

You need the AWS Load Balancer Controller (for ALB ingress) and the External Secrets Operator (to sync secrets from Secrets Manager).

# AWS Load Balancer Controller (see AWS docs for the IAM policy)
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
  -n kube-system --set clusterName=$CLUSTER_NAME \
  --set serviceAccount.create=false \
  --set serviceAccount.name=aws-load-balancer-controller

# External Secrets Operator
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
  -n external-secrets --create-namespace

4. Provision supporting resources with Terraform

This creates the buckets, KMS key, IAM workloads role (IRSA), Secrets Manager secret, Cognito, and scheduler.

cd infrastructure/terraform
terraform init
terraform apply -var-file=environments/prod.tfvars \
  -var="eks_oidc_provider_arn=$OIDC_PROVIDER_ARN" \
  -var="eks_cluster_name=$CLUSTER_NAME"

terraform output helm_config_values
terraform output service_account_annotation
terraform output app_secrets_name

Keep the helm_config_values and service_account_annotation outputs; you will paste them into your Helm values.

5. Populate the secret

Terraform created app-secrets with empty placeholders. Write the real values (database URL, Temporal key, LLM keys). Do this out-of-band, never in Git:

aws secretsmanager put-secret-value \
  --secret-id workflows-prod/app-secrets \
  --region $AWS_REGION \
  --secret-string '{
    "PG_BASE_URL": "postgresql://user:pass@your-db-host:5432/workflows?sslmode=require",
    "TEMPORAL_API_KEY": "...",
    "OPENAI_API_KEY": "...",
    "ANTHROPIC_API_KEY": "..."
  }'

6. Make images available

Either pull directly from the Marketplace registry you are entitled to, or mirror the four images (server, web, worker, migration) into your own ECR and set global.imageRegistry accordingly. To mirror into ECR, enable the ECR module (enable_ecr = true) or create the repositories, then push the tagged images.

7. Request a TLS certificate

aws acm request-certificate --domain-name $DOMAIN \
  --validation-method DNS --region $AWS_REGION
# Complete DNS validation, then capture the ARN:
export CERT_ARN=$(aws acm list-certificates --region $AWS_REGION \
  --query "CertificateSummaryList[?DomainName=='$DOMAIN'].CertificateArn" --output text)

8. Create the SecretStore and deploy

Create a SecretStore pointing at Secrets Manager (an example ships in the chart's examples/secret-store-aws.yaml), then deploy. A minimal values overlay:

# my-values.yaml
global:
  imageRegistry: "123456789012.dkr.ecr.us-west-2.amazonaws.com/"

serviceAccount:
  create: true
  name: workflows-sa
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/workflows-prod-workloads-role

externalSecrets:
  enabled: true
  secretStoreRef: { name: aws-secrets-manager, kind: SecretStore }
  remoteRef: workflows-prod/app-secrets

# attach the synced secret to each component
# Attach the synced secret and set non-secret config per component.
# Fill each `config` from the Terraform helm_config_values output plus the
# Configuration reference; keys are omitted here for brevity.
server:
  envFrom:
    - secretRef: { name: app-secrets }
  config: {}          # server env vars
worker:
  envFrom:
    - secretRef: { name: app-secrets }
  config: {}          # worker env vars (a subset of the server's)
web:
  config: {}          # auth + URLs (no secret needed)
migrations:
  envFrom:
    - secretRef: { name: app-secrets }

ingress:
  enabled: true
  className: alb
  hosts:
    - host: workflows.example.com
      paths:
        - { path: /api, pathType: Prefix, service: server }
        - { path: /,    pathType: Prefix, service: web }
  tls: { enabled: true, certificateArn: "REPLACE_WITH_CERT_ARN" }

Fill *.config from the helm_config_values Terraform output plus the Configuration reference. Then:

helm upgrade --install workflows infrastructure/helm/workflows \
  -f infrastructure/helm/workflows/eks-values.yaml \
  -f my-values.yaml \
  -n workflows --create-namespace \
  --set global.imageRegistry="$IMAGE_REGISTRY/" \
  --set ingress.tls.certificateArn="$CERT_ARN"

The migration job runs first (as a pre-upgrade hook); then the workloads roll out.

9. Configure DNS

Point your domain at the ALB:

kubectl get ingress -n workflows   # note the ADDRESS (ALB hostname)

Create a Route 53 alias (or CNAME) from $DOMAIN to that ALB hostname.

10. Verify

kubectl get pods -n workflows
kubectl logs -f -l app.kubernetes.io/component=server -n workflows
curl -sSf https://$DOMAIN/api/health   # server health via the ingress

All pods should be Running, the migration job Completed, and the health endpoint should return success. If not, see Troubleshooting.

On this page