This is the first part of a IV part series which discusses the process of building a mid-sized project in Go with AWS, including unit testing and an experimental plugin feature.

The first section will discuss the AWS services used in the brief and will contain a basic description for those who are not familiar with them. The second section will discuss Go SDK as well as the project structure itself- how it can be used, improved and how it can help in everyday life. The third section will discuss the experimental plugin system. Finally, we will tackle the unit testing for AWS in Go.

AWS

CloudFormation

If you haven’t yet read about, or know of, AWS’ CloudFormation service, you can either go ahead and read Documentation or continue to read on for a quick summary. If you are familiar with CF, please skip ahead to the CodeDeploy section.

CF is a service which collates other AWS services (for example: EC2, S3, ELB, ASG, RDS) into one, easily manageable stack. After a stack has been created, the resources can be handled as one via CF specific console commands. A stack can be quite versatile as it is possible to define any number of parameters. Parameters include: SSH IP restriction; KeyPair names;  list of tags; the region the stack is located.

To describe how these parts fit together, one must use a CloudFormation Template file which is either written in JSON or YAML. A simple example looks like this:

  1.    Parameters:
  2.       KeyName:
  3.         Description: The EC2 Key Pair to allow SSH access to the instance
  4.         Type: AWS::EC2::KeyPair::KeyName
  5.     Resources:
  6.       Ec2Instance:
  7.         Type: AWS::EC2::Instance
  8.         Properties:
  9.           SecurityGroups:
  10.           - Ref: InstanceSecurityGroup
  11.           - MyExistingSecurityGroup
  12.           KeyName:
  13.             Ref: KeyName
  14.           ImageId: ami-7a11e213
  15.       InstanceSecurityGroup:
  16.         Type: AWS::EC2::SecurityGroup
  17.         Properties:
  18.           GroupDescription: Enable SSH access via port 22
  19.           SecurityGroupIngress:
  20.           - IpProtocol: tcp
  21.             FromPort: '22'
  22.             ToPort: '22'
  23.             CidrIp: 0.0.0.0/0

A myriad of template samples can be accessed here.

“Parameters” define the parameters to customize a stack. “Resources” define the various AWS services which we would like to configure. As demonstrated in the above configuration, we are creating an EC2 instance with a custom Security Group plus an existing security group. “ImageId” is the AMI that is used for the EC2 instance. The InstanceSecurityGroup defines SSH access to the instance.

Please note: This configuration can become inundated relatively quick once VPCs, ELBs and ASGs come into play. CloudFormation templates also contain simple logical switches like conditions, ref for variables, maps and other shenanigans.

Please consider the below example:

  1.      KeyName:
  2.         Ref: KeyName

The key name is a reference variable. Once the template is processed, it will be interpolated into a real value. 

CodeDeploy

If you haven’t heard about CodeDeploy yet, please refer to Documentation or continue reading for a brief description.

CodeDeploy, as the name suggests, deploys code. Any code can be deployed as long as the deployment process is described in a file called appspec.yml. It can be as easy as coping a file to a specific location, or as complex as setting up monitoring services. 

Please consider the below configuration:

  1.    version: 0.0
  2.     os: linux
  3.     files:
  4.       - source: /index.html
  5.         destination: /var/www/html/
  6.       - source: /healthy.html
  7.         destination: /var/www/html/
  8.     hooks:
  9.       BeforeInstall:
  10.         - location: scripts/install_dependencies
  11.           timeout: 300
  12.           runas: root
  13.         - location: scripts/clean_up
  14.           timeout: 300
  15.           runas: root
  16.         - location: scripts/start_server
  17.           timeout: 300
  18.           runas: root
  19.       ApplicationStop:
  20.         - location: scripts/stop_server
  21.           timeout: 300
  22.           runas: root

CodeDeploy’s applications have hooks and life-cycle events that can be utilised to control the deployment process such as: activating the WebServer; making sure files are stored in its correct location; copying files; running configuration management software like puppet, ansible, chef, etc.

What can be done in an appspec.yml file is described here: Appspec Reference Documentation.

Deployment can occur via the following ways: 

GitHub

If the preferred way to deploy the application is with GitHub, a commit hash must be used to identify which “version” of the application is to be deployed. For example:

  1.    rev = &codedeploy.RevisionLocation{
  2.         GitHubLocation: &codedeploy.GitHubLocation{
  3.             CommitId:   aws.String("kajdf94j0f9k309klksjdfkj"),
  4.             Repository: aws.String("Skarlso/furnace-codedeploy-app"),
  5.         },
  6.         RevisionType: aws.String("GitHub"),
  7.     }

Commit Id is the hash of the latest release. Repository is the full account/repository pointing to the application.

S3

The second way to deploy is via a S3 bucket. The bucket will contain an archived version of the application with a given extension. I’m using the term “extension” because it’s to be specified like this (and can be either ‘zip’, ‘tar’ or ‘tgz’):

  1.    rev = &codedeploy.RevisionLocation{
  2.         S3Location: &codedeploy.S3Location{
  3.             Bucket:     aws.String("my_codedeploy_bucket"),
  4.             BundleType: aws.String("zip"),
  5.             Key:        aws.String("my_awesome_app"),
  6.             Version:    aws.String("VersionId"),
  7.         },
  8.         RevisionType: aws.String("S3"),
  9.     }

