Provisioning an Apache HTTP Server with AWS CloudFormation

Image title

Have a code snippet. Or two.

From the AWS CloudFormation homepage:

"CloudFormation allows you to use a simple text file to model and provision, in an automated and secure manner, all the resources needed for your applications across all regions and accounts. This file serves as the single source of truth for your cloud environment."

CloudFormation is a web service that falls under the Infrastructure-as-Code (IaC) category. IaC allows users to define their infrastructure in a text file using a declarative approach to model your infrastructure. The text file is called "template" and is written in either JSON or YAML notation.

You may also enjoy:  Using CloudFormation to Set Up Scalable Apps

In this post, I am going to share two templates file written in YAML which, when executed, provision an EC2 instance and install an Apache HTTP server without you logging in to the EC2 terminal. This can be used for beginners to get started with CloudFormation and add more configurations.

FAQs

Why Not Have Them All In One File?

It is true that you can declare all the resources in a single file, but as a best practice, it is always recommended to modularize your IaC. This helps in individual testing, deployment, the controlled impact of change, and most importantly, sharing. As an example, if there is an issue with my lambda deployment or EC2 deployment, then it helps and saves time to rollback or update only these components and leave the VPC and database as they are.

Why YAML?

To be honest, I have worked a lot on JSON (still using it in my current project), but after reading people's experience and trying both myself, I personally think YAML is suited better for a template. You don't see double-quotes and curly braces in YAML, which makes it more readable, plus you can add a lot of comments. Anyway, if you are comfortable with JSON, then go for it because YAML needs some learning (mine is still in progress).

So, let's get started. These template files are also available on GitHub for your reference.

Step 1

Create a base stack that consists of a VPC, RouteTable, NACL, IGW, and a Subnet. Based on your region, please provide the appropriate parameter values in the CloudFormation dashboard.

AWSTemplateFormatVersion: 2010-09-09
Description: 
  Sample template to create a VPC with IGW and public IP enabled.
  You will be billed for the AWS resources used if you create a stack from this template.
  After deleting stack, remember to delete the associated S3 bucket.

Parameters:
  # CIDR of VPC
  NetworkCIDR:
    Description: CIDR of the new VPC
    Type: String
    Default: 10.0.0.0/16

  # AZ Name where subnet will be created
  AvailabilityZoneName:
    Description: CIDR of the new VPC
    Type: AWS::EC2::AvailabilityZone::Name
    Default: ap-south-1a

  # CIDR of new subnet within this VPC
  SubnetCIDR:
    Description: CIDR of the new subnet within this VPC
    Type: String
    Default: 10.0.1.0/24 

Resources:
  # create VPC
  myVPC:
    Type: AWS::EC2::VPC
    Properties: 
      CidrBlock: !Ref NetworkCIDR
      EnableDnsHostnames: 'true'
      EnableDnsSupport: 'true'
      InstanceTenancy: default
      Tags: 
       - Key: Name
         Value: demo-vpc
       - Key: Application
         Value: !Ref 'AWS::StackName'

  # create Internet Gateway
  myIGW:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags: 
       - Key: Name
         Value: demo-igw
       - Key: Application
         Value: !Ref 'AWS::StackName'           

  # attaching the IGW to my VPC
  vpcToIgw:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref myVPC
      InternetGatewayId: !Ref myIGW

  # create a custom route table for demo vpc
  myRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref myVPC
      Tags: 
       - Key: Name
         Value: demo-public-route-table
       - Key: Application
         Value: !Ref 'AWS::StackName'

  # Add routes entries for public network through igw
  myRoutes:
    Type: AWS::EC2::Route    
    Properties:
      RouteTableId: !Ref myRouteTable
      DestinationCidrBlock: '0.0.0.0/0'
      GatewayId: !Ref myIGW      

  # NACL
  myPublicNACL:
    Type: 'AWS::EC2::NetworkAcl'
    Properties:
      VpcId: !Ref myVPC
      Tags:
        - Key: Name
          Value: demo-vpc-nacl
        - Key: Application
          Value: !Ref 'AWS::StackName'        

  # Allow all Incoming TCP traffic
  myNaclRulesForInboundTCP:
    Type: 'AWS::EC2::NetworkAclEntry'
    Properties:
      NetworkAclId: !Ref myPublicNACL
      RuleNumber: '100'
      Protocol: '6'  # tcp
      RuleAction: allow
      Egress: 'false'  # this rule applies to ingress traffic to the subnet
      CidrBlock: 0.0.0.0/0  # any ip address
      PortRange:
        From: '0'
        To: '65535'

  # Allow all Outgoing TCP traffic
  myNaclRulesForOutboundTCP:
    Type: 'AWS::EC2::NetworkAclEntry'
    Properties:
      NetworkAclId: !Ref myPublicNACL
      RuleNumber: '100'
      Protocol: '6'  # tcp
      RuleAction: allow
      Egress: 'true'  # this rule applies to egress traffic from the subnet
      CidrBlock: 0.0.0.0/0
      PortRange:
        From: '0'  # client will be using ephemeral port, using 80 or 22 here will not work
        To: '65535'

  # creating a public subnet
  myPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties: 
      VpcId: !Ref myVPC
      AvailabilityZone: !Ref AvailabilityZoneName
      CidrBlock: !Ref SubnetCIDR
      MapPublicIpOnLaunch: true
      Tags: 
      - Key: Name
        Value: 
          !Join 
          - ''
          - - 'public-subnet-'
            - !Ref AvailabilityZoneName
      - Key: Application
        Value: !Ref 'AWS::StackName'

  # asscoiate subnet with our route table else by default it is asscoiated with main route table
  mySubnetRouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      SubnetId: !Ref myPublicSubnet
      RouteTableId: !Ref myRouteTable

  # associate subnet with NACL else by default it is asscoiated with main NACLs
  mySubnetNaclAssociation:
    Type: 'AWS::EC2::SubnetNetworkAclAssociation'
    Properties:
      SubnetId: !Ref myPublicSubnet
      NetworkAclId: !Ref myPublicNACL

