DEV Community

Cover image for Create your simple infrastructure using IaC Tool Terraform, CloudFormation or AWS CDK
Kevin Lactio Kemta
Kevin Lactio Kemta

Posted on • Edited on

Create your simple infrastructure using IaC Tool Terraform, CloudFormation or AWS CDK

Day 001 of 100DaysAWSIaCDevopsChallenge

In this article I am going to design a simple AWS infrastructure and build it using three IaC (Infrastructure As Code) tools: AWS CloudFormation, AWS CDK and Terraform. The most popular of these is Terraform which provides the easiest way to create cloud insfrastructure. it gives the hability to build in any cloud providers such AWS, Google Cloud Platform, Microsoft Azure Kubernetes. While you can only build an AWS Infrastructure using CloudFormation or CDK, because these tools are designed by Amazon to fit their solution.

The infrastructure to build consist of creating an EC2 instance, which will be publicly accessible on port 80 (HTTP connection) and port 22 (SSH connection). It will reside inside a Virtual Private Cloud (VPC) that contains one public subnet. To make EC2 connects and communicates with internet, I will create an internet gateway for the VPC that will route traffic from the internet into the subnet and vice versa.

Table of contents

Prerequises

  • Basic acknownleage of AWS Networking (VPC, Subnet and Routing)
  • Basic acknownleage of AWS Security (Security group and Key Pair)
  • Elastic cloud computer (EC2)
  • Terraform
  • CloudFormation & CDK
  • Typescript

Diagram of the infrastructure

Image description

Network section

Virtual Private Cloud (VPC)

In this section I am going to create the network resources that will route users coming from the internet to our cloud. To do this I will start by creating the virtual private cloud (VPC)

_

Terraform version
# The main VPC
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = "MainVpc"
  }
}
Enter fullscreen mode Exit fullscreen mode

In this project, I use IPv4 protocol, it's also possible to use IPv6.

  • cidr_block: This is an optional field, if you don't specify it, you must fill in ipv4_netmax_length which aws will used to derived the CIDR block from IPAM. The allowed block size ranges from /16 to /28. Change this field by ipv6_cidr_block if you want to use IPv6 protocol instead (respectively ipv6_netmax_length if you want amazon to derived it from IPAM).

  • enable_dns_support: To enable/disable DNS support in the VPC.

  • enable_dns_hostnames: To enable/desable DNS hostnames support in the VPC.

  • tags: the map of tags to assign to the VPC resource.

CloudFormation version
Resources:
  MainVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !FindInMap [ CidrMap, Vpc, Value ] # will return "10.0.0.0/16"
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: "MainVpc"
Enter fullscreen mode Exit fullscreen mode

You can see that the configuration are the same like terraform


+----------------------+--------------------+
|   Terraform          | CoudFormation      | 
|----------------------+--------------------| 
| cidr_block           | CidrBlock          | 
| enable_dns_support   | EnableDnsSupport   | 
| enable_dns_hostnames | EnableDnsHostnames | 
| tags                 | Tags               |
+----------------------+--------------------+

Enter fullscreen mode Exit fullscreen mode
AWS CDK - Typescript
const vpc = new ec2.CfnVPC(this, "MainVpc", {
   enableDnsHostnames: true,
   enableDnsSupport: true,
   instanceTenancy: "default",
   cidrBlock: props.cidrVpc,// will return "10.0.0.0/16"
   tags: [{key: 'Name', value: 'MainVpc'}]
,
});
Enter fullscreen mode Exit fullscreen mode

