Complete Terraform for ML Tutorial: Infrastructure as Code for Machine Learning
Terraform is an Infrastructure as Code (IaC) tool that enables you to provision and manage cloud infrastructure declaratively. For ML teams, Terraform helps automate the deployment of training clusters, inference endpoints, and data pipelines.
Why Terraform for ML?
Terraform Advantages:- Reproducibility: Same infrastructure every time
- Version control: Track infrastructure changes
- Multi-cloud: AWS, GCP, Azure support
- Modularity: Reusable infrastructure components
- State management: Track resource state
- ML training infrastructure
- Inference endpoint deployment
- Data pipeline provisioning
- Development environments
- Multi-region deployments
Installation
# macOS
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
Linux
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsbrelease -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
Verify installation
terraform --version
Quick Start
1. Project Structure
ml-infrastructure/
├── main.tf # Main configuration
├── variables.tf # Input variables
├── outputs.tf # Output values
├── providers.tf # Provider configuration
├── terraform.tfvars # Variable values
└── modules/
├── training/
├── inference/
└── storage/
2. Basic Configuration
# providers.tf
terraform {
requiredproviders {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
requiredversion = ">= 1.0"
}
provider "aws" {
region = var.awsregion
}
# variables.tf
variable "awsregion" {
description = "AWS region"
type = string
default = "us-west-2"
}
variable "environment" {
description = "Environment name"
type = string
default = "dev"
}
variable "projectname" {
description = "Project name"
type = string
default = "ml-platform"
}
3. Basic Commands
# Initialize Terraform
terraform init
Format configuration
terraform fmt
Validate configuration
terraform validate
Plan changes
terraform plan
Apply changes
terraform apply
Destroy infrastructure
terraform destroy
S3 for ML Data
1. Data Lake Setup
# modules/storage/main.tf
resource "awss3bucket" "mldata" {
bucket = "${var.projectname}-${var.environment}-ml-data"
tags = {
Environment = var.environment
Project = var.projectname
Purpose = "ML Data Lake"
}
}
resource "awss3bucketversioning" "mldata" {
bucket = awss3bucket.mldata.id
versioningconfiguration {
status = "Enabled"
}
}
resource "awss3bucketlifecycleconfiguration" "mldata" {
bucket = awss3bucket.mldata.id
rule {
id = "archive-old-data"
status = "Enabled"
transition {
days = 90
storageclass = "STANDARDIA"
}
transition {
days = 180
storageclass = "GLACIER"
}
}
}
Folder structure
resource "awss3object" "rawdata" {
bucket = awss3bucket.mldata.id
key = "raw/"
contenttype = "application/x-directory"
}
resource "awss3object" "processeddata" {
bucket = awss3bucket.mldata.id
key = "processed/"
contenttype = "application/x-directory"
}
resource "awss3object" "models" {
bucket = awss3bucket.mldata.id
key = "models/"
contenttype = "application/x-directory"
}
2. Model Registry Bucket
resource "awss3bucket" "modelregistry" {
bucket = "${var.project
name}-${var.environment}-models"
tags = {
Purpose = "Model Registry"
}
}
resource "awss3bucketpublicaccessblock" "modelregistry" {
bucket = awss3bucket.modelregistry.id
blockpublicacls = true
blockpublicpolicy = true
ignorepublicacls = true
restrictpublicbuckets = true
}
EC2 Training Instances
1. GPU Training Instance
# modules/training/main.tf
data "awsami" "deeplearning" {
mostrecent = true
owners = ["amazon"]
filter {
name = "name"
values = ["Deep Learning AMI (Ubuntu 22.04) "]
}
}
resource "awsinstance" "training" {
ami = data.awsami.deeplearning.id
instancetype = var.instancetype # "p3.2xlarge" for GPU
vpcsecuritygroupids = [awssecuritygroup.training.id]
subnetid = var.subnetid
iaminstanceprofile = awsiaminstanceprofile.training.name
rootblockdevice {
volumesize = 200
volumetype = "gp3"
}
userdata = <<-EOF
#!/bin/bash
# Install additional packages
pip install mlflow wandb
# Download training data
aws s3 sync s3://${var.databucket}/processed/ /data/
# Start training (example)
python /opt/ml/train.py
EOF
tags = {
Name = "${var.projectname}-training"
Environment = var.environment
}
}
resource "awssecuritygroup" "training" {
name = "${var.projectname}-training-sg"
description = "Security group for training instances"
vpcid = var.vpcid
egress {
fromport = 0
toport = 0
protocol = "-1"
cidrblocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.projectname}-training-sg"
}
}
2. Spot Instance for Cost Savings
resource "awsspotinstancerequest" "trainingspot" {
ami = data.aws
ami.deeplearning.id
instance
type = "p3.2xlarge"
spotprice = "0.90" # Max price per hour
spottype = "one-time"
waitforfulfillment = true
vpcsecuritygroupids = [awssecuritygroup.training.id]
subnetid = var.subnetid
iaminstanceprofile = awsiaminstanceprofile.training.name
rootblockdevice {
volumesize = 200
volumetype = "gp3"
}
tags = {
Name = "${var.projectname}-training-spot"
}
}
SageMaker Resources
1. SageMaker Domain
# modules/sagemaker/main.tf
resource "awssagemakerdomain" "mldomain" {
domainname = "${var.projectname}-${var.environment}"
authmode = "IAM"
vpcid = var.vpcid
subnetids = var.subnetids
defaultusersettings {
executionrole = awsiamrole.sagemakerexecution.arn
jupyterserverappsettings {
defaultresourcespec {
instancetype = "system"
}
}
kernelgatewayappsettings {
defaultresourcespec {
instancetype = "ml.t3.medium"
}
}
}
tags = {
Environment = var.environment
Project = var.projectname
}
}
resource "awssagemakeruserprofile" "datascientist" {
domainid = awssagemakerdomain.mldomain.id
userprofilename = "data-scientist"
usersettings {
executionrole = awsiamrole.sagemakerexecution.arn
}
}
2. SageMaker Training Job
resource "awssagemakertrainingjob" "modeltraining" {
training
jobname = "${var.projectname}-${var.modelversion}"
role
arn = awsiamrole.sagemakerexecution.arn
algorithm
specification {
trainingimage = var.trainingimage
traininginputmode = "File"
}
resourceconfig {
instancecount = 1
instancetype = "ml.p3.2xlarge"
volumesizeingb = 50
}
inputdataconfig {
channelname = "training"
datasource {
s3datasource {
s3uri = "s3://${var.databucket}/processed/train/"
s3datatype = "S3Prefix"
}
}
}
outputdataconfig {
s3outputpath = "s3://${var.modelbucket}/output/"
}
stoppingcondition {
maxruntimeinseconds = 86400 # 24 hours
}
hyperparameters = {
epochs = "100"
learningrate = "0.001"
batchsize = "32"
}
}
3. SageMaker Endpoint
resource "awssagemakermodel" "inferencemodel" {
name = "${var.projectname}-model-${var.modelversion}"
executionrolearn = awsiamrole.sagemakerexecution.arn
primarycontainer {
image = var.inferenceimage
modeldataurl = "s3://${var.modelbucket}/models/${var.modelversion}/model.tar.gz"
environment = {
SAGEMAKERPROGRAM = "inference.py"
}
}
tags = {
ModelVersion = var.modelversion
}
}
resource "awssagemakerendpointconfiguration" "inference" {
name = "${var.projectname}-endpoint-config"
productionvariants {
variantname = "primary"
modelname = awssagemakermodel.inferencemodel.name
initialinstancecount = var.endpointinstancecount
instancetype = var.endpointinstancetype
initialvariantweight = 1
}
# Enable data capture
datacaptureconfig {
enablecapture = true
initialsamplingpercentage = 100
destinations3uri = "s3://${var.databucket}/endpoint-data-capture/"
captureoptions {
capturemode = "Input"
}
captureoptions {
capturemode = "Output"
}
}
}
resource "awssagemakerendpoint" "inference" {
name = "${var.projectname}-endpoint"
endpointconfigname = awssagemakerendpointconfiguration.inference.name
tags = {
Environment = var.environment
}
}
EKS for ML Workloads
1. EKS Cluster
# modules/eks/main.tf
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 19.0"
clustername = "${var.projectname}-${var.environment}"
clusterversion = "1.28"
vpcid = var.vpcid
subnetids = var.privatesubnetids
clusterendpointpublicaccess = true
eksmanagednodegroups = {
# CPU nodes for general workloads
cpunodes = {
name = "cpu-nodes"
instancetypes = ["m5.xlarge"]
minsize = 1
maxsize = 10
desiredsize = 2
labels = {
workload = "general"
}
}
# GPU nodes for training
gpunodes = {
name = "gpu-nodes"
instancetypes = ["p3.2xlarge"]
minsize = 0
maxsize = 5
desiredsize = 0
amitype = "AL2x8664GPU"
labels = {
workload = "gpu"
"nvidia.com/gpu" = "true"
}
taints = [{
key = "nvidia.com/gpu"
value = "true"
effect = "NOSCHEDULE"
}]
}
}
tags = {
Environment = var.environment
Project = var.projectname
}
}
Install NVIDIA device plugin
resource "kubernetesdaemonset" "nvidiadeviceplugin" {
dependson = [module.eks]
metadata {
name = "nvidia-device-plugin-daemonset"
namespace = "kube-system"
}
spec {
selector {
matchlabels = {
name = "nvidia-device-plugin-ds"
}
}
template {
metadata {
labels = {
name = "nvidia-device-plugin-ds"
}
}
spec {
toleration {
key = "nvidia.com/gpu"
operator = "Exists"
effect = "NoSchedule"
}
container {
name = "nvidia-device-plugin-ctr"
image = "nvcr.io/nvidia/k8s-device-plugin:v0.14.0"
securitycontext {
allowprivilegeescalation = false
capabilities {
drop = ["ALL"]
}
}
volumemount {
name = "device-plugin"
mountpath = "/var/lib/kubelet/device-plugins"
}
}
volume {
name = "device-plugin"
hostpath {
path = "/var/lib/kubelet/device-plugins"
}
}
}
}
}
}
2. ML Workload on Kubernetes
# Training Job
resource "kubernetesjob" "trainingjob" {
metadata {
name = "model-training-${var.modelversion}"
namespace = "ml-workloads"
}
spec {
template {
metadata {
labels = {
app = "model-training"
}
}
spec {
nodeselector = {
"nvidia.com/gpu" = "true"
}
toleration {
key = "nvidia.com/gpu"
operator = "Exists"
effect = "NoSchedule"
}
container {
name = "training"
image = var.trainingimage
resources {
limits = {
"nvidia.com/gpu" = "1"
memory = "32Gi"
cpu = "8"
}
}
env {
name = "S3DATAPATH"
value = "s3://${var.databucket}/processed/"
}
env {
name = "S3MODELPATH"
value = "s3://${var.modelbucket}/models/"
}
}
restartpolicy = "Never"
}
}
backofflimit = 2
}
}
IAM Roles for ML
1. SageMaker Execution Role
# modules/iam/main.tf
resource "awsiamrole" "sagemakerexecution" {
name = "${var.projectname}-sagemaker-execution"
assumerolepolicy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "sagemaker.amazonaws.com"
}
}]
})
}
resource "awsiamrolepolicy" "sagemakers3" {
name = "sagemaker-s3-access"
role = awsiamrole.sagemakerexecution.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket"
]
Resource = [
"arn:aws:s3:::${var.databucket}",
"arn:aws:s3:::${var.databucket}/",
"arn:aws:s3:::${var.modelbucket}",
"arn:aws:s3:::${var.modelbucket}/"
]
},
{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "arn:aws:logs:::"
},
{
Effect = "Allow"
Action = [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
]
Resource = "*"
}
]
})
}
2. Training Instance Role
resource "awsiamrole" "traininginstance" {
name = "${var.projectname}-training-instance"
assumerolepolicy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}]
})
}
resource "awsiaminstanceprofile" "training" {
name = "${var.projectname}-training-profile"
role = awsiamrole.traininginstance.name
}
resource "awsiamrolepolicyattachment" "trainings3" {
role = awsiamrole.traininginstance.name
policyarn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
}
Complete ML Platform Module
# main.tf
module "storage" {
source = "./modules/storage"
projectname = var.projectname
environment = var.environment
}
module "networking" {
source = "./modules/networking"
projectname = var.projectname
environment = var.environment
vpccidr = var.vpccidr
}
module "sagemaker" {
source = "./modules/sagemaker"
projectname = var.projectname
environment = var.environment
vpcid = module.networking.vpcid
subnetids = module.networking.privatesubnetids
databucket = module.storage.databucketid
modelbucket = module.storage.modelbucketid
}
module "eks" {
source = "./modules/eks"
projectname = var.projectname
environment = var.environment
vpcid = module.networking.vpcid
privatesubnetids = module.networking.privatesubnetids
}
outputs.tf
output "sagemakerendpoint" {
value = module.sagemaker.endpointurl
}
output "eksclusterendpoint" {
value = module.eks.clusterendpoint
}
output "databucket" {
value = module.storage.databucketid
}
CI/CD Integration
1. GitHub Actions
# .github/workflows/terraform.yml
name: Terraform
on:
push:
branches: [main]
pullrequest:
branches: [main]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWSACCESSKEYID }}
aws-secret-access-key: ${{ secrets.AWSSECRETACCESSKEY }}
aws-region: us-west-2
- name: Terraform Init
run: terraform init
- name: Terraform Format
run: terraform fmt -check
- name: Terraform Plan
run: terraform plan -out=tfplan
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply -auto-approve tfplan
2. Remote State
# backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "ml-platform/terraform.tfstate"
region = "us-west-2"
dynamodbtable = "terraform-locks"
encrypt = true
}
}
Create state resources
resource "awss3bucket" "terraformstate" {
bucket = "my-terraform-state-bucket"
lifecycle {
preventdestroy = true
}
}
resource "awsdynamodbtable" "terraformlocks" {
name = "terraform-locks"
billingmode = "PAYPERREQUEST"
hashkey = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Best Practices
1. Use Workspaces
# Create workspaces
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
Switch workspace
terraform workspace select prod
Use in configuration
locals {
environment = terraform.workspace
}
2. Module Versioning
module "mlplatform" {
source = "git::https://github.com/org/terraform-ml-platform.git?ref=v1.2.0"
projectname = var.projectname
environment = var.environment
}
3. Cost Tags
locals {
commontags = {
Project = var.projectname
Environment = var.environment
ManagedBy = "terraform"
CostCenter = var.costcenter
Owner = var.owneremail
}
}
resource "awsinstance" "example" {
# ... configuration ...
tags = merge(local.commontags, {
Name = "example-instance"
})
}
Conclusion
Terraform is essential for ML infrastructure with:
Key takeaways:
- Use modules for reusability
- Implement remote state with locking
- Tag resources for cost tracking
- Use workspaces for environments
- Integrate with CI/CD pipelines