Cloud  /  Terraform

IaC Terraform 50 guides · updated 2026

Infrastructure as code done right — providers, state, reusable modules, and the workflow patterns that keep multi-cloud deployments sane in 2026.

Role-Based Access Control in Terraform

Infrastructure-as-Code doesn’t automatically mean secure infrastructure. Terraform must be given credentials to provision resources, and those credentials need to follow least-privilege principles. Getting RBAC right for Terraform pipelines prevents both security breaches and accidental mass-destruction of infrastructure.


The Principle of Least Privilege

Terraform should have exactly the permissions it needs — no more. Two separate roles for plan and apply:

OperationPermissions NeededRisk Level
terraform planRead-only: describe, list, getLow — no changes made
terraform applyRead + Write: create, modify, delete specific resourcesHigh — creates real changes
terraform destroySame as apply + delete permissionsVery High

AWS: IAM Roles for Terraform

Plan Role (Read-Only)

resource "aws_iam_role" "terraform_plan" {
name = "terraform-plan-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/token.actions.githubusercontent.com"
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:myorg/infra-repo:pull_request"
}
}
}]
})
}
resource "aws_iam_role_policy_attachment" "terraform_plan_readonly" {
role = aws_iam_role.terraform_plan.name
policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}

Apply Role (Scoped Write Access)

resource "aws_iam_role" "terraform_apply" {
name = "terraform-apply-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/token.actions.githubusercontent.com"
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
"token.actions.githubusercontent.com:sub" = "repo:myorg/infra-repo:ref:refs/heads/main"
}
}
}]
})
}
resource "aws_iam_policy" "terraform_apply_scoped" {
name = "terraform-apply-scoped"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ec2:*",
"ecs:*",
"rds:*",
"s3:*",
"elasticloadbalancing:*"
]
Resource = "*"
Condition = {
StringEquals = {
"aws:RequestedRegion" = "us-east-1"
}
}
},
{
# Allow Terraform state management
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
Resource = "arn:aws:s3:::my-terraform-state/*"
},
{
Effect = "Allow"
Action = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem"]
Resource = "arn:aws:dynamodb:us-east-1:*:table/terraform-locks"
}
]
})
}

AWS IAM Resources via Terraform

# Create an application IAM role
resource "aws_iam_role" "app_role" {
name = "${var.service_name}-${var.environment}-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ec2.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
tags = local.common_tags
}
# Attach a managed policy
resource "aws_iam_role_policy_attachment" "ssm" {
role = aws_iam_role.app_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
# Custom inline policy
resource "aws_iam_role_policy" "app_s3" {
name = "s3-access"
role = aws_iam_role.app_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject"]
Resource = "${aws_s3_bucket.app_data.arn}/*"
}]
})
}
# Instance profile (attaches role to EC2 instances)
resource "aws_iam_instance_profile" "app" {
name = "${var.service_name}-instance-profile"
role = aws_iam_role.app_role.name
}

Azure RBAC via Terraform

# Get current subscription
data "azurerm_subscription" "current" {}
# Create a custom role
resource "azurerm_role_definition" "terraform_deployer" {
name = "Terraform Deployer"
scope = data.azurerm_subscription.current.id
permissions {
actions = [
"Microsoft.Resources/subscriptions/resourceGroups/*",
"Microsoft.Compute/virtualMachines/*",
"Microsoft.Network/*",
"Microsoft.Storage/storageAccounts/*"
]
not_actions = [
"Microsoft.Authorization/*/Delete",
"Microsoft.Authorization/*/Write"
]
}
assignable_scopes = [data.azurerm_subscription.current.id]
}
# Assign role to service principal
resource "azurerm_role_assignment" "ci_deployer" {
scope = data.azurerm_subscription.current.id
role_definition_name = azurerm_role_definition.terraform_deployer.name
principal_id = var.service_principal_object_id
}

GCP IAM via Terraform

# Create service account for Terraform
resource "google_service_account" "terraform" {
account_id = "terraform-deployer"
display_name = "Terraform Deployment Service Account"
project = var.project_id
}
# Grant permissions
resource "google_project_iam_member" "terraform_compute" {
project = var.project_id
role = "roles/compute.admin"
member = "serviceAccount:${google_service_account.terraform.email}"
}
resource "google_project_iam_member" "terraform_storage" {
project = var.project_id
role = "roles/storage.admin"
member = "serviceAccount:${google_service_account.terraform.email}"
}
# Workload Identity for GKE → no service account keys
resource "google_service_account_iam_binding" "workload_identity" {
service_account_id = google_service_account.app.name
role = "roles/iam.workloadIdentityUser"
members = [
"serviceAccount:${var.project_id}.svc.id.goog[${var.namespace}/${var.k8s_service_account}]"
]
}

Team Access Patterns

# Different permission levels for different teams
locals {
team_access = {
"platform-team" = "full"
"dev-team" = "plan-only"
"security-team" = "readonly"
}
}
resource "aws_iam_group" "terraform_users" {
for_each = local.team_access
name = "terraform-${each.key}"
}
resource "aws_iam_group_policy_attachment" "terraform_users" {
for_each = local.team_access
group = aws_iam_group.terraform_users[each.key].name
policy_arn = each.value == "full" ? aws_iam_policy.terraform_apply_scoped.arn :
each.value == "plan-only" ? "arn:aws:iam::aws:policy/ReadOnlyAccess" :
"arn:aws:iam::aws:policy/SecurityAudit"
}