I use to create my VPC using the Level 1 to avoid the additional resources which could be created if I use level 2 (ec2.Vpc(this, "MainVpc, {...})). For more details about the configuration refere to terraform section.

Public Subnet

Now I am going to create subnet. It is simple to understand. To create a subnet there are three mandatories parameters.

  • The VPC resource ID: we can get this parameter in the previous VPC created
  • The Availlability Zone: the availlability zone in which the subnet will be created
  • The IP address range (CIDR block): The CIDR block will be within our VPC's CIDR block. To have a suitable way, I will use the utility function provided by all IaC tools based on the VPC CIDR.

As the subnet will be public, I am going to add an additional parameter that tell aws to associate a public IP address to our EC2 Instance on launch time.
Let's jump to the code:

</> Terraform
resource "aws_subnet" "public" {
  vpc_id                                      = aws_vpc.main.id
  cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, 4)
  availability_zone                           = "us-east-1a"
  map_public_ip_on_launch                     = true
  enable_resource_name_dns_a_record_on_launch = true
  depends_on = [aws_vpc.main]
  tags = {
    Name = "PublicSubnet"
  }
}
Enter fullscreen mode Exit fullscreen mode

Terraform provide for us cidrsubnet(cidr_based, count, bits) function to create cidr for the subnet based the parent cidr (in this case the vpc's cidr).
I create the dependency with the VPC by using depends_on, it instructs terraform to create the vpc first and then create the subnet. So that we can extract the resource Id of vpc to attach it to the subnet.
Note that I add map_public_ip_on_launch and enable_resource_name_dns_a_record_on_launch to associate Public IP Adress and generate the dns address for us.

</> CloudFormation
Mappings:
  CidrMap:
    Vpc:
      Value: "10.0.0.0/16"
    ...

Resource:
  PublicSubnet:
    Type: AWS::EC2::Subnet
    DependsOn: MainVpc
    Properties:
      VpcId: !Ref MainVpc
      CidrBlock: !Cidr [!FindInMap [ CidrMap, Vpc, Value ], 8, 4 ] 
      AvailabilityZone: !Join [ "", [ !Ref Region, "a" ] ]
      MapPublicIpOnLaunch: true
      PrivateDnsNameOptionsOnLaunch:
        EnableResourceNameDnsARecord: true
        HostnameType: ip-name
      Tags:
        - Key: Name
          Value: "Public-Subnet"

Enter fullscreen mode Exit fullscreen mode

!Ref MainVpc return the VPC Resource ID previously created
and !Join [ "", [ !Ref Region, "a" ] ] return us-east-1a, considering that the current region is us-east-1

</> AWS CDK - Typescript
const publicSubnet = new ec2.CfnSubnet(this, "PublicSubnet", {,
    cidrBlock: Fn.cidr(props.cidrVpc, 8, 4),
    vpcId: vpc.attrVpcId,
    availabilityZone: "us-east-1",
    mapPublicIpOnLaunch: true,
    tags: [{key: 'Name', value: 'PublicSubnet'}]
});

Enter fullscreen mode Exit fullscreen mode

vpc.attrVpcId return the VPC Resource ID previously created

Ineternet Gateway

Remember I call my subnet a Public Subnet because all the underlined resources need to exchange with internet. To make this possible I need to configure an Internet Gateway. An Internet gateway helps us to route traffic throught the internet. Internet gateway is very simple to create whether with terraform, Cloudformation or even CDK. However, there's a slight difference between Terraform and Cloudformation/CDK. When using terraform you need to provide the VPC Resource ID directly, while with Cloudformation and CDK, you to attach it separatly.

</> Terraform
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id //<-- mandatory
  depends_on = [aws_vpc.main]
  tags = {
    Name = "InternetGateway"
  }
}

Enter fullscreen mode Exit fullscreen mode

The vpc_id is required in terraform.


</> CloudFormation
  MyInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: "InternetGateway"
Enter fullscreen mode Exit fullscreen mode

And then attach to the VPC in another resource called AWS::EC2::VPCGatewayAttachment
Let's now attach our VPC to the internet gateway

  IGWVpcAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref MainVpc
      InternetGatewayId: !Ref MyInternetGateway
Enter fullscreen mode Exit fullscreen mode

</> CDK
const igw = new ec2.CfnInternetGateway(this, "MyInternetGateway", {
    tags: [{key: 'Name', value: 'InternetGateway'}],
});
// Attach VPC to internet gateway
new ec2.CfnVPCGatewayAttachment(this, `VpcGatewayAttachment`, {
    vpcId: vpc.attrVpcId,
    internetGatewayId: igw.attrInternetGatewayId,
});
Enter fullscreen mode Exit fullscreen mode

igw.attrInternetGatewayId return the Internet Gateway Resource ID

Routing

Now that our VPC, Subnet and Internet gateway are created, the next step is to create the routing inside our cloud infrastructure. To do this AWS has made availlable for us the Route Table. The concept is simple: the route table determines where network traffic is directed based on the destination IP address. Public subnet in your VPC is associated with a route table that controls the traffic flow between the subnet.
To establish routing withn your cloud infrastructure, I need to first create a Route for our VPC and Internet Gateway, then create a route table to associate with the public subnet. The method differs slightly between terraform and cloudformation/cdk. I will explain the difference in the code section.

</>Terraform
resource "aws_route_table" "rt" {
  vpc_id = aws_vpc.main.id
  route { // <-- the route
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }
  depends_on = [aws_vpc.main]
  tags = {
    Name = "RouteTable"
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the route is directly included in the terraform aws_route_table resource route. Note that, it's possible to configure more than one route into the aws_route_table resource.

On the other hand, if you don't want to include route(s) in the aws_route_table resource, you can create its separactly like this:

# the Route Table
resource "aws_route_table" "rt" {
  vpc_id = aws_vpc.main.id // required
  depends_on = [aws_vpc.main]
  tags = {
    Name = "RouteTable"
  }
}
# The route
resource "aws_route" "myroute" {
  route_table_id         = aws_route_table.rt.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.igw.id
}
Enter fullscreen mode Exit fullscreen mode

And then associate the subnet to the route table created.

resource "aws_route_table_association" "rt_assoc" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.rt.id
}
Enter fullscreen mode Exit fullscreen mode

Op! la! The routing is well configured using the terraform tool 😇.

</> Cloudformation

For cloudformation I need first to create a route table for our VPC, then create the route that associates the Internet Gateway + route table previously created + destination CIDR Block. After the route table and its routes is created, I added a new resource to associate our subnet with the route table. Let's jump into the code right now:

Mappings:
  CidrMap:
    ...
    Internet:
      Value: "0.0.0.0/0"
    ...

PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref MainVpc
      Tags:
        - Key: Name
          Value: "RouteTable"
  MainRoute:
    Type: AWS::EC2::Route
    Properties:
      GatewayId: !Ref InternetGateway
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: !FindInMap [ CidrMap, Internet, Value ]
  PublicSubnetRoutingAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable
Enter fullscreen mode Exit fullscreen mode

Now the routing is well configured using the cloudformation tool.

CDK

The principle is exactly the same as for cloudformation

const routeTable = new ec2.CfnRouteTable(this, "RouteTable", {
    vpcId: vpc.attrVpcId,
    tags: [{key: 'Name', value: 'RouteTable'}],
});

const routeGateway = new ec2.CfnRoute(this, "Route", {
    routeTableId: routeTable.attrRouteTableId,
    destinationCidrBlock: props.cidrVpcInternet,
    gatewayId: igw.attrInternetGatewayId
});

new ec2.CfnSubnetRouteTableAssociation(this, `SubnetRouteTable-attach`, {
    subnetId: publicSubnet.attrSubnetId,
    routeTableId: routeTable.attrRouteTableId
});
Enter fullscreen mode Exit fullscreen mode

Security Section

This part consist of creating the barrier around the Ec2 Instance. I need to expose my instance to the internet on port 80 by using security group. Additionaly, I need to configure ssh access for my instance. Furthermore, I will create Aws KeyPair that allow me to access my Instance.

</> Terraform

Firstly, In Terraform I want to generate the public and private keys that we will need to create the AWS KeyPair. This is the snippets code to generate these keys:

resource "tls_private_key" "key" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

resource "local_file" "privatekey" {
  filename = "day1kp.pem"
  content  = tls_private_key.key.private_key_pem
  depends_on = [tls_private_key.key]
}
Enter fullscreen mode Exit fullscreen mode

As you can see I used tls and local providers to generate keys and save the private key locally (It will help to use ssh bash command). for more about tls and local providers, refer to tls docs🔗,local doc 🔗

now, I create the keypair with the private key previously created

resource "aws_key_pair" "keypair" {
  key_name   = "day1kp"
  public_key = tls_private_key.key.public_key_openssh
  depends_on = [tls_private_key.key]
  tags = {
    Name = "Instance-KeyPair"
  }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Note: here the public key need to store in the EC2 Instance

Let's now create the securities groups which will use by the EC2 Instance.

variable "host_machine_ip_addr" {
  type = string
}
resource "aws_security_group" "ssh" {
  name        = "Allow-SSH"
  description = "Allow SSH inbound traffic"
  vpc_id      = aws_vpc.main.id
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "all"
    cidr_blocks = ["0.0.0.0/0"]
    description = "allow internet access outbound"
  }
  ingress {
    from_port = 22
    to_port   = 22
    protocol  = "tcp"
    cidr_blocks = ["${var.host_machine_ip_addr}/32"]
  }
  depends_on = [data.local_file.ipfile]
  tags = {
    Name = "Allow-SSH"
  }
}

resource "aws_security_group" "http" {
  name        = "http-sg"
  vpc_id      = aws_vpc.main.id
  description = "Allow HTTP traffic from/to ec2"
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "all"
    cidr_blocks = ["0.0.0.0/0"]
    description = "allow internet access outbound"
  }
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "allow internet access inbound"
  }
  tags = {
    Name = "Allow-HTTP"
  }
}
Enter fullscreen mode Exit fullscreen mode

