Sitemap

Terraform + AWS — My first steps and what I would have liked to know beforehand

5 min readApr 22, 2024

--

Recently, I started working on a new project of mine. Being super passionate about the idea, I decided that I would do everything as best as I possibly could. This meant automating anything and everything that could be automated; that way, I could focus entirely on solving the problem I wanted to solve.

Following an agile workflow, I must be able to receive instant feedback from the people I’m working with. Transparency is a key pillar of agile, after all. With that in mind, I set out to find a way to automate not only the deployment, but also the creation of AWS resources.

Enter Terraform. Quoting their docs: “HashiCorp Terraform is an infrastructure as code tool that lets you define both cloud and on-prem resources in human-readable configuration files that you can version, reuse, and share”. It is worth noting that Terraform supports multiple cloud providers. So, even though I’m using it to provision AWS resources now, it can be used for any cloud provider of your choosing.

To help me get started, I found an article by Oscar Pickerill that explains, simply enough, what I needed to know. It even provides a repository that you can use as a starting point!

In a nutshell, I had a NextJS application that I wanted to deploy to AWS ECS. This article walks us through the process of setting up a GitHub Actions workflow that provisions all of the resources we need in AWS + deploys our app. Super easy.

At this point, I started to deviate from the article and ended up running into problems. I like to think these problems are justified.

The article suggests working with an AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY for authentication, but after reading a bit on the subject, I found that this is not actually recommended by AWS. Instead, they recommend creating a role in IAM with a small set of permissions granted to it and assuming it. Read more about the least privilege principle here. I set this up following the instructions provided in this link.

