Fantastic! You’re writing code, using CodeBuild from a repository in CodeCommit, and pushing the result into S3. The problem is that every single time you want to build, you have to make an AWS CLI call. What do you need to make a call succesfully? Credentials! Unfortunately, they expired hours ago when you were cycles deep into writing a very robust blog post. Your 2FA device is upstairs, and you’re so comfortable in your chair that you really don’t want to get up from it. Don’t let anyone tell you that being lazy does not breed innovation!

Enter Amazon CloudWatch Events!

While CodeBuild triggers are a thing, they can only be run to schedule a build once every hour, day, or week. Being cost conscience, I would rather not run a build project on a scheduled interval only because I don’t want builds to go to waste.

Based on my research, it looked like I could trigger on a push to a specific branch in a CodeCommit repository, which was exactly the functionality I was looking for. The documentation wasn’t as clear as I wanted it to be, which is why I felt that this post would be helpful for those who are interested in building the same functionality.

I knew I wanted to build the trigger as a CloudFormation, so I started digging into the documentation. Lo and behold, I discovered that at some point1 Amazon introduced a new service to build out an events bus. In the CloudFormation documentation, instead of looking under CloudWatch, I found what I was looking for under Amazon EventBridge, specifically AWS::Events::Rule.

The resource entity looked straight forward to implement, except for EventPattern and Targets. I started diving into EventPattern, which led me further down a rabbit hole to this piece of documentation ultimately laying out the structure of what I needed to pass in my CloudFormation template. I dug a little further and realized that EventBridge doesn’t yet support a GitPush event directly from CodeCommit. The list of events is short at the moment, but I expect it to grow over time. At this point, I read that EventBridge supported triggering on AWS API Calls in CloudTrail, and started to go down that path. I paused here, only because I didn’t know quite what the CodeCommit CloudTrail event looked like. I fired up my trusty terminal, and looked up the event I wanted to trigger on.

I ran the following command against my target AWS account:

[~]$ aws --profile redacted cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=GitPush --max-results 2

Which produced the following output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
    "Events": [
        {
            "EventId": "redacted",
            "EventName": "GitPush",
            "ReadOnly": "true",
            "AccessKeyId": "redacted",
            "EventTime": 1570064633.0,
            "EventSource": "codecommit.amazonaws.com",
            "Username": "botocore-session-redacted",
            "Resources": [],
            "CloudTrailEvent": "{\"eventVersion\":\"1.05\",\"userIdentity\":{\"type\":\"AssumedRole\",\"principalId\":\"redacted:botocore-session-redacted\",\"arn\":\"arn:aws:sts::redacted:assumed-role/redacted/botocore-session-redacted\",\"accountId\":\"redacted\",\"accessKeyId\":\"redacted\",\"sessionContext\":{\"sessionIssuer\":{\"type\":\"Role\",\"principalId\":\"redacted\",\"arn\":\"arn:aws:iam::redacted:role/account/redacted\",\"accountId\":\"redacted\",\"userName\":\"redacted\"},\"webIdFederationData\":{},\"attributes\":{\"mfaAuthenticated\":\"false\",\"creationDate\":\"2019-10-03T01:01:57Z\"}}},\"eventTime\":\"2019-10-03T01:03:53Z\",\"eventSource\":\"codecommit.amazonaws.com\",\"eventName\":\"GitPush\",\"awsRegion\":\"us-east-1\",\"sourceIPAddress\":\"redacted\",\"userAgent\":\"git/2.23.0\",\"requestParameters\":null,\"responseElements\":null,\"additionalEventData\":{\"protocol\":\"HTTP\",\"dataTransferred\":false,\"repositoryName\":\"redacted\",\"repositoryId\":\"redacted\"},\"requestID\":\"redacted\",\"eventID\":\"redacted\",\"readOnly\":true,\"resources\":[{\"accountId\":\"redacted\",\"type\":\"AWS::CodeCommit::Repository\",\"ARN\":\"arn:aws:codecommit:us-east-1:redacted:redacted\"}],\"eventType\":\"AwsApiCall\",\"recipientAccountId\":\"redacted\"}"
        },
        {
            "EventId": "redacted",
            "EventName": "GitPush",
            "ReadOnly": "false",
            "AccessKeyId": "redacted",
            "EventTime": 1570064633.0,
            "EventSource": "codecommit.amazonaws.com",
            "Username": "botocore-session-redacted",
            "Resources": [],
            "CloudTrailEvent": "{\"eventVersion\":\"1.05\",\"userIdentity\":{\"type\":\"AssumedRole\",\"principalId\":\"redacted:botocore-session-redacted\",\"arn\":\"arn:aws:sts::redacted:assumed-role/redacted/botocore-session-redacted\",\"accountId\":\"redacted\",\"accessKeyId\":\"redacted\",\"sessionContext\":{\"sessionIssuer\":{\"type\":\"Role\",\"principalId\":\"redacted\",\"arn\":\"arn:aws:iam::redacted:role/account/redacted\",\"accountId\":\"redacted\",\"userName\":\"redacted\"},\"webIdFederationData\":{},\"attributes\":{\"mfaAuthenticated\":\"false\",\"creationDate\":\"2019-10-03T01:01:57Z\"}}},\"eventTime\":\"2019-10-03T01:03:53Z\",\"eventSource\":\"codecommit.amazonaws.com\",\"eventName\":\"GitPush\",\"awsRegion\":\"us-east-1\",\"sourceIPAddress\":\"redacted\",\"userAgent\":\"git/2.23.0\",\"requestParameters\":{\"references\":[{\"commit\":\"redacted\",\"ref\":\"refs/heads/release\"}]},\"responseElements\":null,\"additionalEventData\":{\"protocol\":\"HTTP\",\"capabilities\":[\"report-status\",\"side-band-64k\"],\"dataTransferred\":true,\"repositoryName\":\"redacted\",\"repositoryId\":\"redacted\"},\"requestID\":\"redacted\",\"eventID\":\"redacted\",\"readOnly\":false,\"resources\":[{\"accountId\":\"redacted\",\"type\":\"AWS::CodeCommit::Repository\",\"ARN\":\"arn:aws:codecommit:us-east-1:redacted:redacted\"}],\"eventType\":\"AwsApiCall\",\"recipientAccountId\":\"redacted\"}"
        }
    ],
    "NextToken": "redacted"
}

