Skip to content

Deploying on AWS

deploy/cloudformation/panorama.yaml is one template and the whole deployment. There is no bootstrap stack and nothing to install on the machine that deploys it, and your security team can read all of it before it runs.

This is the recommended way to deploy Panorama. The Quickstart follows it from an empty account to a coding tool’s first request; this page has the detail behind each step.

Coding tools and console users reach an Application Load Balancer in your VPC's load balancer subnets, using an ACM certificate you bring and an optional Route 53 alias. The load balancer forwards to two Fargate tasks in the service subnets, which use an Aurora Serverless v2 PostgreSQL database. Through a NAT gateway or VPC endpoints the tasks reach Amazon Bedrock with their task role, Secrets Manager, CloudWatch Logs, and the ECR image with their execution role.Coding tools and console users reach an Application Load Balancer in your VPC's load balancer subnets, using an ACM certificate you bring and an optional Route 53 alias. The load balancer forwards to two Fargate tasks in the service subnets, which use an Aurora Serverless v2 PostgreSQL database. Through a NAT gateway or VPC endpoints the tasks reach Amazon Bedrock with their task role, Secrets Manager, CloudWatch Logs, and the ECR image with their execution role.
The template creates You bring
An ECS cluster and a Fargate service on ARM64, two tasks by default A VPC with subnets in at least two availability zones
An Application Load Balancer with HTTPS, and an HTTP to HTTPS redirect An ACM certificate in the same region, for your hostname
An Aurora Serverless v2 PostgreSQL cluster A NAT gateway or VPC endpoints, if the tasks are in private subnets
Secrets for the database password, the encryption key and the OIDC connection
A task role carrying the Bedrock policy, and an execution role
Three security groups, and optionally a Route 53 alias record
Optionally, an AgentCore gateway for web search
  1. A VPC with subnets in two availability zones. Private subnets behind a NAT gateway are the intended shape. Public subnets work with AssignPublicIp=ENABLED. The load balancer’s subnets may differ from the service’s.
  2. An ACM certificate for the hostname, in the region you deploy to.
  3. Decide about VPC endpoints if your private subnets have no NAT gateway. See Connectivity.
  4. Decide who may reach it. IngressCidr takes up to four CIDRs separated by commas, such as 203.0.113.0/24,198.51.100.7/32, and defaults to everybody. For more ranges, put them in a managed prefix list and pass its id as IngressPrefixListId; leave IngressCidr empty to admit only the list.

The image is public, at public.ecr.aws/dnxsolutions/panorama, so there is nothing to request first.

You do not need to request Bedrock model access, but an account that has never used Claude submits Anthropic’s use case form once first. See Model access.

The first deploy is three commands, because database migrations run before the service starts.

Every command on this page names its region with --region "$REGION". Set it once in each terminal you use. Without it the AWS CLI uses AWS_REGION or your profile’s region, which in AWS CloudShell is the console’s, and a command run against the wrong region fails as though the stack did not exist:

Terminal window
REGION=us-east-1 # the region the stack is in

Every release has its template attached, and that template runs that release’s version. Take the latest one, or a particular release’s from releases/download/<version>/panorama.yaml:

Terminal window
curl -fsSLO https://github.com/dnxlabs/panorama/releases/latest/download/panorama.yaml
aws cloudformation deploy --region "$REGION" \
--stack-name panorama \
--template-file panorama.yaml \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
VpcId=vpc-0123456789abcdef0 \
ServiceSubnetIds=subnet-aaa,subnet-bbb \
LoadBalancerSubnetIds=subnet-ccc,subnet-ddd \
DomainName=panorama.example.com \
CertificateArn=arn:aws:acm:us-east-1:111122223333:certificate/... \
BedrockRegion=us-east-1 \
DesiredCount=0

DesiredCount=0 is the point of this step: the database, the load balancer and the task definition are created, and nothing starts against an empty schema. It takes about fifteen minutes, nearly all of it Aurora.

The Quickstart has a Launch stack button that does the same from the AWS console, with DesiredCount already set to 0.

The stack outputs the exact command, filled in with your cluster, subnets and security group:

Terminal window
aws cloudformation describe-stacks --region "$REGION" --stack-name panorama \
--query "Stacks[0].Outputs[?OutputKey=='MigrateCommand'].OutputValue" --output text

Run what it prints, wait for the task to stop, and confirm it exited zero:

Terminal window
aws ecs describe-tasks --region "$REGION" --cluster panorama --tasks <task-arn> \
--query "tasks[0].containers[0].exitCode"

A non-zero exit is explained in CloudWatch Logs under /aws/ecs/panorama.

Terminal window
aws cloudformation deploy --region "$REGION" --stack-name panorama --template-file panorama.yaml \
--capabilities CAPABILITY_IAM --parameter-overrides DesiredCount=2

Two is the smallest count that survives the loss of an availability zone. aws cloudformation deploy keeps the current value of every parameter you leave out, so leaving DesiredCount out would keep it at 0.

If you passed HostedZoneId, the alias record already exists. Otherwise create a CNAME or alias to the LoadBalancerDnsName output.

A new deployment has no accounts, so you sign in the first time with a one-time setup code. Mint one with a one-off task. With two tasks running, reading the code out of the service’s startup logs is unreliable: both mint one and only the later one works.

The RunTaskNetwork stack output is the service’s network, subnets, security group and public IP setting included, so a one-off task reaches what the service reaches:

Terminal window
NETWORK=$(aws cloudformation describe-stacks --region "$REGION" --stack-name panorama \
--query "Stacks[0].Outputs[?OutputKey=='RunTaskNetwork'].OutputValue" --output text)
aws ecs run-task --region "$REGION" --cluster panorama --task-definition panorama \
--launch-type FARGATE --network-configuration "$NETWORK" \
--overrides '{"containerOverrides":[{"name":"panorama","command":["admin","setup"]}]}'

The code is in that task’s log stream. Open the ConsoleUrl output and enter it, then connect single sign-on as described next.

The cluster, log group and task family are all named after the stack, so these commands assume you called it panorama.

If you have no identity provider to connect yet, skip the setup code and mint an administrator’s gateway token instead, with the same one-off task and the same NETWORK:

Terminal window
aws ecs run-task --region "$REGION" --cluster panorama --task-definition panorama \
--launch-type FARGATE --network-configuration "$NETWORK" \
--overrides '{"containerOverrides":[{"name":"panorama","command":["token","create","-email","you@example.com","-role","admin"]}]}'

The token is in that task’s log stream. Open the ConsoleUrl output, paste it under Gateway token and press Sign in. The console is then yours, as an administrator, until you connect single sign-on, at which point the token field disappears and you sign in through your provider. Accounts for anybody else need a token each, minted the same way. The task’s log holds the token, so delete that log stream once you have copied it.

The stack creates an empty secret for the OIDC connection, and the task definition already reads every field from it. Do not edit the task definition. It belongs to CloudFormation, and a hand edit is reverted by the next stack update without a word.

In the setup session, choose your provider, enter the issuer and client id, and press Check this connection. The screen prints the secret value with your details filled in. Then:

Terminal window
SECRET=$(aws cloudformation describe-stacks --region "$REGION" --stack-name panorama \
--query "Stacks[0].Outputs[?OutputKey=='OidcSecretArn'].OutputValue" --output text)
aws secretsmanager put-secret-value --region "$REGION" --secret-id "$SECRET" --secret-string '{
"issuer": "https://login.microsoftonline.com/<tenant>/v2.0",
"client_id": "...",
"client_secret": "...",
"scopes": "",
"groups_claim": ""
}'
aws ecs update-service --region "$REGION" --cluster panorama --service panorama --force-new-deployment

The last command is not optional. A task reads the secret once, when it starts.

Google Workspace only: also set the OidcAllowedDomains stack parameter to your Workspace’s primary domain, with aws cloudformation deploy and --parameter-overrides OidcAllowedDomains=example.com. Panorama refuses to start on Google’s issuer without it. It is a parameter and not a key of the secret, because a domain is not a secret. See Limit sign-in to your domains.

Leave scopes and groups_claim empty unless your provider differs from the defaults. Do not put a placeholder in issuer. REPLACE_ME fails validation and the task will not start, and a plausible fake URL is worse: it passes validation and the console offers a sign-in button that goes nowhere.

Press End session and sign in. The first person through becomes an administrator. The OidcRedirectUri stack output is the redirect URI to register at your provider.

Single sign-on is the step most likely to go wrong, usually silently. See Single sign-on for how it works and how to map groups to roles, Identity provider guides for your provider, and SSO troubleshooting by symptom.

Then carry on in the console, where an administrator sees a setup checklist: Model providers, Routes, Budgets, and sending engineers to My setup.

To give engineers’ coding tools a web_search tool, update the stack with --parameter-overrides CreateWebSearchGateway=true. It creates an AgentCore gateway with a Web Search Tool target, which is available in us-east-1, eu-west-1 and ap-northeast-1; in any other region the stack refuses the parameter before creating anything. Search is on as soon as the new tasks start, and Settings → Web search shows the gateway with a switch to turn it off. A stack in another region can still use search: create the gateway in one of those three regions and paste its URL there instead. See Web search.

ImageTag takes any of these:

