modver Versioning in AWS CodeBuild

Home, Bangkok, Thailand, 2019-11-17

#devsecops #cloud #aws

 

Previously I’ve shared my approach to versioning which I’m calling modver.

In this post we’ll see how to implement modver versioning when using AWS CodeBuild and optionally AWS CodePipeline as your continuous integration platform.

Revision Number

To refresh: as described in the modver post, assuming we’re using Git for VCS and are practicing trunk-based development or otherwise have a stable branch for our builds and never allow force-push to that branch, we can reliably derive the version number as:

VERSION_REVISION=$(git rev-list HEAD --count)

CodeBuild Implementation

We face two challenges when implementing modver versioning in CodeBuild:

  • Lack of incrementing build number in CodeBuild

  • No .git context when running CodeBuild from CodePipeline

Incrementing Build Number in CodeBuild

In fact an incrementing build number feature has just been released for CodeBuild in the last couple of weeks. The documentation for it is tucked away in the Environment Variables in Build Environments doc where we see there is now a env var called CODEBUILD_BUILD_NUMBER meaning you could do something like this in your build spec:

VERSION_FULL=$VERSION_MAJOR.$VERSION_MINOR.$VERSION_REVISION.$CODEBUILD_BUILD_NUMBER

That’s nice but there’s a problem - the CODEBUILD_BUILD_NUMBER is initialized to zero when you create your CodeBuild definition and if you tear down and redeploy your CodeBuild (e.g. via CloudFormation) it will be reset to zero. As far as I can see for now there’s no UI / CLI / API support for setting this to a value other than zero.

For a solution that persists across setup / teardown iterations of my CodeBuild configuration I track my own incrementing build number which I keep in a Systems Manager parameter. The following Bash fragment gets the current value into a local VERSION_BUILDNUMBER variable, and increments the build number for next time:

VERSION_BUILDNUMBER=$(aws ssm get-parameter --name "${VERSION_BUILDNUMBER_PARAM}" | jq -r ".Parameter.Value")

aws ssm put-parameter \
	--name "${VERSION_BUILDNUMBER_PARAM}" \
	--value "$((VERSION_BUILDNUMBER+1))" \
	--type "String" \
	--overwrite > /dev/null

echo "VERSION_BUILDNUMBER: $VERSION_BUILDNUMBER"

Note the name of the SSM parameter is itself taken from an environment variable. In our CodeBuild definition we can inject the environment variable which lets us keep this code fragment generic.

You might have noticed that we use the jq command to parse the get-parameter response - jq is already present in the CodeBuild standard images so there’s no need to add it in the install phase of our build.

.git Context and CodePipeline

If you run CodeBuild directly, it will do a normal clone of your repo meaning you have the .git directory in the work directory when you’re doing your build. That means you can run git commands like git rev-list freely.

However - if you take that same CodeBuild and trigger it from CodePipeline the result is different. CodePipeline can handle sources of different types - git repo’s are just one. Whatever the source type, CodePipeline will first stage the sources into an S3 bucket as a tarball, but in the case of a git repo it strips out the .git directory. It then provides that tarball to CodeBuild. This means our git rev-list command will just not work.

As crazy as it sounds, the work-around to this is - to reclone the repo from inside your CodeBuild definition. Fortunately CodeBuild does provide an environment variable called CODEBUILD_RESOLVED_SOURCE_VERSION which gives us the hash of the commit that the tarball represents, so we can use that to move to the right commit once we’ve cloned.

Fortunately GItHub user Timothy Jones has provided a very nice drop-in Bash script which takes care of the work-around for you. You just need to pass it the URL and branch you’re building from. See his medium post for a full walkthrough.

Here’s a buildspec.yaml snippet to demonstrate:

version: 0.2

env:
  git-credential-helper: "yes"

phases:
  build:
    commands:
      - ./build/codebuild-git-wrapper.sh build@git.myorg.com master

Putting it Together

codebuild-modver.sh Drop-In Script

With those two challenges solved we can now put together a re-usable drop-in Bash script that handles our version setup for all our CodeBuild / CodePipeline builds:

# Count the git revision to derive the revision number
VERSION_REVISION=$(git rev-list HEAD --count)

# Get and increment the build number
VERSION_BUILDNUMBER=$(aws ssm get-parameter --name "${VERSION_BUILDNUMBER_PARAM}" | jq -r ".Parameter.Value")

aws ssm put-parameter \
	--name "${VERSION_BUILDNUMBER_PARAM}" \
	--value "$((VERSION_BUILDNUMBER+1))" \
	--type "String" \
	--overwrite > /dev/null

# Construct full version
VERSION_FULL="$VERSION_MAJOR.$VERSION_MINOR.$VERSION_REVISION.$VERSION_BUILDNUMBER"

You can grab the complete version of this drop-in script from my GitHub.

Parameter Store Parameter

Next we need to define a build number parameter for the module we want to build under CodeBuild. Each module will require it’s own parameter since we need to track build numbers per-module.

