# Deploy Kortyx Studio on AWS with CDK

`@kortyx/aws-cdk` deploys a private Kortyx Studio installation in an existing VPC:

```ts
new KortyxStudio(this, "Studio", {
  vpc,
  domainName: "studio.example.com",
  version: "v0.4.0",
});
```

```js
new KortyxStudio(this, "Studio", {
    vpc,
    domainName: "studio.example.com",
    version: "v0.4.0",
});
```

The construct creates ECS Fargate, PostgreSQL on RDS, generated credentials in Secrets Manager, an internal HTTPS load balancer, and Route 53 DNS. Studio and telemetry share one hostname.

> **Private by default:** The load balancer accepts traffic from the VPC CIDR, and neither the ECS task nor RDS receives a public IP. Reach it from the VPC, a peered network, or a VPN.

> **Good to know:** Wire topology publication into your application's CI/CD. Deploying this construct does not publish your application's workflow catalog. Run `kortyx topology push` for each application release from a workload that can reach the private telemetry API. A public GitHub-hosted runner cannot reach the internal ALB without additional network connectivity. See [Publish topology from the application release](#publish-topology-from-the-application-release).

## Choose a deployment path

This construct is the maintained, batteries-included AWS path, not a requirement for running Kortyx on AWS. Use it when your infrastructure is managed with CDK and its defaults fit your environment.

Kortyx also publishes ordinary container images and a stable [deployment contract](/docs/self-hosting/configuration-reference). You can implement that contract with:

- your own ECS service using Terraform, Pulumi, CloudFormation, or lower-level CDK;
- EKS or another Kubernetes platform;
- Docker Compose on EC2; or
- an existing database, cluster, certificate, and DNS design.

The CDK package is an infrastructure dependency; the deployed Kortyx workload does not require it at runtime. If your organization already standardizes on another infrastructure tool, keep that tool and use the same containers and environment variables. See [Deploy on a Server](/docs/self-hosting/deploy-server) for the portable container path.

## Architecture

```text
VPC or VPN client
       |
       | HTTPS :443
       v
studio.example.com (internal ALB)
       |-- /v1/telemetry/*, /health -> API :6400
       `-- everything else          -> Studio :6300
                                             |
                                             v
                                    private RDS PostgreSQL
```

The Fargate task starts in this order:

```text
database migration + bootstrap -> healthy telemetry API -> Studio
```

A failed migration or bootstrap prevents that task from serving traffic.

## Before you start

You need:

- an AWS account and AWS CLI credentials with permission to deploy CDK stacks;
- Node.js 22 or newer;
- an existing VPC with private subnets in at least two Availability Zones;
- outbound HTTPS from those subnets, through NAT or equivalent egress, so Fargate can pull the public GHCR images; and
- a public Route 53 hosted zone for the parent domain.

The minimal example infers `example.com` from `studio.example.com` and creates a DNS-validated ACM certificate. Pass an explicit `hostedZone` or `certificate` when your DNS layout differs.

This deployment creates billable ECS, RDS, Application Load Balancer, Secrets Manager, CloudWatch Logs, and networking resources. Review `cdk diff` and your expected AWS cost before deployment.

## 1. Create a CDK application

```bash
mkdir kortyx-studio-infra
cd kortyx-studio-infra
npx aws-cdk@latest init app --language typescript
```

For a JavaScript CDK app, use `--language javascript` and the `.js` examples below. Add the infrastructure dependencies with your project's package manager:

```bash tabs="cdk-dependencies" tab="pnpm"
pnpm add @kortyx/aws-cdk aws-cdk-lib constructs
```

```bash tabs="cdk-dependencies" tab="npm"
npm install @kortyx/aws-cdk aws-cdk-lib constructs
```

AWS requires each account and Region to be [bootstrapped for CDK](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping-env.html) once.

## 2. Add Kortyx Studio

Replace `lib/kortyx-studio-infra-stack.ts` with:

```ts file="lib/kortyx-studio-infra-stack.ts"
import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import { Construct } from "constructs";
import { KortyxStudio } from "@kortyx/aws-cdk";

export class KortyxStudioInfraStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const vpc = ec2.Vpc.fromLookup(this, "Vpc", {
      vpcId: "vpc-0123456789abcdef0",
    });

    new KortyxStudio(this, "Studio", {
      vpc,
      domainName: "studio.example.com",
      version: "v0.4.0",
    });
  }
}
```

```js file="lib/kortyx-studio-infra-stack.js"
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.KortyxStudioInfraStack = void 0;
const cdk = require("aws-cdk-lib");
const ec2 = require("aws-cdk-lib/aws-ec2");
const aws_cdk_1 = require("@kortyx/aws-cdk");
class KortyxStudioInfraStack extends cdk.Stack {
    constructor(scope, id, props) {
        super(scope, id, props);
        const vpc = ec2.Vpc.fromLookup(this, "Vpc", {
            vpcId: "vpc-0123456789abcdef0",
        });
        new aws_cdk_1.KortyxStudio(this, "Studio", {
            vpc,
            domainName: "studio.example.com",
            version: "v0.4.0",
        });
    }
}
exports.KortyxStudioInfraStack = KortyxStudioInfraStack;
```

Use an immutable published version. `latest` and incomplete versions are rejected so a deployment can be reproduced and rolled back intentionally.

If `studio.example.com` is not directly beneath the hosted zone, pass it explicitly:

```ts
import * as route53 from "aws-cdk-lib/aws-route53";

