Terraform Setup On AWS
Please visit my website and subscribe to my youtube channel for more articles
Terraform is a cross-platform tool, which means that it does not only interact with AWS — it can also interact with a multitude of other platforms, such as GCE, VMware, OpenStack, and Azure.
The main benefit of it is that rerunning a declarative definition will never do the same job twice, whereas executing the same shell script will most likely break something on the second run.
- Terraform provides special configuration language to declare your infrastructure in simple text templates.
- Terraform also implements a complex graph logic, which allows you to resolve dependencies, intelligibility and reliability.
- When it comes to servers, Terraform has multiple ways of configuring and wiring them up with existing configuration management tools.
- Terraform is not platform agnostic in the sense described earlier, but it allows you to use multiple providers in a single template, and there are ways to make it somewhat platform agnostic.
- Terraform keeps track of the current state of the infrastructure it created and applies delta changes when something needs to be updated, added, or deleted. It also provides a way to import existing resources and target only specific resources.
- Terraform is easily extendable with plugins, which should be written in the Go programming language.
Install Teraform on Linux
For latest version click here
sudo yum install unzip (if not installed)sudo yum install wget (if not installed)wget https://releases.hashicorp.com/terraform/0.11.6/terraform_0.11.6_linux_amd64.zipsudo unzip terraform_0.11.6_linux_amd64.zip -d /home/ec2-user/terraform_installexport PATH=$PATH:/home/ec2-user/terraform_installFinally, let's verify our installation:terraform -v

Terraform Installation is completed successfully.
Write first terraform template
mkdir packt-terraform && cd packt-terraform
touch template.tf
To apply the template, you need to run the terraform apply command. In Terraform, when you run apply, it will read your templates and it will try to create an infrastructure exactly as it’s defined in your templates
terraform apply
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
After each run is finished, you get the number of resources that you’ve added, changed, and destroyed. In this case, it did nothing, as we just have an empty file instead of a real template.

We have successfully tested terraform installation and how it works.
Now we will launch EC2 instance using Terraform. We already know how we can launch EC2 instance manually using GUI

Configure AWS CLI
You need python-pip on that machine to install awscli
yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpmsudo yum install python-pipsudo pip install awscli --upgrade pipRun the below command after logging to some user, for eg -ec2-user not as a root user
aws configure
Once you configured your keys using aws configure, everything is stored under
cd ~/.aws/
pwd
/home/ec2-user/.aws
cat credentials
Click Here for other options to configure credentials.
Now edit template.tf
vi template.tf
provider "aws" {
region = "us-west-2"
}
run terraform apply
You could get the below error
Plugin reinitialization required. Please run “terraform init”.
Reason: Could not satisfy plugin requirements.
Plugins are external binaries that Terraform uses to access and manipulate
resources. The configuration provided requires plugins which can’t be located,
don’t satisfy the version constraints, or are otherwise incompatible.
1 error(s) occurred:
* provider.aws: no suitable version installed
version requirements: “(any version)”
versions installed: none
Terraform automatically discovers provider requirements from your
configuration, including providers used in child modules. To see the
requirements and constraints from each module, run “terraform providers”.
Error: error satisfying plugin requirements
You must have to run terraform init to do the initialization
terraform initterraform apply