Here is the only significant change I had to make to the deploy-prod.yml file found in the repository. Wherever I found this:

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v3
with:
aws-access-key-id: ${{ env.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ env.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-2

I replaced it with this:

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v3
with:
aws-region: eu-west-2
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
role-session-name: GitHub_to_AWS_via_FederatedOIDC

It was at this moment that I knew I f***ed up.

Well, not really; it was bound to happen. I ran my action, and of course, my role had no permissions granted to it, so it failed. The issue was that it only specified a few of the permissions I was missing in each error message. Since ChatGPT wasn’t particularly helpful, and neither were the AWS docs, I had to run my workflow multiple times and grant permissions following each error message.

I hated every second of this, but in the end, I was able to write a working policy. Sharing this policy is the whole point of this article, because even if it is by no means a perfect solution (it grants more permissions than it should), I wish someone would have shared it with me at the beginning. I hope it gives you a head start in your journey towards automating your infrastructure.

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "a41215244ffb438f830fec2ae1743ad5",
"Effect": "Allow",
"Action": [
"ec2:AuthorizeSecurityGroupIngress",
"ec2:AuthorizeSecurityGroupEgress",
"ec2:CreateDefaultSubnet",
"ec2:CreateSecurityGroup",
"ec2:CreateTags",
"ec2:RevokeSecurityGroupIngress",
"ec2:RevokeSecurityGroupEgress",
"ec2:DeleteSecurityGroup",
"ec2:DescribeSecurityGroups",
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteSubnet",
"ec2:CreateTags",
"ec2:CreateSubnet",
"ec2:DescribeSubnets",
"ec2:ModifySubnetAttribute",
"ec2:DescribeAvailabilityZones",
"ec2:DescribeVpcs",
"ec2:CreateInternetGateway",
"ec2:CreateVpc",
"ec2:DeleteInternetGateway",
"ec2:DeleteVpc",
"ec2:DescribeVpcAttribute",
"ec2:ModifyVpcAttribute",
"ec2:AttachInternetGateway",
"ec2:DetachInternetGateway",
"ec2:DescribeInternetGateways",
"ec2:DescribeAccountAttributes",
"ec2:CreateRoute",
"ec2:DescribeRouteTables",
"ec2:DeleteRoute"
],
"Resource": "*"
},
{
"Sid": "b4e4f1a504a0477299708cfb2f32ef9e",
"Effect": "Allow",
"Action": [
"logs:ListTagsLogGroup",
"logs:DescribeLogGroups",
"logs:DeleteLogGroup",
"logs:TagResource",
"logs:CreateLogGroup"
],
"Resource": "*"
},
{
"Sid": "c63ccec4352a45e7b2343f5a1ec7cf32",
"Effect": "Allow",
"Action": "ecr:*",
"Resource": "*"
},
{
"Sid": "06d4262b991e4e7686bc023ec919f2ac",
"Effect": "Allow",
"Action": [
"ecs:PutAttributes",
"ecs:UpdateCluster",
"ecs:RunTask",
"ecs:CreateCluster",
"ecs:RegisterTaskDefinition",
"ecs:StartTask",
"ecs:DeleteCluster",
"ecs:StopTask",
"ecs:DeleteTaskDefinitions",
"ecs:DescribeTasks",
"ecs:DescribeClusters",
"ecs:DescribeTaskDefinition",
"ecs:DeregisterTaskDefinition",
"ecs:CreateService",
"ecs:DescribeServices",
"ecs:UpdateService",
"ecs:DeleteService"
],
"Resource": "*"
},
{
"Sid": "e22a71fce9824578bbc7f939a0dce111",
"Effect": "Allow",
"Action": [
"elasticloadbalancing:CreateLoadBalancer",
"elasticloadbalancing:CreateTargetGroup",
"elasticloadbalancing:DeleteTargetGroup",
"elasticloadbalancing:DeleteLoadBalancer",
"elasticloadbalancing:DeleteListener",
"elasticloadbalancing:RemoveTags",
"elasticloadbalancing:ModifyRule",
"elasticloadbalancing:AddTags",
"elasticloadbalancing:DescribeAccountLimits",
"elasticloadbalancing:CreateRule",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:ModifyLoadBalancerAttributes",
"elasticloadbalancing:ModifyTargetGroupAttributes",
"elasticloadbalancing:DeleteRule",
"elasticloadbalancing:ModifyTargetGroup",
"elasticloadbalancing:DescribeTargetGroupAttributes",
"elasticloadbalancing:DescribeTags",
"elasticloadbalancing:DescribeLoadBalancers",
"elasticloadbalancing:DescribeLoadBalancerAttributes",
"elasticloadbalancing:CreateListener",
"elasticloadbalancing:DescribeListeners"
],
"Resource": "*"
},
{
"Sid": "577b1c858ac5425887bd995e01464c06",
"Effect": "Allow",
"Action": [
"iam:GetRole",
"iam:ListInstanceProfilesForRole",
"iam:PassRole",
"iam:ListAttachedRolePolicies",
"iam:TagRole",
"iam:CreateRole",
"iam:DeleteRole",
"iam:ListRolePolicies",
"iam:AttachRolePolicy",
"iam:CreateServiceLinkedRole",
"iam:DetachRolePolicy",
"iam:GetRolePolicy",
"iam:CreatePolicy",
"iam:UpdateAssumeRolePolicy",
"iam:GetPolicy",
"iam:GetPolicyVersion",
"iam:ListPolicyVersions",
"iam:DeletePolicy",
"iam:DeleteRolePolicy"
],
"Resource": [
"arn:aws:iam::<account-id>:role/<task-execution-role>",
"arn:aws:iam::<account-id>:role/GitHubAction-AssumeRoleWithAction",
"arn:aws:iam::<account-id>:policy/*"
]
},
{
"Sid": "2336c0a3717d43a18ee23b053d9242ba",
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::terraform-state"
]
},
{
"Sid": "ec2232c98ec94db4ac01ff242317ce14",
"Effect": "Allow",
"Action": "s3:*Object",
"Resource": [
"arn:aws:s3:::terraform-state/*"
]
}
]
}

Don’t ask why I used UUIDs for the Sid values. I just never know what to write there, so it seemed like an easy way out. I did have to remove the - from each UUID, since otherwise it’s not valid for AWS.

Using this policy, this is what you should see after deploying the sample application from Oscar’s repository:

If you want to save some money, destroy your infrastructure after you’re done testing. This policy has you covered for this too:

Remember, for that you must empty your ECR repository first. Terraform won’t be able to delete a non-empty repository.

Again, this solution is by no means perfect. However, if you are getting started and you’re trying to not open up your entire account to your actions using admin credentials, then this is an acceptable starting point. It gives you enough permissions while also setting a limit.

I’d be happy to improve upon this policy definition with the help of readers who know more than me and build upon this so that newcomers and experienced AWS users alike can consult it. Feel free to leave suggestions in the comments!

--

--

Alvaro Nicoli
Alvaro Nicoli

Written by Alvaro Nicoli

I am a Full Stack Software Engineer and barista aficionado. Join me as I begin sharing my learnings and my ideas!

No responses yet