The importance of infrastructure as code
Managing complex cloud infrastructure can become a real issue especially when we want to recover from a disaster, add more resources or enforce certainty that our changes do not break anything.
IAC to the rescue
Infrastucture as code is a declarative language the uses cloud provider API's for resource provisioning.
The way it works is by modelling our entire cloud infrastucture using code thus greatly reduce the stress of managing our resources and at the same time adding certainty that everything will work as expected when we need to make any changes.
Some popular tools for writing IAC are Terraform and CloudFormation. Personally I prefer Terraform since it is a cloud-agnostic solution.
To get started with Terraform go ahead and download it from the official website.
Export the binary's path in your .profile
export PATH="$PATH:~/path-to-binary"
Build Infrastructure
First you need to create a file with the .tf extension.
Inside the file add the following:
provider "aws" {
region = 'eu-west-1'
}
This defines the provider your are about to use.
Note: It is recommended not to hardcode credentials into the *.tf cofiguration files.
Then run the following:
terraform init
By running terraform init
Terraform downloads everything that is necessary to provision resources in your specified provider.
To format your configuration for easy readability run:
terraform fmt
To syntactically validate your configuration run:
terraform validate
Finally to apply the configuration changes run:
terraform apply
When you apply the configuration, Terraform will show you all the actions it'll take in order to change real infrastructure. You'll notice that Terraform uses a similar format to git diff to display additions and deletions in your resources.
Terraform also keeps track of every ID of the created resources into a .tfstate file. Make sure that you commit this file in your version control so that anyone in your team that uses Terraform can use it.
You could also write state data to a remote data store such as Terraform Cloud or AWS S3.
To use Terraform Cloud as the backend add the following in your .tf file (Make sure you have an account with Terraform):
terraform {
backend "remote" {
organization = "your-organization-name"
workspaces {
name = "your-workspace-name"
}
}
}
To inspect the current state using:
terraform show
Change Infrastructure
As your Terraform configuration changes over time, when you run apply it only modifies, creates or destroys what is necessary. By commiting your configuration in version control you can easily see how it is progressing over time.
Destroy Infrastructure
To destroy all resources that you've created by your configuration run:
terraform destroy
Resource Dependencies
You can specify if a resource depends on another resource before you create it by using the depends_on argument. For example you can specify that you want an EC2 instance to be created but only after the S3 bucket is created. All other resources that don't have any dependencies can be created in parallel.
resource "aws_instance" "ec2" {
ami = "ami"
instance_type = "instance-type"
key_name = "key-name"
depends_on = [
aws_s3_bucket.media,
aws_s3_bucket.static,
aws_security_group.ec2
]
Another way to define dependencies is with interpolation expressions. For example to create an aws elastic ip that depends on an instance you would write the following:
resource “aws_eip” “ip” {
vpc = true
instance = aws_instance.ec2.ip
}
Where ec2 is an instance you defined previously.
Provisioners
Terraform also gives you the ability to do post operations after your resources are created or destroyed. This is useful if you want to do some initial setup after an instance creation i.e. run scripts, install software etc.
To define a provisioner, add the following to a resource block:
Provisioner “local-exec” {
command = “ls”
}
local-exec Executes commands on your local machine.
remote-exec To execute commands on a remote machine you must define an ssh connection in the connection block.
Failed Provisioner and Tainted Resources
If a resource has been created successfully but there was a problem with your provisioner, Terraform will give you an error and mark this resource as tainted.
Next time you apply your configuration, Terraform will remove the tainted resource, create it again and try to run the provisioning step.
Manually Tainting Resources
You also have the ability to mark a resource as tainted manually if you want to destroy and recreate it.
Input variables
If your configuration uses a lot of common variables then Terraform lets you create a variables.tf file where you can define all the variables that you need.
variable "region" {
description = "The AWS region the services will use."
default = "eu-central-1"
}
Then to use the variables in your main configuration write the following:
provider "aws" {
region = var.region
}
Output variables
Output variables is a way to access values like an instance's IP after a Terraform build.
Create an output.tf file and define all the values that you want to get outputted when you run apply.
Note: You can also add values to any of your .tf files.
output "instance_ip" {
value = aws_instance.ec2.public_ip
}
Full configuration
Below you will see a full Terraform configuration that creates an IAM group with an attached policy, a security group with an open SSH port, an EC2 instance, a security group for an RDS instance, an RDS instance, an S3 bucket with subfolders and a CloudFront distribution.
terraform {
backend "remote" {
organization = "demo"
workspaces {
name = "demo"
}
}
}
locals {
app = "demo"
s3_media = "demo-media"
database = "demo-db"
}
provider "aws" {
region = var.region
}
resource "aws_iam_group" "administrators" {
name = "administrators-test"
}
resource "aws_iam_group_policy_attachment" "ap1" {
group = "administrators-test"
policy_arn = "arn:aws:iam::aws:policy/IAMFullAccess"
depends_on = [aws_iam_group.administrators]
}
resource "aws_security_group" "ec2" {
name = "ec2"
description = "Default ${local.app} EC2 security group"
vpc_id = "vpc-0522b76d"
tags = {
Name = "ec2"
}
ingress {
from_port = 22
to_port = 22
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_security_group" "rds" {
name = "rds"
description = "Default ${local.app} RDS security group"
vpc_id = "vpc-0522b76d"
tags = {
Name = "rds"
}
ingress {
from_port = 5432
to_port = 5432
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_s3_bucket" "media" {
bucket = local.s3_media
}
resource "aws_s3_bucket_object" "media_folders" {
for_each = toset(var.clients)
bucket = local.s3_media
key = "${each.value}/"
depends_on = [aws_s3_bucket.media]
}
resource "aws_cloudfront_origin_access_identity" "origin_access_identity" {
comment = local.s3_origin_id
}
resource "aws_cloudfront_distribution" "s3_distribution" {
origin {
domain_name = aws_s3_bucket.static.bucket_domain_name
origin_id = local.s3_origin_id
custom_origin_config {
http_port = "80"
https_port = "443"
origin_protocol_policy = "http-only"
origin_ssl_protocols = ["TLSv1", "TLSv1.1", "TLSv1.2"]
}
}
enabled = true
is_ipv6_enabled = true
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = local.s3_origin_id
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "allow-all"
min_ttl = 0
default_ttl = 86400
max_ttl = 31536000
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
depends_on = [aws_s3_bucket.static]
}
resource "aws_db_instance" "production" {
identifier = local.database
allocated_storage = 100
max_allocated_storage = 1000
storage_type = "gp2"
apply_immediately = true
deletion_protection = false
delete_automated_backups = false
final_snapshot_identifier = "${local.database}-final-snapshot"
engine = "postgres"
engine_version = "10.11"
instance_class = "db.t3.small"
port = "5432"
username = "postgres"
password = "test12345!"
vpc_security_group_ids = [aws_security_group.rds.id]
tags = {
Name = local.database
}
depends_on = [aws_security_group.rds]
}
resource "aws_instance" "ec2" {
ami = var.ami
instance_type = var.instance_type
key_name = "demo"
security_groups = [aws_security_group.ec2.name]
tags = {
Name = "demo-test"
}
depends_on = [
aws_s3_bucket.media,
aws_security_group.ec2
]
provisioner "local-exec" {
command = "echo ${aws_instance.ec2.public_ip} > ip_address.txt"
}
}
Newsletter
Get notified about new posts!