Configure AWS instances with Terraform: introduction

By | 21/11/2019

Intoduction

In this blogpost, we will create an AWS instance onto EC2 entirely using Terraform. Nothing too complex as such, but better to start with something fairly easy and continue to expand on it. Which is what we will do in next blog posts.

Code

We will start off with creating a file called ‘provider.tf’. As the filename suggests we will configure the Terraform AWS provider in that file.

provider "aws" {
  version    = "~> 2.0"
  access_key = "${var.ec2_aws_access_key}"
  secret_key = "${var.ec2_aws_secret_key}"
  region     = "${var.ec2_region}"
}

The meat of configuring EC2 instances is in the below file. I have the tendency to call this file the main.tf file but as you likely know, the filename is entirely up to your imagination.

As we want to create an EC2 instance, we will add a resource ‘aws_instance’ and provide it the arguments it requires. I think these speak for itself. In case you are interested what other arguments can be specified, please refer to the Terraform documentation.

As we want to know which IP addresses AWS assigned to our EC2 instance, we are also outputting some these. Again, if you are interested in all the arguments that can be output, please see here.

resource "aws_instance" "OneServer" {
  ami           = "${var.ec2_image}"
  instance_type = "${var.ec2_instance_type}"
  key_name      = "${var.ec2_keypair}"
  tags = {
    Name = "${var.ec2_tags}"
  }
}

output "instance_ip_addr" {
  value       = "${aws_instance.OneServer.private_ip}"
  description = "The private IP address of the main server instance."
}

output "instance_ips" {
  value = ["${aws_instance.OneServer.*.public_ip}"]
}

output "instance_id" {
  value = ["${aws_instance.OneServer.id}"]
}

You will notice that we are referencing some variables in that file so we also need to declare them. This is something we will do in the variables.tf file.

variable "ec2_aws_access_key" {
  default = "AKI***BZ5A"
}

variable "ec2_aws_secret_key" {
  default = "GDl***CnS7V"
}

variable "ec2_region" {
  default = "eu-west-1"
}

variable "ec2_image" {
  default = "ami-00035f41c82244dab"
}

variable "ec2_instance_type" {
  default = "t2.micro"
}

variable "ec2_keypair" {
  default = "AWS-Cisco"
}
variable "ec2_tags" {
  default = "Cisco-Demo-Terraform-1"
}

Deployment

The beauty of Terraform is its simplicity. Start off with running ‘terraform init’. This will essentially download the required plugins.

WAUTERW-M-T3ZT:Terraform_AWS_initial wim$ terraform init

Initializing the backend...

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "aws" (terraform-providers/aws) 2.12.0...

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

Terraform plan will show you a blueprint of things that are about the created/updated. No surprise here we will see that the plan is to create 1 instance (see bottom of the output).

