15.Attach IAM Policy for DynamoDB Access Using Terraform

Published: (February 8, 2026 at 04:11 AM EST)
8 min read
Source: Dev.to

Source: Dev.to

Lab Information

The DevOps team has been tasked with creating a secure DynamoDB table and enforcing fine‑grained access control using IAM. This setup will allow secure and restricted access to the table from trusted AWS services only.

As a member of the Nautilus DevOps Team, your task is to perform the following using Terraform:

  1. Create a DynamoDB Table – a table named datacenter-table with minimal configuration.
  2. Create an IAM Role – a role named datacenter-role that will be allowed to access the table.
  3. Create an IAM Policy – a policy named datacenter-readonly-policy that grants read‑only access (GetItem, Scan, Query) to the specific DynamoDB table and attach it to the role.
  4. Create the main.tf file (do not create a separate .tf file) to provision the table, role, and policy.
  5. Create the variables.tf file with the following variables:
    • KKE_TABLE_NAME – name of the DynamoDB table
    • KKE_ROLE_NAME – name of the IAM role
    • KKE_POLICY_NAME – name of the IAM policy
  6. Create the outputs.tf file with the following outputs:
    • kke_dynamodb_table – name of the DynamoDB table
    • kke_iam_role_name – name of the IAM role
    • kke_iam_policy_name – name of the IAM policy
  7. Define the actual values for these variables in the terraform.tfvars file.
  8. Ensure the IAM policy allows only read access and restricts it to the specific DynamoDB table created.

Lab Solutions

variables.tf

variable "KKE_TABLE_NAME" {
  type = string
}

variable "KKE_ROLE_NAME" {
  type = string
}

variable "KKE_POLICY_NAME" {
  type = string
}

terraform.tfvars

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

main.tf

# DynamoDB table (minimal configuration)
resource "aws_dynamodb_table" "datacenter_table" {
  name         = var.KKE_TABLE_NAME
  billing_mode = "PAY_PER_REQUEST"

  hash_key = "id"

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

# IAM Role
resource "aws_iam_role" "datacenter_role" {
  name = var.KKE_ROLE_NAME

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

# IAM Policy (read‑only access to specific DynamoDB table)
resource "aws_iam_policy" "datacenter_policy" {
  name = var.KKE_POLICY_NAME

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = [
        "dynamodb:GetItem",
        "dynamodb:Scan",
        "dynamodb:Query"
      ]
      Resource = aws_dynamodb_table.datacenter_table.arn
    }]
  })
}

# Attach policy to role
resource "aws_iam_role_policy_attachment" "attach_policy" {
  role       = aws_iam_role.datacenter_role.name
  policy_arn = aws_iam_policy.datacenter_policy.arn
}

outputs.tf

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

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

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

Terraform Commands (run in order)

terraform init
terraform validate
terraform apply
# Type "yes" when prompted

Expected Output

bob@iac-server ~/terraform via 💠 default ➜  terraform apply

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_dynamodb_table.datacenter_table will be created
  + resource "aws_dynamodb_table" "datacenter_table" {
      + arn              = (known after apply)
      + billing_mode     = "PAY_PER_REQUEST"
      + hash_key         = "id"
      + id               = (known after apply)
      + name             = "datacenter-table"
      + read_capacity    = (known after apply)
      + stream_arn       = (known after apply)
      + stream_label     = (known after apply)
      + stream_view_type = (known after apply)
      + tags_all         = (known after apply)
      + write_capacity   = (known after apply)

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

      + point_in_time_recovery (known after apply)

      + server_side_encryption (known after apply)

      + ttl (known after apply)
    }

  # aws_iam_policy.datacenter_policy will be created
  + resource "aws_iam_policy" "datacenter_policy" {
      + arn               = (known after apply)
      + attachment_count = (known after apply)
      + id                = (known after apply)
      + name              = "datacenter-readonly-policy"
      + name_prefix       = (known after apply)
      + path              = "/"
      + policy            = (known after apply)
      + policy_id         = (known after apply)
      + tags_all          = (known after apply)
    }

  # aws_iam_role.datacenter_role will be created
  + resource "aws_iam_role" "datacenter_role" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ec2.amazonaws.com"
                        }
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + name                  = "datacenter-role"
      + path                  = "/"
      + tags_all              = (known after apply)
    }

  # aws_iam_role_policy_attachment.attach_policy will be created
  + resource "aws_iam_role_policy_attachment" "attach_policy" {
      + id        = (known after apply)
      + policy_arn = aws_iam_policy.datacenter_policy.arn
      + role      = aws_iam_role.datacenter_role.name
    }

Plan: 4 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_dynamodb_table.datacenter_table: Creating...
aws_iam_role.datacenter_role: Creating...
aws_iam_policy.datacenter_policy: Creating...
aws_dynamodb_table.datacenter_table: Creation complete after 2s [id=datacenter-table]
aws_iam_role.datacenter_role: Creation complete after 1s [id=datacenter-role]
aws_iam_policy.datacenter_policy: Creation complete after 1s [id=arn:aws:iam::123456789012:policy/datacenter-readonly-policy]
aws_iam_role_policy_attachment.attach_policy: Creating...
aws_iam_role_policy_attachment.attach_policy: Creation complete after 0s [id=datacenter-role-arn:aws:iam::123456789012:policy/datacenter-readonly-policy]

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Outputs:

