Train and Deploy a ML Model on SageMaker

Provision ML infrastructure with Terraform and deploy an XGBoost model on SageMaker.

Introduction

30 Second Summary

Every time you get a product recommendation or a fraud alert on your bank account, a machine learning model is making predictions behind the scenes. Deploying those models to production requires real infrastructure and careful orchestration.

In this project, you will provision ML infrastructure using Terraform, train a binary classification model with Amazon SageMaker's built-in XGBoost algorithm, and deploy it to a live endpoint that responds to prediction requests.

What You'll Build

A complete end-to-end ML pipeline on Amazon SageMaker, with all infrastructure provisioned as code using Terraform and mapped directly to AWS MLA-C01 exam domains.

By the end of this project, you'll have:

  • ML infrastructure (IAM role, S3 bucket, SageMaker notebook instance) provisioned entirely via Terraform.
  • A trained XGBoost classification model deployed to a live SageMaker real-time inference endpoint responding to predictions.
  • Hands-on experience with the full SageMaker ML lifecycle (data prep, training, deployment) mapped to MLA-C01 exam domains.
  • Secret Mission: Add SageMaker Model Monitor to detect data drift on your deployed endpoint.

Are there any prerequisites?

You need an AWS account with billing enabled and the AWS CLI installed and configured on your machine. Some experience with Python is helpful but not required.

Install Terraform and Provision ML Infrastructure

Before you can train a machine learning model, you need infrastructure to run it on. In production ML teams, this infrastructure is defined as code so it can be versioned, reviewed, and replicated.

In this step, you will install Terraform and use it to provision the full ML environment on AWS. This covers an IAM role, an S3 bucket, and an Amazon SageMaker notebook instance.

In this step, get ready to:
  • Install Terraform and create a project directory.
  • Write a Terraform configuration that defines your ML infrastructure.
  • Provision the infrastructure and open JupyterLab.
Install Terraform

Terraform is an Infrastructure as Code tool that lets you define cloud resources in configuration files. Instead of clicking through the AWS Console to create each resource, you declare what you need and Terraform builds it for you.

Why Terraform for ML infrastructure?

The MLA-C01 exam (Domain 3) explicitly tests your ability to create and script infrastructure based on existing architecture and requirements. Terraform gives you a repeatable, version-controlled way to spin up ML environments.

If you ever need to rebuild this environment or share it with a teammate, a single terraform apply recreates everything.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Check if Terraform is already installed by running:
terraform --version

✔️ I see version 1.5.0 or higher

Terraform is ready to go. Skip ahead to creating your project directory below.

ⓧ I see an older version

You have Terraform installed but it is below version 1.5.0. This project requires 1.5.0 or higher.

  • Upgrade Terraform by running these commands:
brew update
brew upgrade hashicorp/tap/terraform
  • Verify the upgrade by running:
terraform --version

You should now see a version of 1.5.0 or higher.

ⓧ Command not found

Terraform is not installed yet. Install it using Homebrew (macOS) by running the following commands.

  • Add the HashiCorp tap and install Terraform by running:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
  • Verify the installation by running:
terraform --version

You should see a version number of 1.5.0 or higher printed in the terminal.

Using Windows or Linux?

Visit the official Terraform install page for platform-specific instructions.

Homebrew not found?

If you see a brew: command not found error, you need to install Homebrew first. Visit brew.sh and follow the install instructions, then retry the Terraform install commands above.

Help me install Homebrew and Terraform on my machine.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir sagemaker-ml-project
cd sagemaker-ml-project
  • Confirm the folder exists by running:
ls

You should see an empty directory. This is where your Terraform configuration will live.

Write the Terraform configuration

Your Terraform configuration will declare four resources: an IAM role that gives SageMaker permission to act on your behalf, an S3 bucket for storing training data, and a SageMaker notebook instance where you will run your ML code.

  • Create a new file called main.tf in your sagemaker-ml-project directory by running:
touch main.tf
  • Open main.tf in your preferred text editor, then paste in the following provider and IAM configuration:
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.51"
    }
  }
  required_version = ">= 1.5.0"
}

provider "aws" {
  region = "us-east-1"
}