At first, I thought the first event was good enough, but then realized that only the second event had the branch that I was pushing to in the CloudTrailEvent field. I used jq to clean up the output so that I could interpret it a bit better.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
{
  "eventVersion": "1.05",
  "userIdentity": {
    "type": "AssumedRole",
    "principalId": "redacted:botocore-session-redacted",
    "arn": "arn:aws:sts::redacted:assumed-role/redacted/botocore-session-redacted",
    "accountId": "redacted",
    "accessKeyId": "redacted",
    "sessionContext": {
      "sessionIssuer": {
        "type": "Role",
        "principalId": "redacted",
        "arn": "arn:aws:iam::redacted:role/account/redacted",
        "accountId": "redacted",
        "userName": "redacted"
      },
      "webIdFederationData": {},
      "attributes": {
        "mfaAuthenticated": "false",
        "creationDate": "2019-10-03T01:01:57Z"
      }
    }
  },
  "eventTime": "2019-10-03T01:03:53Z",
  "eventSource": "codecommit.amazonaws.com",
  "eventName": "GitPush",
  "awsRegion": "us-east-1",
  "sourceIPAddress": "redacted",
  "userAgent": "git/2.23.0",
  "requestParameters": {
    "references": [
      {
        "commit": "redacted",
        "ref": "refs/heads/release"
      }
    ]
  },
  "responseElements": null,
  "additionalEventData": {
    "protocol": "HTTP",
    "capabilities": [
      "report-status",
      "side-band-64k"
    ],
    "dataTransferred": true,
    "repositoryName": "redacted",
    "repositoryId": "redacted"
  },
  "requestID": "redacted",
  "eventID": "redacted",
  "readOnly": false,
  "resources": [
    {
      "accountId": "redacted",
      "type": "AWS::CodeCommit::Repository",
      "ARN": "arn:aws:codecommit:us-east-1:redacted:redacted"
    }
  ],
  "eventType": "AwsApiCall",
  "recipientAccountId": "redacted"
}

Now, I could find exactly what I was looking for, and how I would be able to tell Events where to find it. Ultimately, I decided that I would trigger on a combination of two fields: eventName being equal to GitPush (line 26) and requestParameters:references:ref equal to refs/head/release (lines 30-34). Through the various pages of documentation, I now could build the EventPattern, which ended up looking like this in YAML:

EventPattern:
  source:
    - 'aws.codecommit'
  detail-type:
    - 'AWS API Call via CloudTrail'
  detail:
    eventName:
      - 'GitPush'
    requestParameters:
      references:
        ref:
          - !Sub 'refs/heads/${CodeCommitBranch}'

The source and detail-type were handed to me from the documentation. The section under detail was effectively what I discovered through the analysis from further up in this post.

The next piece of this was figuring out the Targets. This property takes a list of Target properties which required a bit more reading. Most of the parameters made sense, except for the RoleArn. The reason for the confusion was because in the AWS::Events::Rule, there’s also a RoleArn property, which basically describes the same piece of functionality. In reading through the two definitions, I interpreted it as the role in which EventsBridge will assume to trigger the inteded service (in this case CodeBuild). However, when I attempted to validate my initial CloudFormation template that didn’t have the RoleArn in the Target property, it yelled at me.

In the end, CloudFormation was happy with the following target definition:

Targets:
  - Arn: !Ref CodeBuildArn
    Id: !Sub 'target-${RuleName}'
    RoleArn: !GetAtt EventRole.Arn

Id is required per the specification, and the role I put together for EventBridge was this:

EventRole:
  Type: AWS::IAM::Role
  Properties:
    AssumeRolePolicyDocument:
      Version: 2012-10-17
      Statement:
        - Effect: Allow
          Principal:
            Service: events.amazonaws.com
          Action: sts:AssumeRole
    Policies:
      - PolicyName: events-codebuild
        PolicyDocument:
          Version: 2012-10-17
          Statement:
            - Effect: Allow
              Action:
                - codebuild:StartBuild
              Resource: !Ref CodeBuildArn
    RoleName: event-role-codebuild

Simple, ensuring least privilege. I’m a huge fan of named roles, so you’ll need to run this with the CAPABILITY_NAMED_IAM flag.

At the end, I learned a lot about EventBridge (a.k.a. CloudWatch Events), and my laziness sent me down a path that I wouldn’t have traversed otherwise.

For those interested, the template in GitHub is here.

  1. This summer, Amazon announced EventBridge, which augments (read: will replace) the CloudWatch Events API