Terraform Setup On AWS

Please visit my website and subscribe to my youtube channel for more articles

https://devops4solutions.com/

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 = 20
attribute {
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

https://github.com/express42/terraform-ansible-example

Written by

Devops Automation Enginneer

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