var.host_machine_ip_addr contains the IP address which will use for SSH connection.

</> CloudFormation
Mappings:
  CidrMap:
    ...
    Internet:
      Value: "0.0.0.0/0"
    ...
Parameters:
  HostMachineIpAddr:
    Type: String
    Description: "The host machine Ip Address"

Resource:
  InstanceKeyPair:
    Type: AWS::EC2::KeyPair
    Properties:
      KeyName: "day1kp"
      KeyFormat: pem
      KeyType: rsa
      Tags:
        - Key: Name
          Value: "InstanceKeyPair"
  InstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    DependsOn: MainVpc
    Properties:
      GroupDescription: "Allowing traffics in/out ec2 instance"
      GroupName: "Allow-HTTP-SSH"
      VpcId: !Ref MainVpc
      SecurityGroupEgress:
        - CidrIp: !FindInMap [ CidrMap, Internet, Value ]
          Description: "Allow traffic to the internet"
          FromPort: 0
          ToPort: 0
          IpProtocol: "-1"
      SecurityGroupIngress:
        - CidrIp: !FindInMap [ CidrMap, Internet, Value ]
          Description: "Allow HTTP traffic to the instance"
          FromPort: 80
          ToPort: 80
          IpProtocol: "tcp"
        - CidrIp: !Join [ "/", !Ref HostMachineIpAddr, "32" ]
          Description: "Allow SSH traffic to the instance"
          FromPort: 22
          ToPort: 22
          IpProtocol: "tcp"
      Tags:
        - Key: Name
          Value: "InstanceSecurityGroup"
