Multi Cloud Network Architecture

Building Networking Skills in the age of the Cloud

Multi Cloud Network Architecture

Terraform for Infrastructure Architects – Part 1

Share on linkedin
LinkedIn
Share on twitter
Twitter
Share on email
Email

Context

My objective is to give you a first taste of planning, writing, and executing a piece of Infrastructure as Code in Terraform in under 30 minutes.

We’ll build on this example case in future posts. As we execute more and more complex examples, we’ll go deeper into what Terraform is capable of and what problems it can solve in your Cloud projects.

I’m assuming some basic understanding of “Infrastructure as Code” and how Terraform fits into this movement. I may do a background post around this at a later stage, but for the moment, let’s say you know roughly what IaC has as an objective and would like to get a quick practical guide or exposure to one of its leading toolsets, Terraform.

www.terraform.io

What you need to get started

A good attitude and the below:

  1. GitHub or similar repository. Yes, you can code directly from a folder on your desktop, but I strongly advise getting into the habit of using version control. Check out my blog post here for getting up and running : https://mcna.dev/github-getting-started/
  2. Credentials for the Provider you want to use. In this example we will be using AWS, so you’ll need to create an AWS user with programmatic access and copy the appropriate credentials locally so Terraform can make changes
  3. An IDE to actually write your code in. I am a big fan of Visual Studio code and you can get a copy here : https://code.visualstudio.com/
  4. Terraform installed on your machine. I won’t go over that here as the instructions are pretty straightforward. I use a Mac and used homebrew for the installation. Otherwise, check out the link here : https://learn.hashicorp.com/tutorials/terraform/install-cli

Why Amazon Web Services

This blog post, as well as the follow-up series, focuses on AWS as a Provider. This means simply that the code we will write will build infrastructure (resources) on AWS.

I did this on purpose for reasons of simplicity as, whether we like it or not, they have the lion’s share of the public cloud market as of writing, 2021. They also have a free tier making it really easy to get up and running.

The Terraform workflow

At this stage, we will learn and use these four key Terraform commands.

Terraform Init

This instructs Terraform to download any additional helper code specific to the Providers referenced in the code. It generally needs to be run once per session (this code doesn’t change much on the backend) but you can harmlessly run it as often as you want.

Terraform Plan

This is a great due-diligence, optional, command. I recommend running it every time you execute your code. It will tell you quickly if you have any errors in your code.

Terraform Apply

This is the big one. It basically goes out and builds your environment to the specification you outlined in your code.

Terraform Destroy

Careful here. There’s no going back. DO NOT USE IT IN PRODUCTION. This is great for infrastructure scenario testing as you can destroy everything you created at the end of your testing session and avoid being billed for running infrastructure that you are not using.

(Terraform Version)

A useful command that is not necessarily in the workflow. It’s good for making sure you’re up to date.

Check that Terraform is installed correctly and that you are up to date as follows:

A note on main.tf

Terraform generally runs in the project directory implicitly referencing the file “main.tf”. This is what you need to name your code in order for Terraform to execute it. Therefore, in a terminal session on your local machine, navigate to the project directory (clones repository) and run your Terraform commands. Terraform will automatically execute the main.tf file in that directory. Always good to be sure you are in the right directory before execution.

What are we going to build?

Let’s start small, just so we can get used to the workflow, inputs, and outputs.

We are going to launch an EC2 instance in the default VPC in our AWS account, verify on the AWS console that it got built, and then destroy it again.

Not much to it, but even here there is a minimum of information to collect before we code.

Let’s forget about the code for a second. If I asked you to create an EC2 instance in AWS for me, you’d probably have some questions for me:

For the sake of simplicity, we will assume as many defaults as possible in our example. The only 3 main things we absolutely must define are:

So, let’s get started.

1. Create your project repository

Whilst not strictly necessary (you could run the code from your local machine), it’s good practice to have all your code in some form of version control somewhere. 

I use GitHub and you can find my post on getting started in 10 minutes here.

Once you have your repository created, clone it locally and open it in your favorite IDE. I use Visual Code as I find their Terraform plugin really useful.

2. Create AWS credentials if you don’t already have them

Create an AWS IAM user with appropriate permissions (at least for EC2) and create access keys.

Download access keys locally and add them to your local environment. Do not add these to your code, even if it is possible. If it is in a public-facing repository, everyone will have access. We’ll look at cleaner ways of managing these credentials in another post.

Add these locally (mac version here)