kke_dynamodb_table = "datacenter-table"
kke_iam_role_name = "datacenter-role"
kke_iam_policy_name = "datacenter-readonly-policy"

Terraform Apply Output

aws_iam_role.datacenter_role will be created
  + arn                    = (known after apply)
  + assume_role_policy     = (known after apply)
  + create_date            = (known after apply)
  + description            = (known after apply)
  + force_detach_policies  = false
  + id                     = (known after apply)
  + inline_policy          = (known after apply)
  + max_session_duration   = 3600
  + name                   = "datacenter-role"
  + name_prefix            = (known after apply)
  + path                   = "/"
  + tags_all               = (known after apply)
  + unique_id              = (known after apply)

aws_dynamodb_table.datacenter_table will be created
  + arn                     = (known after apply)
  + billing_mode            = "PAY_PER_REQUEST"
  + hash_key                = "id"
  + id                      = (known after apply)
  + name                    = "datacenter-table"
  + read_capacity           = (known after apply)
  + stream_arn              = (known after apply)
  + stream_enabled          = false
  + stream_view_type        = (known after apply)
  + ttl                     = (known after apply)
  + write_capacity          = (known after apply)

aws_iam_policy.datacenter_policy will be created
  + arn                     = (known after apply)
  + description             = (known after apply)
  + id                      = (known after apply)
  + name                    = "datacenter-readonly-policy"
  + path                    = "/"
  + policy                  = (known after apply)
  + policy_id               = (known after apply)

aws_iam_role_policy_attachment.attach_policy will be created
  + id          = (known after apply)
  + policy_arn  = (known after apply)
  + role        = "datacenter-role"

Plan: 4 to add, 0 to change, 0 to destroy.

### Changes to Outputs
- `kke_dynamodb_table`  = "datacenter-table"
- `kke_iam_policy_name` = "datacenter-readonly-policy"
- `kke_iam_role_name`   = "datacenter-role"

Do you want to perform these actions?  
Terraform will perform the actions described above.  
Only **yes** will be accepted to approve.

Enter a value: **yes**

aws_iam_role.datacenter_role: Creating...
aws_dynamodb_table.datacenter_table: Creating...
aws_iam_role.datacenter_role: Creation complete after 0s [id=datacenter-role]
aws_dynamodb_table.datacenter_table: Creation complete after 3s [id=datacenter-table]
aws_iam_policy.datacenter_policy: Creating...
aws_iam_policy.datacenter_policy: Creation complete after 0s [id=arn:aws:iam::000000000000:policy/datacenter-readonly-policy]
aws_iam_role_policy_attachment.attach_policy: Creating...
aws_iam_role_policy_attachment.attach_policy: Creation complete after 0s [id=datacenter-role-20260129134119333700000001]

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

**Outputs**

- `kke_dynamodb_table` = "datacenter-table"
- `kke_iam_policy_name` = "datacenter-readonly-policy"
- `kke_iam_role_name` = "datacenter-role"

Step‑by‑Step Explanation (Simple & Clear)

Why PAY_PER_REQUEST for DynamoDB?

  • billing_mode = “PAY_PER_REQUEST”
    • No capacity planning needed
    • Minimal configuration
    • Cheapest & simplest for labs and light workloads

Why does DynamoDB need a hash key?

  • Every DynamoDB table must have at least one primary key.
  • We used hash_key = "id" to keep the table minimal and valid.

IAM Role vs. IAM Policy (very important)

ComponentSymbolMeaning
IAM Policy📜What actions are allowed
IAM Role🪪Who gets those permissions
Attachment🔗Connects the policy to the role

Why only these DynamoDB actions?

  • dynamodb:GetItem
  • dynamodb:Scan
  • dynamodb:Query

These are read‑only operations (no writes, deletes, or table changes) – the principle of least‑privilege.

Why restrict the policy to one table?

resource = aws_dynamodb_table.datacenter_table.arn
  • The role can access only datacenter-table.
  • It cannot read other DynamoDB tables, providing a strong security boundary.

What happens during terraform apply?

  1. Terraform creates the DynamoDB table.
  2. Terraform creates the IAM role.
  3. Terraform creates the IAM policy.
  4. Terraform attaches the policy to the role.
  5. AWS enforces read‑only access.
  6. Terraform outputs the resource names.

Easy Memory Model

  • DynamoDB table = 📦 data store
  • IAM role = 👤 identity
  • IAM policy = 🔑 permissions
  • Attachment = 🔗 connection
  • Read‑only = 👀 safe access

Common Mistakes to Avoid

  • ❌ Using * for the DynamoDB resource ARN.
  • ❌ Granting write permissions.
  • ❌ Forgetting the hash key.
  • ❌ Hard‑coding names (use variables or name_prefix).
  • ❌ Output name mismatches.
0 views
Back to Blog

Related posts

Read more »

Happy women in STEM day!! <3

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as we...