Create EC2 instance using terraform
Terraform file for creating EC2 instance. You need to find a “ami-id” on AWS site according to your instance type.
provider "aws" {
region = "us-west-2"
}
# Resource configuration
resource “aws_instance” “test-instance” {
ami = “ami-223f945a”
instance_type = “t2.micro”
tags {
Name = “test”
}
}
Run the below command
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: yesFor autoapprove, run below command
terraform apply -auto-approve
This is how EC2 instance get created. You can check on AWS console
Working with State
Terraform didn’t simply create an instance and forget about it. It actually saved everything it knows about this instance to a special file, named the state file. In this file, Terraform stores the state of all the resources it created. This file is saved to the same directory where the Terraform template is, with the .tfstate extension. The format of the state file is simple json
Terraform plan — this command shows you what applying do by checking the template, state file, and actual state of the resource. It is recommended to use this before running apply command to ensure accidental deletion of any resources
terraform validate — to check the syntax of the file
terraform fmt- to do the formatting of the file
Destroy the template
terraform destroy
We have successfully create EC2 instance and destroy it completely.
Resource Dependencies and Modules
Creating a VPC (Virtual Private Cloud)
Create a new file vpc.tf
provider “aws” {
region = “us-west-2”
}
resource “aws_vpc” “my_vpc” {
cidr_block = “10.0.0.0/16”
}
We need subnet to put instance in a network. This subnet belongs to a previously created VPC. This means that we have to pass a VPC ID when we create it. We don’t have to hardcode it though. Terraform, via interpolation syntax, allows us to reference any other resource it manages using the following syntax: ${RESOURCE_TYPE.RESOURCE_NAME.ATTRIBUTE_NAME}.
Subnet creation with VPC
provider “aws” {
region = “us-west-2”
}
resource “aws_vpc” “my_vpc” {
cidr_block = “10.0.0.0/16”
}resource "aws_subnet" "public" {
vpc_id = "${aws_vpc.my_vpc.id}"
cidr_block = "10.0.1.0/24"
}
Dependency Graph
A dependency graph allows us, for example, to properly order the creation or destruction of nodes or to order a set of commands. It’s all about ordering, actually.
There are just three types of nodes in a Terraform graph:
- Resource node
- Provider configuration node
- Resource meta-node
What the resource node and provider configuration node are responsible for is clear: the provider node configures a provider (AWS, in our examples) and the resource node manages an entity of this provider (EC2, VPC, and so on, in the case of AWS). A resource meta-node doesn’t really do anything special; it is used for convenience and makes a graph more pretty. It is applicable only if you specify a count parameter greater than one.
terraform graph
[ec2-user@ip-172–31–22–171 packt-terraform]$ terraform graph
digraph {
compound = “true”
newrank = “true”
subgraph “root” {
“[root] aws_subnet.public” [label = “aws_subnet.public”, shape = “box”]
“[root] aws_vpc.my_vpc” [label = “aws_vpc.my_vpc”, shape = “box”]
“[root] provider.aws” [label = “provider.aws”, shape = “diamond”]
“[root] aws_subnet.public” -> “[root] aws_vpc.my_vpc”
“[root] aws_vpc.my_vpc” -> “[root] provider.aws”
“[root] meta.count-boundary (count boundary fixup)” -> “[root] aws_subnet.public”
“[root] provider.aws (close)” -> “[root] aws_subnet.public”
“[root] root” -> “[root] meta.count-boundary (count boundary fixup)”
“[root] root” -> “[root] provider.aws (close)”
}
}
you could install graphiz, click here
terraform graph | dot -Tpng > graph.png

