aws_instance.app_server will be created

As a DevOps expert, I‘m a big proponent of using infrastructure as code (IaC) to manage cloud resources. IaC brings the same rigor to provisioning infrastructure that developers apply to application code – using modular, reusable components defined in a high-level language.

One of the most popular IaC tools is Hashicorp‘s open source Terraform. Terraform lets you define infrastructure resources across all major cloud providers using a declarative language in human-readable configuration files. You can then use Terraform‘s CLI to preview, provision, modify, and tear down those resources in a fully automated way.

In this tutorial, I‘ll walk you through getting started with Terraform to deploy a basic web application infrastructure on Amazon Web Services (AWS). By the end, you‘ll have hands-on experience with core Terraform concepts and be able to apply them to your own AWS projects. Let‘s get started!

Prerequisites

Before we dive in, there are a few prerequisites you‘ll need:

  • An AWS account. You can sign up for the free tier at aws.amazon.com/free.
  • Terraform installed on your local machine. Download the appropriate package for your OS at terraform.io/downloads.html.
  • AWS credentials (access key ID and secret access key) for programmatic access. Create these in the IAM service.
  • Optionally, an IDE like VS Code with the Terraform extension for syntax highlighting and auto-formatting.

With those in place, open a terminal and verify Terraform is working:

$ terraform version 
Terraform v1.1.9
on darwin_amd64

Deploying Resources with Terraform

Terraform revolves around several key concepts:

  • Providers: plugins that talk to the APIs of services like AWS, Azure, GCP, etc. to manage resources
  • Resources: infrastructure components like servers, databases, load balancers defined as code
  • State: a JSON file recording the current state of managed infrastructure
  • Variables: allow parameterizing configurations for reuse across environments
  • Modules: groups of resources that can be called as a single unit

Here‘s the basic workflow:

  1. Write Terraform configuration (.tf) files defining your desired infrastructure
  2. Initialize Terraform to download the required providers
  3. Create an execution plan to preview any changes
  4. Apply the changes to create/update/delete real infrastructure

Let‘s see this in action by deploying a simple web server on AWS. Start by creating an empty directory and adding a main.tf file with the following:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.16"
    }
  }

required_version = ">= 1.1.0" }

provider "aws" { region = "us-west-2" }

resource "aws_instance" "app_server" { ami = "ami-08d70e59c07c61a3a" instance_type = "t2.micro"

tags = { Name = "MyAppServer" } }

This configuration specifies:

  • The required_providers block specifies we need the AWS provider at least v4.16
  • The provider block configures the AWS provider to use the us-west-2 region
  • A single EC2 instance resource of type t2.micro with a specific AMI ID

Now initialize Terraform:

$ terraform init

Initializing the backend... Initializing provider plugins...

  • Finding hashicorp/aws versions matching "~> 4.16"...
  • Installing hashicorp/aws v4.20.1...
  • Installed hashicorp/aws v4.20.1 (signed by HashiCorp)

Next generate an execution plan:

$ terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:

  • create

Terraform will perform the following actions:

  • resource "aws_instance" "app_server" {
    • ami = "ami-08d70e59c07c61a3a"
    • instance_type = "t2.micro"
    • tags = {
      • "Name" = "MyAppServer" } ... }

Plan: 1 to add, 0 to change, 0 to destroy.

This shows Terraform will create 1 new resource, the EC2 instance. To actually provision it:

$ terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:

  • create

Terraform will perform the following actions: ...

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions? Terraform will perform the actions described above. Only ‘yes‘ will be accepted to approve.

Enter a value: yes

aws_instance.app_server: Creating... aws_instance.app_server: Still creating... [10s elapsed] aws_instance.app_server: Creation complete after 17s [id=i-034b835146684e948]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Congrats, you just provisioned your first resource with Terraform! Head over to the EC2 console in AWS and you‘ll see the new instance up and running.

Referencing Resources and Outputs

We have a server now, but it‘s not very useful without being able to access it over the network. Let‘s add an Elastic IP and security group to open up port 80.

Update your main.tf to the following:

resource "aws_instance" "app_server" {
  ami           = "ami-08d70e59c07c61a3a" 
  instance_type = "t2.micro"
  vpc_security_group_ids = [aws_security_group.sg_web.id]

tags = { Name = "MyAppServer" } }

resource "aws_eip" "app_eip" { instance = aws_instance.app_server.id vpc = true }

resource "aws_security_group" "sg_web" { name = "allow_web_traffic"

ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } }

Here we added two new resources:

  • An aws_eip representing an Elastic IP address
  • An aws_security_group that allows inbound traffic on port 80 from any IP