As seen in the above code, we specify the bucket name, the extension, the name of the file and an optional version id that can be ignored.

Deploying

So how does codedeploy get applications to our EC2 instances? It employs an agent that runs on all of the instances that we have created. In order to do this, the agent needs to be present on our instance. For linux, this can be achieved via the following UserData (UserData in CF is the equivalent of a bootsrap script):

  1.    "UserData" : {
  2.         "Fn::Base64" : { "Fn::Join" : [ "\n", [
  3.             "#!/bin/bash -v",
  4.             "sudo yum -y update",
  5.             "sudo yum -y install ruby wget",
  6.             "cd /home/ec2-user/",
  7.             "wget https://aws-codedeploy-eu-central-1.s3.amazonaws.com/latest/install",
  8.             "chmod +x ./install",
  9.             "sudo ./install auto",
  10.             "sudo service codedeploy-agent start",
  11.         ] ] }
  12.     }

A simple user data configuration in the CloudFormation template will ensure that every instance that we create will have the CodeDeploy agent running and waiting for instructions. This agent is self updating, which can cause some trouble if AWS releases a broken agent. This is unlikely to happen, however, it does occur. Once installed, it’s no longer a concern to be bothered with.

It communicates via HTTPS port 443.

CodeDeploy identifies instances which need to be updated according to our preferences by tagging the EC2 and Auto Scaling groups. Tagging the EC2 and Auto Scaling groups can be done via the CloudFormation template through the AutoScalingGroup settings. For example:


  1.    "Tags" : [
  2.         {
  3.             "Key" : "fu_stage",
  4.             "Value" : { "Ref": "AWS::StackName" },
  5.             "PropagateAtLaunch" : true
  6.         }
  7.     ]

This will give the EC2 instance a tag called fu_stage with value equaling to the name of the stack. Once this is done, CodeDeploy will look like this:

  1.    params := &codedeploy.CreateDeploymentInput{
  2.         ApplicationName:               aws.String(appName),
  3.         IgnoreApplicationStopFailures: aws.Bool(true),
  4.         DeploymentGroupName:           aws.String(appName + "DeploymentGroup"),
  5.         Revision:                      revisionLocation(),
  6.         TargetInstances: &codedeploy.TargetInstances{
  7.             AutoScalingGroups: []*string{
  8.                 aws.String("AutoScalingGroupPhysicalID"),
  9.             },
  10.             TagFilters: []*codedeploy.EC2TagFilter{
  11.                 {
  12.                     Key:   aws.String("fu_stage"),
  13.                     Type:  aws.String("KEY_AND_VALUE"),
  14.                     Value: aws.String(config.STACKNAME),
  15.                 },
  16.             },
  17.         },
  18.         UpdateOutdatedInstancesOnly: aws.Bool(false),
  19.     }

“CreateDeploymentInput” defines the parameters that are needed in order to identify instances to deploy code to. 

As seen in the above example, “CreateDeploymentInput” looks for an AutoScalingGroup by Physical Id and the tag labeled fu_stage. Once located, it will use UpdateOutdatedInstancesOnly to determine whether an instance needs updating or not. When set to false, it will update all instances regardless whether it is outdated or not. 

Furnace

Furnace provides an easy mechanism to create, delete and push code to a CloudFormation stack using CodeDeploy, and a couple of environment properties. "./furnace create" will create a CloudFormation stack according to the provided template. Furnace Create will ask for the parameters defined in it in order to enable flexibility. Delete will remove the stack and all the affiliated resources. Delete will not remove the created CodeDeploy application. For that, there is delete-application. The status command will provide information about the stack such as Outputs, Parameters, Id, Name, and status.

For example:

  1.    2017/03/16 21:14:37 Stack state is:  {
  2.       Capabilities: ["CAPABILITY_IAM"],
  3.       CreationTime: 2017-03-16 20:09:38.036 +0000 UTC,
  4.       DisableRollback: false,
  5.       Outputs: [{
  6.           Description: "URL of the website",
  7.           OutputKey: "URL",
  8.           OutputValue: "http://FurnaceSt-ElasticL-ID.eu-central-1.elb.amazonaws.com"
  9.         }],
  10.       Parameters: [
  11.         {
  12.           ParameterKey: "KeyName",
  13.           ParameterValue: "UserKeyPair"
  14.         },
  15.         {
  16.           ParameterKey: "SSHLocation",
  17.           ParameterValue: "0.0.0.0/0"
  18.         },
  19.         {
  20.           ParameterKey: "CodeDeployBucket",
  21.           ParameterValue: "None"
  22.         },
  23.         {
  24.           ParameterKey: "InstanceType",
  25.           ParameterValue: "t2.nano"
  26.         }
  27.       ],
  28.       StackId: "arn:aws:cloudformation:eu-central-1:9999999999999:stack/FurnaceStack/asdfadsf-adsfa3-432d-a-fdasdf",
  29.       StackName: "FurnaceStack",
  30.       StackStatus: "CREATE_COMPLETE"
  31.     }

Once the stack is CREATE_COMPLETE, a simple push will deliver our application on each instance in the stack. 

Stay tuned for Part II to receive a more detailed explanation about these commands and their workings.