# output key resources ids and export the values for cross-stack referencing
Outputs:
  VpcID:
    Description: ID of the newly created VPC
    Value: !Ref myVPC
    Export:
      Name: !Sub "${AWS::StackName}-VPCID" # the name for cross referencing

  PublicSubnetID:
    Description: SubnetId of the public subnet
    Value: !Ref myPublicSubnet
    Export:
      Name: !Sub "${AWS::StackName}-SUBNET"


Step 2

Create the second stack that consists of a Security Group, rules to allow HTTP and SSH, an EC2 instance, and a user-data to install the Apache HTTP server. This stack will reference the base stack. Please provide the correct image ID which is available in your region. The default value is from the Mumbai (ap-south) region.

AWSTemplateFormatVersion: 2010-09-09
Description: 
  Sample template to provision an EC2 Instance with public IP. Create a Security Group and associate with this EC2.
  You will be billed for the AWS resources used if you create a stack from this template.
  After deleting stack, remember to delete the associated S3 bucket.

# get the name of the base stack which is created first and has VPC details
Parameters:
  VPCStackName:
    Description: Name of the base VPC stack
    Type: String
    Default: BaseStack

  KeyPairName:
    Description: Name of an existing EC2 KeyPair to enable SSH access to the instance
    Type: 'AWS::EC2::KeyPair::KeyName'  # standard type
    ConstraintDescription: must be the name of an existing EC2 KeyPair.

  InstanceType:
    Description: EC2 instance type
    Type: String
    Default: t2.micro

  InstanceImageId:
    Description: EC2 Image Id from this region
    Type: AWS::EC2::Image::Id
    Default: ami-0cb0e70f44e1a4bb5 # defaults for amazon linux in mumbai region  

Resources:
  # create a security group
  mySG:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: Enable http(80) & ssh(22) access
      GroupName: WebServer-SG
      VpcId: 
        Fn::ImportValue: !Sub "${VPCStackName}-VPCID" # note here we are not using AWS::StackName
      SecurityGroupIngress:
        # allow http
        - IpProtocol: tcp
          FromPort: '80'
          ToPort: '80'
          CidrIp: 0.0.0.0/0 # any IP
        # allow ssh  
        - IpProtocol: tcp
          FromPort: '22'
          ToPort: '22'
          CidrIp: 0.0.0.0/0 # only for demo else use your IP or corporate gateway IP
      Tags: 
       - Key: Name
         Value: demo-sg
       - Key: Application
         Value:
           Ref: "AWS::StackName"

  # allow local traffic
  SGBaseIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref mySG
      IpProtocol: '-1'
      FromPort: '-1'
      ToPort: '-1'
      SourceSecurityGroupId: !Ref mySG

  # EC2 instance which will have access for http and ssh
  EC2Instance:
    Type: 'AWS::EC2::Instance'
    Properties:
      InstanceType: !Ref InstanceType
      SubnetId:
        Fn::ImportValue: !Sub "${VPCStackName}-SUBNET"      
      SecurityGroupIds:
        - !Ref mySG
      KeyName: !Ref KeyPairName
      ImageId: !Ref InstanceImageId
      UserData: 
        Fn::Base64: |
          #!/bin/bash -xe
          yum update -y # good practice to update existing packages
          yum install -y httpd # install web server 
          systemctl start httpd
          systemctl enable httpd
          echo "Hello World" > /var/www/html/index.html

      Tags: 
       - Key: Name
         Value: demo-ec2
       - Key: Application
         Value:
           Ref: "AWS::StackName"

# output important values for easy viewing in cloudformation dashboard
Outputs:
  InstanceId:
    Description: InstanceId of the first EC2 instance
    Value: !Ref EC2Instance

  PublicDNS:
    Description: Public DNS Name of the EC2 instance
    Value: !GetAtt 
      - EC2Instance
      - PublicDnsName

  PublicIP:
    Description: Public IP address of the EC2 instance
    Value: !GetAtt 
      - EC2Instance
      - PublicIp

After provisioning, you can check the "Output" section of the CloudFormation dashboard to get the public IP and point your browser to this IP. Don't forget to delete the stack and the S3 bucket to avoid any cost.

Further Reading


Provision a Free AWS EC2 Instance in 5 Minutes


How to Secure an Apache Web Server

 

 

 

 

Top