Skip to main content

Terraform AWS

https://registry.terraform.io/providers/hashicorp/aws

Docs: https://registry.terraform.io/providers/hashicorp/aws/latest/docs

https://aws.amazon.com/solutions/partners/terraform-modules/

Tutorials:

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

default_tags {
tags = {
Name = "my-app"
}
}
}

Get AZs

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones

data "aws_availability_zones" "available" {
state = "available"
}

resource "aws_subnet" "subnet_a" {
availability_zone = data.aws_availability_zones.available.names[0] # "us-east-1a" if we are at "us-east-1"
}

output "list_of_az" {
value = data.aws_availability_zones.available[*].names
}
# list_of_az = [
# tolist([
# "us-east-1a",
# "us-east-1b",
# "us-east-1c",
# "us-east-1d",
# "us-east-1e",
# "us-east-1f",
# ]),
# ]

Get the Amazon Linux AMI

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami

data "aws_ami" "amazon-linux" {
most_recent = true
owners = ["amazon"]

filter {
name = "name"
values = ["amzn-ami-hvm-*-x86_64-ebs"]
}
}

resource "aws_launch_configuration" "example" {
image_id = data.aws_ami.amazon-linux.id
}

EC2 user data

Inline with heredoc

resource "aws_instance" "example" {
ami = "ami-0fb653ca2d3203ac1"
instance_type = "t2.micro"
vpc_security_group_ids = [aws_security_group.instance.id]

user_data = <<-EOF
#!/bin/bash
echo "Hello, World" > index.html
nohup busybox httpd -f -p 8080 &
EOF

user_data_replace_on_change = true

tags = {
Name = "example"
}
}

External file

user-data.sh
#!/bin/bash

# Update the system and install necessary packages
yum update -y
yum install -y httpd

# Start the Apache server
systemctl start httpd
systemctl enable httpd
resource "aws_instance" "example" {
user_data = file("user-data.sh")
}

Base 64

On a aws_launch_template you need to encode the data in base64 (see the docs), otherwise you get this error:

Error: creating EC2 Launch Template (terraform-20241008173227703800000001): operation error EC2: CreateLaunchTemplate, https response error StatusCode: 400, RequestID: 38094e48-f95f-4a60-afc9-87645f3d4f63, api error InvalidUserData.Malformed: Invalid BASE64 encoding of user data.

Use user_data = filebase64("user-data.sh") or user_data = filebase64("${path.module}/user-data.sh"). If you need to interpolate some variable into the user data then do:

locals {
user_data = <<-EOF
#!/bin/bash
echo "Hello, World" > index.html
nohup busybox httpd -f -p ${var.server_port} &
EOF
}

resource "aws_launch_template" "example" {
user_data = base64encode(local.user_data)
}

Examples

Modules

VPC

Auto Scaling

We need to use create_before_destroy in aws_launch_configuration and aws_launch_configuration.

From https://developer.hashicorp.com/terraform/tutorials/aws/aws-asg#ec2-launch-template

You cannot modify a launch configuration, so any changes to the definition force Terraform to create a new resource. The create_before_destroy argument in the lifecycle block instructs Terraform to create the new version before destroying the original to avoid any service interruptions.

From 'Terraform: Up and Running' (p. 68):

Launch configurations are immutable, so if you change any parameter, Terraform will try to replace it. Normally, when replacing a resource, Terraform would delete the old resource first and then creates its replacement, but because your ASG how has a reference to the old resource, Terraform won't be able to delete it.

If you use desired_capacity attribute, when running apply Terraform will scale up or down the number of instances currently running to match the desired_capacity (~ desired_capacity = 2 -> 1). To avoid this, use the ignore_changes lifecycle (source):

resource "aws_autoscaling_group" "example" {
min_size = 1
max_size = 3
desired_capacity = 1

lifecycle {
ignore_changes = [desired_capacity]
}
}

ECS

EKS

Lambda

RDS

Multiple AWS accounts

AWS Control Tower Account Factory for Terraform:

Authentication

See the documentation for all the possible authentication methods: https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration

Ideally, the provider "aws" block should be free of authentication configuration:

provider "aws" {}

Ideal, because it avoids having secrets.

The provider assumes an IAM role.

Authentication with the config and credentials files

If you are authenticated in the AWS CLI with the ~/.aws/config and ~/.aws/credentials files, it picks the credentials automatically. This is because the aws provider uses the AWS SDK for Go under the hood.

By default it uses the default profile, but you can specify different one:

provider "aws" {
profile = "some-profile"
}

Authentication with environment variables

export AWS_ACCESS_KEY_ID="ZIO5FTAECEH4LATF66RY"
export AWS_SECRET_ACCESS_KEY="FSRfz3SLa2fM2Hvzc1EQRjpNGNNO7t6B6WIj5XYg"
export AWS_REGION="us-east-1"

Note that there are no spaces before and after the =.

Advantages:

  • The secrets are not going to be stored in the state.
  • No need to change the code if we change from an IAM user to an IAM role.
  • Portable.

This is the worse option.

Never ever commit the access and secret keys into version control! Even if the repository is private!

provider "aws" {
access_key = "ZIO5FTAECEH4LATF66RY"
secret_key = "FSRfz3SLa2fM2Hvzc1EQRjpNGNNO7t6B6WIj5XYg"
region = "us-east-1"
}

Obviously, we would use variables instead of hardcoding the credentials, and then provide values for them using a .tfvars file, the -var option in the CLI, etcetera.

provider "aws" {
access_key = var.access_key
secret_key = var.secret_key
region = var.region
}

variable "access_key" {
type = string
sensitive = true
description = "AWS Access Key ID"
}

variable "secret_key" {
type = string
sensitive = true
description = "AWS Secret Access Key"
}

variable "region" {
type = string
default = "us-east-1"
description = "AWS region"
}
danger

If we use variables, the secrets are going to be stored in the state file.