Terraform AWS Crash Course

Introduction

Terraform manages resources in a declarative manner. By that I mean that the instructions of our files will not be carried out step by step as we would probably expect. Instead, Terraform is going to ensure that our code matches our state (what we have in the cloud) and make the changes, if needed so that our code matches the state.

Init a new project

Create a new folder, the main.tf file and initialize the terraform project

mkdir tf_aws_sandbox && cd tf_aws_sandbox && touch main.tf && terraform init

1. AWS Setup

Generate keys

First thing you need to get your set of keys from AWS so we can connect to your account from terraform. If you do not have them already this section will cover how to generate them.

  1. Click on your profile
  2. Security credentials

2. Terraform

This section will detail bare bone implementation of the most known and used AWS resources. Baer in mind that it is out of the scope of this guide to cover security best practices.

Create the provider

Thorough the aws provider we can connect to our AWS account using the set of keys prior mentioned in the section above.

# main.tf
provider "aws" {
# AWS REGION
region = "eu-west-2"
# AWS CREDENTIALS
access_key = ""
secret_key = ""
}

How to provision AWS with resources

One of the nicest parts of terraform. And is really the main selling point of terraform is that, regardless of what provider you’re using, so whether you’re trying to create a resource within AWS, or a resource within Azure or GCP, it’s going to use the same exact syntax from a terraform side, so that you don’t actually have to learn the underlying API from you know, Azure, or AWS, or GCP. And the main syntax is going to be you type in the word resource.

resource "<provider>_<resource_type>" "name" {
# stuff goes in here
}

EC2 AMIs & Manual workflow

An AMI is just an image i.e. ubuntu, fedora

  1. Go to AWS console
  2. EC2
  3. Launch instance
  4. Select an image, be sure you are on the free tier
  5. Review and launch. It should look something similar to the image below
  6. Proceed without a key pair
  7. You do not need to launch the instance, this was just a recap of the workflow in the console so it will make more sense when automating that with terraform

Create it using Terraform

To your existing main.tf file, where you have the provider from the section 1 add the following

# main.tf
resource "aws_instance" "test_terraform_ec2" {
# to find the ami go on the aws console, ec2, launch instance
# this ami might not work as they change the amis quite often
ami = "ami-0015a39e4b7c0966f"
instance_type = "t2.micro"
}
terraform plan
terraform apply # type yes and press enter
➜  aws terraform 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: yesaws_instance.test_terraform_ec2: Creating...
aws_instance.test_terraform_ec2: Still creating... [10s elapsed]
aws_instance.test_terraform_ec2: Still creating... [20s elapsed]
aws_instance.test_terraform_ec2: Still creating... [30s elapsed]
aws_instance.test_terraform_ec2: Creation complete after 31s [id=i-081f86ae5ca64fdc3]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
  1. Go to AWS console
  2. EC2
  3. Instances (running)

Updating resources

Now if you are to update the [main.tf](<http://main.tf>) file with some other configs to our EC2 instance, Terraform will update the current existing instance according to our modifications.

# main.tf 
resource "aws_instance" "test_terraform_ec2" {
# to find the ami go on the aws console, ec2, launch instance
# this ami might not work as they change the amis quite often
ami = "ami-0015a39e4b7c0966f"
instance_type = "t2.micro"
tags = {
Name = "EC2_Ubuntu_Test"
}
}
terraform apply
terraform plan
terraform destroy

IAM

When creating a new lambda it is ideal to have a separate IAM for it. This can be achieved in terraform using the code below

resource "aws_iam_role" "iam_for_lambda" {
name = "iam_for_lambda"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}

Local variables

As the code to provision the lambda function has to be inside a .zip file we will set up local variables that we can use later on as references for the archiving process and the creation of the lambda process.

# LOCAL VARIABLES
locals {
lambda_zip_name = "lambda_code.zip"
lambda_zip_location = "${path.module}/${local.lambda_zip_name}"
}

Creating the .zip file

Make your folder structure to resemble the one below

.
├── lambda_code.zip
├── main.tf
├── resources
│ └── lambda
│ └── index.js
├── terraform.tfstate
└── terraform.tfstate.backup
module.exports.handler = async (event) => {
console.log('Event: ', event);
let responseMessage = 'Ciao Cutie!';

return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: responseMessage,
}),
}
}
# main.tf
# CREATE ZIP FIE
data "archive_file" "init" {
type = "zip"
source_file = "${path.module}/resources/lambda/index.js"
output_path = local.lambda_zip_location
}

Lambda

# main.tf
resource "aws_lambda_function" "test_lambda" {
filename = local.lambda_zip_name
function_name = "lambda_function_name"
role = aws_iam_role.iam_for_lambda.arn
handler = "index.handler"
source_code_hash = filebase64sha256("${local.lambda_zip_location}") runtime = "nodejs12.x" environment {
variables = {
testing = "is cool"
}
}
}

Updating resources