# IAM Role for SageMaker
resource "aws_iam_role" "sagemaker_role" {
  name = "sagemaker-ml-project-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "sagemaker.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "sagemaker_full_access" {
  role       = aws_iam_role.sagemaker_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSageMakerFullAccess"
}

What does this code do?

  • The terraform block pins the AWS provider to version ~> 6.51 and requires Terraform >= 1.5.0.
  • The provider block tells Terraform to create resources in the us-east-1 region.
  • The aws_iam_role resource creates a role that SageMaker can assume to access AWS services on your behalf.
  • The aws_iam_role_policy_attachment attaches the AmazonSageMakerFullAccess managed policy so SageMaker has the permissions it needs.
  • Below the IAM resources, paste the S3 bucket, notebook instance, and output definitions:
# S3 Bucket for training data and model artifacts
resource "aws_s3_bucket" "ml_bucket" {
  bucket_prefix = "sagemaker-ml-project-"
  force_destroy = true
}

# SageMaker Notebook Instance
resource "aws_sagemaker_notebook_instance" "notebook" {
  name                = "ml-project-notebook"
  role_arn            = aws_iam_role.sagemaker_role.arn
  instance_type       = "ml.t3.medium"
  platform_identifier = "notebook-al2023-v1"
  volume_size         = 5

  tags = {
    Project = "sagemaker-ml-project"
  }
}

# Outputs
output "notebook_url" {
  value = aws_sagemaker_notebook_instance.notebook.url
}

output "s3_bucket_name" {
  value = aws_s3_bucket.ml_bucket.id
}

output "sagemaker_role_arn" {
  value = aws_iam_role.sagemaker_role.arn
}

What does this code do?

  • The aws_s3_bucket creates a bucket with a unique prefix for storing training data and model artifacts. The force_destroy = true flag lets Terraform delete the bucket even if it contains files (useful for cleanup later).
  • The aws_sagemaker_notebook_instance provisions a ml.t3.medium compute instance running JupyterLab on Amazon Linux 2023.
  • The output blocks print the notebook URL, bucket name, and role ARN after provisioning completes.
  • Save main.tf.

✔️ Awesome, I've got everything!

Great. Double check that you have saved main.tf with all the code above.

ⓧ I'd like to double check the full code

Here is the complete main.tf file for comparison:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.51"
    }
  }
  required_version = ">= 1.5.0"
}

provider "aws" {
  region = "us-east-1"
}

