Blue Green w/S3, Cloudfront, Route53

I tend to code and architect devops with an eye toward NOT being locked into any particular cloud or service.

Netsaint -> Nagios -> Icinga

Hudson -> Jenkins

VMWare -> Vagrant -> Docker -> Kubernetes -> ECS

Everything changes. That ideal cloud you are moving on to right now – will change in five years, probably enough in ten years that part of the reason you will stay with it is simply the code base already written that works with that solution. In other words, inertia. That drops the agility of your company to compete. So, don’t.

If there is an alternate, one that doesn’t lock you into that environment, do it. Between RDS and running mysql/MariaDB on instances, unless there is a compelling case for RDS, run mysql/MariaDB. Between cloud services and running it in the cloud but under your design and control, where possible run it yourself. Unless there is a compelling reason that offsets the future pain of migration when it becomes necessary, don’t move toward lock in.

That said, I work right now for a company that is all in on AWS. (For now.) And one of the products is a javascript app deployed to a static website on s3, pushed out to Cloudfront and then made accessible through route 53. Route 53 also makes a reasonable blue-green deployment possible, in that route 53 can directly alias an AWS resource as the target of an A record. The switch in target aliases in route 53 causes an instant change within AWS, and from there DNS propagation flows out to complete the change.

You can do Lambda@Edge, which adds code and intelligence to the Cloudfront piece itself, but that’s brand new (therefore suspect) (shiny, but suspect). More code, more lock in. So – simpler, and workable is fine for this product.

It starts with a pipeline job in Jenkins which builds the javascript and then pushes it out to the inactive bucket of a blue-green pair. I have 6 of these – Dev blue-green, Prod blue-green, QA blue-green. Each bucket gets tagged, “active” (serving code and the app), “inactive” (NOT serving code, but ready to deploy as reversion), and “hold” (deployed, not yet ever active). The initial tag is “inactive”.

A deploy is made by

  • build the new code
  • push that code out to the inactive bucket
  • tag that bucket “hold”
  • remove the Alias CNAME from the blue (active) bucket Cloudfront distribution
  • add that Alias CNAME to the green (hold) Cloudfront distribution
  • point the Target Alias for the A record in route 53 from the blue Cloudfront url to the green cloudfront url
  • green bucket goes to tag “active”, blue goes to “inactive”
  • verify deploy

All of this is done through AWS cli and bash scripts. I keep promising myself to write in something other than bash, but for this exact manipulation, especially while proving it out, bash rocks. It is native to Linux, accessible, directly runs the AWS commands exactly as they would execute on the command line. It’s ugly – but so was perl and at one point much of the world wide web was running on perl. So deal.

Pushing code out to s3…

#! /bin/bash

echo "dev buckets..."
DEVGREEN=`aws s3api get-bucket-tagging --bucket dashboard-dev-green.fqdn --output text | grep code_state | awk '{ print \$3 }'`
DEVBLUE=`aws s3api get-bucket-tagging --bucket dashboard-dev-blue.fqdn --output text | grep code_state | awk '{ print \$3 }'`
QAGREEN=`aws s3api get-bucket-tagging --bucket dashboard-qa-green.fqdn --output text | grep code_state | awk '{ print \$3 }'`
QABLUE=`aws s3api get-bucket-tagging --bucket dashboard-qa-blue.fqdn --output text | grep code_state | awk '{ print \$3 }'`
PRODGREEN=`aws s3api get-bucket-tagging --bucket dashboard-green.fqdn --output text | grep code_state | awk '{ print \$3 }'`
PRODBLUE=`aws s3api get-bucket-tagging --bucket dashboard-blue.fqdn --output text | grep code_state | awk '{ print \$3 }'`
DATE=`date +%Y%m%d%H%M%S`
if [[ ($DEVBLUE == 'inactive') || ($DEVBLUE == 'hold') ]]; then
    echo "deploying to DEVBLUE bucket..."
    aws s3 rm s3://dashboard-dev-blue.fqdn --recursive
    aws s3 cp ./build-dev s3://dashboard-dev-blue.fqdn --recursive
    aws s3api put-bucket-tagging --bucket dashboard-dev-blue.fqdn --tagging 'TagSet=[{Key=code_state,Value=hold}]'
