Immutable infrastructure is all about immutable components which are recreated and replaced instead of updating after infrastructure creation. Immutable infrastructure reduces the number of places where things can go wrong. This helps reduce inconsistency and improve reliability in the deployment process. Updating a server can be lengthy, painful and things can go wrong easily. When an update is necessary for immutable infrastructure then new servers are provisioned with a preconfigured image and old servers are destroyed. We create a new machine image that is built for deployment and use it for creating new servers. In immutable infrastructure, we are moving configuration setup after server creation process to build process. As all deployments are done by new images, we can keep the history of previous releases in case of reverting to old build. This allows us to reduce deployment time, failure of configuration, scale deployments etc.

Normal Flow

Immutable Flow

We use terraform to provision our servers and then ansible on instances for configuration management. This adds time for provisioning servers as we have to wait till configuration completes. We should do configuration up front using Packer. Packer helps bake configuration into the machine image during image creation time. This helps in creating identical servers in case things go wrong. If you are new to Packer, please read my blog on packer here.

In this post, we are going to bake an AMI using Packer and do configuration using ansible during the baking process. We are going to deploy a static website exactly same as what we did here (Code can be found here). We are going to use same ansible code for provisioning our static website using nginx during the baking process. We need to make sure nginx is enabled in systemctl and starts on every boot so it is ready to process immediately. We are going to assign tags in bake process to AMI. This tag can be used by terraform to identify the latest AMI available and use it for EC2 instance creation. We need to provide subnet id to packer builder so it can use this subnet id while creating AMI. We are going to divide our terraform code into two parts, one which contains all network details: create VPC, subnet, and other network details. Another part which spawns our EC2 instance inside our network using AMI generated by the packer.

Step 1: Setup a network using Terraform

In this step, we are going to create a VPC with public subnet along with key pair which we use to ssh in all EC2 instances. We output subnet id which we need to place in packer builder using which packer can create an AMI used in this VPC.

provider "aws" {

access_key = "${var.access_key}"

secret_key = "${var.secret_key}"

region = "${var.region}"

} #resources

resource "aws_vpc" "vpc" {

cidr_block = "${var.cidr_block_range}"

enable_dns_support = true

enable_dns_hostnames = true

tags {

"Environment" = "${var.environment_tag}"

}

} resource "aws_internet_gateway" "igw" {

vpc_id = "${aws_vpc.vpc.id}"

tags {

"Environment" = "${var.environment_tag}"

}

} resource "aws_subnet" "subnet_public" {

vpc_id = "${aws_vpc.vpc.id}"

cidr_block = "${var.subnet1_cidr_block_range}"

map_public_ip_on_launch = "true"

availability_zone = "${var.availability_zone}"

tags {

"Environment" = "${var.environment_tag}"

"Type" = "Public"

}

} resource "aws_route_table" "rtb_public" {

vpc_id = "${aws_vpc.vpc.id}" route {

cidr_block = "0.0.0.0/0"

gateway_id = "${aws_internet_gateway.igw.id}"

} tags {

"Environment" = "${var.environment_tag}"

}

} resource "aws_route_table_association" "rta_subnet_public" {

subnet_id = "${aws_subnet.subnet_public.id}"

route_table_id = "${aws_route_table.rtb_public.id}"

} resource "aws_key_pair" "ec2key" {

key_name = "publicKey"

public_key = "${file(var.public_key_path)}"

}

Step 2: Create AMI using packer and ansible inside the above-created network

We are going to use our ansible configuration which installs nginx and setup static page. We enable nginx using systemctl so when an EC2 instance is created using this AMI, we have nginx started and ready to process incoming HTTP requests.

{

"variables": {

"aws_access_key": "{{env `AWS_ACCESS_KEY_ID`}}",

"aws_secret_key": "{{env `AWS_SECRET_ACCESS_KEY`}}",

"region": "us-east-2",

"ssh_username": "ec2-user",

"base_ami": "ami-0303c7b2e7066b60d",

"instance_type": "t2.micro",

"subnet_id": "subnet-0553e12f46221b5b7" // Created during network terraform execution

},

"builders": [

{

"type": "amazon-ebs",

"access_key": "{{user `aws_access_key`}}",

"secret_key": "{{user `aws_secret_key` }}",

"region": "{{user `region` }}",

"subnet_id": "{{user `subnet_id` }}",

"source_ami": "{{user `base_ami`}}",

"instance_type": "{{user `instance_type` }}",

"ssh_username": "{{user `ssh_username`}}",

"ami_name": "packer-base-{{timestamp}}",

"associate_public_ip_address": true,

"tags": {

"Name": "Packer-Ansible" // Tag used by terraform during instance creation

}

}

],

"provisioners": [

{

"type": "ansible",

"playbook_file": "../ansible/playbook.yml"

}

]

}

Step 3: Setup EC2 instance inside the network with packer AMI

We are going to read network state file using data resource so that we can use network created resources during instance creation.

data "terraform_remote_state" "network" {

backend = "local" config {

path = "../networkTerraform/terraform.tfstate"

}

}

Next, we need to find the latest created AMI which is available with our created tag so that it can be used during instance creation.

data "aws_ami" "ec2-ami" {

filter {

name = "state"

values = ["available"]

} filter {

name = "tag:Name"

values = ["Packer-Ansible"]

} most_recent = true

}

Once we have all this information, we can simply use this information during EC2 instance creation.

provider "aws" {

access_key = "${var.access_key}"

secret_key = "${var.secret_key}"

region = "${var.region}"

} data "aws_ami" "ec2-ami" {

filter {

name = "state"

values = ["available"]

} filter {

name = "tag:Name"

values = ["Packer-Ansible"]

} most_recent = true

} data "terraform_remote_state" "network" {

backend = "local" config {

path = "../networkTerraform/terraform.tfstate"

}

} module "securityGroupModule" {

source = "./modules/securityGroup"

access_key = "${var.access_key}"

secret_key = "${var.secret_key}"

region = "${var.region}"

vpc_id = "${data.terraform_remote_state.network.vpc_id}"

environment_tag = "${var.environment_tag}"

} module "instanceModule" {

source = "./modules/instance"

access_key = "${var.access_key}"

secret_key = "${var.secret_key}"

region = "${var.region}"

instance_ami = "${data.aws_ami.ec2-ami.id}"

vpc_id = "${data.terraform_remote_state.network.vpc_id}"

subnet_public_id = "${data.terraform_remote_state.network.public_subnets[0]}"

key_pair_name = "${data.terraform_remote_state.network.ec2keyName}"

security_group_ids = ["${module.securityGroupModule.sg_22}", "${module.securityGroupModule.sg_80}"]

environment_tag = "${var.environment_tag}"

}

Once this code is executed we output elastic IP address assign to our EC2 instance. We use this IP address in our browser to confirm that our static website is working fine.

The complete code can be found in this git repository: https://github.com/MiteshSharma/ImmutableInfrastructure

PS: If you liked the article, please support it with claps. Cheers