It’s very easy to create a parameter through the AWS Console but in the spirit of Infrastructure-as-Code we should do it through a CloudFormation template that is kept in a Git repo. Here’s a CloudFormation template for defining a build number parameter:

  # Build Number
  MyModuleBuildNumber:
    Type: "AWS::SSM::Parameter"
    Properties:
      Name: !Sub "/buildnumber/mymodule"
      AllowedPattern: "^\\d+$"
      Type: "String"
      Value: "0"

CodeBuild Permissions

You will probably already be creating a role with custom policy in some form in order to grant CodeBuild access to resources such as your CodeCommit repo and CloudWatch Logs. We can add a statement to that policy to allow CodeBuild to access your build number parameter - see the statement “BuildNumberParamAccess” below:

  BuildServiceRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: !Sub "mymodule-codebuild-service"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - "codebuild.amazonaws.com"
            Action:
              - "sts:AssumeRole"

  BuildServicePolicy:
    Type: "AWS::IAM::Policy"
    Properties:
      PolicyName: !Sub "mymodule-codebuild-service"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          ...
          - Sid: "BuildNumberParamAccess"
            Effect: "Allow"
            Action:
              - "ssm:GetParameter"
              - "ssm:PutParameter"
            Resource:
              - !Ref "MyModuleBuildNumber"
      Roles:
        - !Ref "BuildServiceRole"

Note: the first time I implemented this I packaged the build number management logic as a Lambda function, but I later went back to the drop-in Bash script approach because this allows you to scope the permissions to the build number parameters down to the specific CodeBuild project rather than a single Lambda function requiring access to all build parameters.

buildspec

In the buildspec.yaml we:

  • Invoke Timothy Jones’ drop-in script to get the Git context
  • Source the modver.sh script to get it to generate the version number and to retain the generated VERSION_NUM_FULL in the environment
  • Apply that version number to our build artifacts as required
# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.

version: 0.2

env:
  git-credential-helper: "yes"

phases:
  build:
    commands:
      # Get the Git context
      - ./build/codebuild-git-wrapper.sh ${GIT_REPO_URL} ${GIT_REPO_BRANCH}

      # Build modver version information
      - . ./build/modver.sh
      
      # Apply the version to build artifacts
      - tar cvzf roadrunner-customer-syncbatch-${VERSION_FULL}.tgz target/

      ...

Notice that rather than hardcoding the Git remote URL and branch in the call to codebuild-git-wrapper.sh I’ve used environment vars. These will be defined in the next step.

CodeBuild Environment

We need to set define the following environment variables on our CodeBuild project:

  • VERSION_MAJOR - Major version number
  • VERSION_MINOR - Minor version number
  • VERSION_BUILDNUMBER_PARAM - The name of the SSM parameter where our build number is tracked
  • GIT_REPO_URL - URL to our Git repository so that the codebuild-git-wrapper.sh knows where to re-clone the code from.
  • GIT_REPO_BRANCH - The branch we’re building from

These should be defined in the CloudFormation template for our CodeBuild project:

  Build:
    Type: "AWS::CodeBuild::Project"
    Properties:
      ...
      Environment:
        Type: "LINUX_CONTAINER"
        ComputeType: "BUILD_GENERAL1_SMALL"
        Image: "aws/codebuild/amazonlinux2-x86_64-standard:1.0-1.13.0"
        EnvironmentVariables:
          - Name: "VERSION_MAJOR"
            Type: "PLAINTEXT"
            Value: "1"
          - Name: "VERSION_MINOR"
            Type: "PLAINTEXT"
            Value: "0"
          - Name: "VERSION_BUILDNUMBER_PARAM"
            Type: "PLAINTEXT"
            Value: "/buildnumber/mymod"
          - Name: "GIT_REPO_URL"
            Type: "PLAINTEXT"
            Value: "build@git.myorg.com"
          - Name: "GIT_REPO_BRANCH"
            Type: "PLAINTEXT"
            Value: "master"

You can also view and set these through the CodeBuild console UI but again we should adhere to the principal of Infrastructure-as-Code and maintain them strictly through CloudFormation.

Output

Finally here’s some sample output from a build run:

...

[Container] 2020/05/03 02:56:20 Running command ./build/codebuild-git-wrapper.sh ${GIT_REPO_URL} ${GIT_REPO_BRANCH}
...
Receiving objects:   0% (1/755)   
Receiving objects:   1% (8/755)   
Receiving objects:   2% (16/755)   
...
Receiving objects:  98% (740/755)   
Receiving objects:  99% (748/755)   
Receiving objects: 100% (755/755)   
...
Already on 'master'
Your branch is up to date with 'origin/master'.
...
[Container] 2020/05/03 02:56:24 Running command . ./build/version.sh
VERSION_BUILDNUMBER_PARAM: /buildnumber/mymod
VERSION_MAJOR: 1
VERSION_MINOR: 0
VERSION_REVISION: 122
VERSION_BUILDNUMBER: 164
VERSION_FULL_NUM: 1.0.122.164