How to create EC2 instances with Terraform using an cross account Iam role

Mayamoraiss
8 min readFeb 12, 2023

--

Terraform is a great automation choice of tool to create Iaac (Infraestructure as a service) for AWS. This tutorial is a shorthand to show how to start using this tool.

The requisites of this tutorial are: have a AWS console account. (If you don’t have it: create now: https://us-east-1.console.aws.amazon.com/console/home?nc2=h_ct&region=us-east-1&src=header-signin#) and have a github or other versioning control system account.

The first step is installing terraform: https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli

Download and extract it in your operating system to the root of your main program files folder. Add the folder to your PATH (env. variable). To make sure that the installation succeded type in your terminal or Power Shell:

terraform --version

If the installation succeded it will show the terraform version like:

installation on windows

If your using VSCode you may need to reopen it to apply the changes. After that your able to create your very first Terraform aws cloud infraestructure.

First of all you should have to set two AWS accounts profiles. The first one (source) is going to be used to create the IAM Role to provide AmazonEC2FullAccess role to the the second account (destination) to create the EC2 instance on the source account behalf.

To set up a main AWS account profile in your machine use the commands “aws configure” in a terminal to set the access key ID (aws_access_key_id) and (aws_secret_access_key) and the region (in this case I’m using us-east-1):

Under your user’s computer folder there is a .aws folder (can be hidden). Under this folder you’ll find a credentials file that have your machine AWS accounts profiles like that:

AWS account local profiles

Open that file in your prefered text editor (in that I’m using VSCode) and configure the profiles.

If you don’t know how to obtain this credentials login in into IAM Service through your AWS console account and in users click in the desired user.

In the user page click in the tab “Security credentials” and into the “access keys” section click in the button “Create access key” to create new pair of access key secret key to your IAM user.

Now you’re ready to create your first Terraform AWS cloud resource. The following github repositories are going to be used: https://github.com/hashicorp/learn-terraform-aws-assume-role-iam a account is going to be used to use Terraform to create a cross account IAM role permission to perform EC2 operations to be used to other account. However you don’t need to have two AWS accounts you can use two IAM users to perform this.

The https://github.com/hashicorp/learn-terraform-aws-assume-ec2 is going to be used to use the IAM role created with the other repository to be creating a EC2 instance.

First of all clone or fork the https://github.com/hashicorp/learn-terraform-aws-assume-role-iam and open in your prefered editor. Note the main.tf file:


provider "aws" {
alias = "source"
profile = "source"
region = "us-east-1"
}

provider "aws" {
alias = "destination"
profile = "destination"
region = "us-east-1"
}

data "aws_caller_identity" "source" {
provider = aws.source
}

data "aws_iam_policy" "ec2" {
provider = aws.destination
name = "AmazonEC2FullAccess"
}

data "aws_iam_policy_document" "assume_role" {
provider = aws.destination
statement {
actions = [
"sts:AssumeRole",
"sts:TagSession",
"sts:SetSourceIdentity"
]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${data.aws_caller_identity.source.account_id}:root"]
}
}
}

resource "aws_iam_role" "assume_role" {
provider = aws.destination
name = "assume_role"
assume_role_policy = data.aws_iam_policy_document.assume_role.json
managed_policy_arns = [data.aws_iam_policy.ec2.arn]
tags = {}
}

In this file is set up the AWS as a provider each one of the accounts profile names and the region. It will create a role called assume_rule under the destination account using the account ID.

Inside the folder ./learn-terraform-aws-assume-role-iam run the following command to inicializate Terraform:

terraform init

If the command succeded you’re going to see something like this:

After that run the command terraform apply to create the IAM Role:

terraform apply

If everything worked fine you will see something like that connection the AWS STS service to authenticate and giving the ARN of the created assume_role at the end:

data.aws_caller_identity.source: Reading...
data.aws_iam_policy.ec2: Reading...
data.aws_caller_identity.source: Read complete after 1s [id=385401615023]
data.aws_iam_policy_document.assume_role: Reading...
data.aws_iam_policy_document.assume_role: Read complete after 0s [id=2318653861]
data.aws_iam_policy.ec2: Still reading... [10s elapsed]
data.aws_iam_policy.ec2: Read complete after 16s [id=arn:aws:iam::aws:policy/AmazonEC2FullAccess]

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.assume_role will be created
+ resource "aws_iam_role" "assume_role" {
+ arn = (known after apply)
+ assume_role_policy = jsonencode(
{
+ Statement = [
+ {
+ Action = [
+ "sts:TagSession",
+ "sts:SetSourceIdentity",
+ "sts:AssumeRole",
]
+ Effect = "Allow"
+ Principal = {
+ AWS = "arn:aws:iam::385401615023:root"
}
+ Sid = ""
},
]
+ Version = "2012-10-17"
}
)
+ create_date = (known after apply)
+ force_detach_policies = false
+ id = (known after apply)
+ managed_policy_arns = [
+ "arn:aws:iam::aws:policy/AmazonEC2FullAccess",
]
+ max_session_duration = 3600
+ name = "assume_role"
+ path = "/"
+ tags_all = (known after apply)
+ unique_id = (known after apply)

+ inline_policy {
+ name = (known after apply)
+ policy = (known after apply)
}
}

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

