You are on page 1of 19

In this post, we are going to deploy an AWS EC2 instance and all the related network components (and

their
features) required to access it through SSH.
For that it’s necessary to deploy following AWS services :
– VPC (Virtual Private Cloud)
– Security Group
– Subnet
– Route Table
– Internet Gateway
– And finally the EC2 instance
For a better manageability I do recommend to create one dedicated file per service listed above. This will also give a
good overview what the Terraform Configuration is made of :
joc@joc:$ tree
1
.
2
├── aws-config.tf
3

├── create-igw.tf
4

5 ├── create-instance.tf

6 ├── create-rt.tf

7 ├── create-sbn.tf

8 ├── create-sg.tf
9
├── create-vpc.tf
10
├── variables_output.tf
11
└── variables.tf
12
 
13
0 directories, 9 files

Variables
Using variables in a Terraform Configuration is a good practice. Like with all other tools and programming languages,
their purpose is to create a value only once and to reuse it across the whole deployment configuration.
The best is to create a dedicated file to store them. Here’s what mine looks like :
1 # variables.tf
2  

3 # Variables for general information

4 ######################################
5
 

6
variable "aws_region" {
7
  description = "AWS region"
8
  type        = string
9
  default     = "eu-central-1"
10
}
11
 
12
variable "owner" {
13

14   description = "Configuration owner"

15   type        = string

16 }

17  

18 variable "aws_region_az" {
19
  description = "AWS region availability zone"
20
  type        = string
21
  default     = "a"
22
}
23
 
24
 
25
# Variables for VPC
26
######################################
27

 
28

29 variable "vpc_cidr_block" {

30   description = "CIDR block for the VPC"


31   type        = string

32   default     = "10.0.0.0/16"
33
}
34
 
35
variable "vpc_dns_support" {
36
  description = "Enable DNS support in the VPC"
37
  type        = bool
38
  default     = true
39
}
40

 
41

42 variable "vpc_dns_hostnames" {

43   description = "Enable DNS hostnames in the VPC"

44   type        = bool

45   default     = true
46
}
47
 
48
 
49
# Variables for Security Group
50
######################################
51
 
52
variable "sg_ingress_proto" {
53
  description = "Protocol used for the ingress rule"
54
  type        = string
55

56   default     = "tcp"

57 }

58  

59 variable "sg_ingress_ssh" {
60   description = "Port used for the ingress rule"

61   type        = string
62
  default     = "22"
63
}
64
 
65
variable "sg_egress_proto" {
66
  description = "Protocol used for the egress rule"
67
  type        = string
68
  default     = "-1"
69

70 }

71  

72 variable "sg_egress_all" {

73   description = "Port used for the egress rule"

74   type        = string
75
  default     = "0"
76
}
77
 
78
variable "sg_egress_cidr_block" {
79
  description = "CIDR block for the egress rule"
80
  type        = string
81
  default     = "0.0.0.0/0"
82

83 }

84  

85  

86 # Variables for Subnet

87 ######################################

88  
89 variable "sbn_public_ip" {

90   description = "Assign public IP to the instance launched into the subnet"


91
  type        = bool
92
  default     = true
93
}
94
 
95
variable "sbn_cidr_block" {
96
  description = "CIDR block for the subnet"
97
  type        = string
98

99   default     = "10.0.1.0/24"

100 }

101  

102  

103 # Variables for Route Table

104 ######################################
105
 

106
variable "rt_cidr_block" {
107
  description = "CIDR block for the route table"
108
  type        = string
109
  default     = "0.0.0.0/0"
110
}
111
 
112
 
113
# Variables for Instance
114

115 ######################################

116  

117 variable "instance_ami" {


118   description = "ID of the AMI used"

119   type        = string


120
  default     = "ami-0211d10fb4a04824a"
121
}
122
 
123
variable "instance_type" {
124
  description = "Type of the instance"
125
  type        = string
126
  default     = "t2.medium"
127

128 }

129  

130 variable "key_pair" {

131   description = "SSH Key pair used to connect"

132   type        = string


133
  default     = "joc-key-pair"
134
}
135
 
