Deploy on AWS with Helm
With the infrastructure in place, install the confident-ai Helm chart using the Terraform outputs. The chart pulls its images from Confident AI's registry and installs the app plus in-cluster ClickHouse. Because the app uses EKS Pod Identity, its ServiceAccount needs no annotation.
The recommended setup keeps app secrets in AWS Secrets Manager (synced by the External Secrets Operator) and runs Redis on ElastiCache. Both are provisioned by the Terraform module. In-cluster Redis and a Kubernetes Secret are supported as a simpler alternative, see Simpler option.
Create the namespace
kubectl create namespace confident-aiPut the secrets in Secrets Manager
DATABASE_URLcomes straight from Terraform. Each key in this JSON object becomes an app secret:SECRET_NAME=$(terraform output -raw secrets_manager_secret_name) REGION=$(terraform output -raw region) aws secretsmanager put-secret-value --secret-id "$SECRET_NAME" --region "$REGION" \ --secret-string "{ \"DATABASE_URL\":\"$(terraform output -raw database_url)\", \"BETTER_AUTH_SECRET\":\"$(openssl rand -hex 32)\", \"OPENAI_API_KEY\":\"sk-...\", \"CONFIDENT_LICENSE_KEY\":\"...\" }"Install the External Secrets Operator
Install it into the
confident-ainamespace as theexternal-secrets-saaccount, the exact account Terraform gave the read role to, so ESO inherits the AWS access through Pod Identity with no keys:helm repo add external-secrets https://charts.external-secrets.io && helm repo update helm install external-secrets external-secrets/external-secrets \ -n confident-ai \ --set installCRDs=true \ --set serviceAccount.name=external-secrets-saSet up ALB ingress (for HTTPS)
To serve the app on a domain over HTTPS, install the AWS Load Balancer Controller (it provisions an ALB from your Ingress) and request an ACM certificate in your region covering all four subdomains,
app.,api.,evals., andotel.ofyourdomain.com(a wildcard*.yourdomain.comalso works). You will reference the certificate ARN in the values file below.Write the values file
Save this as
values.aws.yaml. Fill the bracketed values from your Terraform outputs and the credentials Confident AI gave you.# The chart mints and refreshes the ECR pull secret from these credentials. imagePullSecrets: - name: ecr-registry-credentials imagePullSecretRefresh: enabled: true region: us-east-1 awsAccessKeyId: "<from Confident AI>" awsSecretAccessKey: "<from Confident AI>" config: cloudProvider: AWS frontendUrl: https://app.yourdomain.com backendUrl: https://api.yourdomain.com subdomain: yourdomain.com serviceAccount: create: true # Pod Identity is already wired to this SA, no annotation needed storage: testCasesBucket: <test_cases_bucket> payloadsBucket: <payloads_bucket> aws: region: <region> # Recommended: app secrets come from AWS Secrets Manager via ESO. secrets: externalSecrets: enabled: true provider: aws createStore: true remoteKey: <secrets_manager_secret_name> aws: region: <region> clickhouse: internal: true password: "<choose-a-password>" storageClass: gp3 keeper: storageClass: gp3 # Recommended: managed Redis (ElastiCache) from Terraform. redis: internal: false externalUrl: <redis_url> # Required for code-based and transformer metrics (Lambda sandbox from Terraform). codeExecutor: provider: AWS_LAMBDA aws: lambdaFunctionName: <code_executor_function_name> lambdaRegion: <region> ingress: enabled: true className: alb annotations: alb.ingress.kubernetes.io/scheme: internet-facing alb.ingress.kubernetes.io/target-type: ip alb.ingress.kubernetes.io/group.name: confident alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]' alb.ingress.kubernetes.io/certificate-arn: <your-acm-cert-arn> hosts: evals: evals.yourdomain.com otel: otel.yourdomain.comInstall the chart
The chart is published to GHCR as an OCI artifact:
helm install confident-ai \ oci://ghcr.io/confident-ai/charts/confident-ai \ --version 0.2.0 \ -n confident-ai \ -f values.aws.yaml kubectl get pods -n confident-ai -wThe ClickHouse operator starts first, then a migrations job runs, then the app pods come up. This takes a few minutes.
Verify
kubectl get externalsecret -n confident-ai # STATUS should be SecretSynced kubectl get ingress -n confident-ai # ADDRESS is the ALB hostnameCreate
app.,api.,evals., andotel.DNS records (CNAME, or a Route 53 alias) pointing at the ALB hostname, then openhttps://app.yourdomain.comand sign in.
Simpler option: in-cluster Redis and a Kubernetes Secret
If you would rather not run a cloud secret store or managed Redis, the chart can hold secrets in a Kubernetes Secret and run Redis in the cluster. This is less production-hardened (secrets live in the cluster, Redis has no managed backups), but it removes the ESO and ElastiCache steps. Skip steps 2 and 3 above, and replace the secrets and redis blocks in the values file with:
secrets:
data:
DATABASE_URL: "<database_url>"
BETTER_AUTH_SECRET: "<openssl rand -hex 32>"
OPENAI_API_KEY: "sk-..."
CONFIDENT_LICENSE_KEY: "<your license key>"
redis:
internal: true
storageClass: gp3Back up ClickHouse (recommended)
For production, enable the nightly ClickHouse backup to S3. Provision the backup bucket (confident_clickhouse_backup_bucket_enabled = true); the backup pod writes with Pod Identity, so set serviceAccountName to a service account whose role can write to that bucket. Full detail is on the Disaster Recovery page. Add this under your existing clickhouse: block and helm upgrade:
clickhouse:
backup:
enabled: true
provider: s3
schedule: "0 2 * * *" # nightly at 02:00 UTC
serviceAccountName: confident
s3:
bucket: <clickhouse_backup_bucket>
region: <region>Troubleshooting
| Symptom | Cause and fix |
|---|---|
externalsecret never reaches SecretSynced | ESO must run as external-secrets-sa in the confident-ai namespace so it inherits the Pod Identity read role. Recheck the helm install flags and that confident_create_secrets_manager = true. |
Pods stuck Pending, PVCs unbound | EKS ships no default StorageClass. Create the gp3 default from the Infrastructure page; the values file sets storageClass: gp3 on ClickHouse. |
ImagePullBackOff on the app images | The images live in a separate ECR account, so node IAM cannot pull them. Confirm imagePullSecretRefresh is enabled and the AWS keys from Confident AI are correct. |
Ingress never gets an ADDRESS | The AWS Load Balancer Controller is not installed, or the subnets are not tagged. Public subnets need kubernetes.io/role/elb=1; the network step tags them for you. |
Frontend returns 500 with ENOTFOUND confident-backend | The frontend resolves backend services by their chart-prefixed names. Keep fullnameOverride: confident (the chart default); do not change it. |
| S3 access denied from the app | Pod Identity is not associated. Keep serviceAccount.create: true with the default name so it matches the association Terraform created. |
ClickHouse Keeper logs Not authenticated | Stale PersistentVolumeClaims from a previous failed install. helm uninstall, kubectl delete pvc -n confident-ai --all, then reinstall. |
broken pipe errors during kubectl port-forward | Harmless connection resets. Re-run the port-forward. |
Updating and tearing down
- Change the app: edit
values.aws.yaml, thenhelm upgrade confident-ai oci://ghcr.io/confident-ai/charts/confident-ai --version 0.2.0 -n confident-ai -f values.aws.yaml. - Rotate a secret: run
aws secretsmanager put-secret-valueagain. ESO re-syncs it, then restart the pods to pick up the change (they hold secrets as env vars until they restart):kubectl rollout restart deployment -n confident-ai. - Change infrastructure: edit the Terraform config and
terraform apply. - Remove everything:
helm uninstall confident-ai -n confident-ai, thenterraform destroy, then delete the network from the Infrastructure page if Terraform did not own it.
Last updated on