Skip to content

DevOps Day 99: Attach IAM Policy for DynamoDB Access Using Terraform

This document outlines the solution for DevOps Day 99. The objective was to implement secure, fine-grained access control for a DynamoDB table. I used Terraform to provision the table, create a specific IAM role for EC2 instances, and attach a strictly scoped read-only policy.

Table of Contents

  1. Task Overview
  2. Step-by-Step Solution
  3. Deep Dive: Terraform Concepts Used
  4. Troubleshooting

Task Overview

Objective: Create a secure DynamoDB table and an IAM role with read-only access to that specific table.

Requirements: 1. DynamoDB Table: xfusion-table (Billing: PAY_PER_REQUEST, Hash Key: id). 2. IAM Role: xfusion-role (Assumable by EC2). 3. IAM Policy: xfusion-readonly-policy (Permissions: GetItem, Scan, Query). 4. Scope: The policy must apply only to the created table. 5. Structure: Use variables.tf, terraform.tfvars, main.tf, and outputs.tf.


Step-by-Step Solution

1. Define Variables (variables.tf & terraform.tfvars)

I started by defining the input variables to make the code reusable, then assigned their specific values in the .tfvars file.

variables.tf:

variable "KKE_TABLE_NAME" {}
variable "KKE_ROLE_NAME" {}
variable "KKE_POLICY_NAME" {}

terraform.tfvars:

KKE_TABLE_NAME  = "xfusion-table"
KKE_ROLE_NAME   = "xfusion-role"
KKE_POLICY_NAME = "xfusion-readonly-policy"

2. Create Infrastructure (main.tf)

This file ties everything together. It creates the table, the role, the policy, and the attachment between the role and policy.

Command:

vi main.tf

Content:

# 1. Create DynamoDB Table
resource "aws_dynamodb_table" "xfusion_table" {
  name         = var.KKE_TABLE_NAME
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "id"

  attribute {
    name = "id"
    type = "S"
  }

  tags = {
    Name = var.KKE_TABLE_NAME
  }
}

# 2. Create IAM Role
resource "aws_iam_role" "xfusion_role" {
  name = var.KKE_ROLE_NAME

  # Trust Policy: Allows EC2 to assume this role
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "ec2.amazonaws.com"
      }
    }]
  })
}

# 3. Create Read-Only Policy
resource "aws_iam_policy" "xfusion_readonly_policy" {
  name        = var.KKE_POLICY_NAME
  description = "Read-only access to xfusion-table"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = [
        "dynamodb:GetItem",
        "dynamodb:Scan",
        "dynamodb:Query"
      ]
      # Crucial: Restricting access ONLY to this specific table's ARN
      Resource = aws_dynamodb_table.xfusion_table.arn
    }]
  })
}

# 4. Attach Policy to Role
resource "aws_iam_role_policy_attachment" "xfusion_attach" {
  role       = aws_iam_role.xfusion_role.name
  policy_arn = aws_iam_policy.xfusion_readonly_policy.arn
}

3. Define Outputs (outputs.tf)

I defined outputs to verify the created resource names.

Content:

output "kke_dynamodb_table" {
  value = aws_dynamodb_table.xfusion_table.name
}

output "kke_iam_role_name" {
  value = aws_iam_role.xfusion_role.name
}

output "kke_iam_policy_name" {
  value = aws_iam_policy.xfusion_readonly_policy.name
}

4. Initialize and Apply

I initialized Terraform to download the AWS provider and applied the configuration.

terraform init
terraform plan
terraform apply -auto-approve

Deep Dive: Terraform Concepts Used

DynamoDB Resource

* aws_dynamodb_table: Creates a NoSQL table. * billing_mode = "PAY_PER_REQUEST": This is ideal for unpredictable workloads or dev environments as you don't pay for idle capacity. * hash_key: This is the primary key. We defined a simple primary key named id of type String (S).

IAM Roles & Assume Policies

* Role vs. User: A Role is an identity you assume, not one you log in as. * assume_role_policy: This is the "Trust Policy". It defines who can wear the hat. In our code, we specified Principal: { Service: "ec2.amazonaws.com" }, meaning only EC2 instances can use this role.

IAM Policies & JSON Encoding

* jsonencode: Writing policies in native Terraform maps/lists is cleaner and safer than writing raw JSON strings. Terraform handles the formatting and escaping. * Dynamic ARN Reference: Instead of hardcoding the resource ARN (e.g., arn:aws:dynamodb:us-east-1:123:table/xfusion-table), I used aws_dynamodb_table.xfusion_table.arn. This ensures the policy always points to the exact table Terraform created, even if the region or account ID changes.


Troubleshooting

Issue: Invalid JSON Syntax * Error: MalformedPolicyDocument * Cause: Often caused by writing raw JSON strings with incorrect escaping. * Fix: Always use the jsonencode() function in Terraform for policy documents.

Issue: Cycle Error * Cause: If you try to reference the Policy ARN inside the Role creation or vice-versa incorrectly. * Fix: Keep resources separate. Create the Role, create the Policy, and then link them with aws_iam_role_policy_attachment.