elif [[ ($DEVGREEN == 'inactive') || ($DEVGREEN == 'hold') ]]; then
    echo "deploying to DEVGREEN bucket..."
    aws s3 rm s3://dashboard-dev-green.fqdn --recursive
    aws s3 cp ./build-dev s3://dashboard-dev-green.fqdn --recursive
    aws s3api put-bucket-tagging --bucket dashboard-dev-green.fqdn --tagging 'TagSet=[{Key=code_state,Value=hold}]'
else
    echo "neither DEVGREEN nor DEVBLUE are  deployable..."
fi
if [[ ($QABLUE == 'inactive') || ($QABLUE == 'hold') ]]; then
    echo "deploying to QABLUE bucket..."
    aws s3 rm s3://dashboard-qa-blue.fqdn --recursive
    aws s3 cp ./build-qa s3://dashboard-qa-blue.fqdn --recursive
    aws s3api put-bucket-tagging --bucket dashboard-qa-blue.fqdn --tagging 'TagSet=[{Key=code_state,Value=hold}]'
elif [[ ($QAGREEN == 'inactive') || ($QAGREEN == 'hold') ]]; then
    echo "deploying to QAGREEN bucket..."
    aws s3 rm s3://dashboard-qa-green.fqdn --recursive
    aws s3 cp ./build-dev s3://dashboard-qa-green.fqdn --recursive
    aws s3api put-bucket-tagging --bucket dashboard-qa-green.fqdn --tagging 'TagSet=[{Key=code_state,Value=hold}]'
else
    echo "neither QAGREEN nor QABLUE are  deployable..."
fi
if [[ ($PRODBLUE == 'inactive') || ($PRODBLUE == 'hold') ]]; then
    echo "deploying to PRODBLUE bucket..."
    aws s3 rm s3://dashboard-blue.fqdn --recursive
    aws s3 cp ./build-dev s3://dashboard-blue.fqdn --recursive
    aws s3api put-bucket-tagging --bucket dashboard-blue.fqdn --tagging 'TagSet=[{Key=code_state,Value=hold}]'
elif [[ ($PRODGREEN == 'inactive') || ($PRODGREEN == 'hold') ]]; then
    echo "deploying to PRODGREEN bucket..."
    aws s3 rm s3://dashboard-green.fqdn --recursive
    aws s3 cp ./build-dev s3://dashboard-green.fqdn --recursive
    aws s3api put-bucket-tagging --bucket dashboard-green.fqdn --tagging 'TagSet=[{Key=code_state,Value=hold}]'
else
    echo "neither PRODGREEN nor PRODBLUE are  deployable..."
fi

Deploy…

#! /bin/bash

## DEPLOY DEV BLUE
## - set green cloudfront without alias
## - set blue cloudfront with alias
## - set route53 target alias pointing to blue, thus deploy blue

DEVBLUE='Blue Cloudfront ID for Dev'
DEVGREEN='Green Cloudfront ID for Dev'

# ETags
DEVBLUEETAG=`aws cloudfront get-distribution-config --id "${DEVBLUE}" | jq -r '.ETag'`
echo "blue ETag:  ${DEVBLUEETAG}"
DEVGREENETAG=`aws cloudfront get-distribution-config --id "${DEVGREEN}" | jq -r '.ETag'`
echo "green tag:  ${DEVGREENETAG}"

# green remove alias
aws cloudfront update-distribution --id "${DEVGREEN}" --distribution-config file://cicd/aws/cloudfront/dashboard_dev_cloudfront_green_without_alias.json --if-match "${DEVGREENETAG}"

