Self-Hosted Deployment Guide¶
Overview¶
Deploy CloudAEye Code Review in your AWS account with the CloudAEye Terraform configuration. The stack creates the network, ECS Managed Instances cluster, application services, data services, authentication service, queues, and logging required for a self-hosted deployment.
Prerequisites¶
Before you deploy, confirm you have:
- Terminal.
- Terraform
1.15.5installation guide. - AWS CLI configured for your AWS account.
- AWS permissions to create VPC, EC2, ECS, Elastic Load Balancing, IAM, EBS, SQS, Amazon SES, CloudWatch Logs, Cloud Map, and related resources.
- The Terraform deployment ZIP downloaded from your email.
- Tenant admin details for the first admin user.
- A GitHub App for the self-hosted Code Review integration. Follow Create The GitHub App before filling in the
github_*Terraform values. - License key. You will receive it in your email.
What Gets Created¶
| Resource | Purpose |
|---|---|
| VPC, public subnet, private subnet | Isolated network for the deployment |
| Internet gateway and NAT gateway | Public load balancer access and private ECS outbound access |
| Application Load Balancer | Public entry point for CloudAEye routes |
| ECS cluster with ECS Managed Instances | Runtime for application containers and data services |
| MongoDB ECS service | Internal database for CloudAEye application data |
| ChromaDB ECS service | Internal vector database |
| ECS managed EBS volumes | Persistent storage for MongoDB and ChromaDB |
| Cloud Map private DNS | Internal service discovery for MongoDB and ChromaDB |
| SQS queues and dead-letter queues | Queues asynchronous indexing and review work; failed messages move to dedicated 14-day dead-letter queues after five receives |
| Better Auth ECS service | Tenant registration, login, OAuth, 2-step verification, password management, and enterprise SSO |
| CloudWatch log groups and alarms | Logs for application, data, initialization, and cleanup tasks; alarms cover queue backlogs, dead-letter queues, pending ECS tasks, and unhealthy load balancer targets |
| EventBridge Scheduler cleanup job | Runs the private cloudaeye-cleanup task every day at 00:00 UTC to remove expired code-review data |
MongoDB and ChromaDB are private services and are not exposed through the public load balancer.
Terraform Bundle Files¶
CloudAEye provides the deployment bundle as a ZIP file. It contains the deployable Terraform configuration, example inputs, release metadata, and the initializer helper required during the first apply.
| File | What it contains |
|---|---|
terraform.tf |
Terraform CLI and provider version constraints. |
providers.tf |
AWS provider configuration, including the selected region and local AWS profile. |
variables.tf |
Inputs for AWS, networking, HTTPS, tenant initialization, GitHub App authentication, licensing, LLM settings, ECS sizing, storage, service overrides, and autoscaling. |
locals.tf |
Derived names, release-pinned container images, default ECS service definitions, environment variables, public routes, capacity-provider sizing, and shared tags. |
main.tf |
VPC networking, security groups, SQS source queues and dead-letter queues, Cloud Map service discovery, and HTTP/HTTPS load balancer listeners and routing. |
iam.tf |
IAM roles and least-privilege policies for ECS execution and application tasks, managed instances, EBS volumes, SQS, authentication email, and license-agent runtime inspection. |
ecs.tf |
ECS cluster and Managed Instances capacity providers; MongoDB, ChromaDB, license-agent, application, and Fargate-overflow services; persistent volumes; autoscaling; and the one-time initializer task. |
cleanup.tf |
Daily EventBridge Scheduler job, ECS task definition, IAM permissions, logs, and outputs for the private cloudaeye-cleanup task. |
observability.tf |
CloudWatch alarms for non-empty dead-letter queues, old source-queue messages, sustained pending ECS tasks, and unhealthy load balancer targets. The alarms have no notification action by default. |
outputs.tf |
ECS names and role ARNs, application URL, public routes, internal data-service endpoints, storage sizes, and source/dead-letter queue URLs and ARNs. Cleanup outputs are declared in cleanup.tf. |
terraform.tfvars.example |
Example inputs to copy to a local, uncommitted terraform.tfvars file. |
onprem-release.auto.tfvars.json |
Build-generated release pin. Terraform loads it automatically so all CloudAEye images use the release represented by the ZIP. Do not edit it. |
onprem-release-manifest.json |
Build-generated record of the release tag and the exact CloudAEye image references packaged for that release. |
.terraform.lock.hcl |
Provider dependency selections used to produce a reproducible Terraform installation. |
run_cloudaeye_initialize.py |
Helper invoked by Terraform during the first apply to run the initializer ECS task, wait for it to stop, and fail the apply with ECS diagnostics when initialization is unsuccessful. |
README.md |
Concise deployment and operations reference included with the bundle. |
The release bundle intentionally does not contain terraform.tfvars, Terraform state, or CloudAEye's internal release-packaging script. Keep your generated terraform.tfvars and state files private.
Step 1: Prepare The Terraform Bundle¶
Extract the downloaded Terraform ZIP, then go to the extracted deployment directory:
cd /path/to/on-prem-deployment
Create your local variables file:
cp terraform.tfvars.example terraform.tfvars
Sample variable file:
aws_region = "us-east-2" // Replace "us-east-2" with actual value
aws_profile = "default" // Replace "default" with actual value
app_name = "cloudaeye" // Replace "cloudaeye" with actual value
# Restrict this to your office IP range.(Optional)
allowed_app_cidr = "0.0.0.0/0" // Replace "0.0.0.0/0" with actual value
mongodb_password = "password" // Replace "password" with actual value
server_x_api_key = "server-x-api-key" // Replace "server-x-api-key" with actual value
# CloudAEye-hosted LLM access is the default. Set llm_host_type to
# "your_own" only when supplying BYO credentials below.
llm_host_type = "cloudaeye" // Replace "cloudaeye" with actual value
llm_provider = "OpenAI" // Replace "OpenAI" with actual value
llm_model = "gpt-5.4" // Replace "gpt-5.4" with actual value
# BYO LLM credentials. Uncomment only the values required by llm_provider.
# OpenAI:
# llm_openai_api_key = "openai-api-key" // Replace "openai-api-key" with actual value
# Anthropic:
# llm_anthropic_api_key = "anthropic-api-key" // Replace "anthropic-api-key" with actual value
# AWS_Bedrock:
# llm_aws_bedrock_access_key = "aws-bedrock-access-key" // Replace "aws-bedrock-access-key" with actual value
# llm_aws_bedrock_secret_key = "aws-bedrock-secret-key" // Replace "aws-bedrock-secret-key" with actual value
# llm_aws_bedrock_region = "us-east-2" // Replace "us-east-2" with actual value
# GCP_Vertex:
# llm_gcp_service_account_json = "{\"type\":\"service_account\"}" // Replace the example JSON with actual value
# Azure:
# llm_azure_api_key = "azure-api-key" // Replace "azure-api-key" with actual value
# llm_azure_endpoint = "https://example.openai.azure.com" // Replace the example URL with actual value
# llm_azure_api_version = "2024-02-15-preview" // Replace "2024-02-15-preview" with actual value
server_aws_access_key_id = "server-aws-access-key-id" // Replace "server-aws-access-key-id" with actual value
server_aws_secret_access_key = "server-aws-secret-access-key" // Replace "server-aws-secret-access-key" with actual value
tenant_key = "1" // Replace "1" with actual value
tenant_identifier = "company" // Replace "company" with actual value
tenant_admin_email = "admin@example.com" // Replace "admin@example.com" with actual value
tenant_admin_password = "TestPassword@123" // Replace "TestPassword@123" with actual value
tenant_admin_company = "Example Company" // Replace "Example Company" with actual value
tenant_admin_first_name = "Jane" // Replace "Jane" with actual value
tenant_admin_last_name = "Admin" // Replace "Admin" with actual value
# Authentication email through your pre-verified SES identity.
ses_identity_arn = "arn:aws:ses:us-east-2:123456789012:identity/example.com" // Replace the example ARN with actual value
ses_from_email = "CloudAEye <no-reply@example.com>" // Replace the example sender with actual value
# CloudAEye-issued on-prem license token.
license_token = "cla_live_replace-with-cloudAEye-issued-license-token" // Replace the example token with actual value
# Required by on-prem GitHub App auth and webhook handling.
github_app_key = "your-app-key" // Replace "your-app-key" with actual value
github_installation_id = "12345678" // Replace "12345678" with actual value
github_app_id = "123456" // Replace "123456" with actual value
# Set exactly one of github_user or github_organization.
# github_organization = "my_org" // Replace "my_org" with actual value
github_user = "your-github-user" // Replace "your-github-user" with actual value
# Replace the example private key below with actual value.
github_private_key = <<EOT
-----BEGIN RSA PRIVATE KEY-----
replace-with-your-github-app-private-key
-----END RSA PRIVATE KEY-----
EOT
github_webhook_secret = "replace-with-github-webhook-secret" // Replace the example secret with actual value
Step 2: Configure Terraform Variables¶
Edit terraform.tfvars and set the exact values:
Understanding the source labels¶
The Source column identifies who chooses, provides, or creates each value:
| Source | What it means |
|---|---|
| CloudAEye | CloudAEye provides the value. Contact CloudAEye if you have not received it. |
| Generate | You create a new strong random value. Generate a separate value for every secret. |
| AWS | Obtain the value from your AWS account. |
| GitHub | Obtain the value from the GitHub App settings or installation details. |
| LLM provider | Obtain the value from the selected model provider's console, such as OpenAI, Anthropic, AWS, Google Cloud, or Azure. |
| You | Choose or supply the value based on your deployment, organization, or initial admin user. |
AWS and deployment basics¶
| Variable | Source | How to obtain the value | Example |
|---|---|---|---|
aws_region |
You | Choose the AWS region where you want to deploy. | us-east-2 |
aws_profile |
You | Use the profile configured by aws configure. Use default if you configured the default profile. |
default |
app_name |
You | Choose a prefix for the resources created by this deployment. | cloudaeye |
allowed_app_cidr |
You | Use your public IP address with a /32 suffix. |
203.0.113.10/32 |
Generated secrets¶
Generate a different strong random value for each secret.
| Variable | Source | How to obtain the value | Example format |
|---|---|---|---|
mongodb_password |
Generate | Generate a strong random password for the private MongoDB service. | <64-character-random-hex-string> |
server_x_api_key |
Generate | Generate a strong random API key for communication with the CloudAEye application containers. | <64-character-random-hex-string> |
github_webhook_secret |
Generate | Generate a strong random secret. Enter this exact value in both terraform.tfvars and the GitHub App webhook configuration. |
<64-character-random-hex-string> |
AWS credentials for the application runtime¶
These credentials are used by the deployed CloudAEye services. They are separate from the local aws_profile that Terraform uses during deployment.
| Variable | Source | How to obtain the value | Example format |
|---|---|---|---|
server_aws_access_key_id |
AWS | In the AWS console, open IAM > Users > your application user > Security credentials, then create an access key. | <AWS-access-key-id> |
server_aws_secret_access_key |
AWS | Copy the secret access key when AWS displays it after creating the access key. AWS displays this value only once. | <AWS-secret-access-key> |
LLM configuration¶
| Variable | Source | How to obtain the value | Example |
|---|---|---|---|
llm_host_type |
You | Choose cloudaeye for CloudAEye-hosted models, which is the default, or your_own when supplying your own provider credentials. |
cloudaeye |
llm_provider |
You | Choose one of OpenAI, Anthropic, AWS_Bedrock, GCP_Vertex, or Azure. |
OpenAI |
llm_model |
You | Enter a model supported by the selected provider. | gpt-5.4 |
llm_* provider settings |
LLM provider | Required only with llm_host_type = "your_own". Obtain the API key, credentials, endpoint, region, or service account details from your provider console. |
<OpenAI-key> |
Tenant and initial admin¶
| Variable | Source | How to obtain the value | Example |
|---|---|---|---|
tenant_key |
CloudAEye | Assigned by CloudAEye during tenant and license provisioning. | 12345 |
tenant_identifier |
CloudAEye | Assigned by CloudAEye during tenant and license provisioning. | acme-corp |
tenant_admin_email |
You | Enter the email address of your first admin user. Use this when logging into the CloudAEye console. | admin@example.com |
tenant_admin_password |
You | It must include uppercase and lowercase letters, a number, and a symbol, and be at least 8 characters long. Use this when logging into the CloudAEye console. | <strong-admin-password> |
tenant_admin_company |
You | Enter your company name. | Acme Corporation |
tenant_admin_first_name, tenant_admin_last_name |
You | Enter the first and last name of the initial user. | John, Smith |
License¶
| Variable | Source | How to obtain the value | Example format |
|---|---|---|---|
license_token |
CloudAEye | Use the token emailed to you by CloudAEye. Production tokens start with cla_live_. |
<issued-token> |
GitHub App¶
| Variable | Source | How to obtain the value | Example |
|---|---|---|---|
github_app_id |
GitHub | Open the GitHub App settings page and copy App ID. | 123456 |
github_app_key |
GitHub | Use a random generated value. | cloudaeye-code-review-acme |
github_installation_id |
GitHub | Copy the numeric ID from the installation URL: .../settings/installations/<ID>. |
6666666 |
github_user |
GitHub | For a personal-account installation, enter the account username. Set exactly github_user. |
octocat |
github_organization |
GitHub | For an organization installation, enter the organization name. Set exactly github_organization. |
acme-corp |
github_private_key |
GitHub | In the GitHub App settings, select Private keys > Generate a private key. Paste the complete contents of the downloaded .pem file. |
-----BEGIN RSA PRIVATE KEY-----... |
github_webhook_secret |
GitHub | Use the same generated random secret that you entered in the GitHub App webhook configuration. | <64-character-random-hex-string> |
Authentication email settings¶
Set both values to send verification, invitation, password-reset, and security emails through a verified Amazon SES identity.
| Variable | Source | How to obtain the value | Example |
|---|---|---|---|
ses_identity_arn |
AWS | Copy the ARN of the verified identity from Amazon SES in the deployment region. | arn:aws:ses:us-east-2:123456789012:identity/example.com |
ses_from_email |
You / AWS | Enter the sender name and an email address allowed by that SES identity. | <no-reply@example.com> |
Step 3: Deploy¶
Run Terraform:
terraform init
terraform validate
terraform plan
terraform apply
Review the plan before applying. On the first apply, Terraform creates the infrastructure, starts the ECS services, and runs a one-time authentication bootstrap task after Better Auth and the other application services are ready.
The bootstrap task creates the Better Auth tenant owner and credential account and initializes the tenant's product settings in MongoDB. For registration, OAuth, 2-step verification, password lifecycle, and password-policy details, see the CloudAEye authentication guide.
Step 4: Access CloudAEye¶
After terraform apply completes, get the application URL:
terraform output -raw app_base_url
The command prints the deployed application URL. Open the CloudAEye UI by appending /ui to that URL.
You can also inspect the load balancer DNS name and public routes:
terraform output alb_dns_name
terraform output service_routes
Public paths:
| Route | Service |
|---|---|
/ui/* |
cloudaeye-web |
/code-review/* |
cloudaeye-reviews |
/rca/test/* |
cloudaeye-api |
/rca/code-repository-integration/* |
cloudaeye-repositories |
/external-service/* |
cloudaeye-indexer-api |
/secret-scanner/* |
cloudaeye-secrets |
/knowledge-graph |
cloudaeye-knowledge |
/auth/* |
tenant-registration-v2 |
/github-app/webhook |
cloudaeye-webhook |
The cloudaeye-indexer-worker, cloudaeye-cleanup, MongoDB, and ChromaDB services are private and are not exposed through the load balancer. The cloudaeye-cleanup task runs once per day and removes old data from MongoDB.
Upgrade An HTTP Deployment To HTTPS¶
You can deploy over HTTP first and add HTTPS after the load balancer exists. The upgrade requires a DNS name you own and an issued ACM certificate; an ACM certificate cannot provide trusted HTTPS for the AWS-generated *.elb.amazonaws.com hostname.
1. Get the load balancer hostname¶
From the Terraform deployment directory, run:
terraform output -raw alb_dns_name
Save the returned hostname, for example cloudaeye-example.us-east-2.elb.amazonaws.com.
2. Get the certificate ARN¶
Use an existing public certificate in AWS Certificate Manager (ACM) that covers your CloudAEye hostname. The certificate must have the Issued status and be in the same AWS account and region as the load balancer.
Open the certificate in ACM and copy its ARN. You will add this value to terraform.tfvars in Step 4.
If you do not already have a suitable certificate, follow the AWS instructions for requesting a public certificate. Return to this guide after ACM issues the certificate.
3. Point your hostname to the load balancer¶
Create the application DNS record at your DNS provider:
| Record | Name | Value |
|---|---|---|
| CNAME | cloudaeye.example.com |
The value returned by terraform output -raw alb_dns_name |
Squarespace DNS example¶
If Squarespace manages DNS for example.com:
- Open the Squarespace Domains dashboard and select
example.com. - Choose DNS, then DNS Settings.
- Under Custom Records, choose Add record.
- Add the following record:
| Squarespace field | Example value |
|---|---|
| Type | CNAME |
| Name | cloudaeye |
| Data | cloudaeye-example.us-east-2.elb.amazonaws.com |
| TTL | Default |
Replace the example Data with the value returned by terraform output -raw alb_dns_name. Squarespace automatically appends .example.com to the Name, so entering cloudaeye creates cloudaeye.example.com. See Squarespace DNS record troubleshooting if the record does not save.
For an apex domain such as example.com, use your DNS provider's ALIAS, ANAME, or CNAME-flattening feature instead of a standard CNAME. A subdomain such as cloudaeye.example.com is simpler because it can use a standard CNAME. Wait for your hostname to resolve to the load balancer before continuing.
4. Enable HTTPS in Terraform¶
Add both values to terraform.tfvars:
app_domain_name = "cloudaeye.example.com"
acm_certificate_arn = "arn:aws:acm:us-east-2:123456789012:certificate/00000000-0000-0000-0000-000000000000"
Use the complete ARN of the issued certificate. Then review and apply the change:
terraform validate
terraform plan
terraform apply
Expect the plan to update the load balancer listeners and ECS task definitions.
When both values are set, Terraform adds an HTTPS listener on port 443 and redirects HTTP port 80 to your configured hostname over HTTPS while preserving the path and query string. Leave both values empty to keep an HTTP-only deployment; setting only one value is invalid.
5. Verify the upgrade¶
Check the canonical URL and the HTTP redirect:
terraform output -raw app_base_url
curl -I "http://cloudaeye.example.com/ui"
curl -I "https://cloudaeye.example.com/ui"
The first request should return a 301 redirect to HTTPS. The second should complete a TLS handshake and return an application response. Open <app_base_url>/ui and verify the GitHub App webhook after updating its URL to <app_base_url>/github-app/webhook.
Configuration¶
Most configuration specific to your deployment is handled through terraform.tfvars.
To restrict access to the public application, set allowed_app_cidr to your office or CIDR:
allowed_app_cidr = "203.0.113.10/32"
For advanced deployments, the ecs_services map can override service images, CPU, memory, desired count, commands, route paths, and health check paths. Use this only after consulting CloudAEye.
Operations¶
List ECS services:
aws ecs list-services \
--cluster "$(terraform output -raw ecs_cluster_name)"
Describe a service:
aws ecs describe-services \
--cluster "$(terraform output -raw ecs_cluster_name)" \
--services <service-name>
CloudWatch log groups are created under:
/ecs/<app_name>-<environment>/<service-name>
Destroy¶
To remove the deployment:
terraform destroy
This deletes the infrastructure created by the stack, including ECS services and the EBS data volumes. Export or back up any required data before destroying the deployment.