paul@Pauls-iMac Terraform % export AWS_ACCESS_KEY=<your key here>
paul@Pauls-iMac Terraform % export AWS_SECRET_ACCESS_KEY=<your key here>

Running “env” (on a mac in any case) should show them installed:

paul@Pauls-iMac Terraform % env

TERM_PROGRAM=Apple_Terminal
SHELL=/bin/zsh
TERM=xterm-256color
...
AWS_ACCESS_KEY=XXXXXX
AWS_SECRET_ACCESS_KEY=XXXXXXX
...

If you don’t have your credentials installed locally, Terraform will give you an error during your PLAN:

pauly@Pauls-iMac blog_1 % terraform plan
╷
│ Error: error configuring Terraform AWS Provider: no valid credential sources for Terraform AWS Provider found.
│ 
│ Please see https://registry.terraform.io/providers/hashicorp/aws
│ for more information about providing credentials.
│ 
│ Error: NoCredentialProviders: no valid providers in chain. Deprecated.
│ 	For verbose messaging see aws.Config.CredentialsChainVerboseErrors
│ 
│ 
│   with provider["registry.terraform.io/hashicorp/aws"],
│   on main.tf line 17, in provider "aws":
│   17: provider "aws" {
│ 
╵
pauly@Pauls-iMac blog_1 % 

3. Get Terraforming

So far I will assume the following:

  • Terraform is installed locally (validated with terraform -v)
  • Your repository is created, cloned locally and opened in your IDE. See here if you need any further help: https://mcna.dev/github-getting-started/
  • Your main.tf file is created here and ready to have some code inserted

Your environment should look something like this:

Let’s start by telling Terraform that we would like to build infrastructure in AWS. We are basically telling Terraform: the code in this document will need to use the AWS provider in order to be executed, so please go ahead and load everything you need in order to make that happen. This “helper” coder will be downloaded locally after running terraform init


1
2
3
4
5
6
7
8
9
10
#define the terraform providers

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~&gt; 3.0"
    }
  }
}

Now, let’s specify the AWS region we want to work in. This is pretty self-explanatory. In the case below we are telling Terraform that we will be working in the Frankfurt AWS region: eu-central-1.


1
2
3
4
5
#define the AWS region

provider "aws" {
    region = "eu-central-1"
}

And finally, let’s tell Terraform what resource(s) we want created. In this case, an EC2 instance. We are telling Terraform to build a t2.micro from a specific AMI image file.

The “ami” information can be found in the AWS console when you want to launch an instance. It refers to the image build:

Likewise, the available instance type (VM capabilities) can also be found here:

Simply record these values in your code.


1
2
3
4
5
6
7
8
#define AWS resources


resource "aws_instance" "mcna_ec2" {
    ami = "ami-00f22f6155d6d92c5"
    instance_type = "t2.micro"

}

Here is the final code we are going to use all put together. This is what will get executed.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#define the terraform providers

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~&gt; 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"

}

4. Run that code

First off, let’s run the INIT to make sure the provider code is loaded correctly:

Now let’s PLAN:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
pauly@Pauls-iMac blog_1 % 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:

  # 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.

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
pauly@Pauls-iMac blog_1 %

Finally, let’s APPLY:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
pauly@Pauls-iMac blog_1 %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.

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-0884b00dae7f4a22d]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
pauly@Pauls-iMac blog_1 %

5. Verify the result on AWS

Let’s go to the AWS console and check our work:

The EC2 instance has been created successfully.

The machine has been created in the default VPC, in the next available AZ using the default Security Groups.

6. One last thing…

If you don’t want to get billed for stuff you’re not using, let’s go ahead and delete the environment we just created.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
pauly@Pauls-iMac blog_1 % terraform destroy
aws_instance.mcna_ec2: Refreshing state... [id=i-0884b00dae7f4a22d]

Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the last "terraform apply":

  # aws_instance.mcna_ec2 has been changed
  ~ resource "aws_instance" "mcna_ec2" {
        id                                   = "i-0884b00dae7f4a22d"
      + tags                                 = {}
        # (28 unchanged attributes hidden)





        # (5 unchanged blocks hidden)
    }

Unless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes.

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

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

Terraform will perform the following actions:

  # aws_instance.mcna_ec2 will be destroyed
  - resource "aws_instance" "mcna_ec2" {
      - ami                                  = "ami-00f22f6155d6d92c5" -&gt; null
      - arn                                  = "arn:aws:ec2:eu-central-1:823499938473:instance/i-0884b00dae7f4a22d" -&gt; null
      - associate_public_ip_address          = true -&gt; null
      - availability_zone                    = "eu-central-1b" -&gt; null
      - cpu_core_count                       = 1 -&gt; null
      - cpu_threads_per_core                 = 1 -&gt; null
      - disable_api_termination              = false -&gt; null
      - ebs_optimized                        = false -&gt; null
      - get_password_data                    = false -&gt; null
      - hibernation                          = false -&gt; null
      - id                                   = "i-0884b00dae7f4a22d" -&gt; null
      - instance_initiated_shutdown_behavior = "stop" -&gt; null
      - instance_state                       = "running" -&gt; null
      - instance_type                        = "t2.micro" -&gt; null
      - ipv6_address_count                   = 0 -&gt; null
      - ipv6_addresses                       = [] -&gt; null
      - monitoring                           = false -&gt; null
      - primary_network_interface_id         = "eni-0644f659e04d262e9" -&gt; null
      - private_dns                          = "ip-172-31-33-175.eu-central-1.compute.internal" -&gt; null
      - private_ip                           = "172.31.33.175" -&gt; null
      - public_dns                           = "ec2-18-184-71-21.eu-central-1.compute.amazonaws.com" -&gt; null
      - public_ip                            = "18.184.71.21" -&gt; null
      - secondary_private_ips                = [] -&gt; null
      - security_groups                      = [
          - "default",
        ] -&gt; null
      - source_dest_check                    = true -&gt; null
      - subnet_id                            = "subnet-01b6017d" -&gt; null
      - tags                                 = {} -&gt; null
      - tags_all                             = {} -&gt; null
      - tenancy                              = "default" -&gt; null
      - vpc_security_group_ids               = [
          - "sg-a73b80d4",
        ] -&gt; null

      - capacity_reservation_specification {
          - capacity_reservation_preference = "open" -&gt; null
        }

      - credit_specification {
          - cpu_credits = "standard" -&gt; null
        }

      - enclave_options {
          - enabled = false -&gt; null
        }

      - metadata_options {
          - http_endpoint               = "enabled" -&gt; null
          - http_put_response_hop_limit = 1 -&gt; null
          - http_tokens                 = "optional" -&gt; null
        }

      - root_block_device {
          - delete_on_termination = true -&gt; null
          - device_name           = "/dev/xvda" -&gt; null
          - encrypted             = false -&gt; null
          - iops                  = 100 -&gt; null
          - tags                  = {} -&gt; null
          - throughput            = 0 -&gt; null
          - volume_id             = "vol-0cb9d1fc49f86729f" -&gt; null
          - volume_size           = 8 -&gt; null
          - volume_type           = "gp2" -&gt; 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.mcna_ec2: Destroying... [id=i-0884b00dae7f4a22d]
aws_instance.mcna_ec2: Still destroying... [id=i-0884b00dae7f4a22d, 10s elapsed]
aws_instance.mcna_ec2: Still destroying... [id=i-0884b00dae7f4a22d, 20s elapsed]
aws_instance.mcna_ec2: Still destroying... [id=i-0884b00dae7f4a22d, 30s elapsed]
aws_instance.mcna_ec2: Still destroying... [id=i-0884b00dae7f4a22d, 40s elapsed]
aws_instance.mcna_ec2: Still destroying... [id=i-0884b00dae7f4a22d, 50s elapsed]
aws_instance.mcna_ec2: Destruction complete after 51s

Destroy complete! Resources: 1 destroyed.
pauly@Pauls-iMac blog_1 %

Checkpoint

By this stage you should be able to do the following:

  • Create a new Terraform project in version control and clone it locally
  • Verify that your installed Terraform version is up to date using terraform -v
  • Create a simple piece of Terraform code using AWS as a provider in the main.tf file
  • Initialise your code using terraform init
  • Plan your code and check for errors using terraform plan
  • Deploy your code using terraform apply
  • Destroy your environment using terraform destroy

Next session

In the next session, we will consolidate some fundamentals such as state management and getting output from code before we move on to more complex code.

Share if you liked it
Share on linkedin
LinkedIn
Share on twitter
Twitter
Share on email
Email
0 0 votes
Article Rating
Subscribe
Notify of
guest
1 Comment
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
trackback

[…] 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 […]

20 year veteran of the networking industry currently specialising in Cloud Networking and Security.

CCIE #16661 (R&S, SP)

Disclaimer

I am currently an employee of Aviatrix. All opinions, views and statements are my own and do not reflect that of my employer. Any errors are mine and mine alone. Any ignorance is mine, though I do believe my parents and the public school system should shoulder some of that blame. 

Recent Posts

Archives