Enter fullscreen mode Exit fullscreen mode

After the Stack is created, if you want to connect to the instance using ssh, you can import the private-key which is stored in the Secret System Manager Parameter Store during the stack creation. If you want more about the retrieving private key refer to the AWS CloudFormation docs

</> CDK
const cidrVpcInternet = "0.0.0.0/0";
const securityGroup = new ec2.CfnSecurityGroup(this, "SecurityGroup", {
    groupDescription: "Allowing traffic from/to instance",
    groupName: "allow-http-and-ssh",
    vpcId: vpc.attrVpcId,
    securityGroupEgress: [{
        fromPort: 0,
        toPort: 0,
        ipProtocol: '-1',
        description: "Allow the outbound traffic to anywhere",
        cidrIp: cidrVpcInternet
    }],
    securityGroupIngress: [{
        fromPort: 22,
        toPort: 22,
        ipProtocol: "tcp",
        description: "Allow SSH traffic",
        cidrIp: cidrVpcInternet
    },
    {
        fromPort: 80,
        toPort: 80,
        ipProtocol: "tcp",
        description: "Allow HTTP traffic",
        cidrIp: cidrVpcInternet
    }],
    tags: [{Key: 'Name', Value: "WebserverSG"}]
});
// key pair for ssh connection
const keypair = new ec2.CfnKeyPair(this, "InstanceKeyPair", {
    keyName: "day1kp",
    keyType: "rsa",
    keyFormat: "pem",
    tags: [{Key: 'Name', Value: "Webserver-KeyPair"}]
});
Enter fullscreen mode Exit fullscreen mode

Computer Section

Now that everything is configured, let's create the EC2 Instance inside the subnet.

</> tarraform
resource "aws_instance" "webapp" {
  instance_type = "t2.micro"
  ami           = var.ami_ubuntu-2204-tls
  key_name      = aws_key_pair.keypair.key_name
  vpc_security_group_ids = [aws_security_group.http.id, aws_security_group.ssh.id]
  subnet_id     = aws_subnet.public.id
  user_data = templatefile("bootstrap.sh.tpl", {})

  depends_on = [
    aws_subnet.public,
    aws_key_pair.keypair,
    aws_security_group.http,
    aws_security_group.ssh
  ]

  tags = {
    Name = "WebAppInstance"
  }
}
Enter fullscreen mode Exit fullscreen mode