Now if you are to update the [main.tf](<http://main.tf>) file with some other configs to our EC2 instance, Terraform will update the current existing instance according to our modifications.

# main.tf 
resource "aws_instance" "test_terraform_ec2" {
# to find the ami go on the aws console, ec2, launch instance
# this ami might not work as they change the amis quite often
ami = "ami-0015a39e4b7c0966f"
instance_type = "t2.micro"
tags = {
Name = "EC2_Ubuntu_Test"
}
}
terraform apply
terraform plan
terraform destroy

IAM

When creating a new lambda it is ideal to have a separate IAM for it. This can be achieved in terraform using the code below

resource "aws_iam_role" "iam_for_lambda" {
name = "iam_for_lambda"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}

Local variables

As the code to provision the lambda function has to be inside a .zip file we will set up local variables that we can use later on as references for the archiving process and the creation of the lambda process.

# LOCAL VARIABLES
locals {
lambda_zip_name = "lambda_code.zip"
lambda_zip_location = "${path.module}/${local.lambda_zip_name}"
}

Creating the .zip file

Make your folder structure to resemble the one below

.
├── lambda_code.zip
├── main.tf
├── resources
│ └── lambda
│ └── index.js
├── terraform.tfstate
└── terraform.tfstate.backup
module.exports.handler = async (event) => {
console.log('Event: ', event);
let responseMessage = 'Ciao Cutie!';

return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: responseMessage,
}),
}
}
# main.tf
# CREATE ZIP FIE
data "archive_file" "init" {
type = "zip"
source_file = "${path.module}/resources/lambda/index.js"
output_path = local.lambda_zip_location
}

Lambda

# main.tf
resource "aws_lambda_function" "test_lambda" {
filename = local.lambda_zip_name
function_name = "lambda_function_name"
role = aws_iam_role.iam_for_lambda.arn
handler = "index.handler"
source_code_hash = filebase64sha256("${local.lambda_zip_location}") runtime = "nodejs12.x" environment {
variables = {
testing = "is cool"
}
}
}
  1. We set up a new IAM role for our lambda
  2. We define the local variables for the filename and the file location of the zip file
  3. We zip our code
  4. We create the lambda instance, provision it with our code and set up the env vars

S3 Bucket

# main.tf
**resource "aws_s3_bucket" "test_terraform_bucket" {
tags = {
Name = "terraformBucket"
Environment = "Dev"
}
}
resource "aws_s3_bucket_acl" "test_terraform_bucket_acl" {
bucket = aws_s3_bucket.test_terraform_bucket.id
acl = "private"
}**

SQS

# main.tf
resource "aws_sqs_queue" "terraform_deadletter_queue" {
name = "deadletterQueue"
}
# main.tf
resource "aws_sqs_queue" "test_terraform_queue" {
name = "terraform_example_queue"
delay_seconds = 90
max_message_size = 2048
message_retention_seconds = 86400
receive_wait_time_seconds = 10
# SET UP DEAD LETTER QUEUE
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.terraform_deadletter_queue.arn
maxReceiveCount = 4
})
# DEAD LETTER QUEUE PERMISSIONS
redrive_allow_policy = jsonencode({
redrivePermission = "byQueue",
sourceQueueArns = ["${aws_sqs_queue.terraform_deadletter_queue.arn}"]
})
tags = {
Environment = "Dev"
}
}
# main.tf 
resource "aws_sqs_queue_policy" "test_terraform_queue_policy" {
queue_url = aws_sqs_queue.test_terraform_queue.id
policy = file("${path.module}/resources/SQS/delivery_policy.json")
}
# resources/SQS/delivery_policy.json
{
"Version": "2012-10-17",
"Id": "sqspolicy",
"Statement": [
{
"Sid": "First",
"Effect": "Allow",
"Principal": "*",
"Action": "sqs:SendMessage",
"Resource": "${aws_sqs_queue.test_terraform_queue.arn}"
}
]
}

SNS

Create a topic

# main.tf
resource "aws_sns_topic" "test_terraform_topic" {
name = "cute_topic"
delivery_policy = file("${path.module}/resources/SNS/topic/delivery_policy.json")
}

Subscribe a queue to the topic

# main.tf
resource "aws_sns_topic_subscription" "test_terraform_subscription_queue" {
topic_arn = aws_sns_topic.test_terraform_topic.arn
protocol = "sqs"
endpoint = aws_sqs_queue.test_terraform_queue.arn
filter_policy = file("${path.module}/resources/SNS/subscription/filter_policy.json")
}
{
"anyMandatoryKey": ["any", "of", "these"],
"anyOtherOptionalKey": ["any", "of", "these"]
}

Subscribe the lambda function to the topic

# main.tf
resource "aws_sns_topic_subscription" "test_terraform_subscription_lambda" {
topic_arn = aws_sns_topic.test_terraform_topic.arn
protocol = "lambda"
endpoint = aws_lambda_function.test_lambda.arn
}

Parameter store

# main.tf
resource "aws_ssm_parameter" "test_terraform_parameter" {
name = "ciao"
type = "String"
value = "cutie"
}
resource "aws_ssm_parameter" "test_terraform_parameter2" {
name = "supersecret"
type = "SecureString"
value = "megasecret"
}

Takeaways

If you made it this far I’d like to congratulate you. Hopefully now you’ve got a better understanding of terraform and how to work the AWS provider.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store