Last reviewed: May 2026
Build the AWS services on the MLA-C01 exam with plain Terraform — one block at a time, each tied back to an exam domain. The same code works on OpenTofu.
By the end of this lab you'll have provisioned, with plain Terraform, the control plane of a SageMaker-based ML platform — an S3 bucket for training data and model artifacts, a least-privilege IAM role SageMaker assumes, a SageMaker model package group (the registry that catalogs model versions), and an EventBridge rule that reacts to model-approval events so promotion-to-production can be automated.
We deliberately avoid provisioning the data plane — training jobs, endpoints, notebook instances, Studio domains — because they all bill while idle and would turn a lab into a billing trap. Once the control plane is in place, the data plane plugs in cleanly: you point a training job at the role from Step 3, it writes its artifact to the bucket from Step 2, and registers a new version into the model package group from Step 4.
Every resource is plain Terraform. Drop the snippets into a single main.tf, run terraform init, then terraform apply step-by-step.
>= 1.5 or OpenTofu >= 1.6.us-east-1 (SageMaker is available in most regions; us-east-1 has the broadest feature coverage including SageMaker Pipelines).Everything in this lab costs nothing while idle:
Everything we deliberately did not provision is where SageMaker spending lives:
Once you wire training or inference into this lab, watch the bill. The control plane this lab provisions is the safe-to-leave-running part.
Standard opener. SageMaker is regional, and most newer SageMaker features (Pipelines, Model Cards, JumpStart) land first in us-east-1 and us-west-2 — pick one of those for fewest surprises.
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.60"
}
}
}
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
Project = "certlabpro-mla-c01"
ManagedBy = "terraform"
}
}
}Every SageMaker training job reads input data from S3 and writes its model artifact back to S3 — that's the storage interface SageMaker exposes. We create one bucket with a folder convention (training-data/, model-artifacts/) that mirrors the MLA-C01 reference architecture.
Encryption at rest is non-negotiable for any ML data store — the exam tests this explicitly under Security, Compliance, and Governance for ML Solutions. We use AES256 here for simplicity; in production, a customer-managed KMS key gives you finer-grained audit trail.
resource "aws_s3_bucket" "ml" {
bucket_prefix = "certlabpro-mla-c01-"
}
resource "aws_s3_bucket_public_access_block" "ml" {
bucket = aws_s3_bucket.ml.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_server_side_encryption_configuration" "ml" {
bucket = aws_s3_bucket.ml.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_versioning" "ml" {
bucket = aws_s3_bucket.ml.id
versioning_configuration {
status = "Enabled"
}
}SageMaker training jobs, endpoints, and Pipelines all execute under an IAM role. We create one role with a trust policy that names sagemaker.amazonaws.com and scope its permissions to exactly what an ML workload needs: read from the training-data prefix, write to the model-artifact prefix, and emit logs to CloudWatch. The exam tests this least-privilege shape over and over.
For the lab we attach the AWS-managed AmazonSageMakerFullAccess policy on top, because covering every SageMaker action by hand is hundreds of lines and not what MLA-C01 is testing. In production you'd narrow this — that's a separate hardening exercise.
resource "aws_iam_role" "sagemaker_exec" {
name = "certlabpro-mla-c01-sagemaker-exec"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "sagemaker.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "sagemaker_full" {
role = aws_iam_role.sagemaker_exec.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSageMakerFullAccess"
}
resource "aws_iam_role_policy" "sagemaker_lab_bucket" {
name = "lab-bucket-read-write"
role = aws_iam_role.sagemaker_exec.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["s3:GetObject", "s3:ListBucket"]
Resource = [aws_s3_bucket.ml.arn, "${aws_s3_bucket.ml.arn}/training-data/*"]
},
{
Effect = "Allow"
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.ml.arn}/model-artifacts/*"
},
]
})
}A Model Package Group is SageMaker's model registry — a named container for multiple versions of the same model, each with its own status (PendingManualApproval, Approved, Rejected). Every MLOps story MLA-C01 tests goes through this object: training pipeline registers a new version → MLOps engineer reviews → status flipped to Approved → CI/CD pipeline picks up the change and rolls out the new model to the endpoint.
The group itself costs nothing — it's metadata. Once it exists, training jobs and Pipelines can call RegisterModel against it and SageMaker tracks the lineage automatically. We're laying the foundation that the EventBridge rule in Step 5 will react to.
resource "aws_sagemaker_model_package_group" "main" {
model_package_group_name = "certlabpro-mla-c01-models"
model_package_group_description = "Lab-only model registry for the MLA-C01 walkthrough."
}Every SageMaker model registry action emits an EventBridge event — registration, status changes, deletions. The MLA-C01 Deployment and Orchestration domain tests this exact pattern: model approval should kick off the next-step automation (deploy to staging, run integration tests, page on-call) without a human poking buttons in the console.
We create an EventBridge rule that matches Approved status transitions for our specific model package group, and target an SNS topic as the placeholder downstream — in production you'd point at a Step Functions state machine, a Lambda, or a CodePipeline pipeline. The structure stays the same; only the target ARN changes.
With this final piece in place, the control-plane chain is complete: a training job (data-plane, not provisioned here) writes its artifact to S3 from Step 2, assumes the role from Step 3 to do it, registers a new version into the model package group from Step 4, and any approval triggers the downstream automation via the EventBridge rule from Step 5. Plug a training job in and the loop runs itself.
resource "aws_sns_topic" "model_approvals" {
name = "certlabpro-mla-c01-model-approvals"
}
resource "aws_cloudwatch_event_rule" "model_approved" {
name = "certlabpro-mla-c01-model-approved"
description = "Fires when a model version in our registry is approved."
event_pattern = jsonencode({
source = ["aws.sagemaker"]
"detail-type" = ["SageMaker Model Package State Change"]
detail = {
ModelPackageGroupName = [aws_sagemaker_model_package_group.main.model_package_group_name]
ModelApprovalStatus = ["Approved"]
}
})
}
resource "aws_cloudwatch_event_target" "notify_sns" {
rule = aws_cloudwatch_event_rule.model_approved.name
arn = aws_sns_topic.model_approvals.arn
}
resource "aws_sns_topic_policy" "allow_events" {
arn = aws_sns_topic.model_approvals.arn
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "events.amazonaws.com" }
Action = "sns:Publish"
Resource = aws_sns_topic.model_approvals.arn
}]
})
}terraform destroy tears down everything in this lab cleanly. Notes:
force_destroy = false (the safe default) — if you've uploaded any training data to it, empty it via the console (or aws s3 rm s3://<bucket> --recursive) before destroying.aws sagemaker delete-model-package), then destroy.MLA-C01 covers many SageMaker surfaces this lab doesn't provision — Training Jobs (compute that bills per second), Endpoints (instances that bill 24/7), Studio Domains (multi-user IDE), Notebook Instances (single-user IDE that easily bills 24/7 if forgotten), JumpStart (one-click foundation-model deployments), Feature Store, Model Monitor, Clarify (bias detection), Edge Manager, Ground Truth (labeling), Pipelines (the orchestration layer above all this), and Autopilot.
We stick to the control plane — the parts you can leave running without billing surprises — because that's the foundation every other MLA-C01 pattern attaches to. A training job slot in your account points at the role and bucket this lab created. An endpoint deployment reads the model artifact this registry references. Pipelines orchestrate registration into the group this lab built.
For hands-on practice with the data-plane pieces, the right move is a follow-up lab that adds one of them at a time (a single training job that runs once and stops; a single endpoint behind an explicit budget alarm) — never several in one go, because the costs are real and cumulative. Conceptual coverage of the rest lives on the Browse, Playbook, and Editorial sections of this cert page.