For my extracurricular business I have a test server that I can deploy any changes I make to it and test them in the most production like environment I can muster. As it is hosted on AWS, I can easily create & destroy it so I only pay for when it is being used, not 24×7. However, to orchestrate this I wrote a bit of custom code to create each component necessary, poll for when the components were ready, then put it all together. Rather than write code, it is always a better option to go with something that already exists (if it does the job) so I thought I would try out CloudFormation.

I am the type of person who learns quickest from other peoples examples, however when I tried to find examples of CloudFormation scripts, they all involved a huge amount of parameters or not using VPCs or creating fresh VPC & Security Groups from scratch. Now I subscribe to two principles, be as similar as production as possible and KISS (keep it simple stupid). So I wanted to use the same VPC and Security Groups as production and I wanted the script to be so simple it it would be self-explanatory.

So to begin with I created just the server with a public IP in my public subnet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
  "AWSTemplateFormatVersion" : "2010-09-09",
  "Description" : "Test Environment",
  "Resources" : {
    "WebServer": {
      "Type": "AWS::EC2::Instance",  
      "Properties": {
        "Tags" : [
            { "Key" : "Name", "Value" : { "Ref" : "AWS::StackName" } }
        ],
        "ImageId"            : "ami-123456789",
        "InstanceType"       : "m1.small",
        "KeyName"            : "Secure",
        "NetworkInterfaces" : [{
          "AssociatePublicIpAddress" : "true",
          "DeviceIndex"              : "0",
          "DeleteOnTermination"      : "true",
          "SubnetId"                 : "subnet-abcdefgh",
          "GroupSet"                 : [ "sg-a1b2c3d4" ]
        }]
      }
    }
  }
}

There are a few “Gotchas” that took me a little bit of trial and error to work out. If you want a public IP, you have to specify NetworkInterfaces and the key property AssociatePublicIpAddress. When you do this, don’t set any subnet or security groups on the EC2 instance, set them on the NetworkInterfaces instead. Everything else should be pretty self explanatory. The AWS:StackName is just a convenience tool to set the name (or any other tag) to the same name that you called the stack.

The next issue I ran into was setting a role on the EC2 instance. Roles are much better than hard coding Access keys everywhere, but it isn’t as straight forward as setting “Role” : “RoleName”. Add the below at line 5.

5
6
7
8
9
10
11
    "InstanceProfile" : {
      "Type" : "AWS::IAM::InstanceProfile",
      "Properties" : {
        "Path" : "/",
        "Roles" : ["RoleName"]
      }
    },

Then simply add “IamInstanceProfile” : {“Ref” : “InstanceProfile”} to your web server properties. Now when your server is created, it will have any role(s) that you have specified.

The final piece of the puzzle was setting the DNS entry up for the newly created server. I recommend this greatly, it is easier to get someone to test a friendly url such as www.yourbusiness-test.com or even just a sub-domain like beta.yourbusiness.com then some long convoluted EC2 Public DNS name.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
	"DNSRecords" : {
	  "Type" : "AWS::Route53::RecordSetGroup",
	  "Properties" : {
        "HostedZoneName" : "yourbusiness-test.com.",
        "Comment" : "Domain records for my test server.",
        "RecordSets" : [ 
          {
            "Name" : "yourbusiness-test.com.",
            "Type" : "A",
            "TTL" : "300",
            "ResourceRecords" : [ { "Fn::GetAtt" : [ "WebServer", "PublicIp" ]} ]
          }, {
            "Name" : "www.yourbusiness-test.com.",
            "Type" : "CNAME",
            "TTL" : "300",
            "ResourceRecords" : [ { "Fn::GetAtt" : [ "WebServer", "PublicDnsName" ]} ]
          }
        ]
      }
    }

Now the only gotcha here was that if the A or CNAME already exists, it will fail. I assumed it would just update it for me, but it’s not that smart. However it is smart enough to know not to do this until the web server has been created, I thought you would need to specify DependsOn but you don’t. Also don’t forget that the names all end with an extra period.

So putting it all together, this is what you end up with. You can download a copy and try it yourself, remember to substitute for your variables though!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
{
  "AWSTemplateFormatVersion" : "2010-09-09",
  "Description" : "Test Environment",
  "Resources" : {
    "InstanceProfile" : {
      "Type" : "AWS::IAM::InstanceProfile",
      "Properties" : {
        "Path" : "/",
        "Roles" : ["RoleName"]
      }
    },
    "WebServer": {
      "Type": "AWS::EC2::Instance",  
      "Properties": {
        "Tags" : [
            { "Key" : "Name", "Value" : { "Ref" : "AWS::StackName" } }
        ],
        "ImageId"            : "ami-123456789",
        "InstanceType"       : "m1.small",
        "IamInstanceProfile" : { "Ref" : "InstanceProfile" },
        "KeyName"            : "Secure",
        "NetworkInterfaces" : [{
          "AssociatePublicIpAddress" : "true",
          "DeviceIndex"              : "0",
          "DeleteOnTermination"      : "true",
          "SubnetId"                 : "subnet-abcdefgh",
          "GroupSet"                 : [ "sg-a1b2c3d4" ]
        }]
      }
    },
    "DNSRecords" : {
      "Type" : "AWS::Route53::RecordSetGroup",
      "Properties" : {
        "HostedZoneName" : "yourbusiness-test.com.",
        "Comment" : "Domain records for my test server.",
        "RecordSets" : [ 
          {
            "Name" : "yourbusiness-test.com.",
            "Type" : "A",
            "TTL" : "300",
            "ResourceRecords" : [ { "Fn::GetAtt" : [ "WebServer", "PublicIp" ]} ]
          }, {
            "Name" : "www.yourbusiness-test.com.",
            "Type" : "CNAME",
            "TTL" : "300",
            "ResourceRecords" : [ { "Fn::GetAtt" : [ "WebServer", "PublicDnsName" ]} ]
          }
        ]
      }
    }
  }
}

My next post in this series will show how to create a RDS instance from the latest production backup, then associate it with the newly created web server above.

2 Responses to CloudFormation public EC2 instance example using existing VPC & Subnets

  1. Atit onNov 25, 2016

    Hi, I would like to know how to create an instance with the existing IAM role. In the above example it seems you have create a new Role?

  2. anthony onJul 29, 2017

    Thanks for this but for some reason i couldn’t seem to get the role going, maybe im missing something.

Leave a Reply


%d bloggers like this: