Your own AWS account
This page is about the third stack: Metergraph running on ECS Fargate in one AWS account you own, deployed by Terraform. It is not the open source server, and it is not the customer-local bundle. None of the commands on those sections apply here.
One installation belongs to one AWS account. There is no control plane, no replication, no licensing callback, and no Metergraph operator access across the boundary. Traces and provider credentials stay in that account.
The capability delta is the thing to read first
Section titled “The capability delta is the thing to read first”A BYOC installation runs with METERGRAPH_DEPLOYMENT_PROFILE=byoc-core, and
that profile is what gates the product. Gating is by deployment profile, not by
plan: capabilities.py defines two sets, and the managed set has 20
capabilities where byoc-core has 9.
These nine are present:
telemetry, alerts, api_keys, membership, workspace_settings_read,
reports, analysis_runs, optimization, agent_api.
So ingest (native and OTLP), pricing and enrichment, the spend, performance and trace views, API keys, the six deterministic detectors, alert listing and acknowledgement, membership reads, analysis runs, and report viewing and export all work exactly as they do on the hosted service.
Everything else fails closed. A request to an absent capability returns 404
with code: capability_unavailable and the capability’s name, from a
middleware that runs before routing. It is a 404 rather than a 403 on purpose:
the surface does not exist in this deployment.
| Route prefix | Capability | Why it is absent |
|---|---|---|
POST /v1/auth/signup, /v1/organization/invitations |
signup |
Operators provision identities in Cognito, so there is no public signup or invitation flow |
PATCH /v1/organization |
workspace_settings |
Settings are readable, not writable |
/v1/upgrade-requests |
billing |
Nothing is billed by Metergraph |
/v1/catalog, /v1/admin |
platform_admin |
The global model and pricing catalog editor is a hosted surface |
/v1/campaign, /v1/sme-reviews, /v1/judge-qualifications |
evaluations |
Evaluation authoring is a hosted surface |
/v1/dataset |
datasets |
|
/v1/simulation, /v1/tool-replay-cache |
simulations |
|
/v1/canaries |
canaries |
|
/v1/recommendation-deliveries, /v1/github-installations, /v1/email, /internal/deliveries, /internal/github |
external_delivery |
Slack, GitHub and email delivery run from Metergraph’s own infrastructure |
/v1/provider-batch |
provider_probes |
|
/v1/import-sync |
import_sync |
Scheduled import sync is a hosted surface |
Three more are gated by shape rather than prefix, in the same file:
POST /v1/reports/{id}/approveandPOST /v1/recommendations/{id}/approveneedexternal_delivery, so approving a recommendation into a pull request is unavailable.POST /v1/recommendations/{id}/canaryneedscanaries./v1/routes/{route}/constraintsand/v1/routes/{route}/evaluationneedevaluations.
The worker is gated too: under byoc-core it accepts only the ingest_batch
and detector_scan job kinds.
An operational kill switch is separate from all of this and answers 503, not
404. A 404 capability_unavailable means the capability is not part of the
deployment; a 503 means the deployment is deliberately paused.
What Terraform creates
Section titled “What Terraform creates”| Concern | AWS service |
|---|---|
| Compute | ECS Fargate: API, worker, and one-off admin and analysis tasks |
| Ingress | Internal ALB, your ACM certificate, a Route 53 private record |
| Dashboard | The React build, served by the API image on the same hostname |
| Database | RDS PostgreSQL 17, Single-AZ, point-in-time recovery |
| Analytics store | One ClickHouse node on a Graviton EC2 instance (m7g.xlarge) with an encrypted 200 GB gp3 data volume, administered through SSM. It holds a copy of call metadata from PostgreSQL, never prompt or response content, and serves the trace, usage and performance views |
| Objects and queue | Versioned KMS-encrypted S3, SQS with a native dead-letter queue |
| Analysis | A Bedrock-only pipeline profile in an isolated one-off Fargate task, through a module-created private Bedrock Runtime endpoint |
| Identity | A dedicated Cognito user pool with self-registration off |
| Secrets | Secrets Manager, with scoped task roles per service |
| Operations | CloudWatch logs, metrics and alarms under your CMK |
Workloads have no public IP. The deployment runs one API replica and one worker replica after bootstrap, and offers no availability SLA or zone-failure continuity.
Runtime egress
Section titled “Runtime egress”Default runtime egress is TCP/443 through a security group you own, so it can be logged and filtered by your existing controls.
| Destination | Path | Needed by |
|---|---|---|
| The private PostgreSQL endpoint | VPC | API, worker, migration, analysis |
| S3, SQS, Secrets Manager, KMS, CloudWatch | VPC endpoints where configured | every runtime task |
Systems Manager (ssm, ssmmessages, ec2messages) and Secrets Manager |
VPC endpoints where configured, else logged NAT or firewall | the analytics store’s instance: administration through SSM, and its two secrets at boot |
| The Amazon Linux 2023 package repository, a regional S3 bucket | S3 gateway endpoint where configured, else logged NAT or firewall | the analytics store’s first boot and each instance replacement, which install Docker |
| Amazon Bedrock Runtime | The module’s private interface endpoint | the analysis task |
| Your regional Cognito issuer and domain | Logged NAT or firewall | browser login and API token exchange |
public.ecr.aws |
Logged NAT or firewall | platform image pull |
| Docker Hub | Logged NAT or firewall | the analytics store’s ClickHouse image, at first boot and on instance replacement, unless analytics_store_image names your own mirror |
No external model provider, email, Slack, GitHub, analytics, licensing or
Metergraph control-plane destination is part of byoc-core. Model traffic for
an analysis stays on the Bedrock private endpoint.
What it costs
Section titled “What it costs”For the default configuration in us-west-2, the running-state floor is about
$262 per month, computed on 2026-10-19 from public on-demand list prices.
The largest lines are the analytics store’s m7g.xlarge instance and its two
gp3 volumes, the RDS instance and storage, the two Fargate runtime tasks, the
internal ALB, and the Bedrock Runtime interface endpoint. Secrets Manager
secrets, three CloudWatch alarms, the store’s detailed monitoring and one idle
worker’s queue polling make up the rest. This is a floor, not a cap.
The exact running-state total is $262.00 in the table. The line items are rounded individually, while the total is computed before rounding. The analytics store accounts for $142.14 of it. The migration-first zero-replica state, which already has the store, is about $233 per month.
The following are not part of the floor and scale with use: Bedrock spend per
analysis run (bounded per run by analysis_pipeline_max_generation_cost_usd,
default 200 USD, not per month), analysis-task compute, RDS storage growth, S3 objects and
requests, CloudWatch log ingestion and storage, SQS requests, ALB capacity
units, VPC endpoint data, Secrets Manager API calls, data transfer (the
analytics store’s image pull from Docker Hub included), Cognito monthly active
users, and KMS usage. Customer-owned VPC, NAT or firewall,
private DNS, ACM, KMS, SNS, and Terraform backend costs are also outside the
floor. The module cannot see or bound those costs.
The release’s docs/cost-floor.md contains the assumptions, line-item
arithmetic, price-source URLs, and the pre-bootstrap figure.
Getting the module
Section titled “Getting the module”Today, Metergraph provides metergraph-byoc-aws-<tag>.tar.gz directly from the
release package. Contact your Metergraph representative to request the archive,
then extract it next to your root module. The archive’s scripts/ directory
carries byoc_status.py and byoc_smoke.py, which the status_command and
smoke_test_command outputs run from the extracted archive root. Then copy the
11 account inputs from aws/terraform.tfvars.example into this local module
reference:
module "metergraph" { source = "./metergraph-byoc-aws/aws" # the 11 inputs from aws/terraform.tfvars.example}
output "metergraph_release" { value = module.metergraph.release}The extracted aws/release.json already carries that release’s image digests.
If the release did not publish to public ECR, it carries null images. Set both
image and analysis_runner_image in that case. With that output in place,
terraform output metergraph_release shows which release the module is pinned
to.
When the module source is published, the same module is referenced by tag instead:
module "metergraph" { source = "git::https://github.com/<publisher>/metergraph-byoc-aws.git//aws?ref=vX.Y.Z" # the 11 inputs from aws/terraform.tfvars.example}Until then, the release tarball is the delivery path. Verify both defaulted images before applying:
cosign verify --certificate-identity-regexp 'github.com/.*/byoc-release.yml' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ public.ecr.aws/metergraph/app@sha256:<digest>cosign verify --certificate-identity-regexp 'github.com/.*/byoc-release.yml' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ public.ecr.aws/metergraph/analysis-runner@sha256:<digest>Module inputs
Section titled “Module inputs”The module has no defaults for the 11 inputs that describe your account. Copy
them from terraform.tfvars.example, or set them in your root module, before
planning.
| Required input | What it is |
|---|---|
region, vpc_id, private_subnet_ids |
At least two private subnets, for the ALB and RDS subnet groups |
private_zone_id, private_hostname |
The private Route 53 zone and the name the ALB answers on |
certificate_arn, kms_key_arn |
Your ACM certificate and your customer-managed key |
bedrock_model_resource_arns |
The exact foundation-model and inference-profile ARNs the analysis task may invoke. Must be non-empty and must all start with arn:aws...:bedrock: |
cognito_user_pool_domain_prefix |
The hosted login domain prefix |
alarm_sns_topic_arn |
Where CloudWatch alarms go |
egress_security_group_id |
Your logged TCP/443 egress security group |
The ones worth knowing about among the defaulted inputs:
| Input | Default | Note |
|---|---|---|
analysis_pipeline_builder_model |
us.anthropic.claude-sonnet-5 |
The model that writes the analysis |
analysis_pipeline_max_generation_cost_usd |
200 |
A worst-case cap on one run’s generation cost. Must be greater than zero |
automatic_analysis_enabled |
true |
Scheduling keeps its call-count, complete-day, cooldown, spacing and daily-cap policy |
retention_days |
90 |
Relational trace data and every object version expire on this schedule |
backup_retention_days |
7 |
|
log_retention_days |
30 |
|
analytics_store_enabled |
true |
The analytics store is part of the stack. false exists only to stage an apply: the analytics views read only from the store, and without it they answer busy |
analytics_store_image |
the module’s pinned ClickHouse release, from Docker Hub | Set it to your own linux/arm64 copy of the same release, preferably by digest, if the store cannot or should not pull from Docker Hub. A copy in a private ECR repository in the same account needs nothing else |
api_desired_count, worker_desired_count |
0 |
Deliberately zero, so the first apply creates the infrastructure before anything runs against an unmigrated database |
image_architecture |
ARM64 |
Or X86_64, which needs an explicit analysis_runner_image |
image, analysis_runner_image |
the digests of the module release | Read from the module’s release.json. The release’s default analysis-runner image is arm64 only. For X86_64, set an explicit analysis_runner_image. An override is refused unless pinned by @sha256: |
enable_delete_protection |
true |
Keys, database and buckets survive an ordinary terraform destroy |
break_glass_allow_destroy |
false |
The deliberate override for the above |
analysis_runner_anthropic_api_key_secret_arn, ..._openai_... and
..._google_... are optional Secrets Manager ARNs. Each is injected into the
analysis task under the provider’s own variable name, and is read only by
search-enabled workloads. Leave them empty and they are omitted.
Install order
Section titled “Install order”Follow these seven steps. The workspace section below covers the linked operator commands in detail.
-
Apply stateful resources
Section titled “Apply stateful resources”Set
api_desired_count=0andworker_desired_count=0for the first apply. -
Run the admin task
Section titled “Run the admin task” -
Bootstrap the first workspace
Section titled “Bootstrap the first workspace” -
Start the API and worker
Section titled “Start the API and worker”Apply one replica of each, then run the
status_commandoutput:python scripts/byoc_status.py --base-url https://<private-hostname>. It needs no token and reports API health, the deployment profile, the database and the job queue. Publish private DNS only after every check reportsok. -
Prove ingest end to end
Section titled “Prove ingest end to end”Run the
smoke_test_commandoutput from the private network. It proves that native and OTLP/HTTP ingest are accepted and visible in Telemetry. -
Verify alerts and key revocation
Section titled “Verify alerts and key revocation”Verify a deterministic alert, acknowledge it, and revoke the API key.
-
Seed analysis evidence
Section titled “Seed analysis evidence”Seed at least 201 priced calls across the latest seven complete UTC days. Verify the isolated Bedrock task, report import, temporary-key revocation, cost cap, and cooldown.
Bringing up a workspace
Section titled “Bringing up a workspace”There is no public signup here, so the first workspace and its owner are
created by an operator against an exact OIDC identity. Cognito is the issuer,
and the immutable (identity_issuer, identity_subject) pair is what grants
access. Email and display name are presentation data and grant nothing.
-
Apply the module
Section titled “Apply the module”The outputs you will need afterwards are
private_url,ecs_cluster_arn,migration_admin_task_definition_arn,admin_network_configuration,status_commandandsmoke_test_command. -
Run the migration and admin task
Section titled “Run the migration and admin task”The admin task definition runs
python -m metergraph_app.cliwith the database owner role and alist-tenantsdefault command. Every operator command is a container override on that task definition, launched into the subnets and security groups fromadmin_network_configuration:aws ecs run-task \--cluster "$CLUSTER_ARN" \--task-definition "$ADMIN_TASK_DEFINITION_ARN" \--launch-type FARGATE \--network-configuration "$ADMIN_NETWORK_CONFIGURATION" \--overrides '{"containerOverrides":[{"name":"admin","command":["python","-m","metergraph_app.cli","list-tenants"]}]}' -
Create the first workspace and its owner
Section titled “Create the first workspace and its owner”python -m metergraph_app.cli bootstrap-workspace <slug> \--identity-subject <cognito-sub> \--email owner@example.com \--display-name "Owner Name"It refuses if the slug exists, or if that email or identity pair is already registered.
--identity-issuerdefaults to the deployment’s configured issuer. -
Add the rest of the team
Section titled “Add the rest of the team”python -m metergraph_app.cli grant-member <slug> \--identity-subject <cognito-sub> \--email member@example.com \--role memberRoles are
owner,memberandviewer. -
Check readiness
Section titled “Check readiness”terraform output -raw status_commandprints the exact command. Run it from the extracted archive root, on the private network:python scripts/byoc_status.py --base-url https://<private-hostname>It needs no token. It reads
/healthz,/v1/deploymentand/readyzand prints the sameokpluschecksreport as./bin/statuson the local stack, withapi_health,deployment_profile,databaseandqueue. Exit code 0 means every check passed. -
Prove ingest end to end
Section titled “Prove ingest end to end”terraform output -raw smoke_test_commandprints the exact command. Run it from the extracted archive root, on the private network. It submits one native and one OTLP GenAI trace with a disposable ingest key, and refuses to run at all unless the target reportsdeployment_profile=byoc-core. It proves that native and OTLP/HTTP ingest are accepted and visible in Telemetry.python scripts/byoc_smoke.py --base-url https://<private-hostname> \--token "$METERGRAPH_APP_TOKEN"Confirm both smoke routes appear in the dashboard, then revoke the key.
More operator commands, the RLS check, backups and the tenant purge are on Operations.
Content and retention
Section titled “Content and retention”Content capture defaults on, and the per-event opt-out strips content before it
is stored. Relational trace and call data, and every current and historical
object version, expire after retention_days. Operational logs keep
log_retention_days. What the SDK sends and what is stored are two different
limits: see Content and privacy.