Context
In the second part of our series, I would like to take the time to consolidate some of the things we learned in Part 1 and use that simple example to examine some of Terraforms fundamentals.
The learning objectives are:
- How does Terraform maintain state?
- How to get information out of Terraform concerning your environment?
Terraform State
Terraform doesn’t just build an environment and forget about it. When you define your code, there are a lot of unknowns about the infrastructure that you will create. These unknowns are only knowable as the infrastructure is instantiated.
Terraform manages this situation using “state” files. A state file is simply a JSON file that records the current state of your deployed environment allowing Terraform to safely update/modify/destroy only the scoped resources in your code or instruction.
Let’s have a look at my local directory for our previous example BEFORE I run terraform apply:
![](https://mcna.dev/wp-content/uploads/2021/07/image-6.png)
Now let’s have a look AFTER a terraform apply:
![](https://mcna.dev/wp-content/uploads/2021/07/image-7.png)
You can see that Terraform has created a new file called terraform.tfstate.
Let’s have a look at what’s going on in this file:
{
"version": 4,
"terraform_version": "1.0.3",
"serial": 1,
"lineage": "8a84ca1b-3086-aa46-992e-204746b5a311",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "aws_instance",
"name": "mcna_ec2",
"provider": "provider["registry.terraform.io/hashicorp/aws"]",
"instances": [
{
"schema_version": 1,
"attributes": {
"ami": "ami-00f22f6155d6d92c5",
"arn": "arn:aws:ec2:eu-central-1:823499938473:instance/i-09d490123b859aabe",
"associate_public_ip_address": true,
"availability_zone": "eu-central-1b",
"capacity_reservation_specification": [
{
"capacity_reservation_preference": "open",
"capacity_reservation_target": []
}
],
"cpu_core_count": 1,
"cpu_threads_per_core": 1,
"credit_specification": [
{
"cpu_credits": "standard"
}
],
"disable_api_termination": false,
"ebs_block_device": [],
"ebs_optimized": false,
"enclave_options": [
{
"enabled": false
}
],
"ephemeral_block_device": [],
"get_password_data": false,
"hibernation": false,
"host_id": null,
"iam_instance_profile": "",
"id": "i-09d490123b859aabe",
"instance_initiated_shutdown_behavior": "stop",
"instance_state": "running",
"instance_type": "t2.micro",
"ipv6_address_count": 0,
"ipv6_addresses": [],
"key_name": "",
"launch_template": [],
"metadata_options": [
{
"http_endpoint": "enabled",
"http_put_response_hop_limit": 1,
"http_tokens": "optional"
}
],
"monitoring": false,
"network_interface": [],
"outpost_arn": "",
"password_data": "",
"placement_group": "",
"primary_network_interface_id": "eni-0b3d71590cb076845",
"private_dns": "ip-172-31-35-220.eu-central-1.compute.internal",
"private_ip": "172.31.35.220",
"public_dns": "ec2-18-185-92-249.eu-central-1.compute.amazonaws.com",
"public_ip": "18.185.92.249",
"root_block_device": [
{
"delete_on_termination": true,
"device_name": "/dev/xvda",
"encrypted": false,
"iops": 100,
"kms_key_id": "",
"tags": {},
"throughput": 0,
"volume_id": "vol-0178128a9f8806c83",
"volume_size": 8,
"volume_type": "gp2"
}
],
"secondary_private_ips": [],
"security_groups": [
"default"
],
"source_dest_check": true,
"subnet_id": "subnet-01b6017d",
"tags": null,
"tags_all": {},
"tenancy": "default",
"timeouts": null,
"user_data": null,
"user_data_base64": null,
"volume_tags": null,
"vpc_security_group_ids": [
"sg-a73b80d4"
]
},
"sensitive_attributes": [],
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6MTIwMDAwMDAwMDAwMCwidXBkYXRlIjo2MDAwMDAwMDAwMDB9LCJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="
}
]
}
]
}
Never edit this file. Terraform is in charge of maintaining it.
As you can see, Terraform learns and records the real resource information after instantiation. This is how Terraform knows which resource(s) to delete specifically when you run a terraform destroy
.
Let’s take a concrete example. Before instantiating the EC2 instance, we did not know what its public DNS name would be. However, Terraform learned this and stored it in the state file:
"public_dns": "ec2-18-185-92-249.eu-central-1.compute.amazonaws.com"
Terraform Output
We can of course create the instance and then go to the AWS console and check for this information. However, since Terraform finds it out, he could just tell us. We’ll have a look at that below.
Let’s build on the example from last time. Let’s keep the code the same, but we’ll add an additional command:
# Terraform for Infrastructure Architects - Part 1
#define the terraform providers
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
#define the AWS region
provider "aws" {
region = "eu-central-1"
}
#define AWS resources
resource "aws_instance" "mcna_ec2" {
ami = "ami-00f22f6155d6d92c5"
instance_type = "t2.micro"
}
Let’s add the code to present us the output:
output "publicdns" {
value = aws_instance.mcna_ec2.public_dns
}
Now let’s see what terraform apply gives us:
pauly@Pauls-iMac blog_2 % 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:
# aws_instance.mcna_ec2 will be created
+ resource "aws_instance" "mcna_ec2" {
+ ami = "ami-00f22f6155d6d92c5"
+ 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)
+ disable_api_termination = (known after apply)
+ ebs_optimized = (known after apply)
+ get_password_data = false
+ host_id = (known after apply)
+ id = (known after apply)
+ instance_initiated_shutdown_behavior = (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)
+ 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)
+ secondary_private_ips = (known after apply)
+ security_groups = (known after apply)
+ source_dest_check = true
+ subnet_id = (known after apply)
+ tags_all = (known after apply)
+ tenancy = (known after apply)
+ user_data = (known after apply)
+ user_data_base64 = (known after apply)
+ vpc_security_group_ids = (known after apply)
+ capacity_reservation_specification {
+ capacity_reservation_preference = (known after apply)
+ capacity_reservation_target {
+ capacity_reservation_id = (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)
+ tags = (known after apply)
+ throughput = (known after apply)
+ volume_id = (known after apply)
+ volume_size = (known after apply)
+ volume_type = (known after apply)
}
+ enclave_options {
+ enabled = (known after apply)
}
+ ephemeral_block_device {
+ device_name = (known after apply)
+ no_device = (known after apply)
+ virtual_name = (known after apply)
}
+ metadata_options {
+ http_endpoint = (known after apply)
+ http_put_response_hop_limit = (known after apply)
+ http_tokens = (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)
+ device_name = (known after apply)
+ encrypted = (known after apply)
+ iops = (known after apply)
+ kms_key_id = (known after apply)
+ tags = (known after apply)
+ throughput = (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.
Changes to Outputs:
+ publicdns = (known after apply)
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.mcna_ec2: Creating...
aws_instance.mcna_ec2: Still creating... [10s elapsed]
aws_instance.mcna_ec2: Still creating... [20s elapsed]
aws_instance.mcna_ec2: Creation complete after 22s [id=i-0ccdbe1f2aa119285]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
publicdns = "ec2-52-28-71-121.eu-central-1.compute.amazonaws.com"
pauly@Pauls-iMac blog_2 %
Checkpoint
By this stage you should be aware of:
- the notion of state: why and where Terraform maintains this state
- how to get output from your Terraform code
Understanding the basics of how and why Terraform maintains state is critical to building complex infrastructure and collaborating across teams. As you have seen, the state file is currently local on your machine, which would make cross-team collaboration and testing tricky. We’ll look later at how to manage state for collaboration.
Likewise, getting output out of your code is crucial to your workflow. Imagine after instantiating the EC2 instance you want to connect to it in order to test, but you need to go to the AWS console to get the DNS name. Not practical.
Next Steps
In the next session, we will build a web server on an EC2 instance and create a Security Group to protect it. We’ll also look at some additional Terraform commands which will be useful as we move on to more complex projects.