# blue, add in alias
aws cloudfront update-distribution --id "${DEVBLUE}" --distribution-config file://cicd/aws/cloudfront/dashboard_dev_cloudfront_blue_with_alias.json --if-match "${DEVBLUEETAG}"

# point alias to blue resource

# get xone id for catapultsports.info
ZONEID=`aws route53  list-hosted-zones-by-name --dns-name catapultsports.info | jq -r '.HostedZones[0].Id' | awk -F'/' '{ print $3 }'`

# point route53 alias target to blue resource
aws route53 change-resource-record-sets --hosted-zone-id ${ZONEID} --change-batch file://cicd/aws/route53/dashboard_dev_route53_point_target_to_blue.json

The cloudfront json file. This is best gather by running “aws cloudfront get-distribution-config –id

{
    "Comment": "",
    "CacheBehaviors": {
        "Quantity": 0
    },
    "IsIPV6Enabled": true,
    "Logging": {
        "Bucket": "",
        "Prefix": "",
        "Enabled": false,
        "IncludeCookies": false
    },
    "WebACLId": "",
    "Origins": {
        "Items": [
            {
                "S3OriginConfig": {
                    "OriginAccessIdentity": ""
                },
                "OriginPath": "",
                "CustomHeaders": {
                    "Quantity": 0
                },
                "Id": "S3-dashboard-dev-blue.fqdn",
                "DomainName": "dashboard-dev-blue.fqdn.s3.amazonaws.com"
            }
        ],
        "Quantity": 1
    },
    "DefaultRootObject": "",
    "PriceClass": "PriceClass_All",
    "Enabled": true,
    "DefaultCacheBehavior": {
        "TrustedSigners": {
            "Enabled": false,
            "Quantity": 0
        },
        "LambdaFunctionAssociations": {
            "Quantity": 0
        },
        "TargetOriginId": "S3-dashboard-dev-blue.fqdn",
        "ViewerProtocolPolicy": "allow-all",
        "ForwardedValues": {
            "Headers": {
                "Quantity": 0
            },
            "Cookies": {
                "Forward": "none"
            },
            "QueryStringCacheKeys": {
                "Quantity": 0
            },
            "QueryString": false
        },
        "MaxTTL": 31536000,
        "SmoothStreaming": false,
        "DefaultTTL": 86400,
        "AllowedMethods": {
            "Items": [
                "HEAD",
                "GET"
            ],
            "CachedMethods": {
                "Items": [
                    "HEAD",
                    "GET"
                ],
                "Quantity": 2
            },
            "Quantity": 2
        },
        "MinTTL": 0,
        "Compress": false
    },
    "CallerReference": "somenumber",
    "ViewerCertificate": {
        "SSLSupportMethod": "sni-only",
        "ACMCertificateArn": "arn:aws:acm:region:certificate-in-aws",
        "MinimumProtocolVersion": "TLSv1.1_2016",
        "Certificate": "arn:aws:acm:region:certificate-in-aws",
        "CertificateSource": "acm"
    },
    "CustomErrorResponses": {
        "Quantity": 0
    },
    "HttpVersion": "http2",
    "Restrictions": {
        "GeoRestriction": {
            "RestrictionType": "none",
            "Quantity": 0
        }
    },
    "Aliases": {
        "Items": [
            "Arecord in route 53"
        ],
        "Quantity": 1
    }
}

and Route 53 json…

{
     "Comment": "Creating Alias resource record  in Route 53",
     "Changes": [{
                "Action": "UPSERT",
                "ResourceRecordSet": {
                            "Name": "your-a-record-name.",
                            "Type": "A",
                            "AliasTarget":{
                                    "HostedZoneId": "zoneid",
                                    "DNSName": "cloudfront.url.aws",
                                    "EvaluateTargetHealth": false
                              }}
                          }]
}

 

— doug

nbsp;