Skip to content

microsoftexpert/tf-mod-aws-sagemaker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🟧 AWS SageMaker AI Terraform Module

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.

Terraform aws module type resources


🧩 Overview

  • 🧠 Provisions an aws_sagemaker_domain keystone plus two independently-gated sub-features: an interactive Studio environment (aws_sagemaker_user_profile) and production hosting (aws_sagemaker_modelaws_sagemaker_endpoint_configurationaws_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 mandatory iam:PassRole grant impossible to miss.
  • 🧩 Studio and hosting are independently gateduser_profiles gates Studio; models / endpoint_configurations / endpoints gate hosting; populate either, both, or neither beyond the always-created keystone domain.
  • 🎛️ Studio surface-area reductionhidden_instance_types / hidden_app_types restrict which apps and instance sizes users may launch.
  • 🏷️ Universal taggingvar.tags flows to every taggable resource (all six expose tags); tags_all surfaced 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.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

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!


🗺️ Where this fits in the family

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
Loading

🧬 What this module builds

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
Loading
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 this and is always created; a hosting-only caller still provisions an (empty) Studio domain in v1.


✅ Provider / Versions

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.


🔑 Required IAM Permissions

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

📋 AWS Prerequisites

  • auth_mode is effectively immutable — changing it on a populated domain is not supported by the API. defaults to IAM (aligns with the rest of the library); SSO requires 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 (trusting sagemaker.amazonaws.com) is sufficient.
  • VPC-only mode consumes IP addresses — undersized subnets cause ResourceLimitExceeded at 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 — check sagemaker Service 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-1 constraint — all resources are Regional.

📁 Module Structure

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

⚙️ Quick Start

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 have iam:PassRole on module.sm_exec_role.arn, or every SageMaker Create* call fails with AccessDeniedException. Pin the source with ?ref=v1.0.0, never a branch.


🔌 Cross-Module Contract

Consumes

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)

Emits

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

📚 Example Library

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 = {} }
}

⚠️ PublicInternetOnly gives 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" }
}

📥 Inputs

ℹ️ 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

🧾 Outputs

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


🧠 Architecture Notes

  • ARN / ID formats: idd-xxxxxxxxxxxx (domain id); arnarn:aws:sagemaker:<region>:<account>:domain/<domain-id>. Models/endpoints/feature groups emit their own arns in the output maps.
  • Force-new / immutable fields: auth_mode, vpc_id, and domain_name are force-new on the domain — changing any recreates it (and a populated domain must have all user profiles/apps/spaces deleted first).
  • No root_access toggle exists for Studio domains/user profiles in the v6 schema — RootAccess is a legacy aws_sagemaker_notebook_instance control that AWS explicitly does not support for Studio. The lockdown levers are app_network_access_type = "VpcOnly", hidden_app_types / hidden_instance_types, and a least-privilege execution_role_arn. (The original brief's assumed root_access argument was verified non-existent against the live v6.53 schema.)
  • default_user_settings.execution_role is a required block, not a top-level argument — the module's execution_role_arn variable populates it; user_profiles[*].user_settings.execution_role can override per profile.
  • tagstags_alldefault_tags: every resource accepts tags; tags_all (merge over default_tags, resource tags win) is surfaced from the domain keystone. default_tags is 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 per retention_policy (module defaults to Delete to avoid orphaned cost).
  • Eventual consistency: endpoint create/update is asynchronous (CreatingInService); an immediate destroy on a just-created endpoint can race the AWS state machine.
  • No us-east-1 constraint — SageMaker is Regional; the module inherits the caller's region and declares no region variable.

🧱 Design Principles

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.


🚀 Runbook

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 / apply require valid AWS credentials, a region, and iam:PassRole on the execution role. Always pin the module source with ?ref=v1.0.0, never a branch.


🧪 Testing

  • terraform init -backend=false && terraform validate — schema + reference integrity.
  • terraform fmt -check — formatting.
  • terraform plan against 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 reaches InService and endpoints reach InService before invoking inference.
  • Destroy test in a throwaway account — verify user-profile/app teardown ordering before relying on it in shared environments.

💬 Example Output

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" }

🔍 Troubleshooting

  • AccessDeniedException on any Create*: the Terraform identity lacks iam:PassRole on execution_role_arn. Scope it to that exact ARN — SageMaker cannot be handed the role otherwise.
  • ResourceLimitExceeded at 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_access variable: it does not exist for Studio — use VpcOnly, hidden_*, and a least-privilege role instead (see Architecture Notes).
  • auth_mode change plans a replacement: it is force-new / effectively immutable — plan the auth model up front.
  • Tag drift: default_tags overlap; tags_all merges with resource tags winning. Set the value explicitly in var.tags.
  • Credential-chain failures (NoCredentialProviders / ExpiredToken): no valid credentials resolved. Set AWS_PROFILE, refresh SSO, or confirm the OIDC role assumption in CI.

🔗 Related Docs

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