Appearance
BYOC on AWS with Terraform
If your platform team manages infrastructure as code, you can deploy Nomain into your own AWS account from your own Terraform pipeline instead of the AWS Marketplace one-click launch. This page is the how-to for that path.
It is a companion to BYOC on AWS; read that first for the architecture, Bedrock model access, and the account prerequisites. For the network placement options see AWS networking; for the subscribe-and-launch path see AWS Marketplace.
Same infrastructure, different front door
The Terraform module is a thin wrapper around the same published Nomain CloudFormation templates. Its core resource is a single aws_cloudformation_stack pointing at the versioned main.yaml. The infrastructure that comes up is byte-identical to a Marketplace or manual CloudFormation deploy of the same version: the module only turns the CloudFormation parameters into typed Terraform variables and pins every version-bearing value from one nomain_version.
When to use this
Choose the Terraform path when your organization already runs a Terraform/GitOps pipeline and wants Nomain managed the same way as the rest of its estate: the deploy reviewed as a Terraform plan, the VPC wired straight from your own network module, and no launch-form step. If you want the guided subscribe-and-launch experience instead, use the AWS Marketplace listing.
Two networking profiles are available, and the choice drives how the webapp is reached:
- Bring your own VPC: deploy into an existing customer-managed VPC. The stack creates no VPC, subnets, IGW, NAT or route tables, and the webapp load balancer is internal: reach it over your existing private connectivity. This is the usual choice for a self-hosted deployment.
- Dedicated VPC: leave the
existing_*inputs empty and the stack builds its own network. Only this profile can expose the webapp on the internet (web_app_access = "Public"), which requires a publicly trusted certificate.
Prerequisites
Everything in the BYOC on AWS prerequisites, plus:
| # | Item | Notes |
|---|---|---|
| 1 | Terraform (or OpenTofu) and an AWS-authenticated pipeline | The deploy principal needs the same permissions as a manual CloudFormation deploy of the stack (see Deploy permissions). |
| 2 | Registration identity | The tenant_id and the machine-to-machine ClientId / Secret Nomain issues you at onboarding. |
| 3 | Container images | By default, service images are pulled from the Nomain-managed ECR; Nomain grants your account pull access at onboarding and you host nothing. Alternatively, mirror the images into a registry you control and set container_registry (see the parameter reference). |
| 4 | An existing VPC (only for the BYO-VPC profile) | Two private subnets (ECS + internal ALB) and two isolated subnets (RDS), across two Availability Zones. When you supply these the module creates no VPC, subnets, gateways or route tables. Skip this item to let the stack build its own network. |
| 5 | An encrypted Terraform state backend | See State contains the M2M secret. |
Module usage
Reference the published module archive by a pinned version and pass your identity and VPC inputs:
hcl
module "nomain" {
source = "s3::https://nomain-shared-config.s3.eu-north-1.amazonaws.com/byoc-terraform/v1.3.2/nomain-byoc-aws.zip"
# nomain_version defaults to the release this archive was published for
# (v1.3.2 here). Set it only to pin a different published version.
# Identity — provided during Nomain registration
tenant_id = "your-tenant-guid"
m2m_client_id = var.m2m_client_id
m2m_client_secret = var.m2m_client_secret
# Networking — BYO-VPC: deploy into your existing VPC, webapp ALB internal.
# Omit these four to have the stack build its own VPC (see web_app_access).
existing_vpc_id = "vpc-0abc123def4567890"
existing_vpc_cidr = "10.20.0.0/16"
existing_private_subnet_ids = ["subnet-0aaa1111", "subnet-0bbb2222"]
existing_isolated_subnet_ids = ["subnet-0ccc3333", "subnet-0ddd4444"]
}The s3:: source fetches the archive with your own AWS credentials; a plain https:: source also works since the bucket is public-read. Then run the usual flow from your pipeline:
bash
terraform init
terraform plan # your change-set review
terraform applyVerify the archive before you deploy
Every release is signed. Verify the download against Nomain's public key before the first terraform init; see Verifying the download.
Wiring the VPC from your own Terraform
Because you manage your VPC as code, pass the existing_* inputs straight from your own resources or outputs rather than hardcoding IDs, the idiomatic pattern and a key advantage over the launch form:
hcl
# VPC and Nomain in the same configuration
existing_vpc_id = module.vpc.vpc_id
existing_vpc_cidr = module.vpc.vpc_cidr_block
existing_private_subnet_ids = module.vpc.private_subnets
existing_isolated_subnet_ids = module.vpc.database_subnets # or reuse private_subnetsThe reference creates an implicit dependency, so Terraform builds the VPC before the Nomain stack. If a separate network team owns the VPC, read its IDs from a terraform_remote_state data source; with no coupling to state, look them up by tag with aws_vpc / aws_subnets data sources. The module ships examples/byo-vpc (literal IDs) and examples/compose-with-vpc (wired from a VPC module) for this profile, plus examples/dedicated-vpc for a stack-created network with a public webapp.
Parameter reference
Every input the module accepts, grouped by area. Set them in your terraform.tfvars (or as TF_VAR_* from your pipeline's secret store). Only the inputs marked (required) must be provided; everything else has a production-sensible default, so you override only what you need.
Markers: (required) must be set · (sensitive) keep out of version control, inject via TF_VAR_* · (fixed) Nomain-managed, leave at its default. The module covers both networking profiles; the Marketplace image source is the one CloudFormation knob it does not expose, since Terraform deployments pull from the Nomain registry by cross-account IAM.
Identity & tenant
Provisioned when Nomain registers your tenant. Source the two secrets from your CI secret store, never a committed file.
| Variable | Type | Default | Notes |
|---|---|---|---|
tenant_id (required) | string | (none) | Nomain tenant GUID from registration (becomes NOMAIN_TENANT_ID). Not the WorkOS organization id. |
m2m_client_id (required, sensitive) | string | (none) | Machine-to-machine client id for service-to-service auth. |
m2m_client_secret (required, sensitive) | string | (none) | Machine-to-machine client secret. Inject via TF_VAR_m2m_client_secret. |
Deployment target & networking
Supply the four existing_* inputs to deploy into your own VPC, or leave all four empty to have the stack build one. Under BYO-VPC, subnets must span at least two AZs and reach the AWS APIs through your own egress.
The existing_* inputs are all-or-nothing and a partial set is rejected at plan time. This matters because supplying subnets while existing_vpc_id is empty would otherwise deploy successfully into a brand-new VPC and silently discard them.
| Variable | Type | Default | Notes |
|---|---|---|---|
web_app_access | string | "Internal" | Internal keeps the webapp load balancer private to the VPC. Public makes it internet-facing and needs certificate_arn; only available with a stack-created VPC. Both constraints are rejected at plan time. Defaults to the safe value, so omitting it cannot publish the webapp. |
vpc_cidr | string | "10.10.0.0/16" | IPv4 CIDR for the VPC the stack creates. Must be a canonical block between /16 and /21 (EC2 requires /16 or longer, and the stack carves eight /24 subnets out of it); all three constraints are rejected at plan time. Ignored when existing_vpc_id is set. |
existing_vpc_id | string | "" | An existing VPC to deploy into. Empty = the stack creates the VPC, subnets, IGW, NAT and route tables. When set, the deployment is internal-only. |
existing_vpc_cidr | string | "" | Primary IPv4 CIDR of that VPC, used for the internal ALB and security-group ingress. Required with existing_vpc_id. |
existing_private_subnet_ids | list(string) | [] | ≥2 subnets across 2 AZs for ECS, the internal ALB and db-setup. Required with existing_vpc_id. |
existing_isolated_subnet_ids | list(string) | [] | ≥2 subnets across 2 AZs for RDS. May equal the private subnets if you have no dedicated DB tier. Required with existing_vpc_id. |
vpn_client_cidr | string | "" | Extra CIDR allowed to reach the internal ALB (e.g. a VPN client pool or on-prem range). |
create_vpc_endpoints | string | "Auto" | Interface/gateway endpoints for the AWS APIs. Auto creates them when the stack creates the VPC, and skips them for an existing VPC (where your own egress reaches the APIs). Always always creates them, Never never does. Interface endpoints bill hourly per endpoint per AZ, so Never is the value that keeps a dedicated VPC on plain NAT egress. |
Database (RDS PostgreSQL)
Defaults are production-grade. The durability knobs are yours to trade off for cost on non-production stamps.
| Variable | Type | Default | Notes |
|---|---|---|---|
db_instance_class | string | "db.r6g.xlarge" | RDS instance size. Allowed: db.t4g.micro, db.r6g.large/xlarge/2xlarge, db.r7g.large/xlarge/2xlarge. |
db_allocated_storage | number | 500 | Initial storage in GB; autoscales up to the max (20 to 65536). |
db_max_allocated_storage | number | 2000 | Storage-autoscaling ceiling in GB (20 to 65536). |
multi_az | bool | true | RDS standby + second NAT across two AZs. false = single-AZ cost/availability trade-off. |
backup_retention_period | number | 14 | Days to retain automated RDS backups (1 to 35). |
enable_deletion_protection | bool | true | RDS deletion protection + snapshot-on-delete. Set false only for disposable environments. |
Compute sizing (Fargate)
CPU/memory must form a valid Fargate pair. The recipes worker is sized independently and scales to zero when idle.
| Variable | Type | Default | Notes |
|---|---|---|---|
service_cpu | number | 1024 | CPU units for the long-running services. Allowed: 256 / 512 / 1024 / 2048 / 4096. |
service_memory | number | 2048 | Memory (MiB) for the long-running services. Allowed: 512 / 1024 / 2048 / 4096 / 8192. |
recipes_worker_cpu | number | 8192 | CPU units for the recipes worker. Allowed: 2048 / 4096 / 8192 / 16384. |
recipes_worker_memory | number | 32768 | Memory (MiB) for the recipes worker. Allowed: 16384 / 24576 / 32768 / 49152 / 61440. |
recipes_worker_max_count | number | 5 | Maximum recipes-worker tasks the SQS-backlog autoscaler may run in parallel (1 to 20). Minimum stays 0 (scale-to-zero when idle); higher increases throughput and Fargate cost. |
AI / Bedrock models
Model ids are Bedrock model identifiers. Ensure the corresponding models are enabled in your Bedrock account and region.
| Variable | Type | Default | Notes |
|---|---|---|---|
enable_bedrock | bool | true | Enable Amazon Bedrock for model access. |
chat_model | string | "anthropic.claude-sonnet-4-6" | Bedrock model id for chat conversations. |
orchestrator_model | string | "" | High-capacity model for the chat orchestrator. Empty inherits chat_model. |
fast_model | string | "anthropic.claude-haiku-4-5-…" | Bedrock model id for fast, low-latency tasks. |
embedding_model | string | "amazon.titan-embed-text-v2:0" | Bedrock model id for text embeddings. |
rerank_model | string | "disabled" | Model id for reranking search results, or "disabled" to skip reranking. |
ai_provider | string | "bedrock" | AI provider identifier. |
ai_region | string | "" | AWS region for Bedrock API calls. Empty defaults to the stack region. |
llm_service_max_requests_per_minute | number | 10000 | LlmService max Bedrock requests/min (default = the AWS Bedrock default request quota). |
llm_service_max_tokens_per_minute | number | 3000000 | LlmService max Bedrock tokens/min (default = the AWS Bedrock default, Opus 4.6 floor). |
ai_parser_max_requests_per_minute | number | 10000 | AI-fallback parser max Bedrock requests/min. |
ai_parser_max_tokens_per_minute | number | 3000000 | AI-fallback parser max Bedrock tokens/min. |
unused_code_detection_max_requests_per_minute | number | 10000 | Dead-code / liveness (graphs-api only) max Bedrock requests/min. |
unused_code_detection_max_tokens_per_minute | number | 3000000 | Dead-code / liveness (graphs-api only) max Bedrock tokens/min. |
llm_service_max_concurrent_analysis | number | 250 | LlmService internal parallelism: max concurrent in-flight Bedrock calls per analysis. Usually the practical throughput lever for a single large analysis (a step runs on one worker). Default aligns with Azure production; size down to your applied quota if lower. |
ai_parser_max_concurrent_analysis | number | 75 | AI-fallback parser internal parallelism: max concurrent in-flight Bedrock calls per analysis. Default aligns with the Azure production recipes-worker value. |
unused_code_detection_max_concurrent_analysis | number | 15 | Dead-code / liveness (graphs-api only) internal parallelism: max concurrent in-flight Bedrock calls per analysis. Default aligns with Azure production. |
The six rate-limit variables are per-service Bedrock ceilings shared by the graphs-api and recipes-worker tasks. Their defaults match the AWS Bedrock default quota: lower them to your account's applied quota to avoid 429 throttling, since a fresh account's granted quota is often well below the published default. The three *_max_concurrent_analysis variables tune internal parallelism (concurrent in-flight Bedrock calls per analysis) rather than a per-minute ceiling; since one analysis step runs on a single worker, this, not worker count, is the throughput lever for a single large analysis. Their defaults are aligned to the Azure production posture; size them to your account's applied Bedrock quota (down if it is below the published default, up if you have headroom).
TLS & WAF
| Variable | Type | Default | Notes |
|---|---|---|---|
certificate_arn | string | "" | ACM certificate ARN (stack region, ISSUED) to terminate HTTPS on the ALB. Empty = HTTP listener, and empty is rejected when web_app_access = "Public". For a public webapp the certificate must be publicly trusted: one issued by a private CA attaches to an internet-facing ALB without complaint and then fails in every browser that has not installed that CA. |
enable_waf | bool | unset → create | WAFv2 WebACL (AWS managed rules) on the webapp ALB. false to attach your own WebACL or when WAF is managed centrally; associate it to the alb_arn output. A public ALB must carry a WebACL one way or the other; Public with none is not a supported posture. |
Container images & registry
By default images come from the Nomain cross-account ECR. Set container_registry to pull from a mirror you control: that single knob switches the source; the rest tune the layout. An ECR your account can already pull is authenticated by IAM (no credentials secret); you mirror the images yourself, and Nomain can grant your account pull on the source repos at onboarding.
| Variable | Type | Default | Notes |
|---|---|---|---|
container_registry | string | "" | Registry host to pull from instead of the Nomain ECR (e.g. your own <acct>.dkr.ecr.<region>.amazonaws.com, optionally with a /namespace). Setting it derives ImageRegistrySource = CustomRegistry. Empty keeps the Nomain ECR. |
ecr_account_id | string | "717745462956" | Account hosting the Nomain ECR; used only on the default path; ignored when container_registry is set. |
ecr_base_name | string | "nomainregistry" | Per-service repo prefix (nomainregistry-<svc>). Set "" for a path-style mirror (<registry>/<svc>). |
registry_credentials_secret_arn | string | "" | Secrets Manager ARN with {username, password} for a basic-auth registry (e.g. artifactory). Leave empty for your own ECR; it authenticates by IAM. |
otel_collector_image_uri | string | "" | Override for the ADOT collector sidecar image. Empty uses the Nomain-ECR mirror; set for air-gapped installs. |
Audit & observability
ISO 27001 controls default to on. The enable_* toggles let you opt out where your AWS org already provides the control centrally.
| Variable | Type | Default | Notes |
|---|---|---|---|
audit_log_retention_in_days | number | 365 | Retention for audit / security / diagnostic log groups. CloudWatch-supported values only (30 … 3653). |
enable_s3_data_events | bool | true | Record CloudTrail S3 delete data events on the customer storage bucket. |
enable_audit_trail | bool | unset → create | Per-stamp CloudTrail trail (+ object-locked bucket). false when an org-level trail already covers the account. |
enable_vpc_flow_logs | bool | unset → create | Per-stamp VPC Flow Log. false when your org centralizes flow logs. |
permissions_boundary_arn | string | "" | IAM permissions-boundary ARN stamped on every role the stack creates. Required by orgs whose SCP mandates a boundary. |
alarm_notification_email | string | "" | Email subscribed to the CloudWatch alarms SNS topic. Empty creates the topic with no subscription. |
alb_latency_alarm_seconds | number | 3 | ALB p95 target response time (s) that triggers a latency alarm. |
alb_target_5xx_alarm_threshold | number | 10 | Target-level 5xx count per 5-minute window that triggers an alarm. |
alb_elb_5xx_alarm_threshold | number | 5 | ALB-level 5xx count per 5-minute window that triggers an alarm. |
The three governed detective controls (enable_waf, enable_audit_trail, enable_vpc_flow_logs) default to the template's on-by-default posture; leave them unset unless your organization owns those controls centrally and its SCP forbids member-account trails / flow-logs / WebACLs.
Egress proxy (optional)
Route all outbound HTTPS through a forward proxy. Off by default. The CA bundle is only needed for a TLS-intercepting (MITM) proxy and requires the URL.
| Variable | Type | Default | Notes |
|---|---|---|---|
proxy_url (sensitive) | string | "" | Forward-proxy URL for outbound HTTPS (may carry user:pass@). Stored in Secrets Manager, never the task-def env; a managed NO_PROXY keeps AWS-service traffic direct. |
proxy_ca_pem | string | "" | Raw PEM of the proxy's CA (the module base64-encodes it). Only for a TLS-intercepting proxy; requires proxy_url. Tip: proxy_ca_pem = file("proxy-ca.pem"). |
Advanced (optional)
Rarely changed, but yours to set: naming, release pinning, tagging and a debug switch.
| Variable | Type | Default | Notes |
|---|---|---|---|
nomain_version | string | (archive's release) | Release to deploy; pins the template URL, nested-template prefix and image tag in lock-step. Defaults to the release this module archive was published for; override to pin a different one. |
stack_name | string | "nomain" | Name of the CloudFormation stack created in your account. |
base_name | string | "nomain" | Base name prefix for all resources (2 to 21 chars, lowercase). |
application_tag | string | "" | AppRegistry application ARN for the awsApplication tag. Empty to skip. |
disable_rollback | bool | false | OnFailure=DO_NOTHING: a failed deploy stops with resources intact for debugging instead of rolling back. |
tags | map(string) | {} | Tags applied to the CloudFormation stack; propagate to stack resources. |
Nomain-managed (do not change)
These point at Nomain's release hosting and control-plane. Leave them at their defaults; the only reason to override is a specific air-gapped or non-production engagement coordinated with Nomain, which changes other inputs (and credentials) too.
| Variable | Type | Default | Notes |
|---|---|---|---|
environment_name (fixed) | string | "production" | Selects the Nomain control-plane bundle (and naming/tagging). Keep "production" for a production stamp; a different value expects matching Nomain-issued credentials. |
shared_config_url (fixed) | string | Nomain S3 | Base URL of the Nomain-hosted shared-config bucket. Override only for an air-gapped self-host coordinated with Nomain. |
shared_config_region (fixed) | string | "eu-north-1" | Region of the Nomain templates bucket (single-home, independent of your deploy region). |
templates_bucket_name (fixed) | string | "nomain-shared-config" | Bucket hosting the nested CloudFormation templates. Override only when self-hosting them (see Air-gapped / self-hosted templates). |
Outputs and DNS
The module exposes alb_dns_name, alb_canonical_hosted_zone_id, alb_arn, web_app_scheme, rds_endpoint, s3_bucket_name, vpc_id, ecs_cluster_arn, stack_id, and the full stack_outputs map.
After apply, point your domain at alb_dns_name. The web_app_scheme output tells you which shape you got.
With an internal ALB, add an A-alias record in a private hosted zone (or your own internal DNS), reachable over your private connectivity into the VPC: Transit Gateway, Direct Connect, Site-to-Site VPN, or VPC peering.
With a public ALB, add the record in a public zone instead. The zone does not have to be Route 53: a CNAME from your hostname to alb_dns_name works from any DNS provider, and it must resolve to the same name the certificate was issued for.
Either way the stack creates no Route 53 resources; you own the DNS record. When enable_waf = false, associate your centrally-managed WAFv2 WebACL to alb_arn (same account + region as the ALB). On a public ALB this is not optional: the WebACL is the only filtering in front of the webapp, where an internal ALB already sat behind your network boundary.
Upgrading
The version lives in a single place (the nomain_version variable), which defaults to the release the archive was published for, so you normally don't set it. It derives, in lock-step, the root template URL, the nested-templates prefix, and the container image tag for every service. To upgrade, bump the version in the module source URL and re-apply: the Terraform plan is your change-set review. Terraform cannot interpolate a source, so the version appears literally in that URL; that is the single knob. There are no semver constraint operators: you pin a literal version, the same strict-pinning behavior as the CloudFormation flow. Ask your Nomain contact for the current published version.
Verifying the download (supply chain)
Every published release is cosign-signed. Alongside the archive, Nomain publishes a detached signature (nomain-byoc-aws.zip.sig) under the same versioned prefix and the signing public key at the version-independent path byoc-terraform/cosign.pub. The private key is an asymmetric AWS KMS key (ECC_NIST_P256) held in Nomain's account: you never need KMS access; you verify with the public key alone.
bash
BASE=https://nomain-shared-config.s3.eu-north-1.amazonaws.com
VERSION=v1.3.2 # the version you pinned in the module `source`
# one-time: fetch the signing public key (pin it — see the fingerprint below)
curl -fsSLO "$BASE/byoc-terraform/cosign.pub"
# per download: fetch the archive + its detached signature, then verify
curl -fsSLO "$BASE/byoc-terraform/$VERSION/nomain-byoc-aws.zip"
curl -fsSLO "$BASE/byoc-terraform/$VERSION/nomain-byoc-aws.zip.sig"
cosign verify-blob \
--key cosign.pub \
--signature nomain-byoc-aws.zip.sig \
--insecure-ignore-tlog=true \
nomain-byoc-aws.zip--insecure-ignore-tlog=true is expected and correct: the artifact is key-signed without a transparency-log (Rekor) entry, so verification is fully offline against the public key with no call to public Sigstore infrastructure, the right model for air-gapped and egress-restricted networks. The command works with either cosign v2 or v3.
Pin the public key out of band
A cosign.pub fetched from the same bucket as the artifact is only trust-on-first-use. Confirm it matches the fingerprint Nomain publishes out of band (first published with the v1.3.2 release) before you rely on it, then reuse that pinned key for every subsequent download:
SHA-256: 0aec212983e64193fba47b280ed6dd88c482c76f1aeb618d12dae7aaba50a5d7Compute the fingerprint of a fetched key with:
bash
openssl pkey -pubin -in cosign.pub -outform DER | openssl dgst -sha256State contains the M2M secret
m2m_client_secret is marked sensitive, which redacts it from CLI and plan output, but not from Terraform state. Because it flows into the aws_cloudformation_stack parameters, the cleartext value is written into every state file and snapshot. Treat state as a secret: use an encrypted, access-restricted backend (for example S3 with SSE-KMS and a locked-down bucket policy); never a plaintext local terraform.tfstate on a shared path.
Sourcing the credentials from a secrets store (AWS Secrets Manager via a data source, SSM Parameter Store, Vault) keeps them out of committed *.tfvars and gives you one place to rotate them. It does not remove the state exposure, so the encrypted-backend requirement stands either way.
Air-gapped / self-hosted templates
Mirror the module archive into your own bucket and repoint source. To also serve the CloudFormation templates from your own bucket, set templates_bucket_name (and shared_config_region) to your mirror and sync the byoc-templates/<version>/ prefix there. This removes the runtime dependency on Nomain's bucket entirely.
Deploy permissions
Because the module's only resource is an aws_cloudformation_stack over the published main.yaml, the deploy principal needs the same permissions as a Marketplace or manual CloudFormation deploy of the same version: CloudFormation creates the identical resources either way. Terraform-specific points:
- CloudFormation API.
terraform applycallscloudformation:CreateStack/UpdateStack/DeleteStack/DescribeStacks/DescribeStackEvents/GetTemplatewithCAPABILITY_NAMED_IAM+CAPABILITY_AUTO_EXPAND. By default CloudFormation then creates the resources with theterraform applyprincipal's permissions, so that principal needs the full template permission set. Attaching a CloudFormation service role to the stack moves those resource permissions off the Terraform principal (the same option as the CloudFormation flow). - State backend. With an S3 + DynamoDB backend, the principal also needs read/write on the state bucket and lock table (Terraform infrastructure, separate from the deploy target). A local backend needs nothing extra.
- Internal profile needs no
route53:*orwafv2:*. The stack creates no Route 53 resources, and withenable_waf = falseit creates no WebACL. Leaveenable_wafat the default only if the principal can create one. - Centralized audit controls. With
enable_audit_trail = falsethe stack needs nocloudtrail:*; withenable_vpc_flow_logs = falseit needs noec2:CreateFlowLogs. Set these only when the org owns those detective controls. - Permissions boundary. If your org's SCP denies
iam:CreateRole/iam:PutRolePolicywithout a boundary, setpermissions_boundary_arn; it is stamped on every IAM role the stack creates. It is a ceiling (effective permissions are the intersection with each role's own policy), so it must be permissive enough for the task roles' runtime actions or the services fail at runtime even though the deploy succeeds.
Where to go next
- BYOC on AWS: architecture, Bedrock models, and account prerequisites.
- AWS networking options: VPC and subnet placement models.
- Security & compliance: the cross-cloud data boundary and controls.