The Task
This post is going to look at the process of hosting a highly available corporate website using Windows Server 2012 Amazon Machine Image (AMI), and bootstrapping the installation of Internet Information Services (IIS), urlrewrite, and our website. We don’t need a golden image as we release software every week. We also want to make sure that it is a high availability solution, so we need to look at scaling groups and repeatability.
Our high availability solution will contain one load balancer, and a minimum of two Elastic Compute Cloud (EC2) instances across multiple availability zones (AZ’s). In the case, our stack will be created using CloudFormation, and by utilising this, we’ll be able to repeat the process across multiple environments. We’re also going to make a couple of assumptions.
- The website source code has been built and zipped into files with the build number being the name – ie – 1.zip
- The above build has been uploaded into Simple Storage Service (S3)
- Appropriate permissions for the instance to access to S3 and the objects have already been applied
The Process
We start with a blank CloudFormation template
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "", "Parameters" : { }, "Resources" : { }, "Outputs" : { } }
We start with the fundamentals – the first thing we need is an elastic load balancer. We define this under the “Resources” section of the template.
... "Resources" : { "WebLoadBalancer" : { "Type" : "AWS::ElasticLoadBalancing::LoadBalancer", "Properties" : { "AvailabilityZones" : {"Fn::GetAZs" : ""}, "Listeners" : [{ "LoadBalancerPort" : "80", "InstancePort" : "80", "Protocol" : "HTTP" }] } } }
The second thing we need are instances that are going to respond to the load balancer. A key point here though is that even though CloudFormation allows us to create instances directly, we don’t want to do that. By doing that, we lose the ability that the AWS magic sauce provides to ensure that a minimum number of instances are running. So instead – we need two pieces. A scaling group to manage the availability of the instances, and a launch configuration to allow the scaling group to launch new instances. We’ll start with the scaling group.
"WebServerScalingGroup" : { "Type" : "AWS::AutoScaling::AutoScalingGroup", "Properties" : { "AvailabilityZones" : { "Fn::GetAZs" : "" }, "LaunchConfigurationName" : { "Ref" : "WebServerLaunchConfiguration" }, "MinSize" : "2", "MaxSize" : "4", "LoadBalancerNames" : [{"Ref" : "WebLoadBalancer"}] } }
The above piece of code creates a scaling group across all availability zones (Fn::GetAZs) with a minimum of two instances and a maximum of four. It will respond to requests by the WebLoadBalancer that we defined earlier, and when required, will launch new instances by using the WebServerLaunchConfiguration which we will define next. And this is where it starts to get tricky. First – we’re going to use a standard base Windows Server 2012 AMI – so that when AWS upgrade it, we can just plug in the later version. Out of the box, this is missing IIS, and also – critically for us, the UrlRewrite module. The IIS installation is relatively easy as Windows provides management through powershell, but the installation of UrlRewrite and our source code becomes a little more tricky. We’ll start with the basic CloudFormation script, and – using the userdata, bootstrap the IIS installation.
"WebServerLaunchConfiguration" : { "Type" : "AWS::AutoScaling::LaunchConfiguration", "Properties" : { "IamInstanceProfile" : { "Ref" : "WebServerProfile" }, "ImageId" : "ami-1223b028", "InstanceType" : "m1.small", "KeyName" : "production", "SecurityGroups" : ["web", "rdp"], "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [ "<script>\n", "powershell.exe add-windowsfeature web-webserver -includeallsubfeature -logpath $env:temp\\webserver_addrole.log \n", "powershell.exe add-windowsfeature web-mgmt-tools -includeallsubfeature -logpath $env:temp\\mgmttools_addrole.log \n", "cfn-init.exe -v -s ", {"Ref" : "AWS::StackId"}, " -r WebServerLaunchConfiguration --region ", {"Ref" : "AWS::Region"}, "\n", "</script>\n", "<powershell>\n", "new-website -name Test -port 80 -physicalpath c:\\inetpub\\Test -ApplicationPool \".NET v4.5\" -force \n", "remove-website -name \"Default Web Site\" \n", "start-website -name Test \n", "</powershell>" ]]} "Metadata" : { "BuildNumber" : { "Ref" : "BuildNumber" }, "AWS::CloudFormation::Authentication" : { "default" : { "type" : "s3", "buckets" : ["test-production-artifacts"], "roleName" : { "Ref" : "BuildAccessRole" } } }, "AWS::CloudFormation::Init" : { "config" : { "sources" : { "c:\\inetpub\\test" : {"Fn::Join" : ["",[ "https://artifacts.s3.amazonaws.com/", {"Ref":"BuildNumber"},".zip" ]]} }, "packages" : { "msi" : { "urlrewrite" : "http://download.microsoft.com/download/6/7/D/67D80164-7DD0-48AF-86E3-DE7A182D6815/rewrite_2.0_rtw_x64.msi" } }, } } } } },
The above template is a fairly standard Launch Configuration for a base installation. The customization comes when we start looking at the UserData property. User data has to be passed as a Base64 string – hence the cryptic Fn::Base64 command at the top. (Be nice if AWS could just accept straight strings and encode it if necessary). Code that is enclosed in <script> tags is executed as a batchfile, and code enclosed in <powershell> is executed via powershell. Both the batch and the powershell scripts are executed with elevated privileges. I’ve used both types mainly for the illustration.
The first part of the script installs the web server components and all its sub features and logs into the administrators temp folder. This is standard windows powershell scripting of IIS. The second component is cfn-init. This is a helper script which parses the AWS::CloudFormation::Init section of the metadata provided. In the Metadata section, we include a Build Number which is attached to each instance and provided as a parameter to the cloud formation script.
There is also a “source” element, which is a link to a zip file built dynamically based on the build number ie – http://artifacts.s3.amazonaws.com/1.zip representing build 1. The cfn-init script knows to expand the zip file into the location provided by the name of the property. The next element is the packages property – and the urlrewrite property provides a downloadable msi for the UrlRewrite module. cfn-init will execute this msi, and install the module into our previously configured server.
After the completion of the script components – being the configuration of IIS, the installation of the source code, and the installation of the module, the bootstrap process moves onto the powershell section of the script. Again – back to basic IIS configuration, we create a new website pointing to the source download folder, delete the old one, and start it up. The final part is to add the build number as a parameter to the “parameters” section. This represents the name of the file (without the .zip extension).
"Parameters" : { "BuildNumber" : { "Type" : "Number" } },
So to release a new build, you can now copy 2.zip into s3, and run a cloud formation update passing in 2 as the build parameter. Once done, drop each of the servers, and the autoscaling group will automagically create brand new servers, but this time, they’ll install 2.zip into the website.
Next post will look at automagically updating the existing servers when the build number is updated.
That’s great! Even better than the AWS doc
Thanks!
great article, but I could not download the Zip file it says access denied
is there a place i could download a sample template of this ti get a better understanding. i am just looking for a basic windows 2012 formation template that allows me to run powershell to add services and I am rather new at this
Was anyone able to get the actual template? The code snippets seem to be malformed and I am having issues correcting it.
It’s the `WebServerLaunchConfiguration` section (run it through a json linter like http://jsoneditoronline.org or https://chrome.google.com/webstore/detail/json-editor/lhkmoheomjbkfloacpgllgjcamhihfaj?utm_source=chrome-app-launcher-info-dialog)
should be:
“WebServerLaunchConfiguration”: {
“Type”: “AWS::AutoScaling::LaunchConfiguration”,
“Properties”: {
“IamInstanceProfile”: {
“Ref”: “WebServerProfile”
},
“ImageId”: “ami-1223b028”,
“InstanceType”: “m1.small”,
“KeyName”: “production”,
“SecurityGroups”: [
“web”,
“rdp”
],
“UserData”: {
“Fn::Base64”: {
“Fn::Join”: [
“”,
[
“\n”,
“powershell.exe add-windowsfeature web-webserver -includeallsubfeature -logpath $env:temp\\webserver_addrole.log \n”,
“powershell.exe add-windowsfeature web-mgmt-tools -includeallsubfeature -logpath $env:temp\\mgmttools_addrole.log \n”,
“cfn-init.exe -v -s “,
{
“Ref”: “AWS::StackId”
},
” -r WebServerLaunchConfiguration –region “,
{
“Ref”: “AWS::Region”
},
“\n”,
“\n”,
“\n”,
“new-website -name Test -port 80 -physicalpath c:\\inetpub\\Test -ApplicationPool \”.NET v4.5\” -force \n”,
“remove-website -name \”Default Web Site\” \n”,
“start-website -name Test \n”,
“”
]
]
},
“Metadata”: {
“BuildNumber”: {
“Ref”: “BuildNumber”
},
“AWS::CloudFormation::Authentication”: {
“default”: {
“type”: “s3”,
“buckets”: [
“test-production-artifacts”
],
“roleName”: {
“Ref”: “BuildAccessRole”
}
}
},
“AWS::CloudFormation::Init”: {
“config”: {
“sources”: {
“c:\\inetpub\\test”: {
“Fn::Join”: [
“”,
[
“https://artifacts.s3.amazonaws.com/”,
{
“Ref”: “BuildNumber”
},
“.zip”
]
]
}
},
“packages”: {
“msi”: {
“urlrewrite”: “http://download.microsoft.com/download/6/7/D/67D80164-7DD0-48AF-86E3-DE7A182D6815/rewrite_2.0_rtw_x64.msi”
}
}
}
}
}
}
},
nice Windows punctuation in that one…
Very helpful thank you – please fix the metadata tag in the template so that the next person has to figure that out!
I’ve taken a similar approach but utilized Chef-Solo to handle configuring the EC2 instance:
http://thesysadminswatercooler.blogspot.com/2015/11/aws-bootstrap-windows-ec2-instance-with.html
Can show where WebServerProfile is defined?
Found some good aws examples in creately diagram community. There are 1000s of aws examples and templates to be used freely.