Value What it is
empty The release the template came from. The default.
stable The latest release.
beta The latest prerelease.
v0.1.3 One version. A release never overwrites one.
sha256:d7ae… The exact bytes. The strongest form.

Leave it empty unless you have a reason not to. The template attached to a release runs that release, so the image changes when you deploy a newer release’s template, and only then; see Upgrading. Empty in the repository’s own copy of the template means stable.

Do not follow a channel in production. stable and beta move. Deploying the same template twice can then produce two different versions, and ECS does not re-pull a tag it already has, so a moving tag gives you either a deploy that silently does nothing or a fleet running two versions.

To resolve a tag to its digest:

Terminal window
docker buildx imagetools inspect public.ecr.aws/dnxsolutions/panorama:stable \
--format '{{.Manifest.Digest}}'

Each release’s image.json lists the same digest. To check that an image was built from the public source, verify its provenance:

Terminal window
gh attestation verify oci://public.ecr.aws/dnxsolutions/panorama:v0.1.3 -R dnxlabs/panorama

To pull from your own account instead, mirror the image into your own ECR, or create a pull-through cache for public.ecr.aws, and set ImageRepository to it, for example 123456789012.dkr.ecr.eu-west-1.amazonaws.com/panorama. Copy with docker buildx imagetools create, which keeps both architectures; docker pull then docker push flattens it to your machine’s. Private subnets with no NAT gateway need this, because ECR Public has no VPC endpoint. The CloudFormation README has the commands for both.

Tasks run on ARM64. If you point at an amd64-only image, set TaskCpuArchitecture=X86_64, or tasks fail with an exec format error that reads like a corrupt image.

With a NAT gateway, or in public subnets with AssignPublicIp=ENABLED, you need nothing further. Without either, create these VPC endpoints before deploying:

Endpoint What breaks without it
bedrock-runtime Every request to the bedrock provider
bedrock Model discovery: the catalogue stays empty, so no route can be created
bedrock-mantle Only if you enable that provider: its requests and discovery
ecr.api, ecr.dkr, and the s3 gateway endpoint The image pull from your mirror: tasks never start
secretsmanager Secret injection: tasks never start
logs All logging, including the setup code

ECR Public has no VPC endpoint. Without a NAT gateway or public IPs the tasks cannot reach public.ecr.aws, so pull from a mirror or a pull-through cache in your own account instead. See Choose which image to run.

The AWS Pricing API has no VPC endpoint. A fully private deployment either keeps a NAT route for that one call or prices requests at the rates compiled into the binary, which go stale. Decide before you deploy.

Anything you would run with docker compose exec on a server is a one-off ECS task here, built from the current task definition, so it uses the same image, database and secrets as the service:

Terminal window
NETWORK=$(aws cloudformation describe-stacks --region "$REGION" --stack-name panorama \
--query "Stacks[0].Outputs[?OutputKey=='RunTaskNetwork'].OutputValue" --output text)
aws ecs run-task --region "$REGION" --cluster panorama --task-definition panorama \
--launch-type FARGATE --network-configuration "$NETWORK" \
--overrides '{"containerOverrides":[{"name":"panorama","command":["sso","doctor"]}]}'

Swap the command for what you need, such as ["admin","setup"], ["migrate","status"] or ["token","create","-email","you@example.com","-role","admin"]. The output is in that task’s log stream.

For a shell in a running task, ECS Exec is enabled:

Terminal window
aws ecs execute-command --region "$REGION" --cluster panorama --task <task-id> \
--container panorama --interactive --command /busybox/sh

That shell runs as root beside the Bedrock credentials and the database connection, so control who holds ecs:ExecuteCommand on this cluster.

The stack runs around the clock, and the database is most of the bill. With the default parameters, before any Bedrock usage:

Sydney (ap-southeast-2) N. Virginia (us-east-1)
Aurora Serverless v2, 0.5 capacity units at all times $73 $44
Application Load Balancer, hourly charge and light traffic $21 $19
Two Fargate tasks, ARM64, 0.5 vCPU and 1 GB each $35 $29
Two public IPv4 addresses on the load balancer $7 $7
Three Secrets Manager secrets $1 $1
About, per month $137 $100

Prices are AWS’s on-demand list prices in US dollars, for 730 hours. Container Insights metrics, CloudWatch Logs and database storage add a few dollars more with light use. A NAT gateway is not part of the stack; if your private subnets need one for Panorama alone, it adds about $43 a month in Sydney and $33 in N. Virginia.

The database cannot go lower. Its minimum of 0.5 capacity units is the smallest that stays running, and a capacity of 0, which lets Aurora pause, is never reached, because the gateway checks its configuration every second.

