Well-Architected Framework
Build and deploy immutable virtual machines
Virtual machines (VMs) are a self-contained solution for running your application. Virtual machines can be created manually, or automatically with infrastructure as code (IaC). IaC helps codify and version control infrastructure and provides many benefits including more consistent and reliable deployments, enhanced security, and improved scalability and performance.
Virtual machines have two parts that you can define with IaC: the VM image and the running VM instance. The VM image is the underlying blueprint for creating a virtual machine and contains all of the software that the application requires to run properly, including the application code, dependencies, and the operating system (OS). The contents are defined as code in a configuration file like an Ansible playbook, a Packer template, or a collection of Terraform configuration files.
The second part that you can define with IaC is the actual VM instance. This process includes the deployment of the VM infrastructure such as an AWS EC2 instance and is done with a tool like Terraform. Terraform retrieves the VM image stored in an artifact repository and creates an instance with it, configuring settings like networking, security, and storage as well.
Create a machine image with Packer
Packer is a tool for creating identical machine images for multiple platforms from a single source configuration. You can use Packer to create machine images for many cloud providers including AWS, GCP, and Azure.
Packer can install your application with individual shell commands, bash scripts, or more robust methods such as Ansible playbooks. When you’re ready to upgrade your application or dependencies, you create a new image with Packer instead of connecting to the running VM and upgrading it in place.
The following example shows a Packer template that builds an AWS Machine Image (AMI) and uses the Ansible provisioner to run an Ansible playbook and provision the VM.
ubuntu-ami.pkr.hcl
packer {
required_plugins {
amazon = {
source = "github.com/hashicorp/amazon"
version = "~> 1.8.0"
}
ansible = {
version = "~> 1.1.4"
source = "github.com/hashicorp/ansible"
}
}
}
source "amazon-ebs" "example-ubuntu-ami" {
ami_name = "example-ubuntu-ami"
instance_type = "t2.medium"
region = "us-east-1"
source_ami_filter {
filters = {
name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
most_recent = true
owners = ["099720109477"]
}
ssh_username = "ubuntu"
}
build {
sources = ["source.amazon-ebs.example-ubuntu-ami"]
provisioner "ansible" {
playbook_file = "./playbook.yaml"
user = "ubuntu"
}
}
Once the build is complete, Packer outputs an image ID that you can use in your Terraform configuration to build the immutable VM. The output will look similar to the following.
==> Builds finished. The artifacts of successful builds are:
--> learn-packer.amazon-ebs.example-ubuntu-ami: AMIs were created:
us-east-1: ami-0703a21425141501e
Deploy immutable VMs with Terraform
Once you create a new image with your updated application or dependency, you can re-create your immutable virtual server by updating Terraform’s configuration with the new machine image ID. Next time Terraform runs, it will destroy and create a VM with the new image.
The workflow for creating immutable VMs involves creating the VM image, creating the infrastructure as code configurations, running the IaC tool to create the infrastructure, and redeploying as necessary.
- Create IaC with image: Create Terraform configurations that reference the VM image.
- Run IaC tool to create infrastructure: Run Terraform with your configurations to create the VM and any related infrastructure.
- Iterate and redeploy: Update your Packer template and rebuild the VM image. Update your Terraform configurations with the new image and redeploy the VM. Terraform destroys any out-of-date VMs and deploys new infrastructure with the updated VM image.
The following example shows a Terraform configuration that deploys an AWS EC2 instance with the image that Packer created in the previous section with the ID ami-0703a21425141501e.
main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "example_instance" {
ami = "ami-0703a21425141501e"
instance_type = "t2.medium"
tags = {
Name = "example-instance-1"
}
}
Improvements to the workflow can include:
- Using CI/CD and source code repository triggers to automate running the IaC tool
- Using HCP Packer to keep track of image metadata and integrating it with Terraform to get the most up-to-date image automatically with HCP Packer channels
HashiCorp resources:
- Packer template overview
- Packer Ansible plugin
- Packer template built-in blocks
- Get started with Packer tutorials
- Get started with HCP Packer metadata and channels
- Whiteboard video: What is mutable vs. immutable infrastructure?
Next steps
In this section of Define your processes, you learned how to deploy immutable virtual machines. Create immutable virtual machines is part of the Define and automate processes pillar.
Refer to the following documents to learn more about creating immutable infrastructure: