DevOps is not a job title, a division, or a particular technology.
It's a set of processes, ideas, and values to make software delivery vastly more efficient.
Four core values of DevOps are:
Infrastructure as Code contributes to one of four DevOps core values, that is automation.
Simple bash, python, or ruby scripts such as:
# Update the apt-get cache
sudo apt-get update
# Install PHP
sudo apt-get install -y php
# Install Apache
sudo apt-get install -y apache2
# Copy the code from the repository
sudo git clone https://github.com/brikis98/php-app.git /var/www/html/app
# Start Apache
sudo service apache2 start
Simple bash, python, or ruby scripts such as:
# Update the apt-get cache
sudo apt-get update
# Install PHP
sudo apt-get install -y php
# Install Apache
sudo apt-get install -y apache2
# Copy the code from the repository
sudo git clone https://github.com/brikis98/php-app.git /var/www/html/app
# Start Apache
sudo service apache2 start
Chef, Puppet, Ansible, or OpenStack code, for instance:
- name: Update the apt-get cache
apt:
update_cache: yes
- name: Install PHP
apt:
name: php
- name: Install Apache
apt:
name: apache2
- name: Copy the code from the repository
git: repo=https://github.com/brikis98/php-app.git dest=/var/www/html/app
- name: Start Apache
service: name=apache2 state=started enabled=yes
With configuration management tools, we have much better advantages than using ad hoc scripts:
Docker, Packer, or Vagrant manifest, such as:
{
"builders": [{
"ami_name": "packer-example",
"instance_type": "t2.micro",
"region": "us-east-1",
"type": "amazon-ebs",
"source_ami": "ami-40d28157",
"ssh_username": "ubuntu"
}],
"provisioners": [{
"type": "shell",
"inline": [
"sudo apt-get update",
"sudo apt-get install -y php",
"sudo apt-get install -y apache2",
"sudo git clone https://github.com/brikis98/php-app.git /var/www/html/app"
]
}]
}
Terraform, CloudFormation, and Open Stack manifests, ex:
resource "aws_instance" "app" {
instance_type = "t2.micro"
availability_zone = "us-east-1a"
ami = "ami-40d28157"
user_data = <<-EOF
#!/bin/bash
sudo service apache2 start
EOF
}
Entire deployment process can be automated and engineers can write their own deployment codes when necessary.
Computer can provision multiple infrastructure with codes faster than human can.
Proper infrastructure as code technology enable consistency and repeatability, make it less prone to error.
As long as everyone in your team can read the code, you don't have to guess how the existing infrastructure was provisioned.
You can see the history of your infrastructure evolution simply by taking a look at your IaC git commits.
If you build your infrastructure with code, you can do review, automated tests, or any other practices aimed at reducing defects on your code.
You can package your deployments into modules and enable your team to reuse documented and battle tested code instead of starting from scratch every time.
IaC allows you to do what you do best (writing code) and computer to do what it does best (automation).
Changes to code will update existing infrastructure.
Changes to code will create new infrastructure, most of the time.
You write code to tell computer how to do something.
You write code to tell computer what needs to be done and let it figure out by itself how to achieve that.
Require a master server to store the state of your infrastructure and distribute updates.
May also require a master server (for instance, AWS/GCP API server does act like a master server when you run Terraform codes), but does not require you to manage it.
Require you to install an agent software on the nodes you want to configure.
Does not require you to install an agent software on nodes you want to configure.
Type | Mutabililty | Language | Master | Agent | |
---|---|---|---|---|---|
Ansible | CM | Mutable | Procedural | No | No |
Chef | CM | Mutable | Procedural | Yes | Yes |
CloudFormation | Provisioning | Immutable | Declarative | No | No |
Puppet | CM | Mutable | Declarative | Yes | Yes |
SaltStack | CM | Mutable | Declarative | Yes | Yes |
Terraform | Provisioning | Immutable | Declarative | No | No |
Terraform does not enforce any file structure. In fact, you can write the whole manifest of your infrastructure in one single file.
However, since Infrastructure as Code is a *code*, let's treat it as we treat our application code and put a proper structure.
This is my standard Terraform layout:
.
├── backend.tf
├── data.tf
├── provider.tf
├── terraform.tfvars
├── var.tf
└── [resource_files.tf]
The first file we create, this determines what provider we run our Terraform against. Can be GCP, AWS, or any providers listed here.
provider "aws" {
access_key = "${var.aws_access_key}"
secret_key = "${var.aws_secret_key}"
region = "ap-southeast-1"
}
Documentation on provider for AWS can be found here.
Backend stores the state of our infrastructure. When we first initialize Terraform, it will look for backend configuration, if it doesn't find one, it will create one.
When we run `terraform plan` or `terraform apply`, it will compare the desired state with existing state based on information stored in terraform.state.
terraform {
backend "s3" {
bucket = "qbl-terraform"
key = "dt-demo/terraform.tfstate"
region = "ap-southeast-1"
}
}
In Terraform, we can use variables. There are three kinds of variable in Terraform: string, numeric, and list. If a variable does not have a default value, it will prompt user to input the value.
variable "aws_access_key" { type = "string" }
variable "aws_secret_key" { type = "string" }
variable "server_port" {
description = "The port we use for HTTP requests is: "
default = 8080
}
Often times, we don't want to store some variables to our code repository. These variables usually are secret variables containing keys to our account. For these kinds of variables, we store them in `terraform.tfvars` with format as follow:
aws_access_key = "<access key here>"
aws_secret_key = "<secret key here>"
Use of data sources allows a Terraform configuration to build on information defined outside of Terraform, or defined by another separate Terraform configuration.
For example, put the following in our `data.tf`:
data "aws_availability_zones" "all" {}
Now that we have everything set, let's provision a simple web cluster with Terraform. For this exercise, we are going to create three resources:
Write the following code in `aws_security_group.tf`:
resource "aws_security_group" "instance" {
name = "terraform-example-instance"
ingress {
from_port = "${var.server_port}"
to_port = "${var.server_port}"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
lifecycle {
create_before_destroy = true
}
}
# to be continued in the next page...
Write the following code in `aws_security_group.tf`:
# ...continued from previous page
resource "aws_security_group" "elb" {
name = "terraform-example-elb"
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"]
}
}
Write the following code in `aws_launch_configuration.tf`:
resource "aws_launch_configuration" "example" {
image_id = "ami-52d4802e"
instance_type = "t2.micro"
security_groups = ["${aws_security_group.instance.id}"]
user_data = <<-EOF
#!/bin/bash
echo "Hello, World" > index.html
nohup busybox httpd -f -p ${var.server_port} &
EOF
lifecycle {
create_before_destroy = true
}
}
Write the following code in `aws_launch_configuration.tf`:
resource "aws_autoscaling_group" "example" {
launch_configuration = "${aws_launch_configuration.example.id}"
availability_zones = ["${data.aws_availability_zones.all.names}"]
load_balancers = ["${aws_elb.example.name}"]
health_check_type = "ELB"
min_size = 2
max_size = 10
tag {
key = "Name"
value = "terraform-asg-example"
propagate_at_launch = true
}
}
Write the following code in `aws_elb.tf`:
resource "aws_elb" "example" {
name = "terraform-asg-example"
availability_zones = ["${data.aws_availability_zones.all.names}"]
security_groups = ["${aws_security_group.elb.id}"]
listener {
lb_port = 80
lb_protocol = "http"
instance_port = "${var.server_port}"
instance_protocol = "http"
}
health_check {
healthy_threshold = 2
unhealthy_threshold = 2
timeout = 3
interval = 30
target = "HTTP:${var.server_port}/"
}
}
To initialize Terraform in our directory, simply run the following command:
terraform init
To see what resources will be created by Terraform, run the following command:
terraform plan
To actually apply the desired changes, run the following command:
terraform plan
To clean up everything we build we Terraform, run the following command:
terraform destroy