A policy turns a decision into a rule. In infrastructure work, that might mean allowing an EC2 instance to access another AWS service or preventing a costly configuration choice before deployment. section 4.1.4 section 7.5
For AWS permissions, IAM policies combine resources with allowed actions. A role receives those policies and can be assigned to a machine. An EC2 module can provide an IAM role and instance profile, then expose the role so each project can add its own permissions rather than forcing the module to anticipate every service. section 4.1.4
Listing 4.4 Creating a role
data "aws_iam_policy_document" "instance_assume_role_policy" { #1
statement { #2
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"] #3
}
}
}
resource "aws_iam_role" "main" {
name = "${var.name_prefix}-instance-role" #4
assume_role_policy = data.aws_iam_policy_document.instance_assume_role_
policy.json #5
}
resource "aws_iam_instance_profile" "main" { #6
name = aws_iam_role.main.name #7
role = aws_iam_role.main.name
}
resource "aws_instance" "hello_world" {
count = var.instance_count
ami = data.aws_ami.ubuntu.id
subnet_id = var.subnet_id
instance_type = var.instance_type
iam_instance_profile = aws_iam_instance_profile.main.name #8
tags = merge(var.tags, {
Name = "${var.name_prefix}-${count.index}"
})
}
#1 This data source helps us build custom IAM policies. We could use a template or JSON string, but that is more error prone.
#2 AWS Services need permission to use the roles we create.
#3 We are giving the EC2 service permission to use this role, but no other services can.
#4 We’re using the name prefix to ensure that the role has a unique name. Creating two modules with the same name will cause an error.
#5 This is the json generated by our data source.
#6 AWS Instances need a “profile” attached to the “role” to use them with AWS Instances.
#7 It’s best practice to name the profile the same as the role that it works with.
#8 Update our aws_instance definition with our new profile.
The role setup uses an assume-role policy document for EC2 and connects the role to an instance profile. section 4.1.4
A policy can also enforce a team decision in infrastructure code. For example, choosing AWS GP2 storage instead of the faster, cheaper GP3 option is not a code error, but a rule can still catch it. OPA policies use Rego; TFLint’s OPA plugin can raise an issue when a Terraform resource violates one. section 7.5 section 7.5.1
Listing 7.30 OPA example policy
package tflint
import rego.v1
deny_invalid_s3_bucket_name contains issue if { #1
buckets := terraform.resources("aws_s3_bucket", {"bucket": "string"},
{}) #2
name := buckets[_].config.bucket
not startswith(name.value, "example-com-") #3
issue := tflint.issue(`Bucket names should always start with
"example-com-"`, name.range) #4
}
#1 This is where the policy name comes from.
#2 This call looks through resources for S3 buckets.
#3 If the name starts with “example-com-” then the policy ends here and the issue isn’t created.
#4 This last step saves the generated issue so it gets raised by TFLint.
This Rego rule finds S3 buckets whose names lack a required prefix and emits a TFLint issue at the bucket-name attribute. section 7.5.1
Choose the enforcement path deliberately: TFLint’s OPA plugin is experimental and is most useful when a team already uses OPA; Checkov offers an easier route to custom policies. Review every scanner finding. Fix unintended violations, and document the reason for any intentional exception so reviewers can assess it later. section 7.5.1 section 7.4.1












