[DRAFT] CI/CD With CodeCommit

I recently came across a Medium post that claimed "Even though we really like AWS’ services we admit that CodeCommit (Git) and CodePipeline (CI/CD) are not ready for production use, yet. The problem of CodePipeline is that you need to create a pipeline for every branch. You cannot create it easily automatically without running CloudFormation." Which is half true. The part that is true is that CodePipeline is terrible at doing Code Builds that aren't meant to be deployed. If only there was some service in the Code Suite that allowed you to Build Code. If they ever make such a service, I hope it would be name CodeBuilder or BuildCoder, I would hope that this service is capable of running unit/integration tests for any commit on any branch.

Fast forward -835 days and AWS Announces the launch of AWS CodeBuild.

In this post, I will show you how to build a CI/CD pipeline with AWS CodeCommit, CodeBuild, CodePipeline, Lambda, and SNS that will ensure your build process runs for every commit. Here is the high-level overview of what we're going to build.

CFN Template

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: CICD Pipeline that builds every commit on every branch and only merges passing builds to mainline

Parameters:
  ProjectName:
    Description: Name for this project
    Type: String
    Default: "myProject"
Resources:

    CodeCommitRepo:
      Type: AWS::CodeCommit::Repository
      Properties:
        RepositoryDescription: my super awesome repo
        RepositoryName: !Ref ProjectName
        Triggers:
          - DestinationArn: !GetAtt [HelloWorldFunction, Arn]
            CustomData: !Sub ${ProjectName}
            Events:
              - all
            Name: SendToBuild
    CloudWatchEventRule:
      Type: AWS::Events::Rule
      Properties:
        Description: "Rule to Fire SNS Message"
        EventPattern: !Sub |
            {
              "source": [
                "aws.codebuild"
              ],
              "detail-type": [
                "CodeBuild Build State Change"
              ],
              "detail": {
                "build-status": [
                  "IN_PROGRESS",
                  "SUCCEEDED",
                  "FAILED",
                  "STOPPED"
                ],
                "project-name": [
                  "${ProjectName}"
                ]
              }
            }
        State: ENABLED
        Targets:
         - Arn: !Ref DeveloperNotification
           Id: "SNSTopic"
    DeveloperNotification:
      Type: AWS::SNS::Topic
      Properties:
        DisplayName: "Build Notifier"

    SNSPolicy:
      Type: AWS::SNS::TopicPolicy
      Properties:
        PolicyDocument:
          Statement:
            Sid: "MYSID001"
            Effect: "Allow"
            Principal:
              Service: events.amazonaws.com
            Action: sns:Publish
            Resource: !Ref DeveloperNotification
        Topics:
          - !Ref DeveloperNotification

    HelloWorldFunction:
        Type: AWS::Serverless::Function
        Properties:
            CodeUri: ../commit_builder/
            Handler: app.lambda_handler
            Runtime: python3.7
            Timeout: 30
            Policies:
              - AWSLambdaBasicExecutionRole
              - Version: '2012-10-17'
                Statement:
                  - Effect: Allow
                    Action: "*"
                    Resource: "*"
            Environment:
              Variables:
                BUILD_PROJECT_NAME: !Ref ProjectName
                ARTIFACT_BUCKET_NAME: !Ref ArtifactBucket
                SOURCE_LOCATION: !Sub https://git-codecommit.${AWS::Region}.amazonaws.com/v1/repos/${ProjectName}

    InvokePermission:
      Type: AWS::Lambda::Permission
      Properties:
        FunctionName: !Ref HelloWorldFunction
        Action: 'lambda:InvokeFunction'
        Principal: apigateway.amazonaws.com
        SourceArn: !GetAtt [CodeCommitRepo, Arn]

    ArtifactBucket:
      Type: AWS::S3::Bucket
      DeletionPolicy: Retain

    ToolsCodeCommitRole:
      Type: AWS::IAM::Role
      Properties:
        RoleName: !Sub ${ProjectName}-ToolsAcctCodePipelineCodeCommitRole
        AssumeRolePolicyDocument:
          Version: 2012-10-17
          Statement:
            - Effect: Allow
              Principal:
                AWS:
                  - !Ref AWS::AccountId
              Action:
                - sts:AssumeRole
        Path: /

    Policy:
        Type: AWS::IAM::Policy
        Properties:
          PolicyName: !Sub ${ProjectName}-ToolsAcctCodePipelineCodeCommitPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - codecommit:BatchGetRepositories
                  - codecommit:Get*
                  - codecommit:GitPull
                  - codecommit:List*
                  - codecommit:CancelUploadArchive
                  - codecommit:UploadArchive
                  - s3:*
                Resource: "*"
          Roles:
            - !Ref ToolsCodeCommitRole
    BuildProjectRole:
      Type: AWS::IAM::Role
      Properties:
        RoleName: !Sub ${ProjectName}-CodeBuildRole
        AssumeRolePolicyDocument:
          Version: 2012-10-17
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - codebuild.amazonaws.com
              Action:
                - sts:AssumeRole
        Path: /

    BuildProjectPolicy:
      Type: AWS::IAM::Policy
      Properties:
        PolicyName: !Sub ${ProjectName}-CodeBuildPolicy
        PolicyDocument:
          Version: 2012-10-17
          Statement:
            - Effect: Allow
              Action:
                - codecommit:BatchGetRepositories
                - codecommit:Get*
                - codecommit:GitPull
                - codecommit:List*
                - codecommit:CancelUploadArchive
                - codecommit:UploadArchive
                - s3:PutObject
                - s3:GetBucketPolicy
                - s3:GetObject
                - s3:ListBucket
              Resource:
                - !Join ['',['arn:aws:s3:::',!Ref ArtifactBucket, '/*']]
                - !Join ['',['arn:aws:s3:::',!Ref ArtifactBucket]]
                - !GetAtt [CodeCommitRepo, Arn]
            - Effect: Allow
              Action:
                - logs:CreateLogGroup
                - logs:CreateLogStream
                - logs:PutLogEvents
              Resource: arn:aws:logs:*:*:*
            - Effect: Allow
              Action:
                - "*"
              Resource: "*"
        Roles:
          - !Ref BuildProjectRole
    BuildProject:
      Type: AWS::CodeBuild::Project
      Properties:
        Name: !Ref ProjectName
        Description: !Ref ProjectName
        ServiceRole: !GetAtt BuildProjectRole.Arn
        Artifacts:
          Type: CODEPIPELINE
        Environment:
          Type: linuxContainer
          ComputeType: BUILD_GENERAL1_SMALL
          Image: aws/codebuild/python:3.7.1-1.7.0
          EnvironmentVariables:
            - Name: S3Bucket
              Value: !Ref ArtifactBucket
        Source:
          Type: CODEPIPELINE
          BuildSpec: |
            version: 0.1
            phases:
              install:
                commands:
                  - printenv
                  - ls -R
                  - pip install -r requirements.txt -t "$PWD"
              build:
                commands:
                  - aws cloudformation package --template-file templates/databases.yaml --s3-bucket $S3Bucket --s3-prefix databases/codebuild --output-template-file databases.yaml
                  - aws cloudformation package --template-file templates/functions.yaml --s3-bucket $S3Bucket --s3-prefix functions/codebuild --output-template-file functions.yaml
                  - aws cloudformation package --template-file templates/api.yaml --s3-bucket $S3Bucket --s3-prefix api/codebuild --output-template-file api.yaml
            artifacts:
              files:
                - templateConfigurations/*
                - databases.yaml
                - functions.yaml
                - api.yaml
              discard-paths: yes

        TimeoutInMinutes: 10
        Tags:
          - Key: Name
            Value: !Ref ProjectName
    PipeLineRole:
      Type: AWS::IAM::Role
      Properties:
        RoleName: !Sub ${ProjectName}-codepipeline-role
        AssumeRolePolicyDocument:
          Version: 2012-10-17
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - codepipeline.amazonaws.com
              Action:
                - sts:AssumeRole
        Path: /

    PipelinePolicy:
      Type: AWS::IAM::Policy
      Properties:
        PolicyName: !Sub ${ProjectName}-codepipeline-policy
        PolicyDocument:
          Version: 2012-10-17
          Statement:
            - Effect: Allow
              Action:
                - codepipeline:*
                - iam:ListRoles
                - cloudformation:Describe*
                - cloudFormation:List*
                - codecommit:List*
                - codecommit:Get*
                - codecommit:GitPull
                - codecommit:UploadArchive
                - codecommit:CancelUploadArchive
                - codebuild:BatchGetBuilds
                - codebuild:StartBuild
                - cloudformation:CreateStack
                - cloudformation:DeleteStack
                - cloudformation:DescribeStacks
                - cloudformation:UpdateStack
                - cloudformation:CreateChangeSet
                - cloudformation:DeleteChangeSet
                - cloudformation:DescribeChangeSet
                - cloudformation:ExecuteChangeSet
                - cloudformation:SetStackPolicy
                - cloudformation:ValidateTemplate
                - iam:PassRole
                - lambda:*
                - s3:ListAllMyBuckets
                - s3:GetBucketLocation
              Resource:
                - "*"
            - Effect: Allow
              Action:
                - s3:PutObject
                - s3:GetBucketPolicy
                - s3:GetObject
                - s3:ListBucket
              Resource:
                - !Join ['',['arn:aws:s3:::',!Ref ArtifactBucket, '/*']]
                - !Join ['',['arn:aws:s3:::',!Ref ArtifactBucket]]

        Roles:
          - !Ref PipeLineRole
    Pipeline:
        Type: AWS::CodePipeline::Pipeline
        Properties:
          RoleArn: !GetAtt PipeLineRole.Arn
          Name: !Ref AWS::StackName
          Stages:
            - Name: Source
              Actions:
                - Name: App
                  ActionTypeId:
                    Category: Source
                    Owner: AWS
                    Version: 1
                    Provider: CodeCommit
                  Configuration:
                    RepositoryName: !Ref ProjectName
                    BranchName: master
                  OutputArtifacts:
                    - Name: SCCheckoutArtifact
                  RunOrder: 1
            - Name: Build
              Actions:
                - Name: Build
                  ActionTypeId:
                    Category: Build
                    Owner: AWS
                    Version: 1
                    Provider: CodeBuild
                  Configuration:
                    ProjectName: !Ref BuildProject
                  RunOrder: 1
                  InputArtifacts:
                    - Name: SCCheckoutArtifact
                  OutputArtifacts:
                    - Name: BuildOutput
            - Name: DeployToTest
              Actions:
                ##### DYNAMO START #####
                - Name: CreateChangeSetTestDynamo
                  ActionTypeId:
                    Category: Deploy
                    Owner: AWS
                    Version: 1
                    Provider: CloudFormation
                  Configuration:
                    ChangeSetName: databases-test
                    ActionMode: CHANGE_SET_REPLACE
                    StackName: databases-test
                    Capabilities: CAPABILITY_NAMED_IAM
                    TemplatePath: BuildOutput::databases.yaml
                    TemplateConfiguration: "BuildOutput::preProd.json"
                  InputArtifacts:
                    - Name: BuildOutput
                  RunOrder: 1
                - Name: DeployChangeSetTestDynamo
                  ActionTypeId:
                    Category: Deploy
                    Owner: AWS
                    Version: 1
                    Provider: CloudFormation
                  Configuration:
                    ChangeSetName: databases-test
                    ActionMode: CHANGE_SET_EXECUTE
                    StackName: databases-test
                  InputArtifacts:
                    - Name: BuildOutput
                  RunOrder: 2
                ##### DYNAMO END #####
                ##### LAMBDA START #####
                - Name: CreateChangeSetTestLambda
                  ActionTypeId:
                    Category: Deploy
                    Owner: AWS
                    Version: 1
                    Provider: CloudFormation
                  Configuration:
                    ChangeSetName: lambda-test
                    ActionMode: CHANGE_SET_REPLACE
                    StackName: lambda-test
                    Capabilities: CAPABILITY_NAMED_IAM
                    TemplatePath: BuildOutput::functions.yaml
                    TemplateConfiguration: "BuildOutput::preProd.json"
                  InputArtifacts:
                    - Name: BuildOutput
                  RunOrder: 3
                - Name: DeployChangeSetTestLambda
                  ActionTypeId:
                    Category: Deploy
                    Owner: AWS
                    Version: 1
                    Provider: CloudFormation
                  Configuration:
                    ChangeSetName: lambda-test
                    ActionMode: CHANGE_SET_EXECUTE
                    StackName: lambda-test
                  InputArtifacts:
                    - Name: BuildOutput
                  RunOrder: 4
                ##### LAMBDA END #####
                ##### API START #####
                - Name: CreateChangeSetTestAPI
                  ActionTypeId:
                    Category: Deploy
                    Owner: AWS
                    Version: 1
                    Provider: CloudFormation
                  Configuration:
                    ChangeSetName: api-test
                    ActionMode: CHANGE_SET_REPLACE
                    StackName: api-test
                    Capabilities: CAPABILITY_NAMED_IAM
                    TemplatePath: BuildOutput::api.yaml
                    TemplateConfiguration: "BuildOutput::preProd.json"
                  InputArtifacts:
                    - Name: BuildOutput
                  RunOrder: 5
                - Name: DeployChangeSetTestAPI
                  ActionTypeId:
                    Category: Deploy
                    Owner: AWS
                    Version: 1
                    Provider: CloudFormation
                  Configuration:
                    ChangeSetName: api-test
                    ActionMode: CHANGE_SET_EXECUTE
                    StackName: api-test
                  InputArtifacts:
                    - Name: BuildOutput
                  RunOrder: 6
          ArtifactStore:
            Type: S3
            Location: !Ref ArtifactBucket

Lambda Code

import boto3
import os


def lambda_handler(event, context):
    print('Starting a new build ...')
    cb = boto3.client('codebuild')
    build = {
        'projectName': event['Records'][0]['customData'],
        'sourceVersion': event['Records'][0]['codecommit']['references'][0]['commit'],
        'artifactsOverride': {
                                'type': 'S3',
                                'location': os.environ['ARTIFACT_BUCKET_NAME'],
                                'path': 'builds/',
                                'name': event['Records'][0]['codecommit']['references'][0]['commit'],
                                'packaging': 'ZIP',
                                'overrideArtifactName': True
                            },
        'sourceTypeOverride': 'CODECOMMIT',
        'sourceLocationOverride': os.environ['SOURCE_LOCATION'],
    }
    print('Starting build for project {0} from commit ID {1}'.format(build['projectName'], build['sourceVersion']))
    response = cb.start_build(**build)
    print('Successfully launched a new CodeBuild project build!')
    return