Below is the content of bootstrap.sh.tpl

#!/bin/bash
sudo su
apt update -y
apt install nginx -y
systemctl start nginx.service
Enter fullscreen mode Exit fullscreen mode
</> Cloudformation
Mappings:
  Ec2Settings:
    Type:
      Value: t2.micro
    AMI:
      Value: ami-04b70fa74e45c3917

Resources:
  WebInstance:
    Type: AWS::EC2::Instance
    DependsOn:
      - PublicSubnet
      - InstanceSecurityGroup
    Properties:
      ImageId: !FindInMap [ Ec2Settings, AMI, Value ]
      InstanceType: !FindInMap [ Ec2Settings, Type, Value ]
      Tenancy: default
      SubnetId: !Ref PublicSubnet
      SecurityGroupIds:
        - !Ref InstanceSecurityGroup
      KeyName: !Ref InstanceKeyPair
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          sudo apt update -y
          sudo apt install nginx -y
          sudo systemctl start nginx.service
      Tags:
        - Key: Name
          Value: "WebAppInstance"

Enter fullscreen mode Exit fullscreen mode
</> CDK
const userData = UserData.forLinux();
userData.addCommands(readFileSync("./assets/ec2_bootstrap_script.sh", "utf-8"))
const webserver = new ec2.CfnInstance(this, "WebInstance", {
    keyName: keypair.keyName,
    subnetId: subnet.attrSubnetId,
    instanceType: "t2.micro",
    imageId: "ami-04b70fa74e45c3917",
    securityGroupIds: [sg.attrGroupId],
    userData: Fn.base64(userData.render()),
    tags: [{Key: "Name", Value: "Webserver-Instance"}]
});
webserver.addDependency(sg);
webserver.addDependency(subnet);

Enter fullscreen mode Exit fullscreen mode

And the content of ./assets/ec2_bootstrap_script.sh

sudo apt update
sudo apt install nginx -y
sudo systemctl start nginx
Enter fullscreen mode Exit fullscreen mode

⚠️⚠️ Pay attention about the script above. You can notice it doesn't start with a shebang #!/bin/bash, it's because the userData.render() method automatically adds the linux shebang #!/bin/bash if it isn't provided. To avoid the default value we can specify another shebang during the initialization of userData like this:

const userData = ec1.UserData.forLinux({
  shebang: '#!/env/bin/sh sh' // 🤓
});
Enter fullscreen mode Exit fullscreen mode

Run differents codes

Now that we have setup up and explain each layer steb by step, it's time to run our entire infrastructure.
To begin, clone first the code by typing:

git clone \
   --branch day1/create-an-instance-inside-vpc-and-igw \
   https://github.com/nivekalara237/100DaysTerraformAWSDevops.git
Enter fullscreen mode Exit fullscreen mode
</> Terraform
cd 100DaysTerraformAWSDevops/day1/terraform
terraform init -upgrade
terraform plan # and expect that everything is good
# -auto-approve is to avoid the manual approval
terraform apply -auto-approve 
Enter fullscreen mode Exit fullscreen mode
</> CloudFormation
cd 100DaysTerraformAWSDevops/day1/cfn
aws cloudformation deploy \
  --stack-name MyStack \
  --template-file stack.yaml \
  --capabilities "CAPABILILITY_NAMED_IAM" \
  --profile cfn-user
Enter fullscreen mode Exit fullscreen mode

Or you can deploy the stack file directly in aws cloudformation console

</> CDK
cd 100DaysTerraformAWSDevops/day1/cdk
# https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html
npm install -g aws-cdk
npm install
# prepare aws environment for stack deployment
cdk bootstrap --profile cdk-user
# display the resources to deploy (optional)
## same as `terraform plan`
cdk diff --profile cdk-user
# and then deploy
cdk deploy --profile cdk-user
Enter fullscreen mode Exit fullscreen mode

We've reached the end of this tutorial which marks my debut on dev.to 🤩
Find the full code at Github repository

Hope It can help.

Top comments (2)

Collapse
 
ivanbass profile image
cedric basso

Nice article!

Collapse
 
nivekalara237 profile image
Kevin Lactio Kemta

Thank you @ivanbass 🙂