# IAM Role for SageMaker
resource "aws_iam_role" "sagemaker_role" {
  name = "sagemaker-ml-project-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "sagemaker.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "sagemaker_full_access" {
  role       = aws_iam_role.sagemaker_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSageMakerFullAccess"
}

# S3 Bucket for training data and model artifacts
resource "aws_s3_bucket" "ml_bucket" {
  bucket_prefix = "sagemaker-ml-project-"
  force_destroy = true
}

# SageMaker Notebook Instance
resource "aws_sagemaker_notebook_instance" "notebook" {
  name                = "ml-project-notebook"
  role_arn            = aws_iam_role.sagemaker_role.arn
  instance_type       = "ml.t3.medium"
  platform_identifier = "notebook-al2023-v1"
  volume_size         = 5

  tags = {
    Project = "sagemaker-ml-project"
  }
}

# Outputs
output "notebook_url" {
  value = aws_sagemaker_notebook_instance.notebook.url
}

output "s3_bucket_name" {
  value = aws_s3_bucket.ml_bucket.id
}

output "sagemaker_role_arn" {
  value = aws_iam_role.sagemaker_role.arn
}
Initialize and apply your Terraform configuration

With main.tf saved, you can now ask Terraform to create these resources on AWS. This happens in three stages: initialize, plan, then apply.

  • Initialize Terraform (download the AWS provider plugin) by running:
terraform init

What does terraform init do?

This command downloads the AWS provider plugin (version ~> 6.51) and sets up the .terraform directory where Terraform stores its working state. You only need to run it once per project (or after changing providers).

You should see a message ending with Terraform has been successfully initialized! in your terminal.

  • Preview what Terraform will create by running:
terraform plan

You should see a plan listing 4 resources to add: the IAM role, policy attachment, S3 bucket, and notebook instance.

  • Apply the configuration to provision your infrastructure by running:
terraform apply

What will Terraform ask me?

Terraform will show you the plan one more time and ask you to confirm. Type yes and press Enter to proceed. Provisioning takes 3-5 minutes while AWS creates the notebook instance.

When the apply completes, Terraform prints the three outputs: notebook_url, s3_bucket_name, and sagemaker_role_arn.

Seeing an access denied error?

Make sure your AWS CLI credentials have permissions to create IAM roles, S3 buckets, and SageMaker resources. If you are using a restricted IAM user, ask your admin for the AdministratorAccess policy (or at minimum, SageMaker, S3, and IAM permissions).

Help me troubleshoot a Terraform access denied error when creating SageMaker resources.

  • Open the SageMaker Console by navigating to https://console.aws.amazon.com/sagemaker/ in your browser.
  • In the left sidebar, click Notebooks, then click Notebook instances.
  • Wait until the ml-project-notebook instance shows a status of InService.
  • Click Open JupyterLab next to your notebook instance.

JupyterLab opens in a new browser tab. This is the environment where you will prepare data, train your model, and deploy it.

Notebook stuck in Pending?

It can take up to 5 minutes for the notebook instance to move from Pending to InService. Refresh the console page periodically. If it stays in Pending for more than 10 minutes, check the Failure reason column for details.

Help me debug why my SageMaker notebook instance is stuck in Pending.

Your ML infrastructure is ready. Next up, you will load a dataset, format it for SageMaker, and upload it to your S3 bucket for training.

Prepare and Upload Training Data to S3

In the previous step, you provisioned an entire ML environment using Terraform. Your notebook instance is live and ready for data science work.

Before you can train a model, you need training data. In this step, you will download a public dataset, transform it into the format that SageMaker's built-in algorithms expect, and upload it to S3 so SageMaker can access it during training.

In this step, get ready to:
  • Create a Jupyter notebook and configure the SageMaker session.
  • Download and encode a classification dataset for the XGBoost algorithm.
  • Upload the formatted training data to S3.
Create a notebook and load the dataset

Every ML workflow starts with a notebook to organize your code into runnable cells. You will create a new notebook in JupyterLab and use it for the rest of the project.

  • In JupyterLab, click the + button in the file browser on the left to open the Launcher.
  • Under Notebook, click Python 3 to create a new notebook.
  • Right-click the new file (Untitled.ipynb) in the file browser and select Rename.
  • Type ml_pipeline.ipynb and press Enter.

How do I run a notebook cell?

Click inside a cell and press Shift+Enter to run it. The output appears directly below the cell, and a new empty cell is created automatically.

  • Set up your SageMaker session by pasting this code into the first cell and pressing Shift+Enter to run it:
# Cell 1: Imports and Setup
import sagemaker
import boto3
import pandas as pd
import numpy as np
from sagemaker import image_uris
from sagemaker.inputs import TrainingInput

region = boto3.Session().region_name
session = sagemaker.Session()
bucket = session.default_bucket()
prefix = "sagemaker-xgboost-adult-income"
role = sagemaker.get_execution_role()

print(f"Region: {region}")
print(f"Bucket: {bucket}")
print(f"Role: {role}")

What does this code do?

  • `boto3.Session().region_name` detects your AWS region (should be us-east-1 since that is what Terraform provisioned).
  • `sagemaker.Session()` creates a session that manages interactions with SageMaker APIs and S3.
  • `session.default_bucket()` returns the default SageMaker S3 bucket for your account, creating it if it does not exist yet.
  • `sagemaker.get_execution_role()` retrieves the IAM role ARN attached to this notebook instance (the one Terraform created).
  • Confirm the output prints your region, a bucket name starting with sagemaker-, and a role ARN.

Seeing a permissions error?

  • Make sure the notebook instance is using the IAM role created by Terraform (sagemaker-ml-project-role). Check in the SageMaker Console under Notebook instances that the role ARN matches your Terraform output.
  • If get_execution_role() fails, confirm you opened JupyterLab from the SageMaker Console (not a generic Jupyter install).

help me fix a permissions error in my SageMaker notebook

Now you will download the UCI Adult Income dataset. This is a classic binary classification dataset that predicts whether a person earns more than $50K per year.

  • Download and load the dataset by pasting this code into the next cell and pressing Shift+Enter:
# Cell 2: Download and Prepare the Dataset
columns = [
    "age", "workclass", "fnlwgt", "education", "education_num",
    "marital_status", "occupation", "relationship", "race", "sex",
    "capital_gain", "capital_loss", "hours_per_week", "native_country", "income"
]

train_url = "https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data"
test_url = "https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.test"

train_df = pd.read_csv(train_url, names=columns, sep=r",\s*", engine="python", na_values="?")
test_df = pd.read_csv(test_url, names=columns, sep=r",\s*", engine="python", na_values="?", skiprows=1)

print(f"Training set: {train_df.shape}")
print(f"Test set: {test_df.shape}")

What does this code do?

  • The dataset is hosted publicly at the UCI Machine Learning Repository. pandas downloads and loads the CSV files directly into DataFrames.
  • `names=columns` assigns column headers since the raw files have none.
  • `sep=r",\s*"` handles the inconsistent spacing around commas in this dataset.
  • `na_values="?"` marks missing values so pandas can handle them properly.
  • Confirm the output shows the training set shape as approximately (32561, 15) and the test set as approximately (16281, 15).

Download taking too long or failing?

  • The UCI archive server can be slow. Wait up to 30 seconds for the download to complete.
  • If you see a connection error, check that the notebook instance has internet access. The default VPC configuration created by SageMaker allows outbound traffic.

help me troubleshoot a network error downloading data in SageMaker

Encode features and format for XGBoost

SageMaker's built-in XGBoost algorithm has a specific requirement for CSV input: the target label must be in the first column, and there must be no header row.

You also need to convert text-based (categorical) columns into numbers, since XGBoost only accepts numeric features.

  • Encode the features and reformat the data by pasting this code into the next cell and pressing Shift+Enter:
# Cell 3: Encode Features and Format for XGBoost
# Binary encode the target
train_df["income"] = (train_df["income"] == ">50K").astype(int)
test_df["income"] = (test_df["income"].str.strip(".") == ">50K").astype(int)

# One-hot encode categorical columns
categorical_cols = train_df.select_dtypes(include=["object"]).columns.tolist()
train_encoded = pd.get_dummies(train_df, columns=categorical_cols)
test_encoded = pd.get_dummies(test_df, columns=categorical_cols)

# Align columns between train and test
test_encoded = test_encoded.reindex(columns=train_encoded.columns, fill_value=0)

# Move label to first column (SageMaker XGBoost requirement for CSV)
label_col = "income"
cols = [label_col] + [c for c in train_encoded.columns if c != label_col]
train_encoded = train_encoded[cols]
test_encoded = test_encoded[cols]

print(f"Encoded training set: {train_encoded.shape}")
print(f"Encoded test set: {test_encoded.shape}")

What does this code do?

  • The target column income is converted from text (">50K" / "<=50K") to binary (1 / 0). This is what the model will learn to predict.
  • `pd.get_dummies()` one-hot encodes all categorical columns (like workclass and occupation) into numeric indicator columns.
  • `test_encoded.reindex()` aligns the test set columns with the training set so both have the same feature structure.
  • The label column is moved to position 0 because SageMaker's built-in XGBoost expects the label as the first column in CSV mode.
  • Confirm the output shows both encoded sets now have around 108 columns (the original 15 columns expanded through one-hot encoding).

Seeing a KeyError for the income column?

  • Make sure you ran Cell 2 first. Cell 3 depends on train_df and test_df existing in memory.
  • If you restarted the kernel, re-run all previous cells in order from Cell 1.

help me fix a KeyError in my SageMaker notebook

Upload training data to S3

With the data formatted, you need to upload it to S3 so that SageMaker can access it when launching a training job on separate compute instances.

  • Save the files locally and upload them to S3 by pasting this code into the next cell and pressing Shift+Enter:
# Cell 4: Upload Data to S3
train_encoded.to_csv("train.csv", index=False, header=False)
test_encoded.to_csv("validation.csv", index=False, header=False)

train_s3_path = session.upload_data("train.csv", bucket=bucket, key_prefix=f"{prefix}/train")
validation_s3_path = session.upload_data("validation.csv", bucket=bucket, key_prefix=f"{prefix}/validation")

print(f"Train data uploaded to: {train_s3_path}")
print(f"Validation data uploaded to: {validation_s3_path}")

What does this code do?

  • `to_csv()` saves the DataFrames as CSV files without headers or row indices (matching XGBoost's expected format).
  • `session.upload_data()` uploads each file to the SageMaker default bucket under a structured prefix (sagemaker-xgboost-adult-income/train/ and sagemaker-xgboost-adult-income/validation/).
  • The returned S3 paths are stored in train_s3_path and validation_s3_path for use in the training step.
  • Confirm the output shows two S3 paths starting with s3://sagemaker- followed by your bucket name and the prefix paths.

Upload failing with an access denied error?

  • The AmazonSageMakerFullAccess policy grants S3 access to buckets with "sagemaker" in the name. If you are using a custom bucket name without "sagemaker" in it, you will need additional S3 permissions.
  • Verify the role ARN printed in Cell 1 matches the output from terraform output sagemaker_role_arn.

help me fix an S3 access denied error in SageMaker

✔️ Awesome, I've got everything!

Great. Double check you have all four cells saved in your ml_pipeline.ipynb notebook.

ⓧ I'd like to double check the full code

Here is the full notebook code through Cell 4. Compare it against your cells:

# Cell 1: Imports and Setup
import sagemaker
import boto3
import pandas as pd
import numpy as np
from sagemaker import image_uris
from sagemaker.inputs import TrainingInput

region = boto3.Session().region_name
session = sagemaker.Session()
bucket = session.default_bucket()
prefix = "sagemaker-xgboost-adult-income"
role = sagemaker.get_execution_role()

print(f"Region: {region}")
print(f"Bucket: {bucket}")
print(f"Role: {role}")
# Cell 2: Download and Prepare the Dataset
columns = [
    "age", "workclass", "fnlwgt", "education", "education_num",
    "marital_status", "occupation", "relationship", "race", "sex",
    "capital_gain", "capital_loss", "hours_per_week", "native_country", "income"
]

train_url = "https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data"
test_url = "https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.test"

train_df = pd.read_csv(train_url, names=columns, sep=r",\s*", engine="python", na_values="?")
test_df = pd.read_csv(test_url, names=columns, sep=r",\s*", engine="python", na_values="?", skiprows=1)

print(f"Training set: {train_df.shape}")
print(f"Test set: {test_df.shape}")
# Cell 3: Encode Features and Format for XGBoost
# Binary encode the target
train_df["income"] = (train_df["income"] == ">50K").astype(int)
test_df["income"] = (test_df["income"].str.strip(".") == ">50K").astype(int)

# One-hot encode categorical columns
categorical_cols = train_df.select_dtypes(include=["object"]).columns.tolist()
train_encoded = pd.get_dummies(train_df, columns=categorical_cols)
test_encoded = pd.get_dummies(test_df, columns=categorical_cols)

# Align columns between train and test
test_encoded = test_encoded.reindex(columns=train_encoded.columns, fill_value=0)

# Move label to first column (SageMaker XGBoost requirement for CSV)
label_col = "income"
cols = [label_col] + [c for c in train_encoded.columns if c != label_col]
train_encoded = train_encoded[cols]
test_encoded = test_encoded[cols]

print(f"Encoded training set: {train_encoded.shape}")
print(f"Encoded test set: {test_encoded.shape}")
# Cell 4: Upload Data to S3
train_encoded.to_csv("train.csv", index=False, header=False)
test_encoded.to_csv("validation.csv", index=False, header=False)

train_s3_path = session.upload_data("train.csv", bucket=bucket, key_prefix=f"{prefix}/train")
validation_s3_path = session.upload_data("validation.csv", bucket=bucket, key_prefix=f"{prefix}/validation")

print(f"Train data uploaded to: {train_s3_path}")
print(f"Validation data uploaded to: {validation_s3_path}")

Your training data is staged in S3 and ready for SageMaker to consume. Next up, you will configure and launch a training job using SageMaker's built-in XGBoost algorithm to build a classification model from this data.

Train a Model with SageMaker's Built-in XGBoost

Your data is formatted and sitting in S3, ready to be consumed by a training algorithm.

Now you need to actually train a model on that data. SageMaker provides built-in algorithm containers that are pre-optimized for distributed training on AWS infrastructure. You will configure an Estimator (SageMaker's abstraction for a training job), set hyperparameters that control how the model learns, and launch the training job.

In this step, get ready to:
  • Configure a SageMaker Estimator with the XGBoost built-in algorithm container.
  • Set hyperparameters that control the training process.
  • Launch and monitor a training job that produces a deployable model artifact.
Retrieve the XGBoost container and create an Estimator

SageMaker's built-in algorithms ship as pre-built Docker container images stored in Amazon ECR. Instead of writing training code from scratch, you point an Estimator at one of these containers and let SageMaker handle the rest.

  • In your ml_pipeline.ipynb notebook, create a new cell below Cell 4 and paste the following code:
# Cell 5: Configure and Launch Training Job
xgboost_container = image_uris.retrieve("xgboost", region, "1.7-1")
print(f"XGBoost container: {xgboost_container}")

estimator = sagemaker.estimator.Estimator(
    image_uri=xgboost_container,
    role=role,
    instance_count=1,
    instance_type="ml.m5.xlarge",
    volume_size=5,
    output_path=f"s3://{bucket}/{prefix}/output",
    sagemaker_session=session,
)

estimator.set_hyperparameters(
    objective="binary:logistic",
    num_round=100,
    max_depth=5,
    eta=0.2,
    gamma=4,
    min_child_weight=6,
    subsample=0.7,
    eval_metric="auc",
)

What does this code do?

image_uris.retrieve("xgboost", region, "1.7-1") fetches the ECR URI for the XGBoost 1.7-1 container image in your region. This is the pre-built algorithm container that SageMaker will spin up on a training instance.

The Estimator wraps the training job configuration: which container to use, what IAM role to assume, what instance type to train on, and where to store the output model artifact in S3.

The hyperparameters control how XGBoost learns. objective="binary:logistic" tells XGBoost this is a binary classification problem. num_round=100 sets 100 boosting rounds. eval_metric="auc" measures model quality using Area Under the ROC Curve.

Configure training inputs and launch the job

The Estimator knows how to train, but it does not yet know where your data lives. You need to create TrainingInput objects that point SageMaker to the correct S3 paths.

  • Add the following code to the same cell (Cell 5), directly below the hyperparameters block:
train_input = TrainingInput(train_s3_path, content_type="csv")
validation_input = TrainingInput(validation_s3_path, content_type="csv")

estimator.fit({"train": train_input, "validation": validation_input}, wait=True)

What does this code do?

TrainingInput wraps an S3 path with metadata (like content_type="csv") so SageMaker knows how to parse the data when it downloads it to the training instance.

estimator.fit() launches the training job. The dictionary keys "train" and "validation" map to SageMaker's input data channels. Setting wait=True streams the training logs directly into your notebook cell output so you can watch progress in real time.

  • Run Cell 5 by pressing Shift+Enter.

What should I see?

The cell output will show the training job being provisioned and then stream logs from the XGBoost container. You will see hyperparameter confirmations, followed by per-round metrics showing the auc score improving across iterations.

Training takes approximately 3-5 minutes. When it finishes, SageMaker prints a final summary with the billable seconds and the job status.

Training job failed?

Check that your S3 paths (train_s3_path and validation_s3_path) were set correctly in Cell 4. Re-run Cell 4 if needed.

Verify your IAM role has the AmazonSageMakerFullAccess policy attached. Terraform provisioned this, but you can confirm in the IAM console.

Help me debug my failed training job.

Monitor training and locate model artifacts

Once training completes, SageMaker packages the trained model into a compressed archive and uploads it to your S3 output path.

  • Scroll through the cell output and confirm you see a final line indicating the training job completed successfully.
  • Note the model artifact location. SageMaker stores it at a path like s3://{bucket}/sagemaker-xgboost-adult-income/output/{training-job-name}/output/model.tar.gz.

What is model.tar.gz?

SageMaker compresses everything your algorithm writes to /opt/ml/model inside the training container into a single model.tar.gz file. This archive contains the serialized XGBoost model that you will deploy for real-time inference in the next step.

Why does this matter for the MLA-C01 exam?

This step maps directly to Domain 2 (ML Model Development, 26% of the exam). The exam tests your understanding of built-in algorithm selection, hyperparameter configuration, and how SageMaker orchestrates training jobs on managed infrastructure.

✔️ Awesome, I've got everything!

Great. Double check you see training metrics in your cell output and the job completed without errors.

ⓧ I'd like to double check the full code

Here is the complete Cell 5 for comparison:

# Cell 5: Configure and Launch Training Job
xgboost_container = image_uris.retrieve("xgboost", region, "1.7-1")
print(f"XGBoost container: {xgboost_container}")

estimator = sagemaker.estimator.Estimator(
    image_uri=xgboost_container,
    role=role,
    instance_count=1,
    instance_type="ml.m5.xlarge",
    volume_size=5,
    output_path=f"s3://{bucket}/{prefix}/output",
    sagemaker_session=session,
)

estimator.set_hyperparameters(
    objective="binary:logistic",
    num_round=100,
    max_depth=5,
    eta=0.2,
    gamma=4,
    min_child_weight=6,
    subsample=0.7,
    eval_metric="auc",
)

train_input = TrainingInput(train_s3_path, content_type="csv")
validation_input = TrainingInput(validation_s3_path, content_type="csv")

estimator.fit({"train": train_input, "validation": validation_input}, wait=True)

Your model is trained and stored in S3. Next up, you will deploy it to a real-time endpoint and send live prediction requests.

Deploy the Model to a Real-Time Endpoint

Your model is trained and sitting as a model.tar.gz artifact in S3. That artifact is useful for auditing, but it cannot answer questions on its own.

To make predictions on new data, you need to deploy the model behind an API that accepts input and returns a prediction in real time. SageMaker calls this a real-time inference endpoint. In this step, you will deploy your XGBoost model to a live endpoint, send it a test prediction, and then clean it up to avoid charges.

In this step, get ready to:
  • Deploy your trained model to a real-time SageMaker endpoint.
  • Send a test prediction and verify the model responds with a probability.
  • Review the endpoint in the AWS Console, then delete it to stop billing.
Deploy your trained model to a live endpoint

The estimator object from the previous step holds a reference to the trained model artifact. You can call its deploy() method to launch an endpoint backed by a dedicated compute instance.

  • In your notebook, create a new cell below Cell 5 and paste the following code to deploy the model:
# Cell 6: Deploy to Real-Time Endpoint
from sagemaker.serializers import CSVSerializer

predictor = estimator.deploy(
    initial_instance_count=1,
    instance_type="ml.m5.large",
    serializer=CSVSerializer(),
)

print(f"Endpoint name: {predictor.endpoint_name}")

What does this code do?

CSVSerializer tells the predictor to format your input as CSV before sending it to the endpoint. SageMaker's XGBoost container expects CSV input by default.

estimator.deploy() provisions an ml.m5.large instance, loads your trained model onto it, and exposes an HTTPS endpoint you can call for predictions.

  • Run the cell. Deployment takes 5-10 minutes while SageMaker provisions the instance and loads your model.
  • Once the cell finishes, you should see the endpoint name printed below the cell (a string like sagemaker-xgboost-2026-06-20-...).

Why does deployment take so long?

SageMaker spins up a fresh EC2 instance, downloads your model artifact from S3, loads it into memory, and starts a web server. This is a one-time cost. Once the endpoint is InService, predictions return in milliseconds.

Cell stuck for more than 15 minutes?

Check the SageMaker Console for the endpoint status. If it shows Failed, look at the failure reason. The most common cause is the IAM role missing permissions to pull the model from S3.

help me debug my SageMaker endpoint deployment failure

Send a test prediction to the endpoint

With the endpoint running, you can send it a row of feature data and get a prediction probability back. You will use a row from the validation set so you can compare the model's prediction against the known label.

  • Create a new cell below Cell 6 and paste the following code to invoke the endpoint:
# Cell 7: Make a Prediction
# Take a sample row from validation set (exclude label column)
sample = test_encoded.iloc[0, 1:].values
sample_csv = ",".join([str(v) for v in sample])

result = predictor.predict(sample_csv)
prediction = float(result.decode("utf-8"))
actual = test_encoded.iloc[0, 0]

print(f"Prediction probability: {prediction:.4f}")
print(f"Predicted class: {int(prediction > 0.5)}")
print(f"Actual label: {actual}")

What does this code do?

  • test_encoded.iloc[0, 1:] grabs the first row from the validation set, skipping column 0 (the label). The model should never see the label as an input feature.
  • predictor.predict(sample_csv) sends the comma-separated feature values to the endpoint over HTTPS and returns the raw response bytes.
  • The result is a probability between 0 and 1. A value above 0.5 means the model predicts this person earns more than $50K per year.
  • Run the cell.
  • You should see three printed lines: a prediction probability (e.g. 0.0547), a predicted class (0 or 1), and the actual label from the dataset.

Seeing a ModelError or validation error?

Make sure the sample_csv string has the correct number of features (all columns except the label). If your training data had 108 feature columns, the prediction input must also have exactly 108 comma-separated values.

help me fix my SageMaker prediction error

Review the endpoint in the Console and clean up

Before deleting the endpoint, take a moment to see how it appears in the AWS Console. This is the same view the MLA-C01 exam references when asking about endpoint management.

  • Switch to the AWS Management Console tab in your browser.
  • Open the SageMaker service (search for SageMaker in the top search bar).
  • In the left navigation pane, click Inference.
  • From the dropdown, click Endpoints.
  • Click your endpoint name in the list to open the details page.
  • Confirm the status shows InService. Note the instance type (ml.m5.large) and creation time displayed on the page.

What is a real-time inference endpoint?

A real-time endpoint is an always-on HTTPS API backed by one or more compute instances. It accepts a request, runs inference on your model, and returns a prediction in milliseconds. This is the deployment pattern the MLA-C01 exam tests under Domain 3 (Deployment and Orchestration).

The endpoint costs approximately $0.12/hour while active. Now that you have confirmed it works, delete it immediately to avoid ongoing charges.

  • Return to your JupyterLab notebook.
  • Create a new cell below Cell 7 and paste the following code to delete the endpoint:
# Cell 8: Cleanup
predictor.delete_endpoint()
print("Endpoint deleted.")

Why delete the endpoint now?

Unlike training jobs (which stop billing when finished), endpoints bill continuously while active. Deleting now stops the meter. You can always redeploy later by calling estimator.deploy() again.

  • Run the cell. You should see Endpoint deleted. printed below.
  • In the SageMaker Console, refresh the Endpoints page. Your endpoint should no longer appear (or show a Deleting status briefly before disappearing).

Endpoint still showing in the Console?

Deletion can take 1-2 minutes to propagate. Refresh the page after a short wait. If the endpoint shows Failed status instead of disappearing, you can manually select it and click Delete from the Actions dropdown.

help me delete a stuck SageMaker endpoint

✔️ Awesome, I've got everything!

Great. Double check you have saved your notebook with all 8 cells.

ⓧ I'd like to double check the full code

Here is the full notebook content through Cell 8:

# Cell 1: Imports and Setup
import sagemaker
import boto3
import pandas as pd
import numpy as np
from sagemaker import image_uris
from sagemaker.inputs import TrainingInput

region = boto3.Session().region_name
session = sagemaker.Session()
bucket = session.default_bucket()
prefix = "sagemaker-xgboost-adult-income"
role = sagemaker.get_execution_role()

print(f"Region: {region}")
print(f"Bucket: {bucket}")
print(f"Role: {role}")

# Cell 2: Download and Prepare the Dataset
columns = [
    "age", "workclass", "fnlwgt", "education", "education_num",
    "marital_status", "occupation", "relationship", "race", "sex",
    "capital_gain", "capital_loss", "hours_per_week", "native_country", "income"
]

train_url = "https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data"
test_url = "https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.test"

train_df = pd.read_csv(train_url, names=columns, sep=r",\s*", engine="python", na_values="?")
test_df = pd.read_csv(test_url, names=columns, sep=r",\s*", engine="python", na_values="?", skiprows=1)

print(f"Training set: {train_df.shape}")
print(f"Test set: {test_df.shape}")

# Cell 3: Encode Features and Format for XGBoost
train_df["income"] = (train_df["income"] == ">50K").astype(int)
test_df["income"] = (test_df["income"].str.strip(".") == ">50K").astype(int)

categorical_cols = train_df.select_dtypes(include=["object"]).columns.tolist()
train_encoded = pd.get_dummies(train_df, columns=categorical_cols)
test_encoded = pd.get_dummies(test_df, columns=categorical_cols)

test_encoded = test_encoded.reindex(columns=train_encoded.columns, fill_value=0)

label_col = "income"
cols = [label_col] + [c for c in train_encoded.columns if c != label_col]
train_encoded = train_encoded[cols]
test_encoded = test_encoded[cols]

print(f"Encoded training set: {train_encoded.shape}")
print(f"Encoded test set: {test_encoded.shape}")

# Cell 4: Upload Data to S3
train_encoded.to_csv("train.csv", index=False, header=False)
test_encoded.to_csv("validation.csv", index=False, header=False)

train_s3_path = session.upload_data("train.csv", bucket=bucket, key_prefix=f"{prefix}/train")
validation_s3_path = session.upload_data("validation.csv", bucket=bucket, key_prefix=f"{prefix}/validation")

print(f"Train data uploaded to: {train_s3_path}")
print(f"Validation data uploaded to: {validation_s3_path}")

# Cell 5: Configure and Launch Training Job
xgboost_container = image_uris.retrieve("xgboost", region, "1.7-1")
print(f"XGBoost container: {xgboost_container}")

estimator = sagemaker.estimator.Estimator(
    image_uri=xgboost_container,
    role=role,
    instance_count=1,
    instance_type="ml.m5.xlarge",
    volume_size=5,
    output_path=f"s3://{bucket}/{prefix}/output",
    sagemaker_session=session,
)

estimator.set_hyperparameters(
    objective="binary:logistic",
    num_round=100,
    max_depth=5,
    eta=0.2,
    gamma=4,
    min_child_weight=6,
    subsample=0.7,
    eval_metric="auc",
)

train_input = TrainingInput(train_s3_path, content_type="csv")
validation_input = TrainingInput(validation_s3_path, content_type="csv")

estimator.fit({"train": train_input, "validation": validation_input}, wait=True)

# Cell 6: Deploy to Real-Time Endpoint
from sagemaker.serializers import CSVSerializer

predictor = estimator.deploy(
    initial_instance_count=1,
    instance_type="ml.m5.large",
    serializer=CSVSerializer(),
)

print(f"Endpoint name: {predictor.endpoint_name}")

# Cell 7: Make a Prediction
# Take a sample row from validation set (exclude label column)
sample = test_encoded.iloc[0, 1:].values
sample_csv = ",".join([str(v) for v in sample])

result = predictor.predict(sample_csv)
prediction = float(result.decode("utf-8"))
actual = test_encoded.iloc[0, 0]

print(f"Prediction probability: {prediction:.4f}")
print(f"Predicted class: {int(prediction > 0.5)}")
print(f"Actual label: {actual}")

# Cell 8: Cleanup
predictor.delete_endpoint()
print("Endpoint deleted.")

Your model has been deployed, tested, and cleaned up. You have completed the full ML lifecycle on SageMaker: from provisioning infrastructure with Terraform, to preparing data, training a model, and serving live predictions. If you want an extra challenge, head to the Secret Mission to add Model Monitor for data drift detection.

Secret mission

Add Model Monitoring

Your model is deployed and serving predictions. But how do you know if the data hitting your endpoint starts drifting away from what the model was trained on? In this secret mission, you will add SageMaker Model Monitor to automatically detect data drift on your production endpoint.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. The notebook instance costs ~$0.05/hour while running, but the endpoint (the most expensive resource) was already deleted in Cell 8.

Cost warning

Your SageMaker notebook instance is still running and costs ~$0.05/hour (~$1.20/day). If you leave it running overnight, it will accrue charges. Delete or stop it when you are done experimenting.

Resources you used:

  • SageMaker notebook instance (ml-project-notebook) running on ml.t3.medium.
  • IAM role (sagemaker-ml-project-role).
  • S3 bucket created by Terraform (prefix sagemaker-ml-project-).
  • Training data and model artifacts in the SageMaker default S3 bucket (under the sagemaker-xgboost-adult-income/ prefix).
  • Local project directory (sagemaker-ml-project) on your Desktop.

Keep everything running

No action needed. Choose this if you are still actively experimenting with your notebook or plan to deploy more models today.

  • Your notebook instance continues running at ~$0.05/hour.
  • The endpoint was already deleted in Cell 8 of your notebook.
  • S3 storage costs are negligible (a few cents per month).
  • Your Terraform state file in sagemaker-ml-project lets you manage these resources later.

Pause - I'll come back to this later

Stop the notebook instance to eliminate compute charges while keeping all your data and Terraform state intact.

  • Open the SageMaker Console at https://console.aws.amazon.com/sagemaker/.
  • Under Notebook, choose Notebook instances.
  • Select ml-project-notebook, choose Actions, then choose Stop.
  • Wait for the Status to change to Stopped.

When you are ready to return, select the notebook and choose Actions > Start. Your files and work will still be on the attached storage volume.

Delete - I don't want to use this again

Remove all project resources and start fresh. This is the recommended path once you have finished the project.

Delete training data from the SageMaker default bucket

The notebook uploaded data to the SageMaker SDK's default bucket (separate from the Terraform-created bucket). Remove the project prefix by running this command in your local terminal:

aws s3 rm s3://sagemaker-us-east-1-[[AWS_ACCOUNT_ID="your-aws-account-id"]]/sagemaker-xgboost-adult-income/ --recursive

What is the default bucket name?

The SageMaker SDK creates a bucket named sagemaker-<region>-<account-id>. You can find the exact name by checking the output of Cell 1 in your notebook (the line that printed your bucket name).

Destroy Terraform-managed infrastructure
  • Navigate to your project directory by running:
  • This deletes the notebook instance, the IAM role, and the Terraform-created S3 bucket in one command.
cd ~/Desktop/sagemaker-ml-project
  • Destroy all Terraform-managed resources by running:
terraform destroy
  • When prompted, type yes and press Enter to confirm.
  • Wait for the output to show Destroy complete! confirming all resources are removed.
Remove local project files
  • Delete the project directory by running:
rm -rf ~/Desktop/sagemaker-ml-project

That's a Wrap!

That's a Wrap!

Nice work! You just built a complete end-to-end ML pipeline on Amazon SageMaker, from provisioning infrastructure with Terraform all the way through to deploying a live prediction endpoint.

You've learned how to:

  • Provision ML infrastructure (IAM role, S3 bucket, SageMaker notebook instance) entirely through Terraform configurations.
  • Train a binary classification model using SageMaker's built-in XGBoost algorithm and upload formatted training data to S3.
  • Deploy the trained model to a real-time inference endpoint and invoke live predictions against it.
  • Secret Mission: Configured SageMaker Model Monitor to detect data drift on your production endpoint.

Ready to quiz yourself?