Creating a Simple CloudFormation Template That Deploys a Basic Apache Website

Overview

CloudFomation makes it easy to deploy infrastructure as code. It allows us to create a template containing logical resources and deploy it as a "stack" in 1 step. This is faster, repeatable and more consistent than deploying resources manually in AWS.

This is a simple portable CloudFormation template (YAML) that deploys a basic Apache website on an EC2 instance. This article breaks down the code and shows you how to deploy the template in CloudFormation.

Before we take a deep dive, take a look at the full code snippet here:

Full CloudFomation Template (YAML)

Parameters:
  LatestAmiId:
    Description: "AMI for EC2"
    Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
    Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'
  SubnetId:
    Description: "Subnet ID"
    Type: 'AWS::EC2::Subnet::Id'
  KeyName:
    Type: "AWS::EC2::KeyPair::KeyName"
    Description: "Key Pair for EC2"
  UserDataMessage:
    Description: "Message for HTML page"
    Default: "Your EC2 With Apache Successfully Deployed"
    Type: "String"
Resources:
  InstanceSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: Enable SSH and HTTP access via port 22 IPv4 & port 80 IPv4
      SecurityGroupIngress:
        - Description: 'Allow SSH IPv4 IN'
          IpProtocol: tcp
          FromPort: '22'
          ToPort: '22'
          CidrIp: '0.0.0.0/0'
        - Description: 'Allow HTTP IPv4 IN'
          IpProtocol: tcp
          FromPort: '80'
          ToPort: '80'
          CidrIp: '0.0.0.0/0'
  Instance:
    Type: 'AWS::EC2::Instance'
    Properties:
      InstanceType: "t2.micro"
      ImageId: !Ref "LatestAmiId"
      SubnetId: !Ref "SubnetId"
      SecurityGroupIds:
        - !GetAtt InstanceSecurityGroup.GroupId
      KeyName: !Ref "KeyName"
      Tags:
        - Key: Name
          Value: Base-Config-Apache-Instance
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -xe
          yum -y update
          yum -y upgrade
          yum install -y httpd
          systemctl enable httpd
          systemctl start httpd
          echo "<html><head><title>Apache Website</title></head><body><h1><center>${UserDataMessage}</center></h1></body></html>" > /var/www/html/index.html

Outputs:
  PublicIp:
    Description: Server Public IP
    Value: !GetAtt Instance.PublicIp
    Export:
      Name: !Sub "${AWS::StackName}-PublicIp"
  PublicDnsName:
    Description: Server Public DNS Name
    Value: !GetAtt Instance.PublicDnsName
    Export:
      Name: !Sub "${AWS::StackName}-PublicDnsName"
  PrivateIp:
    Description: Server Private IP
    Value: !GetAtt Instance.PrivateIp
    Export:
      Name: !Sub "${AWS::StackName}-PrivateIp"
  AvailabilityZone:
    Description: Server Availability Zone
    Value: !GetAtt Instance.AvailabilityZone
    Export:
      Name: !Sub "${AWS::StackName}-AvailabilityZone" 



Parameters

The first part of this template specifies the Parameters. Parameters enable you to input custom values to your template each time you create or update a stack.

Resources specified later in the template reference these values and use the data.

Parameters:
LatestAmiId:
    Description: "AMI for EC2"
    Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
    Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'
  SubnetId:
    Description: "Subnet ID"
    Type: 'AWS::EC2::Subnet::Id'
  KeyName:
    Type: "AWS::EC2::KeyPair::KeyName"
    Description: "Key Pair for EC2"
  UserDataMessage:
    Description: "Message for HTML page"
    Default: "Your EC2 With Apache Successfully Deployed"
    Type: "String"

LatestAmiId: The "LatestAmiId" parameter we set specifies the AMI that the EC2 instance will use. If you look at the "type" field you'll notice that we are using the public SSM parameter store to specify the AMI. This is a much more flexible option and we also have the ability to change that value when we create our stack.
SubnetId: The "SubnetId" parameter we set allows us to choose a subnet from our current list of available subnets in our account. This is used later in the template when we define our EC2 logical resource.
KeyName: The "KeyName" parameter we set allows us to chose an EC2 key pair from the available key pairs in our account
UserDataMessage: The "UserDataMessage" parameter we set is a simple custom string that will be used later by the EC2 instance resource


Resources

The next section in our template is the "Resources" section. Unlike the Parameters and Output section, the resources section is required. In the resources, we define the logical resources that we want to deploy with our stack.

Resources:
InstanceSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: Enable SSH and HTTP access via port 22 IPv4 & port 80 IPv4
      SecurityGroupIngress:
        - Description: 'Allow SSH IPv4 IN'
          IpProtocol: tcp
          FromPort: '22'
          ToPort: '22'
          CidrIp: '0.0.0.0/0'
        - Description: 'Allow HTTP IPv4 IN'
          IpProtocol: tcp
          FromPort: '80'
          ToPort: '80'
          CidrIp: '0.0.0.0/0'

The first resource we define in this template is "InstanceSecurityGroup". "InstanceSecurityGroup" is the logical ID.

Type: In the type section we define the kind of resource we want to deploy. In our case, we set this to "AWS::EC2::SecurityGroup". "AWS::EC2::SecurityGroup" means we are defining a security group. We will use this security group later when we define our EC2 instance.

