Skip to main content

Command Palette

Search for a command to run...

Day 3: Deploying Your First Server with Terraform: A Beginner's Guide

Updated
6 min read
Day 3: Deploying Your First Server with Terraform: A Beginner's Guide

Having completed setting up my environment from day 2, today I will be deploying my first server on AWS. Let's get into it.

Terraform Provider Block

A provider block in Terraform basically defines the cloud provider to use, e.g. AWS. It will also define the region to deploy to.

Create a main.tf in a directory. This is where we will add our code.

provider "aws" {
  region = "us-east-1"
}

The above provider block specifies we will use AWS and deploy in the us-east-1 region.

Terraform Resource Block

The resource block is used to specify resources from the provider

Resource syntax:

resource "<PROVIDER>_<TYPE>" "<NAME>" {
    [CONFIG ...]
}

PROVIDER is the name of the provider we are using, in my case, AWS. TYPE is the resource to deploy, e.g. an EC2 instance, and NAME is the label that we will use locally to refer to this resource.

resource "aws_instance" "terraform-example" { 
    ami = "ami-0fb0b230890ccd1e6" 
    instance_type = "t2.micro" 
    vpc_security_group_ids = [aws_security_group.instance.id]
    user_data = <<-EOF
            #!/bin/bash
            echo "Hello, world" > index.html
            nohup busybox httpd -f -p 8080 &
            EOF

    user_data_replace_on_change = true
    tags = {
      Name = "terraform-example"
    }
}

resource "aws_security_group" "instance" {
    name = "terraform-example-instance"

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

There are two resource blocks in my main.tf. Let's break them down.

resource "aws_instance" "terraform-example"

This creates an AWS instance and labels it terraform-example.

 ami = "ami-0fb0b230890ccd1e6" 
 instance_type = "t2.micro" 
 vpc_security_group_ids = [aws_security_group.instance.id]

Define the AMI ID of the AMI to use.

The EC2 instance type to use.

vpc_security_group_ids = [aws_security_group.instance.id] In terraform this is called a reference. It is a type of expression.

An expression is anything that returns a value. A reference allows us to access values from other parts of the code.

References have the syntax <PROVIDER>_<TYPE>.<NAME>.<ATTRIBUTE>

PROVIDER in our case is AWS, TYPE is security group, NAME is instance, and ATTRIBUTE is id. aws_security_group.instance.id

We are getting an id attribute from the aws_security_group we just created and using it to attach our EC2 instance to the security group. Different resources produce/export different attribute references. For aws_security_group, one of them is id. Therefore, whenever we need to attach a security group to an instance, we need to reference it using the id attribute.

user_data = <<-EOF
            #!/bin/bash
            echo "Hello, world" > index.html
            nohup busybox httpd -f -p 8080 &
            EOF

Whenever you launch an EC2 instance, configuration instructions are passed through USER DATA. These instructions are executed in the very first boot.

The above user data is a bash script that creates an index.html file and starts an HTTP daemon that will serve the HTML file. nohup ensures the command keeps running even after the shell closes. BusyBox is an executable to run httpd, and is installed by default on Ubuntu. The file is served on port 8080 and & runs the command in the background. EOF is used so we don't have to add \n for new lines.

user_data_replace_on_change = true
    tags = {
      Name = "terraform-example"
    }

user_data_replace_on_change = true stops and recreates the EC2 instance. tags defines the name of the instance.

name = "terraform-example-instance"

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

Inside the security group resource, we define the ports to open (8080) and the protocol to use (TCP). Setting CIDR blocks to 0.0.0.0/0 allows requests to come from any IP address.

With this in my main.tf, I ran terraform init in the directory where my main.tf is. This gets all the code required to access the provider you specified. terraform plan command shows all the changes that will be made. This is the output.

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:

  # aws_instance.terraform-example will be created
  + resource "aws_instance" "terraform-example" {
      + ami                                  = "ami-0fb0b230890ccd1e6"
      + arn                                  = (known after apply)
      + associate_public_ip_address          = (known after apply)
      + availability_zone                    = (known after apply)
      + disable_api_stop                     = (known after apply)
      + disable_api_termination              = (known after apply)
      + ebs_optimized                        = (known after apply)
      + enable_primary_ipv6                  = (known after apply)
      + force_destroy                        = false
      + get_password_data                    = false
      + host_id                              = (known after apply)
      + host_resource_group_arn              = (known after apply)
      + iam_instance_profile                 = (known after apply)
      + id                                   = (known after apply)
      + instance_initiated_shutdown_behavior = (known after apply)
      + instance_lifecycle                   = (known after apply)
      + instance_state                       = (known after apply)
      + instance_type                        = "t2.micro"
      + ipv6_address_count                   = (known after apply)
      + ipv6_addresses                       = (known after apply)
      + key_name                             = (known after apply)
      + monitoring                           = (known after apply)
      + outpost_arn                          = (known after apply)
      + password_data                        = (known after apply)
      + placement_group                      = (known after apply)
      + placement_group_id                   = (known after apply)
      + placement_partition_number           = (known after apply)
      + primary_network_interface_id         = (known after apply)
      + private_dns                          = (known after apply)
      + private_ip                           = (known after apply)
      + public_dns                           = (known after apply)
      + public_ip                            = (known after apply)
      + region                               = "us-east-1"
      + secondary_private_ips                = (known after apply)
      + security_groups                      = (known after apply)
      + source_dest_check                    = true
      + spot_instance_request_id             = (known after apply)
      + subnet_id                            = (known after apply)
      + tags                                 = {
          + "Name" = "terraform-example"
        }
      + tags_all                             = {
          + "Name" = "terraform-example"
        }
      + tenancy                              = (known after apply)
      + user_data                            = <<-EOT
            #!/bin/bash
            echo "Hello, world" > index.html
            nohup busybox httpd -f -p 8080 &
        EOT
      + user_data_base64                     = (known after apply)
      + user_data_replace_on_change          = true
      + vpc_security_group_ids               = (known after apply)

      + capacity_reservation_specification (known after apply)

      + cpu_options (known after apply)

      + ebs_block_device (known after apply)

      + enclave_options (known after apply)

      + ephemeral_block_device (known after apply)

      + instance_market_options (known after apply)

      + maintenance_options (known after apply)

      + metadata_options (known after apply)

      + network_interface (known after apply)

      + primary_network_interface (known after apply)

      + private_dns_name_options (known after apply)

      + root_block_device (known after apply)

      + secondary_network_interface (known after apply)
    }

  # aws_security_group.instance will be created
  + resource "aws_security_group" "instance" {
      + arn                    = (known after apply)
      + description            = "Managed by Terraform"
      + egress                 = (known after apply)
      + id                     = (known after apply)
      + ingress                = [
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + from_port        = 8080
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "tcp"
              + security_groups  = []
              + self             = false
              + to_port          = 8080
                # (1 unchanged attribute hidden)
            },
        ]
      + name                   = "terraform-example-instance"
      + name_prefix            = (known after apply)
      + owner_id               = (known after apply)
      + region                 = "us-east-1"
      + revoke_rules_on_delete = false
      + tags_all               = (known after apply)
      + vpc_id                 = (known after apply)
    }

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

The + sign shows what will be created.

Execute terraform apply -auto-approve to create the resources.

Check whether the instance is created.

Once you confirm the Instance is created, you can remove the resources.

terraform destroy

Conclusion

That is all for today. See you on day 4.