Skip to content

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}/approve and POST /v1/recommendations/{id}/approve need external_delivery, so approving a recommendation into a pull request is unavailable.
  • POST /v1/recommendations/{id}/canary needs canaries.
  • /v1/routes/{route}/constraints and /v1/routes/{route}/evaluation need evaluations.

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.

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.

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.

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.

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>

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.

Follow these seven steps. The workspace section below covers the linked operator commands in detail.

  1. Set api_desired_count=0 and worker_desired_count=0 for the first apply.

  2. Run the published admin task and migration.

  3. Create the first workspace and owner.

  4. Apply one replica of each, then run the status_command output: 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 reports ok.

  5. Run the smoke_test_command output from the private network. It proves that native and OTLP/HTTP ingest are accepted and visible in Telemetry.

  6. Verify a deterministic alert, acknowledge it, and revoke the API key.

  7. 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.

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.

  1. The outputs you will need afterwards are private_url, ecs_cluster_arn, migration_admin_task_definition_arn, admin_network_configuration, status_command and smoke_test_command.

  2. The admin task definition runs python -m metergraph_app.cli with the database owner role and a list-tenants default command. Every operator command is a container override on that task definition, launched into the subnets and security groups from admin_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"]}]}'
  3. 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-issuer defaults to the deployment’s configured issuer.

  4. python -m metergraph_app.cli grant-member <slug> \
    --identity-subject <cognito-sub> \
    --email member@example.com \
    --role member

    Roles are owner, member and viewer.

  5. terraform output -raw status_command prints 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/deployment and /readyz and prints the same ok plus checks report as ./bin/status on the local stack, with api_health, deployment_profile, database and queue. Exit code 0 means every check passed.

  6. terraform output -raw smoke_test_command prints 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 reports deployment_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 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.