When EC2 instances need to interact with AWS services like S3, DynamoDB, or Secrets Manager, they need credentials. The best practice is to use IAM instance profiles rather than embedding access keys in the instance. An instance profile is a container for an IAM role that you can attach to an EC2 instance at launch. Terraform makes it straightforward to create and manage instance profiles as part of your infrastructure. This guide covers everything about creating IAM instance profiles in Terraform, from basic setups to advanced patterns with multiple roles and environments.
Understanding the IAM Instance Profile Architecture
An IAM instance profile acts as a bridge between an EC2 instance and an IAM role. When you launch an EC2 instance with an instance profile, the instance can retrieve temporary security credentials from the instance metadata service. Applications running on the instance use these credentials to make AWS API calls. The relationship works like this: An IAM role defines the permissions. An instance profile wraps the role. The EC2 instance uses the instance profile to assume the role and get temporary credentials. These credentials are rotated automatically by AWS.
The fundamental reason for using this architecture is security. Static access keys embedded in configuration files or scripts on an EC2 instance are a significant risk if the instance is compromised. By contrast, temporary credentials obtained via the instance metadata service are short-lived, scoped to the specific role, and managed entirely by AWS. This eliminates the need for static credentials and reduces the attack surface.
The workflow is as follows:
- The EC2 instance is launched with an instance profile attached.
- The application on the instance requests credentials from the instance metadata service (IMDS).
- The IMDS returns temporary security credentials associated with the IAM role wrapped in the instance profile.
- The application uses these credentials to make authorized API calls to AWS services.
- AWS automatically refreshes the credentials before they expire.
Prerequisites and Basic Configuration
To begin implementing IAM instance profiles in Terraform, specific prerequisites must be met. You need:
- Terraform 1.0 or later
- An AWS account with permissions to create IAM roles, instance profiles, and EC2 instances
- AWS CLI configured with valid credentials
A basic instance profile requires three components: the IAM role, the instance profile, and the policy attachments. Each of these components serves a distinct purpose in the authorization chain. The IAM role defines who can assume the role and what permissions are available. The policy attachments grant specific permissions to the role. The instance profile is the wrapper that allows the EC2 instance to assume the role.
The first step in creating a basic instance profile is to define the trust policy for EC2. This policy allows the EC2 service to assume the role on behalf of the instance. In Terraform, this is often handled using the aws_iam_policy_document data source to generate the JSON document programmatically. This approach is more robust than hardcoding JSON strings and reduces the risk of syntax errors.
```hcl
Step 1: Create the trust policy for EC2
data "awsiampolicydocument" "ec2trust" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
}
}
```
The next step is to create the IAM role itself. This role will be wrapped by the instance profile. It requires a name and the assume role policy, which is the output from the trust policy data source. Tags are also added for management and tracking purposes.
```hcl
Step 2: Create the IAM role
resource "awsiamrole" "ec2role" {
name = "ec2-application-role"
assumerolepolicy = data.awsiampolicydocument.ec2_trust.json
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
```
Once the role is created, policies must be attached to it. These policies define the specific permissions the role has. In the following example, two managed policies are attached: one for read-only access to S3 and another for the CloudWatch agent. Using managed policies is a common starting point, but custom policies are often required for production environments to enforce the principle of least privilege.
```hcl
Step 3: Attach policies to the role
resource "awsiamrolepolicyattachment" "s3access" {
role = awsiamrole.ec2role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
resource "awsiamrolepolicyattachment" "cloudwatchagent" {
role = awsiamrole.ec2role.name
policy_arn = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy"
}
```
Finally, the instance profile is created. The instance profile resource takes the name of the role and wraps it. It is important to note that an instance profile can contain only one IAM role. If you need different permission sets, create separate instance profiles.
```hcl
Step 4: Create the instance profile
resource "awsiaminstanceprofile" "ec2profile" {
name = "ec2-application-profile"
role = awsiamrole.ec2_role.name
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
```
Attaching the Instance Profile to EC2 Resources
Once the instance profile is created, it must be referenced in your EC2 instance resource to be effective. There are two primary ways to do this: directly in the aws_instance resource or via an aws_launch_template.
Direct Attachment to EC2 Instance
In a standard aws_instance resource, the instance profile is referenced by its name. This is the most common pattern for simple deployments.
```hcl
Launch an EC2 instance with the instance profile
resource "awsinstance" "appserver" {
ami = "ami-0c55b159cbfafe1f0" # Amazon Linux 2
instancetype = "t3.micro"
# Attach the instance profile
iaminstanceprofile = awsiaminstanceprofile.ec2_profile.name
tags = {
Name = "app-server"
}
}
```
Using Launch Templates
When using launch templates, the syntax is slightly different. Instead of a single argument, you use an iam_instance_profile block. This block can contain either the name or the ARN of the instance profile. Using the ARN is generally recommended for more explicit references, but the name is sufficient in most cases.
```hcl
Using a launch template
resource "awslaunchtemplate" "app" {
nameprefix = "app-"
imageid = "ami-0c55b159cbfafe1f0"
instancetype = "t3.micro"
# Instance profile in a launch template uses iaminstanceprofile block
iaminstanceprofile {
name = awsiaminstanceprofile.ec2profile.name
# Alternatively, use arn instead of name:
# arn = awsiaminstanceprofile.ec2_profile.arn
}
}
```
Advanced Patterns and Custom Policies
Most real applications need custom policies tailored to their specific AWS resource access patterns. Managed policies like AmazonS3ReadOnlyAccess are too broad for production environments. Custom policies allow you to restrict access to specific resources, actions, and conditions.
In the following example, a custom policy is defined using jsonencode. This function ensures that the JSON structure is valid and properly escaped. The policy allows reading from a specific S3 bucket and writing to a specific path, demonstrating how to scope permissions tightly.
```hcl
Define a custom policy for the application
resource "awsiampolicy" "app_policy" {
name = "app-server-policy"
description = "Policy for the application server EC2 instances"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
# Allow reading from a specific S3 bucket
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:ListBucket"
]
Resource = [
"arn:aws:s3:::my-app-bucket",
"arn:aws:s3:::my-app-bucket/*"
]
}
]
})
}
```
Once the custom policy is created, it is attached to the IAM role. This creates a more secure and precise permission set for the instance.
```hcl
Attach the custom policy to the role
resource "awsiamrolepolicyattachment" "appcustom" {
role = awsiamrole.approle.name
policyarn = awsiampolicy.apppolicy.arn
}
Create the instance profile
resource "awsiaminstanceprofile" "appprofile" {
name = "app-server-profile"
role = awsiamrole.app_role.name
}
```
Managing Multiple Environments
In complex deployments, it is common to manage multiple environments such as dev, staging, and prod. Terraform's dynamic for_each loops and variables allow you to create instance profiles for each environment efficiently. This approach ensures that each environment has its own set of roles, profiles, and policies, maintaining separation and security.
The first step is to define variables for the environments and their associated policies. This allows you to specify which policies should be attached to which environment.
```hcl
variable "environments" {
description = "List of environments to create instance profiles for"
type = list(string)
default = ["dev", "staging", "prod"]
}
variable "environment_policies" {
description = "Map of environments to their additional policy ARNs"
type = map(list(string))
default = {
dev = [
"arn:aws:iam::aws:policy/AmazonS3FullAccess",
]
staging = [
"arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess",
]
prod = [
"arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess",
]
}
}
```
Next, a role is created for each environment using a for_each loop. The trust policy is the same for all environments, but the role names and tags differ.
```hcl
Create a role for each environment
resource "awsiamrole" "envroles" {
foreach = toset(var.environments)
name = "ec2-role-${each.value}"
assumerolepolicy = data.awsiampolicydocument.ec2trust.json
tags = {
Environment = each.value
}
}
```
Instance profiles are then created for each environment, linking them to their respective roles.
```hcl
Create instance profiles for each environment
resource "awsiaminstanceprofile" "envprofiles" {
foreach = toset(var.environments)
name = "ec2-profile-${each.value}"
role = awsiamrole.envroles[each.value].name
tags = {
Environment = each.value
}
}
```
Finally, environment-specific policies are attached using a flattened local variable and a nested for_each loop. This pattern allows you to iterate over multiple policies for multiple environments without creating separate resources for each combination.
```hcl
Attach environment-specific policies
locals {
envpolicyattachments = flatten([
for env in var.environments : [
for policyarn in var.environmentpolicies[env] : {
env = env
policyarn = policyarn
}
]
])
}
resource "awsiamrolepolicyattachment" "envpolicies" {
foreach = {
for item in local.envpolicyattachments :
"${item.env}-${item.policyarn}" => item
}
role = awsiamrole.envroles[each.value.env].name
policyarn = each.value.policyarn
}
```
Reusability with Modules
Encapsulating the instance profile pattern in a module makes it reusable. This is particularly useful when the same pattern is needed across multiple projects or services. A well-designed module takes inputs for the name, managed policy ARNs, and custom policy JSON, and outputs the instance profile name and ARN.
In the following example, a module is created that includes the trust policy, role, instance profile, and policy attachments. The module uses for_each loops to attach multiple managed policies.
```hcl
modules/ec2-instance-profile/main.tf
variable "name" {
type = string
}
variable "managedpolicyarns" {
type = list(string)
default = []
}
variable "custompolicyjson" {
type = string
default = ""
}
data "awsiampolicydocument" "ec2trust" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
}
}
resource "awsiamrole" "this" {
name = "${var.name}-role"
assumerolepolicy = data.awsiampolicydocument.ec2trust.json
}
resource "awsiaminstanceprofile" "this" {
name = "${var.name}-profile"
role = awsiam_role.this.name
}
resource "awsiamrolepolicyattachment" "managed" {
foreach = toset(var.managedpolicyarns)
role = awsiamrole.this.name
policyarn = each.value
}
```
By using modules, you can standardize the creation of instance profiles across your organization. This ensures consistency and reduces the chance of errors.
Operational Considerations and Troubleshooting
While the implementation of IAM instance profiles in Terraform is straightforward, there are several operational considerations that must be addressed to ensure smooth operation.
Propagation Delay
After creating an instance profile, there can be a brief delay before it is available for use. If you immediately reference it in an EC2 instance, the launch might fail. To avoid this, add a depends_on relationship to ensure that the instance profile is fully created before the EC2 instance is launched.
hcl
resource "aws_instance" "app_server" {
# ... other arguments
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
depends_on = [aws_iam_instance_profile.ec2_profile]
}
Changing the Role
If you change the role in an instance profile, running instances will not pick up the change until they are stopped and restarted, or until the temporary credentials expire. This is a critical consideration for updates that require new permissions. In such cases, you may need to plan for a rolling restart of the instances to ensure they assume the new role.
Session Duration
The default maximum session duration for EC2 instance roles is one hour. AWS automatically refreshes the credentials before they expire. This means that applications running on the instance will always have valid credentials, provided they are properly configured to retrieve and use the refreshed credentials.
Comparison of Approaches
The following table summarizes the different approaches to creating IAM instance profiles in Terraform, highlighting their use cases and complexity.
| Approach | Complexity | Use Case | Key Features |
|---|---|---|---|
| Basic Setup | Low | Simple applications with static policies | Single role, single profile, managed policies |
| Custom Policies | Medium | Applications with specific resource access needs | Custom JSON policies, least privilege |
| Multi-Environment | High | Organizations with multiple environments | for_each loops, variable-driven policies |
| Module-Based | High | Reusable patterns across projects | Encapsulation, standardization |
Data Sources and Lookups
Terraform also provides a data source for aws_iam_instance_profile. This data source provides details about a specific IAM instance profile. It is useful when you need to reference an existing instance profile that was created outside of Terraform.
A minimal configuration to get started looks like this:
hcl
data "aws_iam_instance_profile" "example" {
# Required arguments
# Refer to the Terraform Registry docs for details
}
Refer to the Terraform Registry docs for all available arguments and details on how to use this data source.
Conclusion
IAM instance profiles are the proper way to grant EC2 instances access to AWS services. They eliminate the need for static credentials, automatically rotate security tokens, and integrate cleanly with Terraform. By using variables, loops, and modules, you can manage instance profiles at scale across multiple environments. Always follow the principle of least privilege when defining the policies attached to your instance profile roles.
The integration of IAM instance profiles with Terraform allows for declarative management of infrastructure that is both secure and scalable. Whether you are deploying a single application or a complex multi-environment setup, understanding the nuances of instance profiles, trust policies, and custom policies is essential. By leveraging Terraform's capabilities, such as for_each loops, custom modules, and data sources, you can build a robust and maintainable infrastructure that adheres to best practices.
As you advance in your Terraform journey, consider exploring more advanced IAM concepts such as SCPs (Service Control Policies) and organization-level policies. These can further enhance the security posture of your AWS account by enforcing boundaries at a higher level. Additionally, monitoring and auditing the usage of IAM roles and instance profiles can provide valuable insights into the security of your infrastructure. Tools like CloudTrail can help you track API calls made using the temporary credentials from instance profiles, ensuring that there are no unauthorized access attempts.
In summary, IAM instance profiles in Terraform are a cornerstone of secure AWS infrastructure. By following the patterns and best practices outlined in this guide, you can ensure that your EC2 instances have the appropriate access to AWS services while minimizing security risks.