Create your simple infrastructure using IaC Tool Terraform, CloudFormation or AWS CDK
Tags: terraform, cdk, aws, 100daysawsiacdevopschallenge
Published: 2024-07-17 11:01:10


###### <span style="color: #1B9CFC;font-weight: bold;font-size: 22px;border: 1px solid #FC427B;padding: 1rem;border-radius: 5% ">Day 001 of 100DaysAWSIaCDevopsChallenge</span>

____

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
* [Diagram of the infrastructure](#diagram)
* [Network section](#network)
* [Security section](#security)
* [Compute section](#computer)
* [Deploy Infrastructure](#run)


### 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 <a name="diagram"></a>

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t1zwdq45fm584myj1fau.jpg)

### Network section <a name="network"></a>
#### 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_ <a name="vpc_tf"></a>

```hcl
# 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"
  }
}
```
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 <u>/16</u> to <u>/28</u>. 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_ <a name="vpc_cfn"></a>

```yaml
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"
```

You can see that the configuration are the same like [terraform](#vpc_tf)

<code>
+----------------------+--------------------+
|   Terraform          | CoudFormation      | 
|----------------------+--------------------| 
| cidr_block           | CidrBlock          | 
| enable_dns_support   | EnableDnsSupport   | 
| enable_dns_hostnames | EnableDnsHostnames | 
| tags                 | Tags               |
+----------------------+--------------------+
</code>

###### AWS CDK - Typescript <a name="vpc_cdk"></a>

```ts
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'}]
,
});
```
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](#vpc_tf).

##### 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
```hcl 
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"
  }
}
```

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
```yaml
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"

```
`!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
```ts
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'}]
});

```

`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

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

```

The `vpc_id` is required in terraform.

________
###### </> CloudFormation
```yaml
  MyInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: "InternetGateway"
```

And then attach to the VPC in another resource called <span style="color: #2ec4b6">AWS::EC2::VPCGatewayAttachment</span>
Let's now attach our VPC to the internet gateway
```yaml
  IGWVpcAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref MainVpc
      InternetGatewayId: !Ref MyInternetGateway
```
_________
###### </> CDK
```ts
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,
});
```
`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 <span style='color: #ffbf69'>Route Table</span>. 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
```hcl
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"
  }
}
```

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:

```hcl
# 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
}
```

And then associate the subnet to the route table created.

```hcl
resource "aws_route_table_association" "rt_assoc" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.rt.id
}
```

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:
```yaml
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
```

Now the routing is well configured using the cloudformation tool.

###### CDK
The principle is exactly the same as for cloudformation
```js
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
});
```

### Security Section <a name='security'></a>
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:

```hcl
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]
}
```
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:link:](https://registry.terraform.io/providers/hashicorp/tls/latest/docs),[local doc :link:](https://registry.terraform.io/providers/hashicorp/local/latest/docs)

now, I create the keypair with the private key previously created
```hcl
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"
  }
}
```

⚠️ `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.
```hcl
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"
  }
}
```

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

###### </> CloudFormation

```yaml
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"
```

<a name="cfn_private-key-desc"></a>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](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/create-key-pairs.html#create-key-pair-cloudformation)


###### </> CDK
```ts
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"}]
});
```


### Computer Section <a name='computer'></a>
Now that everything is configured, let's create the EC2 Instance inside the subnet.

###### </> tarraform

```hcl
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"
  }
}
```

Below is the content of <span style="color: gray; text-decoration: underline">bootstrap.sh.tpl</span>
```sh
#!/bin/bash
sudo su
apt update -y
apt install nginx -y
systemctl start nginx.service
```

###### </> Cloudformation

```yaml
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"

```
###### </> CDK

```ts
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);

```

And the content of <span style="color: gray; text-decoration: underline">./assets/ec2_bootstrap_script.sh</span>
```sh
sudo apt update
sudo apt install nginx -y
sudo systemctl start nginx
```
⚠️⚠️ 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:
```js
const userData = ec1.UserData.forLinux({
  shebang: '#!/env/bin/sh sh' // 🤓
});
```

### Run differents codes <a name='run'></a>
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:
```bash 
git clone \
   --branch day1/create-an-instance-inside-vpc-and-igw \
   https://github.com/nivekalara237/100DaysTerraformAWSDevops.git
```
###### </> Terraform
```bash
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 
```

###### </> CloudFormation
```bash
cd 100DaysTerraformAWSDevops/day1/cfn
aws cloudformation deploy \
  --stack-name MyStack \
  --template-file stack.yaml \
  --capabilities "CAPABILILITY_NAMED_IAM" \
  --profile cfn-user
```

Or you can deploy the stack file directly in [aws cloudformation console](https://console.aws.amazon.com/cloudformation/)

###### </> CDK

```bash
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
```
_____
We've reached the end of this tutorial which marks my debut on dev.to 🤩
Find the full code at [Github repository](https://github.com/nivekalara237/100DaysTerraformAWSDevops/tree/day1/create-an-instance-inside-vpc-and-igw/day_001)

Hope It can help.