terraform taint -> marks a single resource for recreation. The resource will be destroyed and then created again.
terraform taint aws_vpc.my_vpc
The resource aws_vpc.my_vpc in the module root has been marked as tainted!Terraform will perform the following actions:-/+ aws_subnet.public (new resource required)
id: "subnet-d16cd1a8" => <computed> (forces new resource)
assign_ipv6_address_on_creation: "false" => "false"
availability_zone: "us-west-2b" => <computed>
cidr_block: "10.0.1.0/24" => "10.0.1.0/24"
ipv6_cidr_block: "" => <computed>
ipv6_cidr_block_association_id: "" => <computed>
map_public_ip_on_launch: "false" => "false"
tags.%: "1" => "1"
tags.Name: "my-subnet" => "my-subnet"
vpc_id: "vpc-1b87d062" => "${aws_vpc.my_vpc.id}" (forces new resource)-/+ aws_vpc.my_vpc (tainted) (new resource required)
id: "vpc-1b87d062" => <computed> (forces new resource)
assign_generated_ipv6_cidr_block: "false" => "false"
cidr_block: "10.0.0.0/16" => "10.0.0.0/16"
default_network_acl_id: "acl-29904d51" => <computed>
default_route_table_id: "rtb-961857ee" => <computed>
default_security_group_id: "sg-e18a209f" => <computed>
dhcp_options_id: "dopt-f4c22b8d" => <computed>
enable_classiclink: "false" => <computed>
enable_classiclink_dns_support: "false" => <computed>
enable_dns_hostnames: "false" => <computed>
enable_dns_support: "true" => "true"
instance_tenancy: "default" => <computed>
ipv6_association_id: "" => <computed>
ipv6_cidr_block: "" => <computed>
main_route_table_id: "rtb-961857ee" => <computed>
tags.%: "1" => "1"
tags.Name: "my-vpc" => "my-vpc"
Terraform has got us covered: after recreating a VPC, it will also recreate a subnet because it knows that a subnet depends on the VPC to exist. As AWS doesn’t allow simply changing the VPC ID of an existing subnet, Terraform will force the creation of a completely new subnet.
Controlling dependencies with depends_on and ignore_changes
For each resource, you can specify the depends_on parameter, which accepts a list of resources that this resource depends on. As a result, this resource won’t be created until the ones listed inside this parameter are created.
resource "aws_instance" "master-instance" {
ami = "ami-9bf712f4"
instance_type = "t2.micro"
subnet_id = "${aws_subnet.public.id}"
}
resource "aws_instance" "slave-instance" {
ami = "ami-9bf712f4"
instance_type = "t2.micro"
subnet_id = "${aws_subnet.public.id}"
depends_on = ["aws_instance.master-instance"]
}
With depends_on, all resources would be created sequentially. Without it, both EC2 instances will be created in parallel
Now, let’s say we want to include a private hostname of master in the list of tags of the slave, but we don’t want to update it if master was recreated. To achieve this, we will use the ignore_changes parameter. This parameter is part of lifecycle block, responsible for a few other create/destroy-related parameters. The ignore_changes parameter accepts the list of parameters to ignore when updating, in our case -tags:
resource "aws_instance" "slave-instance" {
ami = "ami-9bf712f4"
instance_type = "t2.micro"
subnet_id = "${aws_subnet.public.id}"
tags {
master_hostname = "${aws_instance.master-instance.private_dns}"
}
lifecycle {
ignore_changes = ["tags"]
}
}
The most common use case for ignore_changes is, perhaps, user_data for cloud instances. For most providers, if you change user_data (the script to be executed on instance creation by the cloud-init utility), Terraform will try to recreate the instance. It is often unwanted behavior because, most likely, you use the same user_data string for multiple instances and you want changes to be applied only for new instances, while keeping the others running (or by recreating them one by one yourself).
- The create_before_destroy Boolean parameter allows us to tell Terraform to first create a new resource and then destroy the previous one in the case of recreation.
- The prevent_destroy parameter, also Boolean, marks a resource as indestructible and can save you some nerves. One example of a resource that can benefit from this option is an Elastic IP — a dedicated IP address inside AWS that you can attach to an EC2 instance.
Create EC2 instance with security group
provider "aws" {
region = "us-west-2"
}
resource "aws_vpc" "my_vpc" {
cidr_block = "10.0.0.0/16"
tags {
Name = "my-vpc"
}
}
resource "aws_subnet" "public" {
vpc_id = "${aws_vpc.my_vpc.id}"
cidr_block = "10.0.1.0/24"
tags {
Name = "my-subnet"
}
}resource "aws_security_group" "allow_http" {
name = "allow_http"
description = "Allow HTTP traffic"
vpc_id = "${aws_vpc.my_vpc.id}"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "mighty-trousers" {
ami = "ami-223f945a"
instance_type = "t2.micro"
subnet_id = "${aws_subnet.public.id}"
vpc_security_group_ids = ["${aws_security_group.allow_http.id}"]
}
Configuration in Terraform ( How to use variables)
A template with only hardcoded data in it is a bad template. You can’t reuse it in other projects without modifying it. You will always have to update it by hand if some value changes. And you have to store a lot of information that doesn’t really belong to the infrastructure template. how to make Terraform templates more configurable.
With the default value in place, Terraform won’t ask for the value interactively anymore. It will pick default value unless other sources of variables are present.
Variables.tf
Define a variable
variable "region" {
description = "AWS region. Changing it will lead to loss of complete stack."
default = "eu-central-1"
}
template.tf
provider "aws" {
region = "${var.region}"
}
There are three types of variables you can set:
- the string variables (default ones)
- the map variables
- the list variables
Map Variables
Map is a lookuptable, where you specify multiple keys with different values. You can then pick the value depending on the key.
For the development and testing purpose, its ok to use t2.micro instance but when it comes to production we need high level instances so what we want, actually, is a way to use different instance types depending on the environment stack is deployed to. Let’s assume that we have only three environments: dev, prod, and test.
Lets define two variable environment and instance_type
variable "environment" { default = "dev" }
variable "instance_type" {
type = "map"
default = {
dev = "t2.micro"
test = "t2.medium"
prod = "t2.large"
}
}
Configure DataSource
Create one VPC manually and then use it as a VPC data source. Add it to your template
data "aws_vpc" "management_layer" {
id = "vpc-c36cbdab"
}
Create VPC peering connection between manually created VPC and terraform created VPC
data "aws_vpc" "management_layer" {
id = "vpc-c36cbdab"
}
resource "aws_vpc" "my_vpc" {
cidr_block = "${var.vpc_cidr}"
}
resource "aws_vpc_peering_connection" "my_vpc-management" {
peer_vpc_id = "${data.aws_vpc.management_layer.id}"
vpc_id = "${aws_vpc.my_vpc.id}"
auto_accept = true
}
Modules
Modules in Terraform are used to group multiple resource
create a directory structure as shown below. We are creating separate folder structure for all the modules.
Package structure as shown below

Inside a modules folder, for now we will use only vpc and ec2-instance
Now in main.tf, this show how to access the modules and also the variable like vpc_id for ec2-instance
provider "aws" {
alias="sandbox2"
region = "${var.region}"
access_key = "${var.aws_access_key_sandbox2}"
secret_key = "${var.aws_secret_key_sandbox2}"}module "vpc" {
providers = {
"aws" = "aws.sandbox2"
}source = "./modules/vpc"}
module "ec2_instance" {
providers = {
"aws" = "aws.sandbox2"
}
source = "./modules/ec2-instance"
vpc_id = "${module.vpc.vpc_id}"}
Now in VPC folder, you need to create a vpc_id as output so that it can be used with other modules like this
resource “aws_vpc” “terraform_vpc” {
cidr_block = “${var.vpc-fullcidr}”
#### this 2 true values are for use the internal vpc dns resolution
enable_dns_support = true
enable_dns_hostnames = true
tags {
Name = “terraform_vpc”
}
}
output “vpc_id” {
value = “${aws_vpc.terraform_vpc.id}”
}
Now how to use that vpc_id, you need to create a variable and then use it as shown below
variable vpc_id {}
resource "aws_key_pair" "auth" {
key_name = "${var.key_name}"
public_key = "${file(var.public_key_path)}"
}
resource "aws_instance" "webserver" {
ami = "${lookup(var.ami, var.region)}"
connection {
# The default username for our AMI
user = "ec2-user"
host = "${aws_instance.webserver.public_ip}"# The connection will use the local SSH agent for authentication.
}instance_type = "t2.micro"
associate_public_ip_address = "true"
subnet_id = "${var.public_subnet_id}"
vpc_security_group_ids = ["${var.FrontEnd_SG_id}"]
key_name = "${aws_key_pair.auth.id}"
tags {
Name = "webserver"
}
}
Retrieving module data with outputs — one more example
In an output, you define which data you want to be returned by the module
You have 2 folders (VPC,Network), now VPC is created inside VPC folder so the vpc_id is not directly accessible on network folder. You will add below line on the vpc.tf file
output “vpc_id” {
value = “${aws_vpc.demo_vpc.id}”
}module
Now on main.tf
module "network" {
source = "./modules/network"
vpc_id = "${module.vpc.vpc_id}"
}
on route-network.tf where you want to use vpc_id. You have to declare variable , name of the variable should be same as we have declared in output and then access it using var.
variable “vpc_id” {}
resource “aws_internet_gateway” “gw” {
vpc_id = “${var.vpc_id}”
tags {
Name = “internet gw terraform generated”
}
}
Now for modules first you need to run
terraform get
this command will first load all the modules

terraform init

terraform plan ( to check what will get installed before running it)

terraform apply

Path to the SSH public key to be used for authentication. Ensure this keypair is added to your local SSH agent so provisioners can connect.
ssh-keygen (Generate keys if not exist already)
Adding your SSH key to the ssh-agent
Ensure ssh-agent is enabled:
start the ssh-agent in the background
eval "$(ssh-agent -s)"
Agent pid 59566
Add your SSH key to the ssh-agent. If you used an existing SSH key rather than generating a new SSH key, you’ll need to replace id_rsa in the command with the name of your existing private key file.
$ ssh-add ~/.ssh/id_rsa
How to make ssh connection to host
variable public_subnet_id {}
variable private_subnet_id {}
variable FrontEnd_SG_id {}
variable Database_SG_id {}
variable vpc_id {}
resource "aws_key_pair" "auth" {
key_name = "${var.key_name}"
public_key = "${file(var.public_key_path)}"
}
resource "aws_instance" "webserver" {
ami = "${lookup(var.ami, var.region)}"
connection {
# The default username for our AMI
user = "ec2-user"
host = "${aws_instance.webserver.public_ip}"# The connection will use the local SSH agent for authentication.
}instance_type = "t2.micro"
associate_public_ip_address = "true"
subnet_id = "${var.public_subnet_id}"
vpc_security_group_ids = ["${var.FrontEnd_SG_id}"]
key_name = "${aws_key_pair.auth.id}"
tags {
Name = "webserver"
}
}
SSH connection to EC2 Instance is completed successfully.
Maintain Terraform state file to S3 or dynamoDB
For now, our terraform state file is storing locally. Now we will store it in S3
If you are working on a team, then its best to store the terraform state file remotely so that many people can access it. In order to setup terraform to store state remotely you need two things: an s3 bucket to store the state file in and an terraform s3 backend resource.
DynamoDB
If the state file is stored remotely so that many people can access it, then you risk multiple people attempting to make changes to the same file at the exact same time. So we need to provide a mechanism that will “lock” the state if its currently in-use by another user. We can accomplish this by creating a dynamoDB table for terraform to use.
Example to create S3 bucket and dynamodb table
provider "aws" {
region = "${var.region}"
access_key = "${var.aws_access_key_sandbox2}"
secret_key = "${var.aws_secret_key_sandbox2}"}# terraform state file setup
# create an S3 bucket to store the state file in
resource "aws_s3_bucket" "terraform-state-storage-s3" {
bucket = "name of your bucket"versioning {
enabled = true
}lifecycle {
prevent_destroy = true
}tags {
Name = "S3 Remote Terraform State Store"
}
}# create a dynamodb table for locking the state file
resource "aws_dynamodb_table" "dynamodb-terraform-state-lock" {
name = "terraform-state-lock-dynamo"
hash_key = "LockID"
read_capacity = 20
write_capacity = 20attribute {
name = "LockID"
type = "S"
}tags {
Name = "DynamoDB Terraform State Lock Table"
}
}
Now in your terraform code include this
Your backend
configuration cannot contain interpolated variables. This is because this configuration is initialized prior to Terraform parsing these variables.
terraform {
backend “s3”
{ bucket = “bucketname”
dynamodb_table = “terraform-state-lock-dynamo”
region = “us-west-2”
key = “terraform.tfstate”
access_key = “test”
secret_key = “test” }
How to Download terraform state file from S3
sh ‘AWS_ACCESS_KEY_ID=”yourkey” AWS_SECRET_ACCESS_KEY=”yourkey” aws s3 sync s3://yourbucketname
How to access remote state files in other modules
We might have a requirement where we want to have separate terraform state file for network components which will build our vpc,subnets etc and other for EC2 instances. So how we will access the vpc id in EC2 instance ?
Only the root level outputs from the remote state are accessible. Outputs from modules within the state cannot be accessed. If you want a module output to be accessible via a remote state, you must thread the output through to a root output.
In your vpc.tf, you will create a output as we have discussed above , now we have to thread that output through to a root output, so we will create a root output.tf file at the root of the project hierarchy like this
output "vpc_id_root" {
value = "${module.vpc.vpc_id}"
}
Now in ec2-instance.tf
data "terraform_remote_state" "network_state" {
backend = "s3"config {
bucket = "bucketname"
key = "terraform.tfstate"
region = "us-west-2"
access_key = "youracesskey"
secret_key = "secretkey"
}
}how you access it subnet_id = "${data.terraform_remote_state.network_state.public_subnet_id_root}"
if everything is at root you dont need to create outputs at multiple place, but module configuration is recommended and its best
Now we have setup everything, we have understood how to pass outputs, module configuration etc.
How to get the IP’s of all dynamic EC2 instances for ansible inventory
Use Terraform Inventory Click Here
wget https://github.com/adammck/terraform-inventory/releases/download/v0.7-pre/terraform-inventory_v0.7-pre_linux_amd64.zipcd to terraform-inventory and then run the below command, this will read from the tfstate file and put the inventory in ansible host file. In main.yml we have defined which group of EC2 instances we need to runansible-playbook --inventory-file=/home/ec2-user/opt/terraform-inventory/terraform-inventory /home/ec2-user/opt/main.yml
How to create JenkinsPipeline
pipeline {
agent any
stages {
stage('checkout') {
steps {
git url: 'git@giturl'
}
}
stage('Set Terraform path') {
steps {
script {
def tfHome = tool name: 'Terraform'
env.PATH = "${tfHome}:${env.PATH}"
}
sh 'terraform --version'
}
}
stage('Provision infrastructure') {
steps {
sh 'terraform init'
sh 'terraform plan -out=plan'
sh 'terraform apply plan'
}
}
}
}
How to maintain aws secret key using EC2 Instance role
An instance profile is a container for an IAM role that you can use to pass role information to an EC2 instance when the instance starts.
On Sandbox2 account
Created one role Build_Infrastructure_Terraform_Role with Administrative access.
On pipeline account
Create a policy like this which has resource arn from sandbox account
{
“Version”: “2012–10–17”,
“Statement”: [
{
“Effect”: “Allow”,
“Action”: “sts:AssumeRole”,
“Resource”: [
“arole “
]
}
]
}
Create a role and attach this policy to that role
Code Details can be found here
References