Properties: The properties section allows us to define the settings for our security group. To see what properties we can set, we can review the AWS documentation for "AWS::EC2::SecurityGroup".

SecurityGroupIngress: One of the properties we set is "SecurityGroupIngress". This property allows us to set the inbound rules for the security group. In our case, we are allowing port 22 for SSH (from all networks '0.0.0.0/0') so we can manage the instance. We also allow port 80/HTTP (from all networks '0.0.0.0/0') so we can access the apache site. You'll notice that we don't have any outbound rules. This is because security groups are stateful. If you send a request to the instance, the response traffic for that request is allowed to flow out regardless of the outbound security group rules.

Instance:
    Type: 'AWS::EC2::Instance'
    Properties:
      InstanceType: "t2.micro"
      ImageId: !Ref "LatestAmiId"
      SubnetId: !Ref "SubnetId"
      SecurityGroupIds:
        - !GetAtt InstanceSecurityGroup.GroupId
      KeyName: !Ref "KeyName"
      Tags:
        - Key: Name
          Value: Base-Config-Apache-Instance
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -xe
          yum -y update
          yum -y upgrade
          yum install -y httpd
          systemctl enable httpd
          systemctl start httpd
          echo "<html><head><title>Apache Website</title></head><body><h1><center>${UserDataMessage}</center></h1></body></html>" > /var/www/html/index.html

The second and last resource we define in this template is "Instance". Just like with the first resource "Instance" is just the logical ID of the resource.

Type: For this resource we have defined that we want to create an EC2 instance. We defined this by setting the type as 'AWS::EC2::Instance'

Properties: We defined several properties (or "settings") for this resource. The available properties can be referenced in the AWS documentation. Let's review them...

InstanceType: We hard-coded this value to use the "t2.micro" instance type.

ImageId: The "ImageId" specifies what AMI we want to use with our EC2 instance. We used the !Ref intrinsic function to reference the value we set in our "LatestAmiId" parameter.

SubnetId: The "SubnetId" specifies what subnet we want our EC2 instance to deploy to. We used the !Ref intrinsic function to reference our "SubnetId" parameter that we set. The "SubnetId" parameter lets us choose an available subnet in our account.

SecurityGroupIds: The "SecurityGroupIds" value specifies what security group we want to use with our EC2 instance. We used the !GetAtt intrinsic function to reference the security group ID of the group that we defined in our template.

KeyName: The "KeyName" value specifies what SSH keypair we want to use with our instance. We used the !Ref intrinsic function to get the value specified in the "KeyName" parameter.

Tags: Tags allow us to add key/value identifiers to resources. We specified a tag for our EC2 instance.

UserData: UserData allows us to bootstrap our EC2 instance. It allows us to run commands on the instance when it is first created. In our case, we ran a few commands to install apache on our Linux instance. If you look we also ran a command to create an HTML file that apache will use. That command also references our "UserDataMessage" parameter.


Outputs

The Outputs section of the template declares output values that you can import into other stacks. You can also view these values from the CloudFormation console.

As an example, if you were making a nested stack with a VPC you may want to export the VPC ID to another stack for later use.

Outputs:
PublicIp:
    Description: Server Public IP
    Value: !GetAtt Instance.PublicIp
    Export:
      Name: !Sub "${AWS::StackName}-PublicIp"
  PublicDnsName:
    Description: Server Public DNS Name
    Value: !GetAtt Instance.PublicDnsName
    Export:
      Name: !Sub "${AWS::StackName}-PublicDnsName"
  PrivateIp:
    Description: Server Private IP
    Value: !GetAtt Instance.PrivateIp
    Export:
      Name: !Sub "${AWS::StackName}-PrivateIp"
  AvailabilityZone:
    Description: Server Availability Zone
    Value: !GetAtt Instance.AvailabilityZone
    Export:
      Name: !Sub "${AWS::StackName}-AvailabilityZone"

In our case, we defined 4 outputs. We pulled these values from the EC2 instance we defined. You can reference the available outputs for EC2 in the AWS documentation.


Deploying the template

CloudFormation accepts YAML and JSON files. Our specific template is written in YAML. If we save the template as a .yaml file we can deploy it to CloudFormation.

Let's view what that looks like in the console:

1) Navigate to CloudFormation and hit create stack:
1-create stack.jpg
2) Upload the CloudFormation template:
2-upload file.jpg
3) Set the parameter values:
3 - params.jpg
4) Monitor the events tab and allow the stack to finish creating:
4 - stack complete.jpg
5) Get the public IP from the outputs section and confirm the site is active
a) Get the public IP of the EC2 instance from the outputs tab: 5 - 1.jpg b) Navigate to the public IP and confirm the site is active: 5-2.jpg
6) Test the SSH connectivity to the instance:
Confirm you can access the instance via SSH: 6-test connactivity.jpg

Conclusion

Our simple template deployed out 1 EC2 instance. With the power of CloudFormation we can deploy out a fleet of EC2 instances in a fraction of the time it takes to manually deploy them.

Thank you to everyone who made it to the end of this article!

Feel free to connect with me on LinkedIn!: linkedin.com/in/donovansearles

No Comments Yet