Writing IAM policies as raw JSON inside Terraform has always been a pain point. The awsiampolicy_document data source gives you a proper HCL-native way to define IAM policies. It validates your policy structure, supports merging multiple policy fragments, and catches many structural errors before you ever hit the AWS API. This shift from heredoc JSON strings to declarative HCL blocks changes how teams author, review, and maintain permissions at scale. The data source renders to JSON through its .json attribute and never requires manual JSON authoring.
The awsiampolicydocument data source uses HCL to generate a JSON representation of an IAM policy document. Writing the policy as a Terraform configuration has several advantages over defining your policy inline in the awsiampolicy resource. Terraform data sources makes applying policies to your AWS resources more flexible. You can overwrite, append, or update policies with this resource by using the sourcepolicydocuments and overridepolicy_documents arguments. Terraform data sources make it easier to reuse policies throughout your environment. Terraform error checking automatically formats your policy document into correct JSON when you run your apply.
For simple policies or one-off configurations, heredoc JSON is acceptable. However, as your policies grow more complex and you begin to reuse them throughout your environment, it can be difficult to parse policies using heredoc strings. For complex policies with merging, conditions, and not-principals, the data source is the better tool.
Raw JSON Pain Points and HCL-Native Motivation
The direct fact is that raw JSON policy definitions inside Terraform require heredoc strings and manual quoting. The impact layer is that teams spend time debugging commas, brackets, and escaping instead of modeling intent. The real-world consequence is slower reviews, plan failures, and policy drift that is invisible until apply. The contextual layer connects this to the broader Terraform workflow where HCL is validated statically while JSON is only validated by AWS on apply.
Writing IAM policies as raw JSON inside Terraform has always been a pain point. The awsiampolicy_document data source gives you a proper HCL-native way to define IAM policies. The structure is easier to scan than a long JSON blob. Terraform data sources make it easier to reuse policies throughout your environment.
The awsiampolicy_document configuration can be copied into main.tf file. This data source uses HCL syntax to define the same IAM privileges as the policy in the heredoc string.
Basic Syntax and Rendering to awsiampolicy
The data source defines statements with sid, effect, actions, resources, and optional condition blocks. The rendered JSON is consumed by awsiampolicy via the policy attribute.
A simple S3 read policy looks using the data source:
hcl
data "aws_iam_policy_document" "s3_read" {
statement {
sid = "AllowS3Read"
effect = "Allow"
actions = [
"s3:GetObject",
"s3:ListBucket",
]
resources = [
aws_s3_bucket.data.arn,
"${aws_s3_bucket.data.arn}/*",
]
}
}
The data source renders to JSON through its .json attribute. You never write JSON yourself.
The rendered JSON is attached to an IAM policy:
hcl
resource "aws_iam_policy" "s3_read" {
name = "s3-read-access"
policy = data.aws_iam_policy_document.s3_read.json
}
The direct fact is the .json attribute reference. The impact is that policy output is always well-formed JSON validated at plan time. The contextual layer ties this to CI pipelines where plan output can be inspected for compliance before deployment.
An example with two statements:
hcl
data "aws_iam_policy_document" "example" {
statement {
actions = ["s3:ListAllMyBuckets"]
resources = ["arn:aws:s3:::*"]
effect = "Allow"
}
statement {
actions = ["s3:*"]
resources = [aws_s3_bucket.bucket.arn]
effect = "Allow"
}
}
Both statements in this policy apply to any user, group, or role with this policy attached.
The first policy statement allows the user to list every S3 bucket in the AWS account. The second policy statement allows the user to perform any action on the bucket you create in this configuration, but not on other buckets in the account.
Update your iam_policy resource policy attribute to use the IAM policy document and save your changes:
hcl
resource "aws_iam_policy" "policy" {
name = "${random_pet.pet_name.id}-policy"
description = "My test policy"
policy = data.aws_iam_policy_document.example.json
}
The iampolicy resource and iampolicy_document data source used together will create a policy, but this configuration does not apply this policy to any users or roles. You must create a policy attachment for your policy to apply to your users.
Reuse and Composability Through Merging
The data source supports merging multiple policy fragments. The advantage is composable policies that can be built from small reusable pieces rather than one massive document.
Terraform data sources make applying policies to your AWS resources more flexible. You can overwrite, append, or update policies with this resource by using the sourcepolicydocuments and overridepolicydocuments arguments.
Best practices include keeping policy documents focused. Rather than one massive document, create smaller composable ones and merge them. Store reusable policy fragments in modules so teams can share common patterns.
The impact layer is faster change control. A change to a condition in one fragment does not require editing a monolithic JSON blob. The contextual layer connects merging to module design where policy fragments are outputs consumed by higher-level modules.
A best practice is to always set the sid field on statements. It makes policies easier to audit and helps with merging.
Conditions, Principals and Advanced Statement Blocks
IAM conditions work through the condition block inside a statement. The condition block is cleaner than writing nested JSON objects for conditions.
An example with VPC endpoint restriction:
hcl
data "aws_iam_policy_document" "restricted_s3" {
statement {
sid = "AllowS3FromVPC"
effect = "Allow"
actions = [
"s3:GetObject",
"s3:PutObject",
]
resources = [
"${aws_s3_bucket.private.arn}/*",
]
condition {
test = "StringEquals"
variable = "aws:sourceVpce"
values = [aws_vpc_endpoint.s3.id]
}
}
statement {
sid = "DenyUnencryptedUploads"
effect = "Deny"
actions = [
"s3:PutObject",
]
resources = [
"${aws_s3_bucket.private.arn}/*",
]
condition {
test = "StringNotEquals"
variable = "s3:x-amz-server-side-encryption"
values = ["AES256", "aws:kms"]
}
}
}
A dynamic scenario with multiple conditions:
hcl
data "aws_iam_policy_document" "s3_upload_policy" {
statement {
effect = "Allow"
actions = [
"s3:PutObject",
]
resources = [
"${aws_s3_bucket.uploads.arn}/uploads/${var.environment}/*"
]
condition {
test = "StringEquals"
variable = "s3:x-amz-server-side-encryption"
values = ["aws:kms"]
}
condition {
test = "StringEquals"
variable = "s3:x-amz-server-side-encryption-aws-kms-key-id"
values = [aws_kms_key.uploads_key.arn]
}
condition {
test = "IpAddress"
variable = "aws:SourceIp"
values = var.allowed_ip_ranges
}
}
}
The direct fact is the condition block with test, variable, values. The impact is enforcement of encryption and network controls directly in HCL, reducing misconfiguration risk. The contextual layer links this to security baselines where KMS encryption and source IP allowlisting are mandatory.
Terraform Module Wrapper Pattern
This is AWS IAM Policy module for Terraform v0.12 and above. It uses Dynamic Nested Blocks, which are not supported by earlier versions of Terraform. It aims to create both awsiampolicy resource and awsiampolicy_document data blocks.
Usage of this module is quite straightforward. It accepts most of the inputs of resource awsiampolicy. Instead of policy input, it takes a required argument statements, which is a list of maps similar to statement from awsiampolicy_document data source.
The advantage of this is that whole definition in a single block.
Example:
hcl
module "grafana_policy" {
source = "grodzik/iam_policy/aws"
description = "Provides read-only access for grafana user"
name = "grafana-ro-access"
path = "/"
statements = [
{
sid = "GrafanaEC2AccessRO"
actions = [
"ec2:DescribeTags",
"ec2:DescribeRegions",
"ec2:DescribeInstances",
]
effect = "Allow"
resources = ["*"]
}
]
}
The code above will create both awsiampolicy resource and awsiampolicy_document data resource.
The impact layer is reduced boilerplate across teams. The contextual layer is standardization of naming, path, and description via module defaults.
Policy Attachment and Lifecycle Integration
Creating the policy is only half the workflow. Attachment binds the policy to principals.
Basic policy attachment example:
hcl
resource "aws_iam_policy" "s3_read_only" {
name = "S3ReadOnlyAccess"
description = "Read-only access to specific S3 bucket"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:ListBucket"
]
Resource = [
aws_s3_bucket.app_data.arn,
"${aws_s3_bucket.app_data.arn}/*"
]
}
]
})
}
Attach to a role:
hcl
resource "aws_iam_role_policy_attachment" "s3_read_only" {
role = aws_iam_role.app_role.name
policy_arn = aws_iam_policy.s3_read_only.arn
}
For user attachment:
hcl
resource "aws_iam_user_policy_attachment" "attachment" {
user = aws_iam_user.new_user.name
policy_arn = aws_iam_policy.policy.arn
}
The policy attachment resource has two required attributes: the user and the policy_arn.
The direct fact is the separation of policy creation and attachment. The impact is independent lifecycle management. The contextual layer is that policy updates can be rolled out without recreating users or roles.
Best Practices for Auditability and Maintenance
A few tips for working with awsiampolicy_document:
- Always set the sid field on statements. It makes policies easier to audit and helps with merging.
- Keep policy documents focused. Rather than one massive document, create smaller composable ones and merge them.
- Use terraform plan to inspect the generated JSON. The rendered output appears in the plan when you reference the.json attribute.
- Store reusable policy fragments in modules so teams can share common patterns.
The awsiampolicy_document data source is the most robust way to write IAM policies in Terraform. It validates your policy structure at plan time, supports merging for composable policies, and provides clean syntax for conditions and principals. While jsonencode works for simple cases, the data source scales better as your IAM requirements grow. Adopt it as your default approach for IAM policies, and you will spend less time debugging policy JSON structure.
Use terraform plan to inspect the generated JSON. The rendered output appears in the plan when you reference the.json attribute.
Comparison of Authoring Approaches
| Aspect | Heredoc JSON | awsiampolicy_document |
| Method | Inline JSON string in resource | HCL data source with statements |
| Validation | Deferred to AWS API | Plan time validation |
| Reuse | Copy paste required | Merging and module reuse |
| Readability | Long JSON blob | Structured blocks |
| Conditions | Nested objects | condition blocks |
The table shows why HCL-native authoring improves operational safety.
Conclusion
The awsiampolicy_document data source reframes IAM policy authoring from string manipulation to structured configuration. The data source validates your policy structure at plan time, supports merging for composable policies, and provides clean syntax for conditions and principals. The shift eliminates manual JSON errors, enables reuse through modules, and makes audits traceable via explicit sid fields.
Adopting the data source as the default approach for IAM policies reduces debugging time and aligns permission definitions with Terraform's declarative model. As IAM requirements grow in complexity with conditions, not-principals, and cross-account trust, the HCL-native representation scales where raw JSON becomes brittle. The combination of data source authoring, module wrappers, and explicit attachments creates a maintainable permission layer that can be reviewed, tested, and evolved alongside infrastructure code.