const hostedZone = route53.HostedZone.fromLookup(this, "HostedZone", {
  domainName: "internal.example.com",
});

new KortyxStudio(this, "Studio", {
  vpc,
  hostedZone,
  domainName: "studio.internal.example.com",
  version: "v0.4.0",
});
```

```js
import * as route53 from "aws-cdk-lib/aws-route53";
const hostedZone = route53.HostedZone.fromLookup(this, "HostedZone", {
    domainName: "internal.example.com",
});
new KortyxStudio(this, "Studio", {
    vpc,
    hostedZone,
    domainName: "studio.internal.example.com",
    version: "v0.4.0",
});
```

## 3. Review the defaults

| Resource | Default |
| --- | --- |
| ECS | One Fargate task with database-init, API, and Studio containers |
| RDS | PostgreSQL 17, `db.t4g.micro`, 20 GiB with storage autoscaling |
| Secrets | Generated database password, key pepper, Project keys, and Basic Auth password |
| Load balancer | Internal ALB; one hostname with path-based routing |
| Networking | ALB ingress from the VPC CIDR; no task or database public IP |
| Recovery | Seven-day database backups, retained secrets and logs, database deletion protection |

Common overrides remain concise:

```ts
new KortyxStudio(this, "Studio", {
  vpc,
  domainName: "studio.example.com",
  version: "v0.4.0",
  allowedCidrs: [vpc.vpcCidrBlock, "10.50.0.0/16"],
  databaseMultiAz: true,
  databaseInstanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T4G,
    ec2.InstanceSize.SMALL,
  ),
});
```

```js
new KortyxStudio(this, "Studio", {
    vpc,
    domainName: "studio.example.com",
    version: "v0.4.0",
    allowedCidrs: [vpc.vpcCidrBlock, "10.50.0.0/16"],
    databaseMultiAz: true,
    databaseInstanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.SMALL),
});
```

You can also supply an existing `cluster`, `hostedZone`, `certificate`, or `database`. The TypeScript API documents the required shape for each value.

## Availability: what one application task means

The `@kortyx/aws-cdk` convenience construct runs one active ECS task. This is an **availability boundary, not a data-durability boundary**:

- telemetry, projects, and Studio state live in PostgreSQL, not in the task;
- if the task crashes, ECS starts a replacement against the same database;
- RDS backups, deletion protection, and an optional Multi-AZ standby protect the database; but
- Studio and telemetry are briefly unavailable while the replacement initializes, and planned version updates also have a short interruption.

The Kortyx runtime supports multiple replicas, but the convenience construct intentionally keeps its cohesive task topology and one-step API. It does not model an existing load balancer, independently scaled services, or a separate deployment job. Do not deploy two copies of the construct against one database to imitate high availability.

When an AWS environment needs continuous application availability, implement the [multi-replica deployment contract](/docs/self-hosting/high-availability) with lower-level CDK, Terraform, CloudFormation, ECS, or EKS: run migrations once, keep at least two replicas healthy across Availability Zones, and follow each release's `rolling` or `recreate` marker.

For stronger database availability, enable `databaseMultiAz`; it does not remove the convenience construct's single application task restart window.

For evaluations, [worker retirement and the consumer application's lifetime](/docs/self-hosting/deployment-lifecycle)
are additional requirements. Do not assume an ECS replica setting transfers
active evaluations. Independent worker services and their rollout orchestration
belong in an operator-managed topology; they are not added by duplicating this
convenience construct.

## 4. Bootstrap, inspect, and deploy

Confirm the AWS identity and Region:

```bash
aws sts get-caller-identity
aws configure get region
```

Then bootstrap, compile, inspect, and deploy:

```bash
npx cdk bootstrap
npm run build
npx cdk synth
npx cdk diff
npx cdk deploy
```

Commit the CDK source to your infrastructure repository. For shared environments, deploy through your reviewed CI/CD workflow.

The deployment is ready when the ECS service has one running task and both target groups report it healthy.

## 5. Retrieve the initial credentials

The construct outputs the Studio URL and generated secret names. Retrieve the browser password without placing it in source control:

```bash
studio_password_secret="$(aws cloudformation describe-stacks \
  --stack-name KortyxStudioInfraStack \
  --query "Stacks[0].Outputs[?contains(OutputKey, 'StudioPasswordSecretName')].OutputValue" \
  --output text)"

aws secretsmanager get-secret-value \
  --secret-id "$studio_password_secret" \
  --query SecretString \
  --output text
