HubSpot in an AWS World
We recently moved our corporate website from WPEngine to HubSpot and as part of that, you have to do some DNS trickery. HubSpot helpfully provides instructions for various DNS providers, but not Route 53. Reading the ones they do provide though provides a good idea what is needed;
- Add a CNAME for your HubSpot domain as the www record
- Add an S3 hosting bucket to redirect everything to www.yourdomain.com
- Add a CloudFront distribution to point to your bucket
Now, this is likely 5 minutes of clicking, but AWS should be done with minimal clicking, in favour of using CloudFormation (or TerraForm or such). As such, it took about 10 hours…
Lesson 1 – Don’t create your Hosted Zones by hand.
Currently, all our Hosted Domains in Route 53 were either created by hand as the domains were registered somewhere else, or created at registration time by Route 53. This is a challenge as CloudFormation cannot edit (to add or update) records in Hosted Zones that were not created by CloudFormation. This meant I needed to use CloudFormation to create a duplicate Hosted Zone and let that propagate through the internets and then delete the existing one.
Here’s the CloudFormation template for doing that — minus 70+ individual records. Future iterations likely would have Parameters and Outputs sections, but because this was a clone of what was already there I just hardcoded things.
<pre lang="json">
{
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"zonemobilexcocom": {
"Type": "AWS::Route53::HostedZone",
"Properties": {
"Name": "mobilexco.com."
}
},
"dnsmobilexcocom": {
"Type": "AWS::Route53::RecordSetGroup",
"Properties": {
"HostedZoneId": {
"Ref": "zonemobilexcocom"
},
"RecordSets": [
{
"Name": "mobilexco.com.",
"Type": "MX",
"TTL": "900",
"ResourceRecords": [
"1 ASPMX.L.GOOGLE.COM",
"5 ALT1.ASPMX.L.GOOGLE.COM",
"5 ALT2.ASPMX.L.GOOGLE.COM",
"10 ALT3.ASPMX.L.GOOGLE.COM",
"10 ALT4.ASPMX.L.GOOGLE.COM"
]
}
]
}
},
"dns80808mobilexcocom": {
"Type": "AWS::Route53::RecordSetGroup",
"Properties": {
"HostedZoneId": {
"Ref": "zonemobilexcocom"
},
"RecordSets": [
{
"Name": "80808.mobilexco.com.",
"Type": "A",
"TTL": "900",
"ResourceRecords": [
"45.33.43.207"
]
}
]
}
}
}
}
Lesson 2 – Don’t forget that DNS is all about caching and you could clone a domain and forget to include the MX record because you blindly trusted the output of CloudFormer only to realize you had stopped incoming mail overnight but worked for you because you had things cached…
Lesson 3 – Even though you are using an S3 Hosted Website to do the redirection, you are not actually using an S3 Hosted Website in the eyes of Cloud Front.
This cost me the most amount of grief as it led me to try and create an S3OriginPolicy, an Origin Access Identity, etc. that I didn’t need.
Note: in order to make this template work, you need to first have issued a certificate for your domain through ACM. Which is kinda a pain. My current top ‘AWS Wishlist’ item is auto-provisioning of certificates for domains that are both registered and hosted within your account.
<pre lang="json">
{
"AWSTemplateFormatVersion": "2010-09-09",
"Parameters": {
"DomainNameParameter": {
"Type": "String",
"Description": "The domain to connect to hubspot (don't include the www.)"
},
"HubspotCNameParameter": {
"Type": "String",
"Description": "The CName for your hubspot site"
},
"AcmCertificateArnParameter": {
"Type": "String",
"Description": "ARN of certificate to use in ACM"
}
},
"Resources": {
"s3mobilexcocom": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": {"Ref": "DomainNameParameter"},
"AccessControl": "Private",
"WebsiteConfiguration": {
"RedirectAllRequestsTo": {
"HostName": {"Fn::Join": ["", ["www.", {"Ref": "DomainNameParameter"}]]},
"Protocol": "https"
}
}
}
},
"dnswwwmobilexcocom": {
"Type": "AWS::Route53::RecordSetGroup",
"Properties": {
"HostedZoneId": {
"Fn::ImportValue" : "hosted-zone-mobilexco-com:HostedZoneId"
},
"RecordSets": [
{
"Name": {"Fn::Join": ["", ["www.", {"Ref": "DomainNameParameter"}, "."]]},
"Type": "CNAME",
"TTL": "900",
"ResourceRecords": [
{"Ref": "HubspotCNameParameter"}
]
}
]
}
},
"dnsmobilexcocom": {
"Type": "AWS::Route53::RecordSetGroup",
"Properties": {
"HostedZoneId": {
"Fn::ImportValue" : "hosted-zone-mobilexco-com:HostedZoneId"
},
"RecordSets": [
{
"Name": {"Fn::Join": ["", [{"Ref": "DomainNameParameter"}, "."]]},
"Type": "A",
"AliasTarget": {
"DNSName": {"Fn::GetAtt": ["httpsDistribution", "DomainName"]},
"HostedZoneId": "Z2FDTNDATAQYW2"
}
}
]
}
},
"httpsDistribution" : {
"Type" : "AWS::CloudFront::Distribution",
"Properties" : {
"DistributionConfig": {
"Aliases": [
"mobilexco.com"
],
"Origins": [{
"DomainName": {"Fn::Join": ["", [{"Ref": "DomainNameParameter"}, ".s3-website-", {"Ref": "AWS::Region"}, ".amazonaws.com"]]},
"Id": "bucketOriginId",
"CustomOriginConfig": {
"HTTPPort": 80,
"HTTPSPort": 443,
"OriginProtocolPolicy": "http-only"
}
}],
"Enabled": "true",
"DefaultCacheBehavior": {
"ForwardedValues": {
"QueryString": "false"
},
"TargetOriginId": "bucketOriginId",
"ViewerProtocolPolicy": "allow-all"
},
"ViewerCertificate": {
"AcmCertificateArn": {"Ref": "AcmCertificateArnParameter"},
"SslSupportMethod": "sni-only"
},
"PriceClass": "PriceClass_100"
}
}
}
}
}
Lesson 4 – Naming conventions are a thing. Use them.
As soon as you start doing ImportValue or AWS::CloudFormation::Stack. In theory the ImportValue lines could use DomainNameParameter with Fn::Sub to switch the . to a – and this would be an entirely generic template, but this is working well enough for me. And of course, your naming convention could be (and likely is) different.