136
variable "root_device_type" {
137
  description = "Type of the root block device"

  type        = string

  default     = "gp2"

variable "root_device_size" {

  description = "Size of the root block device"

  type        = string
  default     = "50"

Each of these variable will be used to describe our environment and they will need to be prefixed by var. when we will
be using them.
Let’s start with the first step.

Provider
As already said, Terraform supports infrastructure deployment to several Cloud providers. Thus, the first things to do
in our Configuration is to define the provider we want to use.
As the providers’ API have several versions available, it’s important to define a version constraint to force Terraform
to select a single version
that all parts of your configuration are compatible with. If you don’t do that you take the risk to use a newer version
that might not be compatible with your Configuration.
A good way to force the maximum version we want to work with is to use the ~> operator. This will allow to use only
patch releases within a specific minor release :
# aws_config.tf
1
 
2
terraform {
3
  required_providers {
4
    aws = {
5

6
      source  = "hashicorp/aws"

7       version = "~> 3.26"

8     }

9   }
10
}
11
 

12
provider "aws" {
13
  profile = "default"
14
  region  = var.aws_region
15
}
As I don’t set my AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY (security), Terraform will use the one
coming from my environment variables.

VPC
VPC (Virtual Private Cloud) is one of the fundamental component to create when starting an AWS project. It allows to
launch resources in a virtual and isolated network. Creating a VPC is optional, but it becomes mandatory when you
want to create interconnections with others networks or if you want to isolate some EC2 instances in a subnet
unreachable from outside :
# create-vpc.tf
1
 
2
resource "aws_vpc" "vpc" {
3

  cidr_block           = var.vpc_cidr_block
4

5   enable_dns_hostnames = var.vpc_dns_hostnames

6   enable_dns_support   = var.vpc_dns_support

7  

8   tags = {

9     "Owner" = var.owner
10
    "Name"  = "${var.owner}-vpc"
11
  }
12
}

This defines the IP range that will be used and enable DNS support and hostname so the instance can get a DNS name.

Security Group
To control inbound and outbound access to an EC2 instance, it’s required to create a Security Group.
A Security Group is like the local firewall of the instance. If we want to be able to connect to the instance via SSH we
need to create an ingress rule allowing our IP to connect to TCP port 22.
In order to be able to connect to the instance from wherever we are located (home, office, …), let’s store our current
public IP using the data sources module of Terraform. Then it can be used as a variable to define the value of
the cidr_blocks argument in the ingress attributes :
1 # create-sg.tf

2  
3 data "http" "myip" {

4   url = "http://ipv4.icanhazip.com"
5
}
6
 
7
resource "aws_security_group" "sg" {
8
  name        = "${var.owner}-sg"
9
  description = "Allow inbound traffic via SSH"
10
  vpc_id      = aws_vpc.vpc.id
11
 
12
  ingress = [{
13

14     description      = "My public IP"

15     protocol         = var.sg_ingress_proto

16     from_port        = var.sg_ingress_ssh

17     to_port          = var.sg_ingress_ssh
18
    cidr_blocks      = ["${chomp(data.http.myip.body)}/32"]
19
    ipv6_cidr_blocks = []
20
    prefix_list_ids  = []
21
    security_groups  = []
22
    self             = false
23
 
24
  }]
25

 
26

27   egress = [{

28     description      = "All traffic"

29     protocol         = var.sg_egress_proto

30     from_port        = var.sg_egress_all
31
    to_port          = var.sg_egress_all

    cidr_blocks      = [var.sg_egress_cidr_block]

32     ipv6_cidr_blocks = []
33
    prefix_list_ids  = []
34
    security_groups  = []
35
    self             = false
36
 
37
  }]
38
 
39
  tags = {
40

    "Owner" = var.owner
41

42     "Name"  = "${var.owner}-sg"

  }

A second rules exists in this Security Group to allow the instance to connect outside.

Subnet
A subnet must be created inside the VPC, with its own CIDR block, which is a subset of the VPC CIDR block :
1 #create-sbn.tf

2  

3 resource "aws_subnet" "subnet" {


4
  vpc_id                  = aws_vpc.vpc.id
5
  cidr_block              = var.sbn_cidr_block
6
  map_public_ip_on_launch = var.sbn_public_ip
7
  availability_zone       = "${var.aws_region}${var.aws_region_az}"
8
 
9
  tags = {
10
    "Owner" = var.owner
11
    "Name"  = "${var.owner}-subnet"
12
  }
13
}

If you plan to create more than one subnet, think about deploying them into different Availability Zones.

Internet Gateway
We also need an Internet Gateway to enable access over the Internet.
# create-igw.tf
1
 
2

resource "aws_internet_gateway" "igw" {


3

4   vpc_id = aws_vpc.vpc.id

5  

6   tags = {

7     "Owner" = var.owner

8     "Name"  = "${var.owner}-igw"
9
  }
10
}

If the traffic of a subnet is routed to the Internet Gateway, the subnet is known as a public subnet. That means that all
instances connected to this subnet can connect the Internet through the Internet Gateway.
To define this association, we need a Route Table :

Route Table
1 #create-rt.tf

2  

3 resource "aws_route_table" "rt" {


4
  vpc_id = aws_vpc.vpc.id
5
 
  route {
6
    cidr_block = var.rt_cidr_block
7

8
    gateway_id = aws_internet_gateway.igw.id

9   }

10  

11   tags = {

12     "Owner" = var.owner
13
    "Name"  = "${var.owner}-rt"
14
  }
15
 
16
}

The link between the subnet and the route table is done by creating an association :

1 resource "aws_route_table_association" "rt_sbn_asso" {

2   subnet_id      = aws_subnet.subnet.id

3   route_table_id = aws_route_table.rt.id

4 }

Instance
The network layer is now ready. We can create the EC2 instance in the subnet of our VPC :
1 # create-instance.tf

2  

3 resource "aws_instance" "instance" {


4
  ami                         = var.instance_ami
5
  availability_zone           = "${var.aws_region}${var.aws_region_az}"
6
  instance_type               = var.instance_type
7
  associate_public_ip_address = true
  vpc_security_group_ids      = [aws_security_group.sg.id]
8
  subnet_id                   = aws_subnet.subnet.id
9
  key_name                    = var.key_pair
10
 
11
  root_block_device {
12
    delete_on_termination = true
13

14
    encrypted             = false

15     volume_size           = var.root_device_size

16     volume_type           = var.root_device_type

17   }
18  

19
  tags = {
20
    "Owner"               = var.owner
21
    "Name"                = "${var.owner}-instance"
22
    "KeepInstanceRunning" = "false"
23
  }
24
}

Init
Everything is ready. To start the deployment, we need first of all to initialize our working directory :
1 joc@joc:$ terraform init

2  

3 Initializing the backend...


4
 

5
Initializing provider plugins...
6
- Finding latest version of hashicorp/http...
7
- Finding hashicorp/aws versions matching "~> 3.26"...

8
- Installing hashicorp/http v2.0.0...

9 - Installed hashicorp/http v2.0.0 (signed by HashiCorp)

10 - Installing hashicorp/aws v3.26.0...

11 - Installed hashicorp/aws v3.26.0 (signed by HashiCorp)


12  

13
Terraform has created a lock file .terraform.lock.hcl to record the provider
14
selections it made above. Include this file in your version control repository
15
so that Terraform can guarantee to make the same selections by default when
16
you run "terraform init" in the future.
17
 
18
Terraform has been successfully initialized!
19
 
20
You may now begin working with Terraform. Try running "terraform plan" to see
21

22
any changes that are required for your infrastructure. All Terraform commands

23 should now work.

24  

25 If you ever set or change modules or backend configuration for Terraform,

26 rerun this command to reinitialize your working directory. If you forget, other
27 commands will detect it and remind you to do so if necessary.

joc@joc:$

Validate
Then we can check if the code of our Configuration is syntactically valid :
1 joc@joc:$ terraform validate

2 Success! The configuration is valid.


3  

4 joc@joc:$

Plan
Terraform can show what will be deployed using the plan command. This is very useful to check all the resources
before creating them.
As the output is quite long I’ll truncate it and keep only some lines :
1 joc@joc:$ terraform plan

2 var.owner
3
  Configuration owner
4
 
5
  Enter a value: joc
6
 
7
 
8
An execution plan has been generated and is shown below.
9
Resource actions are indicated with the following symbols:
10
  + create
11
 
12
Terraform will perform the following actions:
13

14 ...

15 ...

16 ...

17 Plan: 7 to add, 0 to change, 0 to destroy.


18
 

19
Changes to Outputs:
20
  + public_ip = (known after apply)
21
 
------------------------------------------------------------------------
22
 
23
Note: You didn't specify an "-out" parameter to save this plan, so Terraform
24
can't guarantee that exactly these actions will be performed if
25
"terraform apply" is subsequently run.
26
 
27
joc@joc:~$

At line n° 5 we are prompted to give a name to the owner of the Configuration. This is because we didn’t set a default
value to the variable “owner” (see variables.tf). As you may have noticed, this variable is regularly used to name the
resources as well as to tag them. This will be helpful when working with these resources.

Apply
Finally we can apply the execution plan and everything will be created in few minutes only :
1 joc@joc:$ terraform apply

2 var.owner
3
  Configuration owner
4
 
5
  Enter a value: joc
6
 
7
 
8
An execution plan has been generated and is shown below.
9
Resource actions are indicated with the following symbols:
10
  + create
11
 
12
Terraform will perform the following actions:
13

14 ...

15 ...

16 ...
aws_vpc.vpc: Creating...

aws_vpc.vpc: Creation complete after 3s [id=vpc-00aa8a9136306adf2]


17
aws_internet_gateway.igw: Creating...
18
aws_subnet.subnet: Creating...
19
aws_security_group.sg: Creating...
20
aws_internet_gateway.igw: Creation complete after 1s [id=igw-069cde646f3d4967f]
21

22 aws_route_table.rt: Creating...

23 aws_subnet.subnet: Creation complete after 1s [id=subnet-07034f2432203028e]

24 aws_route_table.rt: Creation complete after 0s [id=rtb-06fd67d98d0d8d24c]

25 aws_route_table_association.rt_sbn_asso: Creating...
26
aws_route_table_association.rt_sbn_asso: Creation complete after 1s [id=rtbassoc-
04b8fd0f0984d648b]
27

28 aws_security_group.sg: Creation complete after 2s [id=sg-0c3547bc9189af478]

29 aws_instance.instance: Creating...

30 aws_instance.instance: Still creating... [10s elapsed]

31 aws_instance.instance: Still creating... [20s elapsed]


32
aws_instance.instance: Still creating... [30s elapsed]
33
aws_instance.instance: Creation complete after 33s [id=i-0c102b97c854bbb80]
34
 
35
Apply complete! Resources: 7 added, 0 changed, 0 destroyed.
36
 
37
Outputs:
38
 
39
public_ip = "18.194.28.183"

joc@joc:$

The last line displays the public IP of the newly created instance. The is done thanks to the output variable defined
into the file variables_output.tf :
# variable_output.tf
1

 
2

3 # Variables to show after the deployment

4 #########################################

5  

6 output "public_ip" {

7   value = aws_instance.instance.public_ip
8
}

 
We should now be able to connect to the instance :
joc@joc:$ ssh -i "~/joc-key-pair.pem" ec2-user@18.194.28.183
1

2 The authenticity of host '18.194.28.183 (18.194.28.183)' can't be established.

3 ECDSA key fingerprint is SHA256:aFg3EBxHgGENRKvFyMpZbfFPbAqz0RRiZqpsXM8T1po.

4 Are you sure you want to continue connecting (yes/no/[fingerprint])? yes

5 Warning: Permanently added '18.194.28.183' (ECDSA) to the list of known hosts.


6
[ec2-user@ip-10-0-1-194 ~]$

 
That’s it ! Let’s see what it looks like from the AWS Console…
VPC :

 
 
 
Security Group :
Subnet :

Internet Gateway :

Route Table :

Instance :

You might also like