> ## Documentation Index
> Fetch the complete documentation index at: https://docs.definite.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Install on AWS

> Stand up Definite in your own AWS account: EKS, RDS, S3, all driven by Terraform and the `definite` CLI.

This guide walks you through a production-grade on-prem install of Definite on AWS. You'll provision the infrastructure with Terraform, install the platform with the `definite` CLI, and tear it down cleanly when you're done.

End to end: about 30 to 40 minutes, most of which is `terraform apply` waiting on EKS and RDS.

## What gets created

| Resource              | Notes                                                                                        |
| --------------------- | -------------------------------------------------------------------------------------------- |
| VPC                   | Private subnets for nodes and the database, public subnets for NAT and load balancers        |
| EKS cluster           | Kubernetes 1.30 (configurable), managed node group, OIDC provider enabled for IRSA           |
| RDS Postgres 15       | Private (not publicly accessible), reachable only from the cluster security group            |
| S3 bucket             | Lakehouse data; public access blocked, encryption and versioning on                          |
| IAM IRSA roles        | Least-privilege roles for the lakehouse and for Fi's Bedrock access                          |
| IAM user + access key | Static-credential fallback for the lakehouse (see [IRSA vs access key](#irsa-vs-access-key)) |
| `gp3` StorageClass    | Cluster-default StorageClass the lakehouse PVC needs                                         |

<Note>
  Every name, instance size, CIDR, and count is a Terraform variable with a sane default. Override only what you need to in `terraform.tfvars`.
</Note>

***

## Prerequisites

<Steps>
  <Step title="AWS account access">
    AWS credentials in your shell with permission to create VPC, EKS, RDS, S3, and IAM resources. The Terraform provider reads the standard AWS credential chain (env vars, shared config, SSO, instance profile). No secrets live in the Terraform code.
  </Step>

  <Step title="Local tooling">
    Install the following on the machine you'll run `terraform` and `definite` from:

    | Tool        | Version | Check                      |
    | ----------- | ------- | -------------------------- |
    | `terraform` | 1.5+    | `terraform version`        |
    | `aws` CLI   | recent  | `aws --version`            |
    | `kubectl`   | 1.28+   | `kubectl version --client` |
    | `helm`      | 3.12+   | `helm version`             |
  </Step>

  <Step title="LLM access">
    Decide which LLM provider Fi will use. Bedrock is the most common choice for AWS deployments; this guide uses it as the default. Anthropic, Vertex, and Azure OpenAI are also supported. If you go with Bedrock, make sure you've requested model access for Claude in the target region.
  </Step>

  <Step title="(Optional) Remote Terraform state">
    For team use, configure an S3 backend in `versions.tf` before applying. A backend stub is committed in the module. Single-operator installs can skip this and use local state.
  </Step>
</Steps>

***

## Phase 1: Provision AWS infrastructure with Terraform

Clone the on-prem repo and change into the AWS Terraform module:

```bash theme={null}
git clone https://github.com/definite-app/definite-onprem
cd definite-onprem/deploy/terraform/aws
```

### 1. Configure inputs

Copy the example tfvars file and edit it:

```bash theme={null}
cp terraform.tfvars.example terraform.tfvars
$EDITOR terraform.tfvars
```

A minimal `terraform.tfvars` is just two lines:

```hcl theme={null}
region      = "<your-region>"
name_prefix = "<your-name-prefix>"
```

Everything else has a default. Common overrides:

```hcl theme={null}
# Lock the EKS public API endpoint to your office or VPN CIDRs.
cluster_endpoint_public_access_cidrs = ["<your-cidr>/24"]

# Production hardening.
rds_multi_az            = true
rds_deletion_protection = true

# Bedrock model access (must be enabled in the AWS console first).
bedrock_model_ids = ["anthropic.claude-sonnet-4-20250514-v1:0"]
```

See the module's [README](https://github.com/definite-app/definite-onprem/blob/main/deploy/terraform/aws/README.md) for the full list of variables.

### 2. Init, plan, apply

<CodeGroup>
  ```bash terminal theme={null}
  terraform init
  terraform plan
  terraform apply
  ```
</CodeGroup>

`terraform apply` takes 20 to 25 minutes (mostly EKS and RDS). When it finishes, every value you need is in `terraform output`.

### 3. Read the outputs

Inspect the non-sensitive outputs:

```bash theme={null}
terraform output
```

For sensitive values, read them explicitly with `-raw`:

```bash theme={null}
terraform output -raw rds_password
terraform output -raw lakehouse_s3_secret_access_key
terraform output -raw postgres_url
```

The values you'll feed into `config.yaml` are summarized here:

| Terraform output                             | Goes into                                   |
| -------------------------------------------- | ------------------------------------------- |
| `cluster_name`                               | `aws eks update-kubeconfig --name ...`      |
| `region`                                     | `object_store.region`, `llm.region`         |
| `postgres_url` (sensitive)                   | `postgres.url`                              |
| `rds_password` (sensitive)                   | `POSTGRES_PASSWORD` env var                 |
| `lakehouse_bucket_name`                      | `object_store.bucket`                       |
| `lakehouse_prefix`                           | `lakehouse.prefix`                          |
| `lakehouse_s3_access_key_id`                 | `S3_ACCESS_KEY_ID` env var                  |
| `lakehouse_s3_secret_access_key` (sensitive) | `S3_SECRET_ACCESS_KEY` env var              |
| `bedrock_irsa_role_arn`                      | `serviceAccount.annotations` (Bedrock path) |

The module also emits a ready-to-paste, non-secret `config.yaml` snippet:

```bash theme={null}
terraform output -raw config_yaml_fragment
```

### IRSA vs access key

The module emits both an **IRSA role** and an **IAM user + access key** for lakehouse S3 access. Both share one identical least-privilege policy.

| Option                                                                          | When to use                                                                                                                                                                            |
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Access key** (`lakehouse_s3_access_key_id`, `lakehouse_s3_secret_access_key`) | **Use this today.** The lakehouse reads S3 through DuckDB's `httpfs` extension, which speaks the S3 API with static credentials, not the AWS SDK, so it can't assume an IRSA role yet. |
| **IRSA role** (`lakehouse_irsa_role_arn`)                                       | **The end state.** Once the lakehouse gains SDK-based S3 support, annotate the ServiceAccount with this role's ARN and delete the IAM user. No infra change needed.                    |

Fi's Bedrock access uses IRSA today (no static credentials).

***

## Phase 2: Install Definite with the `definite` CLI

### 1. Install the CLI

```bash theme={null}
curl -fsSL https://storage.googleapis.com/definite-public/definite-onprem/install.sh | sh
```

The install script detects your OS and architecture, downloads the matching prebuilt binary from a public Google Cloud Storage bucket, verifies its SHA256 checksum, and places `definite` on your PATH (default: `$HOME/.local/bin`). Binaries are published for macOS and Linux (arm64 and x86\_64), and are uploaded by the release workflow using short-lived Workload Identity Federation credentials (no long-lived keys).

To pin a version, set `DEFINITE_VERSION` before piping to `sh`:

```bash theme={null}
curl -fsSL https://storage.googleapis.com/definite-public/definite-onprem/install.sh \
  | DEFINITE_VERSION=v0.1.0 sh
```

Verify:

```bash theme={null}
definite version
```

### 2. Point `kubectl` at the new cluster

```bash theme={null}
aws eks update-kubeconfig \
  --region "$(terraform output -raw region)" \
  --name   "$(terraform output -raw cluster_name)"

kubectl get nodes
```

You should see your managed node group's nodes in the `Ready` state.

### 3. Bootstrap cluster prerequisites

`definite bootstrap` installs the cluster-level pieces that `definite init` assumes are already present:

| Prerequisite                     | What it provides                                                    |
| -------------------------------- | ------------------------------------------------------------------- |
| Ingress controller               | HTTP/S routing for the deployment's `Ingress` resource              |
| cert-manager (+ CRDs)            | TLS certificate issuance for `tls: cert_manager`                    |
| `letsencrypt-prod` ClusterIssuer | The issuer the ingress references for automatic Let's Encrypt certs |
| agent-sandbox CRDs               | Custom resources the Fi runtime uses to dispatch per-run sandboxes  |

Run it once against the fresh cluster:

```bash theme={null}
definite bootstrap --acme-email you@yourcompany.com
```

`--dry-run` prints what it would install without touching the cluster. The command is idempotent; safe to re-run.

### 4. Discover the load balancer hostname

The ingress controller provisions an AWS Network Load Balancer. Wait for it to land, then grab its hostname:

```bash theme={null}
kubectl get svc ingress-nginx-controller -n ingress-nginx \
  -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
```

You'll get something like `<random-id>.elb.<your-region>.amazonaws.com`.

You now have two choices for what to set as `deployment.hostname` in your `config.yaml`:

<Tabs>
  <Tab title="Quick install (nip.io)">
    For demos and internal pilots, resolve the LB hostname to an IP and use a `<ip-with-dashes>.nip.io` host. No DNS configuration needed:

    ```bash theme={null}
    LB_HOST=$(kubectl get svc ingress-nginx-controller -n ingress-nginx \
      -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
    LB_IP=$(dig +short "$LB_HOST" | head -1)
    HOSTNAME="${LB_IP//./-}.nip.io"
    echo "Use this as deployment.hostname: $HOSTNAME"
    ```

    `nip.io` wildcards every `<ip>.nip.io` host to that IP, so Let's Encrypt issues a real cert with no extra setup.

    <Warning>
      Some OAuth providers (Google, Slack, HubSpot) reject `nip.io` redirect URIs. Use a real CNAME for production integrations.
    </Warning>
  </Tab>

  <Tab title="Production (real CNAME)">
    Create a CNAME record in your DNS provider pointing your chosen hostname (e.g. `definite.acme.com`) at the LB hostname:

    ```
    definite.acme.com    CNAME    <lb-hostname>.elb.<your-region>.amazonaws.com
    ```

    Wait for the record to propagate, then use that hostname in `config.yaml`. cert-manager will issue a Let's Encrypt cert against it on first deploy.
  </Tab>
</Tabs>

### 5. Build `config.yaml`

Start from the EKS example in the repo: [`examples/minimal-eks.yaml`](https://github.com/definite-app/definite-onprem/blob/main/examples/minimal-eks.yaml). The shape:

```yaml theme={null}
deployment:
  name: definite
  namespace: definite
  hostname: <your-hostname>          # from step 4
  tls: cert_manager

postgres:
  url: postgres://<rds-user>:${POSTGRES_PASSWORD}@<rds-endpoint>:5432/<db-name>

object_store:
  type: s3
  bucket: <bucket-name>              # terraform output lakehouse_bucket_name
  region: <your-region>              # terraform output region
  credentials:
    key_id:
      env: S3_ACCESS_KEY_ID
    secret:
      env: S3_SECRET_ACCESS_KEY

lakehouse:
  prefix: lake/                      # terraform output lakehouse_prefix
  storage:
    size: 50Gi
    storage_class_name: gp3

auth:
  mode: oidc                         # or `local` for username/password auth
  issuer: https://<your-okta-domain>
  client_id: definite-onprem
  client_secret:
    env: OIDC_CLIENT_SECRET

llm:
  provider: bedrock
  region: <your-region>
  model: anthropic.claude-sonnet-4-20250514-v1:0
  # Credentials via IRSA; no credentials block needed.

resources:
  api:
    replicas: 2
    cpu: "1"
    memory: 2Gi
  lakehouse:
    replicas: 1
    cpu: "4"
    memory: 16Gi
  frontend:
    replicas: 2
    cpu: 500m
    memory: 512Mi
  job_runner:
    replicas: 1
    cpu: 500m
    memory: 1Gi
```

<Tip>
  The fastest way to fill in `postgres.url`, `object_store.bucket`, and friends is to paste the output of `terraform output -raw config_yaml_fragment` directly into your `config.yaml`.
</Tip>

For the full list of knobs (image registry overrides, ingress class, sandbox configuration, etc.), see the [config reference](https://github.com/definite-app/definite-onprem/blob/main/docs/config.md).

### 6. Export secrets

`config.yaml` references env vars for every secret. Export them from Terraform outputs:

```bash theme={null}
export POSTGRES_PASSWORD=$(terraform output -raw rds_password)
export S3_ACCESS_KEY_ID=$(terraform output -raw lakehouse_s3_access_key_id)
export S3_SECRET_ACCESS_KEY=$(terraform output -raw lakehouse_s3_secret_access_key)
export OIDC_CLIENT_SECRET=...        # only if auth.mode: oidc
```

<Warning>
  Don't commit `config.yaml`, `terraform.tfvars`, or `terraform.tfstate` to a public repo. The state file holds the RDS password and the S3 secret in cleartext.
</Warning>

### 7. Preflight with `definite doctor`

```bash theme={null}
definite doctor --config config.yaml
```

`doctor` runs a battery of preflight checks: it connects to Postgres and runs `SELECT version()`, validates the Kubernetes context, checks object-store config shape, and (for Anthropic) pings the LLM API. Fix anything it flags before moving on.

### 8. Deploy with `definite init`

```bash theme={null}
definite init --config config.yaml
```

`init` re-runs preflight, renders the bundled Helm chart with your values, and runs `helm upgrade --install`. It waits for pods to reach `Ready` by default. Useful flags:

| Flag               | Purpose                                              |
| ------------------ | ---------------------------------------------------- |
| `--dry-run`        | Render values, don't apply                           |
| `--wait=false`     | Return as soon as Helm finishes; don't wait for pods |
| `--skip-preflight` | Skip `doctor` (not recommended)                      |

Watch the rollout in another terminal:

```bash theme={null}
definite status --config config.yaml
definite logs api --follow
```

### 9. Set the initial admin user

The chart does not auto-create an initial admin. After `definite init` returns successfully and the pods are `Ready`, set `INITIAL_ADMIN_EMAIL` and `INITIAL_ADMIN_PASSWORD` on the api Deployment; the api creates the admin record on its next start.

```bash theme={null}
ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 24)
kubectl set env deploy/definite-api -n definite \
  INITIAL_ADMIN_EMAIL=<your-email> \
  INITIAL_ADMIN_PASSWORD="$ADMIN_PASSWORD"
kubectl rollout status deploy/definite-api -n definite
echo "Admin password: $ADMIN_PASSWORD"
```

Save the password somewhere safe (1Password, Vault, etc.).

<Warning>
  A subsequent `helm upgrade` or `definite upgrade` clears these env vars, so re-set them after any upgrade. (Tracking issue: chart-managed initial admin so this step goes away.)
</Warning>

When cert-manager has issued a cert, open your hostname in a browser and log in with the admin email and password from above.

<Note>
  **Want Google SSO instead of local auth?** See the [Google SSO guide](/on-prem/sso). You'll add an `auth.oidc` block to your `config.yaml` and re-run `definite upgrade` — no need to redo the install.
</Note>

***

## Day-2 operations

The same CLI handles upgrades, logs, license, and lakehouse maintenance. A few of the most common commands:

```bash theme={null}
definite status   --config config.yaml         # `kubectl get pods,svc,ingress` for the namespace
definite logs api --follow                     # stream component logs
definite upgrade  --config config.yaml         # re-render and re-apply with the current CLI version
definite license  show                         # decode + verify the active license
definite run maintenance stats                 # lakehouse file/snapshot stats
```

See the [CLI reference](https://github.com/definite-app/definite-onprem/blob/main/docs/cli.md) for every command and flag.

***

## Phase 3: Teardown

When you're done, tear down the cluster and supporting infra with Terraform:

```bash theme={null}
cd deploy/terraform/aws
terraform destroy
```

A couple of safety rails are on by default:

| Variable                  | Default                                | Effect                                                                                                         |
| ------------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `rds_deletion_protection` | `false` (set to `true` for production) | When `true`, RDS refuses to be deleted; flip it to `false` first                                               |
| `lakehouse_force_destroy` | `false`                                | Non-empty buckets won't be deleted; flip it to `true` if you really mean to remove the bucket and its contents |

For production teardowns, snapshot RDS and back up the S3 bucket first; once Terraform deletes them, they're gone.

***

## Troubleshooting

| Symptom                                | Likely cause                                                                                      | Fix                                                                                                                                                           |
| -------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `terraform apply` hangs on EKS         | Cluster takes 15-20 min to provision; this is normal                                              | Wait                                                                                                                                                          |
| Lakehouse pod stuck `Pending` on PVC   | No default StorageClass                                                                           | Confirm `gp3` StorageClass is present: `kubectl get storageclass`. The module creates it by default; set `create_gp3_storage_class = true` if you disabled it |
| `definite doctor` Postgres check fails | RDS security group only allows the cluster security group                                         | This is by design. Run `doctor` from a pod in the cluster, or temporarily allowlist your IP on the RDS security group                                         |
| LB hostname never appears              | Ingress controller isn't running, or the AWS Load Balancer Controller fights with `ingress-nginx` | `kubectl get pods -n ingress-nginx`; if you installed both controllers, pick one                                                                              |
| Cert never issues                      | `letsencrypt-prod` ClusterIssuer missing, or DNS doesn't resolve to the LB                        | `kubectl describe certificate -n definite` shows the cert-manager error                                                                                       |
| Fi can't reach Bedrock                 | The Bedrock IRSA ServiceAccount isn't annotated, or the model isn't enabled in the region         | Confirm `serviceAccount.annotations` in `config.yaml`, and request model access in the AWS Bedrock console                                                    |

***

## Next steps

* [Connect a data source](/connect-your-database) to start ingesting data.
* [Set up the MCP server](/mcp/mcp) so Claude, Cursor, or Windsurf can query your deployment.
* For deeper config (custom container registry, sandbox network policies, OIDC tuning), see the [config reference](https://github.com/definite-app/definite-onprem/blob/main/docs/config.md).

## Support

For issues or questions, contact [hello@definite.app](mailto:hello@definite.app) or open an issue on [definite-app/definite-onprem](https://github.com/definite-app/definite-onprem/issues).