Also note how we reference the ID attributes of other resources, like aws_instance.app_server.id. Terraform interpolates this into the actual runtime value.

Run terraform apply again to sync these changes with AWS. The output shows 2 new resources created and 1 modified in place:

$ terraform apply
aws_security_group.sg_web: Creating...
aws_security_group.sg_web: Creation complete after 2s [id=sg-096fd0ef0f7da6eb4] 
aws_instance.app_server: Modifying... [id=i-034b835146684e948]
aws_instance.app_server: Modifications complete after 3s [id=i-034b835146684e948]
aws_eip.app_eip: Creating...
aws_eip.app_eip: Creation complete after 1s [id=eipalloc-024daed33815c92df]

Apply complete! Resources: 2 added, 1 changed, 0 destroyed.

Elastic IP attached to EC2 instance

To actually access our instance, we need to output the public IP address. Add the following to the end of main.tf:

output "public_ip" {
  value = aws_eip.app_eip.public_ip
}

The output block defines values that Terraform will print after each apply. When you re-run terraform apply, you‘ll now see:

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

public_ip = "54.186.206.213"

Try accessing that IP in a browser. You should see the default Apache page, since our AMI has httpd pre-installed:

Apache default page served from EC2 instance

Just like that we have a functional web server, all defined and provisioned with code!

Using Variables and Modules

So far we‘ve been hardcoding values like the instance type, AMI ID, etc. To make our configuration more reusable, let‘s extract those into variables.

Create a file variables.tf with the contents:

variable "instance_type" {
  type        = string
  default     = "t2.micro"
  description = "Type of EC2 instance to provision"
}

variable "ami_id" { type = string default = "ami-08d70e59c07c61a3a" description = "AMI ID for EC2 instance" }

This defines two input variables we can reference elsewhere. Update your aws_instance resource in main.tf to use them:

resource "aws_instance" "app_server" {
  ami           = var.ami_id
  instance_type = var.instance_type

... }

By prefixing with var., you reference the variable values. The default values will be used unless overridden at runtime.

We can go a step further and extract our web server into a reusable module. Create a directory modules/webserver and move the main.tf file into it.

Then create a new top-level main.tf in the root directory:

module "websvr" {
  source        = "./modules/webserver"
  instance_type = "t3.small"
}

output "public_ip" { value = module.websvr.public_ip
}

Here we call our webserver module and pass in a different instance type than the default. We also output the public IP by referencing the module‘s output value.

Run terraform init and terraform apply from the root level. Everything should provision successfully, showing how modules make your code composable:

$ terraform init
Initializing modules...
- websvr in modules/webserver

Initializing the backend... ...

$ terraform apply

Outputs:

public_ip = "54.245.3.166"

Cleaning Up

To avoid incurring ongoing costs, it‘s important to clean up your infrastructure when you‘re done. You can tear everything down by running:

$ terraform destroy

Terraform will perform the following actions:

  • resource "aws_instance" "app_server" {
    • ami = "ami-08d70e59c07c61a3a" -> null ... }
  • resource "aws_eip" "app_eip" {
    • public_ip = "34.220.140.193" -> null ... }
  • resource "aws_security_group" "sg_web" {
    • name = "allow_web_traffic" -> null ... }

Plan: 0 to add, 0 to change, 3 to destroy.

Do you really want to destroy all resources? Terraform will destroy all your managed infrastructure, as shown above. There is no undo. Only ‘yes‘ will be accepted to confirm.

Enter a value: yes

...

Destroy complete! Resources: 3 destroyed.

Terraform will delete all the resources it manages, ensuring you don‘t leave anything lingering.

Best Practices

As you use Terraform more, keep these best practices in mind:

  • Use version control for your .tf files, just like application code
  • Manage Terraform state securely with remote backends like S3, not local files
  • Separate environments (dev/staging/prod) with different directories and state files
  • Make heavy use of modules and composition over duplication
  • Pin versions of providers and modules to avoid unintended changes
  • Write tests for your infrastructure code with tools like Terratest

Conclusion

Whew, that was a whirlwind tour of infrastructure as code with Terraform and AWS! I hope this gives you a solid foundation to start managing your own cloud resources as code.

We covered:

  • Configuring Terraform and the AWS provider
  • Declaring infrastructure resources and referencing their attributes
  • Initializing providers and modules
  • Previewing changes with terraform plan
  • Applying and destroying resources with terraform apply and terraform destroy
  • Extracting reusable variables and modules
  • Outputting values and using them across modules

Terraform has a large ecosystem of providers, modules, and tooling to explore. It‘s an invaluable skill for DevOps and infrastructure automation. For more info, check out:

Happy automating!

Similar Posts