```

Open the `StudioUrl` output and sign in as `admin` with that password.

Build the telemetry write key from its generated secret suffix:

```bash
telemetry_secret_name="$(aws cloudformation describe-stacks \
  --stack-name KortyxStudioInfraStack \
  --query "Stacks[0].Outputs[?contains(OutputKey, 'TelemetryKeySecretName')].OutputValue" \
  --output text)"
telemetry_secret="$(aws secretsmanager get-secret-value \
  --secret-id "$telemetry_secret_name" \
  --query SecretString \
  --output text)"

printf 'ktyx_live_default-telemetry_%s\n' "$telemetry_secret"
unset telemetry_secret
```

Treat both outputs as secrets. Do not paste them into build logs, tickets, or screenshots.

## 6. Connect and verify

Give each application's server runtime:

```bash file="server environment"
KORTYX_TELEMETRY_API_URL=https://studio.example.com
KORTYX_TELEMETRY_API_KEY=ktyx_live_default-telemetry_...
KORTYX_TELEMETRY_ENVIRONMENT=production
KORTYX_TELEMETRY_SERVICE_NAME=my-agent
```

The application must have a network route to the internal load balancer. Never put the telemetry key in a browser bundle. Continue with [Connect Your Project](/docs/studio/connect-project) to publish workflow topology.

### Publish topology from the application release

Keep the catalog entrypoint with your **agent application's source**, not in the Studio infrastructure repository. It should export the same workflows the agent uses without starting an HTTP server or connecting to application persistence. For example:

```ts file="src/catalog.ts"
export { workflows } from "./workflows.js";
```

```js file="src/catalog.js"
export { workflows } from "./workflows.js";
```

Include that module and the installed Kortyx CLI in your application's release image. If your build emits `dist/catalog.js`, run this command from the application directory inside that image:

```bash
./node_modules/.bin/kortyx topology push --entry dist/catalog.js
```

For ECS applications, the release workflow can launch a **separate one-off ECS task** using the same immutable application image, with its command overridden to run the CLI. This is not an exec into a running application container, and it does not run in the Studio container. Configure the task's network access and inject the application's four telemetry environment variables above through the workload definition and secret manager.

The workflow runner calls the AWS ECS API to start the task; the task itself sends topology to the telemetry API behind the internal ALB. It needs working DNS, a route to that ALB, and allowed security-group/CIDR access. Network access alone is not authentication: the CLI also needs the Project-scoped telemetry write key.

Wire this as an explicit application-release step, ideally before enabling traffic to the new application version:

1. Wait for the Kortyx telemetry API to be ready.
2. Launch the catalog task for the application version being released.
3. Wait for it to finish and fail the release step if its exit code is nonzero.
4. Open **Workflows** in Studio to confirm the catalog, then execute a real request to verify **Runs**.

A runner already connected to the allowed private network can run the CLI directly instead. Keep the same service name and environment as the application runtime, and never print the write key in CI logs. This job belongs to the application's release workflow; it is separate from the Studio database migration/bootstrap job.

From an allowed VPC or VPN client:

```bash
curl --fail https://studio.example.com/health
curl --head https://studio.example.com
```

The API health request should return `200`. Studio may return `401` before browser credentials are provided; that confirms Basic Auth is active.

If a target remains unhealthy, inspect the task's `database-init`, `api`, and `studio` CloudWatch log streams in that order. Common causes are missing subnet egress to GHCR, DNS or certificate validation, a CIDR that does not match the caller's observed address, or database connectivity blocked by a security group.

> **Good to know:** The CDK convenience construct runs a single application task. For multiple replicas, follow the [high-availability contract](/docs/self-hosting/high-availability); [backups and restore checks](/docs/self-hosting/operations#restore) remain necessary in either layout.

## Upgrade with CDK

ECS deployments are controlled by CDK, not by the local Docker updater shown in Studio.

1. Back up the RDS database and deployment secrets.
2. Change `version` to the next immutable Studio release.
3. Run `npm run build`, `npx cdk synth`, and `npx cdk diff`.
4. Deploy through the same reviewed path.
5. Wait for database initialization and both target groups to become healthy.

The ECS deployment circuit breaker can roll back a task definition when a replacement fails; it cannot roll back a database schema. Database downgrade is unsupported, so restore the matching pre-upgrade snapshot if a newer migration is incompatible with the previous release.

> **Expected Studio message:** "This installation does not have an in-product updater. Manage updates through your deployment workflow, or use the Kortyx installer for local Docker update management." This is normal for an orchestrated installation: the workflow might be CDK/ECS on AWS, Terraform or Cloud Deploy on Google Cloud, or Helm/GitOps on Kubernetes.

The [High Availability guide](/docs/self-hosting/high-availability) includes a scheduled GitHub Actions pattern that proposes image updates in the infrastructure repository and keeps rollout ownership in the platform.

For backups, credential handling, and failure recovery, continue with [Operations and Troubleshooting](/docs/self-hosting/operations) and the [Configuration Reference](/docs/self-hosting/configuration-reference).
