DEV Community

Cover image for 17.Access Secrets Manager with IAM Role Using Terraform
Thu Kha Kyawe
Thu Kha Kyawe

Posted on

17.Access Secrets Manager with IAM Role Using Terraform

Lab Information

To enable secure retrieval of secrets, the Nautilus DevOps team needs to configure access to a secret in AWS Secrets Manager using IAM roles and policies. The objective is to allow EC2 instances to retrieve secrets securely. Please complete the following tasks:

Create a secret in AWS Secrets Manager named nautilus-app-secret with the following secret string:

{"db_user":"admin","db_pass":"supersecret"}

Create an IAM role named nautilus-app-role with EC2 as the trusted entity.

Attach an inline IAM policy named nautilus-app-policy that grants permission to retrieve the secret from AWS Secrets Manager.

Use the main.tf file (do not create a separate .tf file) to provision the IAM Role and IAM Policy.

Create the variables.tf file, ensure the following variables are defined in variables.tf file:
    KKE_SECRET_NAME for the secret name.
    KKE_SECRET_VALUE for the secret value.
    KKE_ROLE_NAME for the IAM role name.
    KKE_POLICY_NAME for the IAM policy name.

Create the outputs.tf file, and use the following:
    KKE_secret_name: The secret name
    KKE_role_name: The IAM role name
    KKE_policy_name: The IAM policy name
Enter fullscreen mode Exit fullscreen mode

Lab Solutions

1️⃣ variables.tf

variable "KKE_SECRET_NAME" {
  type = string
}

variable "KKE_SECRET_VALUE" {
  type = string
}

variable "KKE_ROLE_NAME" {
  type = string
}

variable "KKE_POLICY_NAME" {
  type = string
}
Enter fullscreen mode Exit fullscreen mode

2️⃣ terraform.tfvars

KKE_SECRET_NAME  = "nautilus-app-secret"
KKE_SECRET_VALUE = "{\"db_user\":\"admin\",\"db_pass\":\"supersecret\"}"
KKE_ROLE_NAME    = "nautilus-app-role"
KKE_POLICY_NAME  = "nautilus-app-policy"
Enter fullscreen mode Exit fullscreen mode

⚠️ The JSON string must be escaped properly — this is important.

3️⃣ main.tf

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

# Create Secrets Manager secret
resource "aws_secretsmanager_secret" "app_secret" {
  name = var.KKE_SECRET_NAME
}

# Store secret value
resource "aws_secretsmanager_secret_version" "app_secret_value" {
  secret_id     = aws_secretsmanager_secret.app_secret.id
  secret_string = var.KKE_SECRET_VALUE
}

# IAM Role (trusted by EC2)
resource "aws_iam_role" "app_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"
    }]
  })
}

# Inline IAM Policy (read secret only)
resource "aws_iam_role_policy" "app_inline_policy" {
  name = var.KKE_POLICY_NAME
  role = aws_iam_role.app_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret"
      ]
      Resource = aws_secretsmanager_secret.app_secret.arn
    }]
  })
}
Enter fullscreen mode Exit fullscreen mode

4️⃣ outputs.tf

output "KKE_secret_name" {
  value = aws_secretsmanager_secret.app_secret.name
}

output "KKE_role_name" {
  value = aws_iam_role.app_role.name
}

output "KKE_policy_name" {
  value = aws_iam_role_policy.app_inline_policy.name
}
Enter fullscreen mode Exit fullscreen mode

5️⃣ Terraform Commands (Run in Order)
terraform init
terraform validate
terraform apply

Type:

yes

✅ 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_iam_role.app_role will be created
  + resource "aws_iam_role" "app_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)
      + max_session_duration  = 3600
      + name                  = "nautilus-app-role"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + tags_all              = (known after apply)
      + unique_id             = (known after apply)

      + inline_policy (known after apply)
    }

  # aws_iam_role_policy.app_inline_policy will be created
  + resource "aws_iam_role_policy" "app_inline_policy" {
      + id          = (known after apply)
      + name        = "nautilus-app-policy"
      + name_prefix = (known after apply)
      + policy      = (known after apply)
      + role        = (known after apply)
    }

  # aws_secretsmanager_secret.app_secret will be created
  + resource "aws_secretsmanager_secret" "app_secret" {
      + arn                            = (known after apply)
      + force_overwrite_replica_secret = false
      + id                             = (known after apply)
      + name                           = "nautilus-app-secret"
      + name_prefix                    = (known after apply)
      + policy                         = (known after apply)
      + recovery_window_in_days        = 30
      + tags_all                       = (known after apply)

      + replica (known after apply)
    }

  # aws_secretsmanager_secret_version.app_secret_value will be created
  + resource "aws_secretsmanager_secret_version" "app_secret_value" {
      + arn                  = (known after apply)
      + has_secret_string_wo = (known after apply)
      + id                   = (known after apply)
      + secret_id            = (known after apply)
      + secret_string        = (sensitive value)
      + secret_string_wo     = (write-only attribute)
      + version_id           = (known after apply)
      + version_stages       = (known after apply)
    }

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

