Secure-by-default Amazon SageMaker AI platform — a Studio domain keystone plus user profiles, and an independently-gated model → endpoint-configuration → endpoint hosting chain and Feature Store, from a single composite call. VPC-only networking, KMS encryption, model network isolation, and least-privilege execution-role wiring are all on by default. Built for the AWS provider v6.x.
- 🧠 Provisions an
aws_sagemaker_domainkeystone plus two independently-gated sub-features: an interactive Studio environment (aws_sagemaker_user_profile) and production hosting (aws_sagemaker_model→aws_sagemaker_endpoint_configuration→aws_sagemaker_endpoint), plus the Feature Store (aws_sagemaker_feature_group). - 🔒 VPC-only by default (
app_network_access_type = "VpcOnly") — no direct internet egress; all traffic flows through the caller's VPC. - 🔐 KMS encryption throughout — domain EFS, endpoint EBS volumes, and Feature Store online/offline stores accept a customer-managed
kms_key_arn(AWS-managed key otherwise; there is no unencrypted state). - 🚧 Model network isolation on by default (
enable_network_isolation = true) — containers make no outbound calls unless a caller explicitly opts out. - 🔑 Execution role is a required input (
execution_role_arn) — never created here. The description makes the mandatoryiam:PassRolegrant impossible to miss. - 🧩 Studio and hosting are independently gated —
user_profilesgates Studio;models/endpoint_configurations/endpointsgate hosting; populate either, both, or neither beyond the always-created keystone domain. - 🎛️ Studio surface-area reduction —
hidden_instance_types/hidden_app_typesrestrict which apps and instance sizes users may launch. - 🏷️ Universal tagging —
var.tagsflows to every taggable resource (all six exposetags);tags_allsurfaced from the domain keystone.
💡 Why it matters: ML platforms touch training data that is frequently PII. A SageMaker environment that is VPC-only, KMS-encrypted, network-isolated, and least-privilege from a single module call keeps the data-science blast radius contained — the posture a regulated FI has to start from, not bolt on.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
tf-mod-aws-sagemaker is a consumer module: it wires an execution role, VPC/subnets/SG, KMS, S3 artifact/offline-store buckets, and ECR images from upstream foundations, and emits endpoint ARNs that applications invoke for inference.
flowchart LR
iam["tf-mod-aws-iam-role<br/>execution_role_arn"]
vpc["tf-mod-aws-vpc<br/>vpc_id / subnet_ids"]
sg["tf-mod-aws-security-group"]
kms["tf-mod-aws-kms<br/>kms_key_arn"]
s3["tf-mod-aws-s3-bucket<br/>model artifacts / offline store"]
ecr["tf-mod-aws-ecr<br/>container image URIs"]
sm["tf-mod-aws-sagemaker"]
app["Application / API Gateway<br/>(inference callers)"]
pol["tf-mod-aws-iam-policy<br/>(resource-scoped policies)"]
iam -->|"execution_role_arn + iam:PassRole"| sm
vpc -->|"vpc_id / subnet_ids"| sm
sg -->|"security_group_ids"| sm
kms -->|"kms_key_arn"| sm
s3 -.->|"model_data_url / offline store S3 URI"| sm
ecr -.->|"container image URI"| sm
sm -->|"endpoint_arns"| app
sm -->|"model_arns / endpoint_arns"| pol
style sm fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
flowchart TB
subgraph SMMOD["tf-mod-aws-sagemaker"]
dom["aws_sagemaker_domain.this<br/>(keystone — VpcOnly, KMS EFS)"]
up["aws_sagemaker_user_profile.this<br/>for_each user_profiles (Studio)"]
mdl["aws_sagemaker_model.this<br/>for_each models (network-isolated)"]
ec["aws_sagemaker_endpoint_configuration.this<br/>for_each endpoint_configurations"]
ep["aws_sagemaker_endpoint.this<br/>for_each endpoints"]
fg["aws_sagemaker_feature_group.this<br/>for_each feature_groups"]
end
dom --> up
mdl --> ec --> ep
dom -. "shared role / VPC / KMS wiring".- mdl
dom -. "shared role / VPC / KMS wiring".- fg
style dom fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
| Resource | Role | Cardinality | Taggable |
|---|---|---|---|
aws_sagemaker_domain.this |
Keystone Studio domain | 1 | ✅ |
aws_sagemaker_user_profile.this |
Studio user profiles | per user_profiles entry |
✅ |
aws_sagemaker_model.this |
Deployable model | per models entry |
✅ |
aws_sagemaker_endpoint_configuration.this |
Production-variant config | per endpoint_configurations entry |
✅ |
aws_sagemaker_endpoint.this |
Deployed inference endpoint | per endpoints entry |
✅ |
aws_sagemaker_feature_group.this |
Online/offline Feature Store | per feature_groups entry |
✅ |
ℹ️ The domain is the keystone
thisand is always created; a hosting-only caller still provisions an (empty) Studio domain in v1.
| Requirement | Version |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/aws |
>= 6.0, < 7.0 |
No provider {} block is declared inside the module — the caller's configured provider (region, credentials, default_tags) is inherited. There are no credential variables.
Least-privilege actions the Terraform execution identity needs:
| Action | Required for | Notes |
|---|---|---|
sagemaker:CreateDomain, sagemaker:DescribeDomain, sagemaker:UpdateDomain, sagemaker:DeleteDomain |
Domain lifecycle | Core CRUD |
sagemaker:CreateUserProfile, sagemaker:DescribeUserProfile, sagemaker:UpdateUserProfile, sagemaker:DeleteUserProfile |
Studio user profiles | Only when user_profiles set |
sagemaker:CreateModel, sagemaker:DescribeModel, sagemaker:DeleteModel |
Hosting: model registration | Only when models set |
sagemaker:CreateEndpointConfig, sagemaker:DescribeEndpointConfig, sagemaker:DeleteEndpointConfig |
Hosting: endpoint configuration | — |
sagemaker:CreateEndpoint, sagemaker:DescribeEndpoint, sagemaker:UpdateEndpoint, sagemaker:DeleteEndpoint |
Hosting: endpoint deploy/update/destroy | — |
sagemaker:CreateFeatureGroup, sagemaker:DescribeFeatureGroup, sagemaker:DeleteFeatureGroup |
Feature Store | Only when feature_groups set |
sagemaker:AddTags, sagemaker:DeleteTags, sagemaker:ListTags |
Tagging | — |
iam:PassRole (scoped to execution_role_arn) |
Passing the execution role to SageMaker on every create/update | Mandatory. SageMaker assumes the role to read S3 artifacts, pull ECR images, write Feature Store data, and mount EFS. Missing this fails every Create* with AccessDeniedException. Never wildcard the resource. |
ec2:CreateNetworkInterface, ec2:DescribeNetworkInterfaces, ec2:DeleteNetworkInterface, ec2:DescribeVpcs, ec2:DescribeSubnets, ec2:DescribeSecurityGroups |
SageMaker-managed ENIs for VPC-only domains/endpoints | ENI lifecycle is exercised by the SageMaker service role, surfaced via Describe calls |
kms:CreateGrant, kms:DescribeKey |
CMK-encrypted EFS/EBS/Feature Store | Only when a customer-managed kms_key_arn is supplied |
auth_modeis effectively immutable — changing it on a populated domain is not supported by the API. defaults toIAM(aligns with the rest of the library);SSOrequires IAM Identity Center enabled first.- Minimum 2 subnets in 2 AZs — the module mandates ≥ 2 subnets for domain HA; Studio apps and endpoints place ENIs in these subnets.
- No service-linked role required for Domain/Model/Endpoint creation; the supplied
execution_role_arn(trustingsagemaker.amazonaws.com) is sufficient. - VPC-only mode consumes IP addresses — undersized subnets cause
ResourceLimitExceededat app-launch time, not domain-creation time. - Quotas — Studio domains are 1-per-Region-per-account by default (soft, raiseable); endpoint instance-count quotas (e.g.
ml.m5.xlarge) are commonly hit at scale — checksagemakerService Quotas before provisioning many endpoints. - Feature Store online store is DynamoDB-backed capacity managed by the service; no separate table is created here.
- Region: no
us-east-1constraint — all resources are Regional.
tf-mod-aws-sagemaker/
├── providers.tf # terraform{} + required_providers (aws >= 6.0, < 7.0); no provider block
├── variables.tf # domain identity, network/KMS, studio settings, and hosting/feature maps
├── main.tf # aws_sagemaker_domain.this + 5 for_each child resources
├── outputs.tf # id + arn first, domain attrs, hosting/feature maps, tags_all
├── README.md # this file
└── SCOPE.md # in/out-of-scope, Consumes/Emits, IAM, prerequisites, gotchas
module "sagemaker" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-sagemaker?ref=v1.0.0"
domain_name = "casey-ds"
execution_role_arn = module.sm_exec_role.arn # tf-mod-aws-iam-role (trusts sagemaker.amazonaws.com)
vpc_id = module.vpc.vpc_id # tf-mod-aws-vpc
subnet_ids = values(module.vpc.private_subnet_ids) # >= 2 subnets
security_group_ids = [module.sm_sg.id] # tf-mod-aws-security-group
kms_key_arn = module.kms.arn # tf-mod-aws-kms
user_profiles = {
analyst = {} # opens Studio for a data scientist
}
tags = { Environment = "prod", Team = "data-science" }
}
⚠️ The Terraform identity must haveiam:PassRoleonmodule.sm_exec_role.arn, or every SageMakerCreate*call fails withAccessDeniedException. Pin the source with?ref=v1.0.0, never a branch.
| Input | Type | Source module |
|---|---|---|
execution_role_arn |
string (ARN, required) |
tf-mod-aws-iam-role (requires iam:PassRole) |
vpc_id |
string |
tf-mod-aws-vpc |
subnet_ids |
list(string) (≥ 2) |
tf-mod-aws-vpc |
security_group_ids |
list(string) |
tf-mod-aws-security-group |
kms_key_arn |
string (ARN, optional) |
tf-mod-aws-kms |
| model artifact / offline-store S3 URIs | string |
tf-mod-aws-s3-bucket |
| container image URIs | string |
tf-mod-aws-ecr (or a SageMaker prebuilt image) |
| Output | Description | Consumed by |
|---|---|---|
id / domain_id |
Domain id (d-xxxxxxxxxxxx) |
user profiles, audit |
arn / domain_arn |
Domain ARN | IAM policy conditions, cross-module reference |
url |
Studio URL | operator bookmarking / SSO landing |
user_profile_arns / user_profile_names |
Map keyed by caller key | Studio access provisioning |
model_arns / model_names |
Map keyed by caller key | tf-mod-aws-iam-policy, CI/CD deploy |
endpoint_configuration_arns / _names |
Map keyed by caller key | endpoints, rollback tooling |
endpoint_arns / endpoint_names |
Map keyed by caller key | application / API Gateway inference |
feature_group_arns / _names |
Map keyed by caller key | ML pipelines, Glue Data Catalog |
tags_all |
All tags incl. provider default_tags |
governance / audit |
1 · Minimal — Studio domain + one user profile
module "sagemaker" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-sagemaker?ref=v1.0.0"
domain_name = "casey-ds"
execution_role_arn = module.sm_exec_role.arn
vpc_id = module.vpc.vpc_id
subnet_ids = values(module.vpc.private_subnet_ids)
security_group_ids = [module.sm_sg.id]
user_profiles = { analyst = {} }
}2 · Customer-managed KMS across EFS / EBS / Feature Store
module "sagemaker" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-sagemaker?ref=v1.0.0"
domain_name = "casey-ds"
execution_role_arn = module.sm_exec_role.arn
vpc_id = module.vpc.vpc_id
subnet_ids = values(module.vpc.private_subnet_ids)
security_group_ids = [module.sm_sg.id]
kms_key_arn = module.kms.arn # domain EFS + endpoint EBS + Feature Store
user_profiles = { analyst = {} }
}3 · Restrict Studio instance/app types (surface-area reduction)
module "sagemaker" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-sagemaker?ref=v1.0.0"
domain_name = "casey-ds"
execution_role_arn = module.sm_exec_role.arn
vpc_id = module.vpc.vpc_id
subnet_ids = values(module.vpc.private_subnet_ids)
security_group_ids = [module.sm_sg.id]
studio_user_settings = {
studio_web_portal_settings = {
hidden_instance_types = ["ml.p4d.24xlarge", "ml.p5.48xlarge"] # block expensive GPU boxes
hidden_app_types = ["RStudioServerPro"]
}
}
user_profiles = { analyst = {}, reviewer = {} }
}4 · Production hosting — model → endpoint config → endpoint
module "sagemaker" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-sagemaker?ref=v1.0.0"
domain_name = "casey-ds"
execution_role_arn = module.sm_exec_role.arn
vpc_id = module.vpc.vpc_id
subnet_ids = values(module.vpc.private_subnet_ids)
security_group_ids = [module.sm_sg.id]
kms_key_arn = module.kms.arn
models = {
scorer = {
primary_container = {
image = "${module.ecr.repository_url}:latest"
model_data_url = "s3://${module.artifacts.id}/models/scorer/model.tar.gz"
}
# enable_network_isolation defaults to true (secure baseline)
}
}
endpoint_configurations = {
scorer = {
production_variants = [{
variant_name = "AllTraffic"
model_name = "scorer" # references models["scorer"]
instance_type = "ml.m5.xlarge"
initial_instance_count = 2
}]
}
}
endpoints = {
scorer = { endpoint_config_name = "scorer" }
}
}5 · Endpoint with data capture to an encrypted S3 destination
module "sagemaker" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-sagemaker?ref=v1.0.0"
domain_name = "casey-ds"
execution_role_arn = module.sm_exec_role.arn
vpc_id = module.vpc.vpc_id
subnet_ids = values(module.vpc.private_subnet_ids)
security_group_ids = [module.sm_sg.id]
kms_key_arn = module.kms.arn
models = { scorer = { primary_container = { image = "${module.ecr.repository_url}:latest", model_data_url = "s3://${module.artifacts.id}/m.tar.gz" } } }
endpoint_configurations = {
scorer = {
production_variants = [{ variant_name = "AllTraffic", model_name = "scorer", instance_type = "ml.m5.xlarge", initial_instance_count = 1 }]
data_capture_config = {
enable_capture = true
initial_sampling_percentage = 20
destination_s3_uri = "s3://${module.capture_bucket.id}/capture"
kms_key_id = module.kms.arn
capture_options = [{ capture_mode = "Input" }, { capture_mode = "Output" }]
}
}
}
endpoints = { scorer = { endpoint_config_name = "scorer" } }
}6 · Feature Store — online + offline group
module "sagemaker" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-sagemaker?ref=v1.0.0"
domain_name = "casey-ds"
execution_role_arn = module.sm_exec_role.arn
vpc_id = module.vpc.vpc_id
subnet_ids = values(module.vpc.private_subnet_ids)
security_group_ids = [module.sm_sg.id]
kms_key_arn = module.kms.arn
feature_groups = {
borrower = {
record_identifier_feature_name = "borrower_id"
event_time_feature_name = "event_time"
feature_definitions = [
{ feature_name = "borrower_id", feature_type = "String" },
{ feature_name = "event_time", feature_type = "String" },
{ feature_name = "score", feature_type = "Fractional" },
]
online_store_config = { enable_online_store = true }
offline_store_config = { s3_storage_config = { s3_uri = "s3://${module.offline.id}/feature-store" } }
}
}
}7 · Blue/green endpoint deployment with canary traffic shifting
endpoints = {
scorer = {
endpoint_config_name = "scorer"
deployment_config = {
blue_green_update_policy = {
traffic_routing_configuration = {
type = "CANARY"
canary_size = { type = "CAPACITY_PERCENT", value = 10 }
wait_interval_in_seconds = 300
}
termination_wait_in_seconds = 600
}
auto_rollback_configuration = {
alarm_names = ["casey-scorer-5xx-errors"] # CloudWatch alarm NAMES (not ARNs) — from tf-mod-aws-cloudwatch-alarm
}
}
}
}8 · Serverless inference endpoint configuration
endpoint_configurations = {
scorer = {
production_variants = [{
variant_name = "AllTraffic"
model_name = "scorer"
serverless_config = {
max_concurrency = 20
memory_size_in_mb = 2048
}
}]
}
}9 · Async inference endpoint
endpoint_configurations = {
scorer = {
production_variants = [{ variant_name = "AllTraffic", model_name = "scorer", instance_type = "ml.m5.xlarge", initial_instance_count = 1 }]
async_inference_config = {
output_config = {
s3_output_path = "s3://${module.async_out.id}/inference"
kms_key_id = module.kms.arn
}
client_config = { max_concurrent_invocations_per_instance = 4 }
}
}
}10 · Retain EFS on destroy (compliance data preservation)
module "sagemaker" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-sagemaker?ref=v1.0.0"
domain_name = "casey-ds"
execution_role_arn = module.sm_exec_role.arn
vpc_id = module.vpc.vpc_id
subnet_ids = values(module.vpc.private_subnet_ids)
security_group_ids = [module.sm_sg.id]
home_efs_file_system_retention_policy = "Retain" # keep notebook/user data after teardown
user_profiles = { analyst = {} }
}11 · Public-internet domain — secure-default opt-out (requires exception)
module "sagemaker" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-sagemaker?ref=v1.0.0"
domain_name = "casey-ds-sandbox"
execution_role_arn = module.sm_exec_role.arn
vpc_id = module.vpc.vpc_id
subnet_ids = values(module.vpc.private_subnet_ids)
security_group_ids = [module.sm_sg.id]
app_network_access_type = "PublicInternetOnly" # opt out of VpcOnly — document the regulated-industry exception
user_profiles = { analyst = {} }
}
⚠️ PublicInternetOnlygives Studio apps direct internet egress. Do not use for PII workloads without a documented exception.
12 · Model allowing outbound calls (network isolation opt-out)
models = {
licensed = {
primary_container = { image = "${module.ecr.repository_url}:licensed", model_data_url = "s3://${module.artifacts.id}/m.tar.gz" }
enable_network_isolation = false # opt out — e.g. the container calls a license server
}
}13 · Per-profile execution role override
user_profiles = {
analyst = {} # inherits domain default execution role
admin = {
execution_role = module.sm_admin_role.arn # per-profile override (top-level field on the profile)
}
}14 · Tags — merge with provider default_tags
provider "aws" {
region = "us-east-1"
default_tags { tags = { Owner = "platform", ManagedBy = "terraform" } }
}
module "sagemaker" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-sagemaker?ref=v1.0.0"
domain_name = "casey-ds"
execution_role_arn = module.sm_exec_role.arn
vpc_id = module.vpc.vpc_id
subnet_ids = values(module.vpc.private_subnet_ids)
security_group_ids = [module.sm_sg.id]
tags = { Environment = "prod", Owner = "data-science" } # overrides default_tags Owner
user_profiles = { analyst = { tags = { Role = "analyst" } } } # merged over module tags
}
# module.sagemaker.tags_all => { Owner="data-science", ManagedBy="terraform", Environment="prod" }15 · End-to-end composition — role + VPC + KMS + S3 + ECR (finale)
module "kms" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-kms?ref=v1.0.0"
name = "casey-sagemaker"
}
module "sm_exec_role" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iam-role?ref=v1.0.0"
name = "casey-sagemaker-exec"
# assume_role_policy: trusts sagemaker.amazonaws.com;
# attach S3 (artifacts + offline store), ECR pull, and kms:Decrypt on module.kms.arn
}
module "artifacts" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-s3-bucket?ref=v1.0.0"
name = "casey-sagemaker-artifacts"
}
module "ecr" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-ecr?ref=v1.0.0"
name = "casey-scorer"
}
module "sagemaker" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-sagemaker?ref=v1.0.0"
domain_name = "casey-ds"
execution_role_arn = module.sm_exec_role.arn
vpc_id = module.vpc.vpc_id # tf-mod-aws-vpc
subnet_ids = values(module.vpc.private_subnet_ids)
security_group_ids = [module.sm_sg.id] # tf-mod-aws-security-group
kms_key_arn = module.kms.arn
user_profiles = { analyst = {} }
models = {
scorer = {
primary_container = {
image = "${module.ecr.repository_url}:latest"
model_data_url = "s3://${module.artifacts.id}/models/scorer/model.tar.gz"
}
}
}
endpoint_configurations = {
scorer = { production_variants = [{ variant_name = "AllTraffic", model_name = "scorer", instance_type = "ml.m5.xlarge", initial_instance_count = 2 }] }
}
endpoints = { scorer = { endpoint_config_name = "scorer" } }
tags = { Environment = "prod", App = "credit-scoring" }
}ℹ️ High-level grouping:
- Identity / required:
domain_name,execution_role_arn,vpc_id,subnet_ids(≥ 2) - Networking / auth:
security_group_ids,auth_mode(IAM|SSO),app_network_access_type(VpcOnly|PublicInternetOnly),app_security_group_management,tag_propagation - Encryption / storage:
kms_key_arn,home_efs_file_system_retention_policy - Studio:
studio_user_settings,studio_space_settings,domain_settings,user_profiles(map) - Hosting:
models(map),endpoint_configurations(map),endpoints(map) - Feature Store:
feature_groups(map) - Universal:
tags
- Primary:
id/domain_id,arn/domain_arn - Domain attributes:
url,home_efs_file_system_id,security_group_id_for_domain_boundary,single_sign_on_application_arn,single_sign_on_managed_application_instance_id - Studio:
user_profile_arns,user_profile_names,user_profile_home_efs_file_system_uids - Hosting:
model_arns/model_names,endpoint_configuration_arns/_names,endpoint_arns/endpoint_names - Feature Store:
feature_group_arns/feature_group_names - Tags:
tags_all
ℹ️ No outputs are marked
sensitive— this module emits no secrets.
- ARN / ID formats:
id→d-xxxxxxxxxxxx(domain id);arn→arn:aws:sagemaker:<region>:<account>:domain/<domain-id>. Models/endpoints/feature groups emit their ownarns in the output maps. - Force-new / immutable fields:
auth_mode,vpc_id, anddomain_nameare force-new on the domain — changing any recreates it (and a populated domain must have all user profiles/apps/spaces deleted first). - No
root_accesstoggle exists for Studio domains/user profiles in the v6 schema —RootAccessis a legacyaws_sagemaker_notebook_instancecontrol that AWS explicitly does not support for Studio. The lockdown levers areapp_network_access_type = "VpcOnly",hidden_app_types/hidden_instance_types, and a least-privilegeexecution_role_arn. (The original brief's assumedroot_accessargument was verified non-existent against the live v6.53 schema.) default_user_settings.execution_roleis a required block, not a top-level argument — the module'sexecution_role_arnvariable populates it;user_profiles[*].user_settings.execution_rolecan override per profile.tags↔tags_all↔default_tags: every resource acceptstags;tags_all(merge overdefault_tags, resource tags win) is surfaced from the domain keystone.default_tagsis the caller's provider-block concern.- Destroy ordering: endpoint → endpoint configuration → model (Terraform's implicit graph handles this via name references, all three declared here); every user profile (and any out-of-band Studio apps/spaces) must be deleted before the domain, or delete fails with
ResourceInUse. The domain EFS volume is deleted perretention_policy(module defaults toDeleteto avoid orphaned cost). - Eventual consistency: endpoint create/update is asynchronous (
Creating→InService); an immediatedestroyon a just-created endpoint can race the AWS state machine. - No
us-east-1constraint — SageMaker is Regional; the module inherits the caller's region and declares noregionvariable.
Secure-by-default posture and the explicit opt-out for each:
| Hardened default | Behavior | Opt-out / control |
|---|---|---|
| Domain network access | app_network_access_type = "VpcOnly" |
"PublicInternetOnly" (documented exception) |
| Domain EFS encryption | CMK when kms_key_arn supplied; AWS-managed otherwise (never unencrypted) |
kms_key_arn = null |
| Endpoint volume encryption | kms_key_arn wired into every production variant |
omit kms_key_arn |
| Feature Store encryption | online/offline stores KMS-encrypted from kms_key_arn |
omit kms_key_arn |
| Model network isolation | enable_network_isolation = true |
false per-model (documented) |
| Model VPC placement | vpc_config wired from subnet_ids/security_group_ids |
omit both (not recommended) |
| Studio surface area | hidden_instance_types / hidden_app_types available |
leave lists empty for the full catalog |
| Data capture | off by default (destinations must be deliberately provisioned) | enable_capture = true + encrypted destination_s3_uri |
| EFS retention on destroy | retention_policy = "Delete" (no orphaned cost) |
"Retain" for compliance data preservation |
Other principles: one composite owns the domain plus its Studio, hosting, and Feature Store children; Studio and hosting are independently gated by their maps; every child is a for_each map keyed by a stable string (never count); the execution role, VPC, KMS, S3, and ECR are consumed by reference so their policy/lifecycle stays with the owning module; legacy notebook instances and custom Studio images are deliberately excluded from v1.
terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan # requires valid AWS credentials (profile / SSO / OIDC) + a region
terraform apply
terraform output
⚠️ plan/applyrequire valid AWS credentials, a region, andiam:PassRoleon the execution role. Always pin the module source with?ref=v1.0.0, never a branch.
terraform init -backend=false && terraform validate— schema + reference integrity.terraform fmt -check— formatting.terraform planagainst a sandbox account — confirm the domain plans VpcOnly with KMS EFS, models plan network-isolated, and the model → endpoint-config → endpoint chain resolves by name.- After
apply, confirm the domain reachesInServiceand endpoints reachInServicebefore invoking inference. - Destroy test in a throwaway account — verify user-profile/app teardown ordering before relying on it in shared environments.
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
arn = "arn:aws:sagemaker:us-east-1:123456789012:domain/d-abcdefghijkl"
id = "d-abcdefghijkl"
url = "https://d-abcdefghijkl.studio.us-east-1.sagemaker.aws"
endpoint_arns = { "scorer" = "arn:aws:sagemaker:us-east-1:123456789012:endpoint/scorer" }
model_arns = { "scorer" = "arn:aws:sagemaker:us-east-1:123456789012:model/scorer" }
tags_all = { "Environment" = "prod", "App" = "credit-scoring" }
AccessDeniedExceptionon anyCreate*: the Terraform identity lacksiam:PassRoleonexecution_role_arn. Scope it to that exact ARN — SageMaker cannot be handed the role otherwise.ResourceLimitExceededat Studio app launch (not domain create): VpcOnly mode ran out of free IPs in the supplied subnets. Size subnets for peak concurrent apps + kernels.- Domain won't delete (
ResourceInUse): running Studio apps/spaces or undeleted user profiles. Delete apps/spaces (some are out-of-band from Terraform) and profiles first. - Endpoint stuck / update fails: endpoint create/update is asynchronous; check the endpoint status in the console. Immediate destroy after create can race the state machine — retry.
- Looking for a
root_accessvariable: it does not exist for Studio — useVpcOnly,hidden_*, and a least-privilege role instead (see Architecture Notes). auth_modechange plans a replacement: it is force-new / effectively immutable — plan the auth model up front.- Tag drift:
default_tagsoverlap;tags_allmerges with resource tags winning. Set the value explicitly invar.tags. - Credential-chain failures (
NoCredentialProviders/ExpiredToken): no valid credentials resolved. SetAWS_PROFILE, refresh SSO, or confirm the OIDC role assumption in CI.
- Terraform Registry —
hashicorp/aws:aws_sagemaker_domain,aws_sagemaker_user_profile,aws_sagemaker_model,aws_sagemaker_endpoint_configuration,aws_sagemaker_endpoint,aws_sagemaker_feature_group - AWS — Amazon SageMaker AI Developer Guide (Studio domains, VPC-only mode, execution roles)
- AWS — Connect SageMaker Studio to resources in a VPC (ENIs, subnet IP consumption)
- AWS — Protect data at rest / in transit with encryption (EFS/EBS/Feature Store KMS)
- AWS — Deploy models for inference (production variants, blue/green, async, serverless)
- —
tf-mod-aws-iam-role,tf-mod-aws-vpc,tf-mod-aws-security-group,tf-mod-aws-kms,tf-mod-aws-s3-bucket,tf-mod-aws-ecr,tf-mod-aws-cloudwatch-alarm(sibling modules)
🧡 "Infrastructure as Code should be standardized, consistent, and secure."