WAUTERW-M-65P7:_Create_1_EC2_instance_3_files wauterw$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_instance.OneServer will be created
  + resource "aws_instance" "OneServer" {
      + ami                          = "ami-00035f41c82244dab"
      + arn                          = (known after apply)
      + associate_public_ip_address  = (known after apply)
      + availability_zone            = (known after apply)
      + cpu_core_count               = (known after apply)
      + cpu_threads_per_core         = (known after apply)
      + get_password_data            = false
      + host_id                      = (known after apply)
      + id                           = (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                     = "AWS-Cisco"
      + network_interface_id         = (known after apply)
      + password_data                = (known after apply)
      + placement_group              = (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)
      + security_groups              = (known after apply)
      + source_dest_check            = true
      + subnet_id                    = (known after apply)
      + tags                         = {
          + "Name" = "Cisco-Demo-Terraform-1"
        }
      + tenancy                      = (known after apply)
      + volume_tags                  = (known after apply)
      + vpc_security_group_ids       = (known after apply)

      + ebs_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + snapshot_id           = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      + ephemeral_block_device {
          + device_name  = (known after apply)
          + no_device    = (known after apply)
          + virtual_name = (known after apply)
        }

      + network_interface {
          + delete_on_termination = (known after apply)
          + device_index          = (known after apply)
          + network_interface_id  = (known after apply)
        }

      + root_block_device {
          + delete_on_termination = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }
    }

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

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

And finally, we will run ‘terraform’ apply. This will take the execution plan and effectively apply that plan to AWS.

WAUTERW-M-65P7:_Create_1_EC2_instance_3_files wauterw$ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_instance.OneServer will be created
  + resource "aws_instance" "OneServer" {
      + ami                          = "ami-00035f41c82244dab"
      + arn                          = (known after apply)
      + associate_public_ip_address  = (known after apply)
      + availability_zone            = (known after apply)
      + cpu_core_count               = (known after apply)
      + cpu_threads_per_core         = (known after apply)
      + get_password_data            = false
      + host_id                      = (known after apply)
      + id                           = (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                     = "AWS-Cisco"
      + network_interface_id         = (known after apply)
      + password_data                = (known after apply)
      + placement_group              = (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)
      + security_groups              = (known after apply)
      + source_dest_check            = true
      + subnet_id                    = (known after apply)
      + tags                         = {
          + "Name" = "Cisco-Demo-Terraform-1"
        }
      + tenancy                      = (known after apply)
      + volume_tags                  = (known after apply)
      + vpc_security_group_ids       = (known after apply)

      + ebs_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + snapshot_id           = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      + ephemeral_block_device {
          + device_name  = (known after apply)
          + no_device    = (known after apply)
          + virtual_name = (known after apply)
        }

      + network_interface {
          + delete_on_termination = (known after apply)
          + device_index          = (known after apply)
          + network_interface_id  = (known after apply)
        }

      + root_block_device {
          + delete_on_termination = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }
    }

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.OneServer: Creating...
aws_instance.OneServer: Still creating... [10s elapsed]
aws_instance.OneServer: Still creating... [20s elapsed]
aws_instance.OneServer: Still creating... [30s elapsed]
aws_instance.OneServer: Creation complete after 40s [id=i-01f0430652eb0430b]

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

Outputs:

instance_id = [
  "i-01f0430652eb0430b",
]
instance_ip_addr = 172.31.3.67
instance_ips = [
  [
    "34.254.199.197",
  ],
]

If you are following along with this blogpost, log in to your AWS account and you will see your instance up and running.

Something noteworthy is the tfstate that Terraform maintains. The tfstate is found in your local directory (from where you run the Terraform commands) and should never be touched. It contains all the information related to the state of your configuration. I added a full output below for you to verify.

{
  "version": 4,
  "terraform_version": "0.12.10",
  "serial": 2,
  "lineage": "9ce053fb-793f-6e61-efd7-bbc4ca54a7d9",
  "outputs": {
    "instance_id": {
      "value": [
        "i-01f0430652eb0430b"
      ],
      "type": [
        "tuple",
        [
          "string"
        ]
      ]
    },
    "instance_ip_addr": {
      "value": "172.31.3.67",
      "type": "string"
    },
    "instance_ips": {
      "value": [
        [
          "34.254.199.197"
        ]
      ],
      "type": [
        "tuple",
        [
          [
            "tuple",
            [
              "string"
            ]
          ]
        ]
      ]
    }
  },
  "resources": [
    {
      "mode": "managed",
      "type": "aws_instance",
      "name": "OneServer",
      "provider": "provider.aws",
      "instances": [
        {
          "schema_version": 1,
          "attributes": {
            "ami": "ami-00035f41c82244dab",
            "arn": "arn:aws:ec2:eu-west-1:852350637351:instance/i-01f0430652eb0430b",
            "associate_public_ip_address": true,
            "availability_zone": "eu-west-1b",
            "cpu_core_count": 1,
            "cpu_threads_per_core": 1,
            "credit_specification": [
              {
                "cpu_credits": "standard"
              }
            ],
            "disable_api_termination": false,
            "ebs_block_device": [],
            "ebs_optimized": false,
            "ephemeral_block_device": [],
            "get_password_data": false,
            "host_id": null,
            "iam_instance_profile": "",
            "id": "i-01f0430652eb0430b",
            "instance_initiated_shutdown_behavior": null,
            "instance_state": "running",
            "instance_type": "t2.micro",
            "ipv6_address_count": 0,
            "ipv6_addresses": [],
            "key_name": "AWS-Cisco",
            "monitoring": false,
            "network_interface": [],
            "network_interface_id": null,
            "password_data": "",
            "placement_group": "",
            "primary_network_interface_id": "eni-0e85df79e36b49d3a",
            "private_dns": "ip-172-31-3-67.eu-west-1.compute.internal",
            "private_ip": "172.31.3.67",
            "public_dns": "ec2-34-254-199-197.eu-west-1.compute.amazonaws.com",
            "public_ip": "34.254.199.197",
            "root_block_device": [
              {
                "delete_on_termination": true,
                "encrypted": false,
                "iops": 100,
                "kms_key_id": "",
                "volume_id": "vol-0bb5a836e1fe3f40d",
                "volume_size": 8,
                "volume_type": "gp2"
              }
            ],
            "security_groups": [
              "default"
            ],
            "source_dest_check": true,
            "subnet_id": "subnet-62df4504",
            "tags": {
              "Name": "Cisco-Demo-Terraform-1"
            },
            "tenancy": "default",
            "timeouts": null,
            "user_data": null,
            "user_data_base64": null,
            "volume_tags": {},
            "vpc_security_group_ids": [
              "sg-c3b88cbd"
            ]
          },
          "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwidXBkYXRlIjo2MDAwMDAwMDAwMDB9LCJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
        }
      ]
    }
  ]
}

Also deleting the instance is very simple using Terraform. Just issue a ‘terraform destroy’ command, hit enter, accept the change and off you go. No more instances on AWS. While easy and nice, I’m sure you will also understand the danger of this.

WAUTERW-M-65P7:_Create_1_EC2_instance_3_files wauterw$ terraform destroy
aws_instance.OneServer: Refreshing state... [id=i-01f0430652eb0430b]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aws_instance.OneServer will be destroyed
  - resource "aws_instance" "OneServer" {
      - ami                          = "ami-00035f41c82244dab" -> null
      - arn                          = "arn:aws:ec2:eu-west-1:852350637351:instance/i-01f0430652eb0430b" -> null
      - associate_public_ip_address  = true -> null
      - availability_zone            = "eu-west-1b" -> null
      - cpu_core_count               = 1 -> null
      - cpu_threads_per_core         = 1 -> null
      - disable_api_termination      = false -> null
      - ebs_optimized                = false -> null
      - get_password_data            = false -> null
      - id                           = "i-01f0430652eb0430b" -> null
      - instance_state               = "running" -> null
      - instance_type                = "t2.micro" -> null
      - ipv6_address_count           = 0 -> null
      - ipv6_addresses               = [] -> null
      - key_name                     = "AWS-Cisco" -> null
      - monitoring                   = false -> null
      - primary_network_interface_id = "eni-0e85df79e36b49d3a" -> null
      - private_dns                  = "ip-172-31-3-67.eu-west-1.compute.internal" -> null
      - private_ip                   = "172.31.3.67" -> null
      - public_dns                   = "ec2-34-254-199-197.eu-west-1.compute.amazonaws.com" -> null
      - public_ip                    = "34.254.199.197" -> null
      - security_groups              = [
          - "default",
        ] -> null
      - source_dest_check            = true -> null
      - subnet_id                    = "subnet-62df4504" -> null
      - tags                         = {
          - "Name" = "Cisco-Demo-Terraform-1"
        } -> null
      - tenancy                      = "default" -> null
      - volume_tags                  = {} -> null
      - vpc_security_group_ids       = [
          - "sg-c3b88cbd",
        ] -> null

      - credit_specification {
          - cpu_credits = "standard" -> null
        }

      - root_block_device {
          - delete_on_termination = true -> null
          - encrypted             = false -> null
          - iops                  = 100 -> null
          - volume_id             = "vol-0bb5a836e1fe3f40d" -> null
          - volume_size           = 8 -> null
          - volume_type           = "gp2" -> null
        }
    }

Plan: 0 to add, 0 to change, 1 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

aws_instance.OneServer: Destroying... [id=i-01f0430652eb0430b]
aws_instance.OneServer: Still destroying... [id=i-01f0430652eb0430b, 10s elapsed]
aws_instance.OneServer: Still destroying... [id=i-01f0430652eb0430b, 20s elapsed]
aws_instance.OneServer: Still destroying... [id=i-01f0430652eb0430b, 30s elapsed]
aws_instance.OneServer: Destruction complete after 31s

Destroy complete! Resources: 1 destroyed.

In AWS, you will see the instance we created earlier in the blog post is in a terminated state.

Thanks for reading guys, hope to see you back in a next blog post some time.

Leave a Reply

Your email address will not be published. Required fields are marked *