Balancing governance and agility with AWS CodeBuild

June 8, 2023 By Mark Otto Off

Introduction

In my role I regularly have conversations with customers who want to enforce security and governance best practices while providing developers the flexibility and agility they need to innovate quickly. As you embrace DevSecOps, you likely seek to balance governance and agility in your Continuous Integration & Continuous Delivery CI/CD pipeline. In this blog post I will discuss how I use AWS CodePipeline and AWS CodeBuild to achieve these goals.

Background

Before I explore this scenario, let me discuss the typical personas involved – platform engineers and developers. Traditionally operations teams were responsible for the process of designing, deploying, configuring, and maintaining the different components that comprise IT infrastructure. Development teams, on the other hand, were responsible for the process of designing, developing, deploying, and maintaining the applications that run on the underlying IT infrastructure. In this model, there was a clear separation of duties. However, with the advent of DevOps these roles have changed. Development teams use Infrastructure as Code (IaC) to provision the infrastructure as part of the application code. As this happened, the operations team evolved to play a supervisory role ensuring the resources being deployed adhere to best practices. I’ll refer to this new supervisory role as the platform engineering team to distinguish from the traditional operations team.

Building on DevOps, DevSecOps promotes the introduction of security controls early in the software development lifecycle. Security teams define best practices for the incorporation of security controls throughout the process. Often these controls are enforced in the CI/CD pipeline. As a result, the role of the security team has also changed. Rather than performing manual security reviews, the security team defines automated controls and ensures they are implemented in the pipeline, often created by the platform engineers. However, the platform engineers must enforce these controls without hindering the agility of the developers. Afterall, DevOps is all about increasing the velocity of the development lifecycle. In this post, I discuss how to separate the security controls and build process into different phases of the pipeline. This allows the platform engineers to enforce security controls in phases they own, while the developers can quickly update the phases they own.

AWS CodePipeline is a fully managed continuous delivery service that helps you automate your deployment pipelines. Pipelines consist of actions that are organized into stages. These actions are often implemented with AWS CodeBuild. Each CodeBuild action defines where to get the source code, which build environment to use, and which build commands to run. The build commands are defined, in YAML format, in a buildspec. The buildspec can be stored in the CodeBuild configuration or as a file in the source code. When the buildspec is defined in the CodeBuild configuration, the platform engineers can retain ownership. When the buildspec is defined in the source code, the developers can easily change it. The pipeline can contain multiple actions to balance governance and agility.

In the following walkthrough I introduce a simple pipeline with two CodeBuild actions. The platform engineers create the pipeline, including a build action defining security controls. As an example, I will use OWASP Dependency Checker to perform Software Composition Analysis. The buildspec for this action is defined in the CodeBuild configuration and the platform engineers retain ownership along with the overall pipeline definition. A second build action is defined to build and test the source code. This buildspec is defined in the source code and the developers can change it whenever they want. Note that the pipeline pictured also includes a Source action implemented with AWS CodeCommit and a Deploy action implemented with AWS CodeDeploy, but these are out of scope for this post.

Pipeline with four stages including: Source; Software Composition Analysis; Build and Unit Test; and Deploy

Walkthrough

In this section, I will play the role of a platform engineer and walk you through the creation of the simple pipeline described previously. I will use the AWS console for simplicity, however I recommend using AWS CloudFormation or AWS Cloud Development Kit (CDK) Pipelines for real world deployments.

The development team has already added their code to a CodeCommit repository. In addition to their source code, they have included the following buildspec.yaml file in the root of the repository.

version: 0.2
phases: install: runtime-versions: python: 3.11 build: commands: - pip install -r requirements.txt - pylint helloworld - coverage run --branch -m pytest

As you can see, this buildspec uses the python 3.11 runtime. It installs requirements, runs a linter, and runs unit tests with code coverage. Given that the buildspec.yaml is included in the source code, the developers can add anything they want. They have full control over this action’s build process. This gives them a lot of autonomy and agility.

I am now ready to begin creating the pipeline. First, I will create a new CodeBuild project to run the development team’s build that is defined in the buildspec.yaml examined previously. I open the CodeBuild Console and select Create Project. I name my project “BuildAndTest.” The process of creating a CodeBuild project is covered in the CodeBuild User Guide section titled Create a build project; however, I want to call your attention to the Buildspec configuration. Here, I choose Use a buildspec file and provide the location of the file in the repository.

CodeDeploy Buildspec configuration with “use a buildspec file” selected

With the buildspec defined in the source code, I am allowing the development team to define the build process. This gives the developers the control and autonomy to change their build process as the project evolves over time. They will not have to ask me to update the build commands every time they want to make a change. In addition, because I will add a second build action with the required security tools, I do not have to audit the content of their buildspec.yaml file. There is no need for me to confirm the developers have included the required security tools.

Next, I will create another CodeBuild project to run Software Composition Analysis (SCA). I return to the CodeBuild Console and select Create Project. I name this project “SoftwareCompositionAnalysis.” The configuration of this project is nearly identical to the prior project, other than the Buildspec configuration. This time, I choose Insert build commands and enter the build commands directly into the project configuration. In the following image, I have added the build commands to install and run OWASP Dependency Checker.

CodeDeploy Buildspec configuration with “Insert build commands” selected

With the buildspec defined in the project definition, I can use a AWS Identity and Access Management (IAM) policy to ensure the development team cannot change it. I now have confidence that the required security tools are properly installed and configured. I do not need to depend on the developers to properly configure the security tools. In addition, I have the autonomy to change the tools used without interrupting the developers or modifying their code.

With my two build actions defined, I can create a pipeline to automate the overall build process. I created the following pipeline following the instruction in Create a pipeline in CodePipeline. In the build phase I run the SoftwareCompositionAnalysis action followed by the BuildAndTest action. Note that these actions execute in order. Therefore, the BuildAndTest action will not run if issues are discovered in the SoftwareCompositionAnalysis action.

CodePipeline editor with Source, Build, and Deploy stages

I now have confidence that my security tools are properly configured in the pipeline. In addition, the developers retain control over the build and test action allowing them to change the build steps as needed.

I now have confidence that my security tools are properly configured in the pipeline. In addition, the developers retain control over the build and test action allowing them to change the build steps as needed. Furthermore, if I support many similar projects, I could define the pipeline using CloudFormation or CDK. This would allow me to deploy multiple pipelines from the same template, and update all the pipelines as security standards evolve. Obviously, the pipeline I created in this post is a trivial example. A real-world pipeline would likely have many actions defined. Some of these actions would be defined in the project definition and be managed by the platform engineers. Other tasks would refer to buildspec files stored in the source code and be managed by the developers. Review the Deployment Pipeline Reference Architecture (DPRA) for an example of a robust pipeline with many stages and actions.

Cleanup

If you have been following along, delete the two CodeBuild projects and delete the CodePipeline pipeline along with any other actions you added to the pipeline (e.g. CodeCommit or CodeDeploy).

Conclusion

As customers embrace DevSecOps, they seek to balance governance and agility in their CI/CD pipeline. In this post I discussed how to use CodeBuild to allow the platform engineer to configure some actions while the developer configures other actions. This allows you to ensure that security controls are properly enforced without compromising the flexibility and agility of the development teams. To get started, read more about AWS CodePipeline and AWS CodeBuild.