Changes to Outputs:
+ role_arn = (known after apply)

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.assume_role: Creating...
aws_iam_role.assume_role: Creation complete after 5s [id=assume_role]

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

Outputs:

role_arn = "arn:aws:iam::<your_aws_account_id>:role/assume_role"

Finally fork or clone the https://github.com/hashicorp/learn-terraform-aws-assume-ec2 repository and open the main folder ./learn-terraform-aws-assume-role-ec2 folder. Look at the file main.tf inside it in your prefered editor:

provider "aws" {
region = "us-east-1"
profile = "source"

assume_role {
role_arn = "ROLE_ARN"
}
}

data "aws_ami" "ubuntu" {
most_recent = true

filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}

filter {
name = "virtualization-type"
values = ["hvm"]
}

owners = ["099720109477"]
}

resource "aws_instance" "example" {
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
tags = {
Name = "learn-terraform-aws-assume-role"
}
}

The main.tf file is specifing the instance type and the AMI used to create. You can see how to set other configurations in the Terraform documentation: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ami

Change the role_arn to the ARN of the IAM role you got in the last step opening the IAM service under “Roles” you’re going to see the created role.

Click in this and copy the ARN and paste there.

Finally run the “terraform init” and after that the “terraform apply” to create the instance.

data.aws_ami.ubuntu: Reading...
data.aws_ami.ubuntu: Read complete after 1s [id=ami-09cd747c78a9add63]

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_instance.example will be created
+ resource "aws_instance" "example" {
+ ami = "ami-09cd747c78a9add63"
+ arn = (known after apply)
+ associate_public_ip_address = (known after apply)
+ availability_zone = (known after apply)
+ cpu_core_count = (known after apply)
+ cpu_threads_per_core = (known after apply)
+ get_password_data = false
+ host_id = (known after apply)
+ id = (known after apply)
+ instance_initiated_shutdown_behavior = (known after apply)
+ instance_state = (known after apply)
+ instance_type = "t2.micro"
+ ipv6_address_count = (known after apply)
+ ipv6_addresses = (known after apply)
+ key_name = (known after apply)
+ outpost_arn = (known after apply)
+ password_data = (known after apply)
+ placement_group = (known after apply)
+ primary_network_interface_id = (known after apply)
+ private_dns = (known after apply)
+ private_ip = (known after apply)
+ public_dns = (known after apply)
+ public_ip = (known after apply)
+ secondary_private_ips = (known after apply)
+ security_groups = (known after apply)
+ source_dest_check = true
+ subnet_id = (known after apply)
+ tags = {
+ "Name" = "learn-terraform-aws-assume-role"
}
+ tags_all = {
+ "Name" = "learn-terraform-aws-assume-role"
}
+ tenancy = (known after apply)
+ vpc_security_group_ids = (known after apply)

+ capacity_reservation_specification {
+ capacity_reservation_preference = (known after apply)

+ capacity_reservation_target {
+ capacity_reservation_id = (known after apply)
}
}

+ ebs_block_device {
+ delete_on_termination = (known after apply)
+ device_name = (known after apply)
+ encrypted = (known after apply)
+ iops = (known after apply)
+ kms_key_id = (known after apply)
+ snapshot_id = (known after apply)
+ tags = (known after apply)
+ throughput = (known after apply)
+ volume_id = (known after apply)
+ volume_size = (known after apply)
+ volume_type = (known after apply)
}

+ enclave_options {
+ enabled = (known after apply)
}

+ ephemeral_block_device {
+ device_name = (known after apply)
+ no_device = (known after apply)
+ virtual_name = (known after apply)
}

+ metadata_options {
+ http_endpoint = (known after apply)
+ http_put_response_hop_limit = (known after apply)
+ http_tokens = (known after apply)
}

+ network_interface {
+ delete_on_termination = (known after apply)
+ device_index = (known after apply)
+ network_interface_id = (known after apply)
}

+ root_block_device {
+ delete_on_termination = (known after apply)
+ device_name = (known after apply)
+ encrypted = (known after apply)
+ iops = (known after apply)
+ kms_key_id = (known after apply)
+ tags = (known after apply)
+ throughput = (known after apply)
+ volume_id = (known after apply)
+ volume_size = (known after apply)
+ volume_type = (known after apply)
}
}

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

Changes to Outputs:
+ instance_id = (known after apply)

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_instance.example: Creating...
aws_instance.example: Still creating... [10s elapsed]
aws_instance.example: Still creating... [20s elapsed]
aws_instance.example: Still creating... [30s elapsed]
aws_instance.example: Still creating... [40s elapsed]
aws_instance.example: Creation complete after 50s [id=i-0d4c73cabed34290b]

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

Outputs:

instance_id = "i-0d4c73cabed34290b"

If everything worked fine you’ll something like above at the end giving the instance ID.

That’s all! See you later.

--

--

Mayamoraiss

AWS Cloud engineer. A big fan of technology, coffee and chocolate.