Changes to Outputs:
  + KKE_policy_name = "nautilus-app-policy"
  + KKE_role_name   = "nautilus-app-role"
  + KKE_secret_name = "nautilus-app-secret"

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_secretsmanager_secret.app_secret: Creating...
aws_iam_role.app_role: Creating...
aws_secretsmanager_secret.app_secret: Creation complete after 1s [id=arn:aws:secretsmanager:us-east-1:000000000000:secret:nautilus-app-secret-XTRWVy]
aws_secretsmanager_secret_version.app_secret_value: Creating...
aws_secretsmanager_secret_version.app_secret_value: Creation complete after 0s [id=arn:aws:secretsmanager:us-east-1:000000000000:secret:nautilus-app-secret-XTRWVy|terraform-20260130144343211100000002]
aws_iam_role.app_role: Creation complete after 1s [id=nautilus-app-role]
aws_iam_role_policy.app_inline_policy: Creating...
aws_iam_role_policy.app_inline_policy: Creation complete after 0s [id=nautilus-app-role:nautilus-app-policy]

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

Outputs:

KKE_policy_name = "nautilus-app-policy"
KKE_role_name = "nautilus-app-role"
KKE_secret_name = "nautilus-app-secret"
Enter fullscreen mode Exit fullscreen mode

🧠 Step-by-Step Explanation (Simple & Clear)

Let’s understand what you built and why.

🔹 Why use AWS Secrets Manager?

Secrets Manager is used to store:

Database passwords

API keys

Credentials

Benefits:

Encrypted at rest

IAM-controlled access

No secrets in code

🔹 Why split secret & secret version?

Terraform requires:

aws_secretsmanager_secret → container

aws_secretsmanager_secret_version → actual value

This allows:

Secret rotation

Versioning

Updates without deleting the secret

🔹 Why an IAM Role?

EC2 instances should:

Access secrets without access keys

Use temporary credentials

IAM Role does exactly that.

🔹 Why an inline IAM policy?

Inline policy means:

Policy is embedded inside the role

Cannot be reused elsewhere

Strong binding → perfect for sensitive access

The lab explicitly asked for this.

🔹 Why only these permissions?
"secretsmanager:GetSecretValue"
"secretsmanager:DescribeSecret"

These allow:

Reading the secret

Nothing else

❌ No delete
❌ No update
❌ No list all secrets

This is least-privilege access 🔐

🔹 Why restrict policy to one secret?
Resource = aws_secretsmanager_secret.app_secret.arn

This ensures:

Role can access only xfusion-app-secret

Other secrets stay protected

🔹 What happens during terraform apply?

1️⃣ Terraform creates the secret
2️⃣ Stores the secret value securely
3️⃣ Creates IAM role
4️⃣ Attaches inline policy to role
5️⃣ AWS enforces permissions
6️⃣ Terraform outputs names

🧠 Easy Memory Model

Secret = 🔒 sensitive data

Secret version = 📄 actual value

IAM role = 👤 identity

Inline policy = 🔑 tightly bound permission

EC2 = 🖥️ consumer

🚨 Common Mistakes

❌ Using managed policy instead of inline
❌ Granting secretsmanager:*
❌ Using * for resource
❌ Forgetting JSON escaping
❌ Output name mismatch


Resources & Next Steps
📦 Full Code Repository: KodeKloud Learning Labs
📖 More Deep Dives: Whispering Cloud Insights - Read other technical articles
💬 Join Discussion: DEV Community - Share your thoughts and questions
💼 Let's Connect: LinkedIn - I'd love to connect with you

Credits
• All labs are from: KodeKloud
• I sincerely appreciate your provision of these valuable resources.

Top comments (0)