For a trial that nobody depends on, these parameters run one task instead of two, about $17 a month less in Sydney, need no NAT gateway, and remove the migration step:

Terminal window
aws cloudformation deploy --region "$REGION" \
--stack-name panorama \
--template-file panorama.yaml \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
VpcId=vpc-0123456789abcdef0 \
ServiceSubnetIds=subnet-ccc,subnet-ddd \
LoadBalancerSubnetIds=subnet-ccc,subnet-ddd \
AssignPublicIp=ENABLED \
DomainName=panorama.example.com \
CertificateArn=arn:aws:acm:us-east-1:111122223333:certificate/... \
DesiredCount=1 \
AutoMigrate=true
  • One task (DesiredCount=1). It does not survive the loss of an availability zone, and a deploy replaces it with a short gap.
  • Migrations at startup (AutoMigrate=true), which is safe with exactly one task and is why there is no migration step.
  • Public subnets for the tasks and the database (AssignPublicIp=ENABLED), so no NAT gateway is needed. The database is still reachable only from the tasks.

A certificate is still required: every credential Panorama issues is a bearer token, and the load balancer serves only HTTPS. A certificate from ACM costs nothing. To go from here to production, set DesiredCount=2 and AutoMigrate=false.

Cheaper still is one small server. Deploying on a server runs Panorama with Docker Compose and SQLite on one machine; a t4g.small is about $15 a month in Sydney. It is the same console and gateway without the managed database or the load balancer.

Four settings you should not tune casually

Section titled “Four settings you should not tune casually”

A streaming gateway behind a load balancer needs these, and three of them are not the AWS defaults. The template sets all four.

Setting Value AWS default Why
ALB idle timeout 600s 60s It measures the gap between bytes. A long time-to-first-token or a long thinking block would cut the stream mid-turn.
Target group deregistration delay 300s 300s The whole window an in-flight stream gets when its task is replaced.
ECS stopTimeout 120s 30s Fargate’s maximum.
server.shutdown_timeout 110s 30s Just under stopTimeout, so the process finishes draining before it is killed.

A turn still streaming when the deregistration delay runs out loses its connection. Coding tools retry the turn, so in practice this is survivable.

The stack generates an encryption key and injects it into every task. It protects the GitHub credential stored in the database. Copy the value of the EncryptionKeySecretArn secret somewhere outside this account. Changing or losing it does not fail loudly: the GitHub integration goes from connected to failing at the next pass, and the credential has to be entered again.

The data moves as a database snapshot. The new stack restores it and carries on with the same users, routes, budgets, audit trail and gateway tokens, so engineers’ tools keep working once the domain points at it.

  1. Copy the value of the EncryptionKeySecretArn secret and of the OidcSecretArn secret, and save the stack’s parameters.
  2. Request a certificate for the same domain in the new region.
  3. Set DesiredCount to 0 on the old stack, so nothing is written after the snapshot.
  4. Take a snapshot of the database cluster, and copy it to the new region with aws rds copy-db-cluster-snapshot --kms-key-id alias/aws/rds.
  5. Store the encryption key in a secret in the new region.
  6. Create the new stack as in Deploy, with the old parameters plus DatabaseSnapshotIdentifier set to the copied snapshot, EncryptionKeySecretArn set to the secret from step 5, and the new region’s certificate, network and subnets. If you pull from a mirror, set ImageRepository to one the new region can reach.
  7. Put the single sign-on values into the new OidcSecretArn secret, run the migrations, and start the service.
  8. Point the domain at the new load balancer, check it, then delete the old stack.

Keep DatabaseSnapshotIdentifier set for the life of the new stack: changing or clearing it replaces the database. The model providers keep the regions they had, so Bedrock is still called in the old region until it is changed in Settings → Model providers.

  • Audit writes are not drained on shutdown. On a rolling deploy, the last requests a stopping task served may be missing from the audit trail. This is to be fixed before this path is recommended for production.
  • Background jobs have no leader election. With N tasks you get N times the GitHub API calls. Harmless at two.
  • Client IP addresses are not recorded correctly behind the load balancer. The audit trail shows the load balancer’s address.
  • SQLite is not an option here. It allows one writer, so it cannot back two tasks.

The stack keeps three log groups, all with the LogRetentionDays parameter’s retention and all deleted with the stack: the service’s own, /aws/ecs/<stack>, the database’s, /aws/rds/cluster/<cluster>/postgresql, and Container Insights’, /aws/ecs/containerinsights/<stack>/performance.

Upgrading covers new releases. The template’s own README goes further on database password rotation, sizing, log queries and deleting the stack.

This manual is licensed underCC BY 4.0. Code samples in it are part of Panorama, licensed under theGNU AGPL v3.0 only.