Running Cypress Tests on AWS CodeBuild

Cypress is a relatively new web testing tool that is easier to use than Selenium, and it’s gaining in popularity. But using it in a continuous integration environment like AWS CodeBuild requires some additional steps compared to running it directly on your own computer.

This blog post contains helpful information to configure CodeBuild on AWS to run Cypress.

Requirements for running Cypress on CodeBuild

You’ll need several things to run Cypress on AWS:

Things not covered here:

  • How to reset the environment before testing
  • Security via Cognito or other security systems
  • Additional security such as VPC configurations

Proper permissions for running these samples

An AWS administrator will need to grant access to the following permissions:

  • AWSCodeBuildAdminAccess
  • AmazonS3FullAccess
  • AWSCloudFormationFullAccess
  • CloudWatchLogsFullAccess

Be careful to work with an AWS account with credits. Whatever you create and whatever bills you generate are your responsibility.

A sample application

For this post, you’ll deploy a simple Angular JavaScript application to a public S3 bucket for testing.

Here’s the beginning of the CloudFormation script that sets up the S3 bucket, named based on the stack name you pass when you create it.

AWSTemplateFormatVersion:                           "2010-09-09"
Description: "Self hosted AWS application"

# See example at
# Create an S3 bucket and using it to host a website written in Angular

Type: AWS::S3::Bucket
# Key setting: without this, the bucket cannot be accessed via an URL
AccessControl: PublicRead
# Give the bucket a predictable name based on the stack name passed
# when creating the CloudFormation stack
BucketName: !Sub "${AWS::StackName}-hosting-bucket"
IndexDocument: index.html
ErrorDocument: error.html
DeletionPolicy: Retain

In order to expose the bucket for web access, you need to assign a bucket policy:

Type: AWS::S3::BucketPolicy
Id: MyPolicy
Version: 2012-10-17
- Sid: PublicReadForGetBucketObjects
Effect: Allow
Principal: '*'
Action: 's3:GetObject'
Resource: !Sub "arn:aws:s3:::${S3Bucket}/*"
Bucket: !Ref S3Bucket

This policy allows all principals (everyone) to get objects from the bucket, allowing read access to the objects inside of it. Remember to remove this stack when you’re done with this example or you’ll pay for additional usage.

Next, you need to expose the URL and bucket name:

Value: !GetAtt "S3Bucket.WebsiteURL"
Description: URL for website hosted on S3
Value: !Ref S3Bucket

A simple shell script installs this stack (you provide a stack name as the argument):


# This script deploys the Angular app to an S3 bucket for hosting

aws cloudformation deploy
--stack-name ${1} \
--capabilities CAPABILITY_IAM --template-file \
./angular-app/cloudformation.yml \
|| { echo 'cloudformation deploy failed'; exit -1; }
WEBSITE_URL=`aws cloudformation describe-stacks \
--stack-name ${1} --output text \
--query "Stacks[*].Outputs[?OutputKey=='WebsiteURL'].OutputValue"`
BUCKET_NAME=`aws cloudformation describe-stacks \
--stack-name ${1} --output text \
--query "Stacks[*].Outputs[?OutputKey=='S3BucketName'].OutputValue"`
pushd ./angular-app
npm install || { echo 'npm install failed'; exit -2; }
npm run build || { echo 'build failed.'; exit -3; }
aws s3 cp dist/angular-app/ \
s3://${BUCKET_NAME} \
--recursive || { echo 'cp failed'; exit -4; }
echo "The website is now available at ${WEBSITE_URL}"
echo "The S3 bucket name is ${BUCKET_NAME}"

Now your webapp can be deployed to a stack with ./ my-stack.

What are we deploying, anyway?

Here’s a super simple Angular application component that implements a little todo list. Well, at least one action of the todo list, adding an entry via a form. Because we have to test something

The Component

import { Component, OnInit } from '@angular/core';
import { ListItem } from '../support/list-item';
import { FormBuilder, FormGroup } from '@angular/forms';
import { ItemType } from '../support/item-type.enum';

selector: 'app-shopping-list',
templateUrl: './shopping-list.component.html',
styleUrls: ['./shopping-list.component.scss']
export class ShoppingListComponent implements OnInit {
shoppingList: Array<ListItem> = [];
shoppingItemForm: FormGroup;
itemTypes = [ItemType.BAKERY, ItemType.DRINK, ItemType.FOOD, ItemType.PHARMACY,
constructor(private builder: FormBuilder) { }

ngOnInit(): void {
this.shoppingItemForm ={
'purchased': [false, [], []],
'name': ['', [], []],
'category': ['', [], []]
}, []);

addItem() {
const formData = this.shoppingItemForm.value as ListItem;
this.shoppingItemForm.reset({purchased: false});

The component’s view

<h3>Add an Item</h3>
<form [formGroup]=shoppingItemForm
<div class="form-group">
<label for="name">Item Name</label>
<input id="name" class="form-control"
<div class="form-group">
<label for="category">Category</label>
<select id="category" class="form-control" formControlName="category">
<option [value]="itemTypes[0]">{{ itemTypes[0] }}</option>
<option [value]="itemTypes[1]">{{ itemTypes[1] }}</option>
<option [value]="itemTypes[2]">{{ itemTypes[2] }}</option>
<option [value]="itemTypes[3]">{{ itemTypes[3] }}</option>
<option [value]="itemTypes[4]">{{ itemTypes[4] }}</option>
<option [value]="itemTypes[5]">{{ itemTypes[5] }}</option>
<div class="button-group">
<button class="btn-large btn-primary">Add...</button>
<div *ngIf="shoppingList && shoppingList.length > 0">
<h3>Shopping List</h3>
<table class="table table-bordered table-striped">
<tr *ngFor="let item of shoppingList">
<td><input type="checkbox"></td>
<td>{{ }} </td>
<td>{{ item.category }}</td>

Testing with Cypress

Cypress is a web testing tool that Chariot has begun to use instead of Selenium for a number of web applications. It is relatively easy to set up, has a good developer tool with live reloading that makes it easy for developers to iterate with, and it can run headless in the cloud.

A simple test against the app above would look like this:

describe('Adding a list item', () => {
beforeEach(() => {
// the visit is based on a "baseUrl" config
// setting that establishes the root
// website address
it('adds an item', () => {
.type('Soap Flakes')
.should('have.value', 'Soap Flakes')


cy.get('table').should('contain', 'Soap Flakes');

.should('have.value', '');

Cypress has a relatively easy to manage configuration system. Normally, with local development, the cypress.json configuration file can point to your local web server (so you can iterate on the application and re-run tests as you change the code).

For that purpose, the local repository points to the local Angular stack:

"baseUrl": "http://localhost:4200",
"viewportWidth": 1024,
"viewportHieght": 800

To test the app locally, you fire up the app in one terminal, and Cypress in another.

Creating the Cypress AWS CodeBuild project via CloudFormation

There are a variety of ways to host Cypress tests. CircleCI has its own Cypress “ORB”, there is an official Cypress Docker container, and you can also install and run Cypress directly from a test platform supporting NodeJS and Chromium.

It is that method that we are going to use to run Cypress in an AWS CodeBuild project.

The CloudFormation configuration for the CodeBuild project has several major parts. It begins with two required parameters, GitHubProjectUrl and CypressBaseUrl, to fetch the project source code and point to the right website for testing:

AWSTemplateFormatVersion:               "2010-09-09"
Description: "Cypress Test"

Description: "The GitHub Project URL"
Type: "String"

Description: "The base URL for the application under test"
Type: "String"

You need to provision an S3 Bucket to hold artifacts/reports (by default, it will
be a generated S3 bucket, so this gives you more control over where the assets will live):


Type: AWS::S3::Bucket
BucketName: !Sub "${AWS::StackName}-cypress-reports"

Next, you need to configure an IAM Role to run CodeBuild projects. This role needs to have access to the newly created reporting bucket, have access to the EC2 AMI registry to launch the CodeBuild VM, write to CloudWatch logs, and have the ability to read CloudFormation stack settings to get its configuration.

Type: "AWS::IAM::Role"
RoleName: !Sub "${AWS::StackName}-CypressRole"
Version: "2012-10-17"
- Effect: "Allow"
- ""
- "sts:AssumeRole"
- "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser"
- "arn:aws:iam::aws:policy/AWSCloudFormationReadOnlyAccess"
- # for writing to CloudWatch
PolicyName: CodeBuildLoggingPolicy
Version: "2012-10-17"
- Effect: "Allow"
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource: "*"
- # For uploading artifacts
PolicyName: CodeBuildS3ArtifactBucketAccess
Version: "2012-10-17"
- # Static Hosting Bucket upload...
Effect: "Allow"
- "s3:*"
- !Sub "arn:aws:s3:::${CypressReportBucket}"
- !Sub "arn:aws:s3:::${CypressReportBucket}/*"

Next, you define the Cypress runner as an AWS CodeBuild Build Project. We’ll break this down piece by piece, as it is a long configuration.

First, the overall runner settings:

Type: AWS::CodeBuild::Project
# Define the codebuild project with the stack name
Name: !Sub "${AWS::StackName}-cypress-ui"
Description: Test the application via Cypress
# Tie in the codebuild role
ServiceRole: !GetAtt CypressRole.Arn
# set a reasonable time here
TimeoutInMinutes: 5
# for now, we're storing the report as an artifact
# the actual CodeBuild reports are trickier to configure
# and perhaps are a story for another day
Type: S3
NamespaceType: BUILD_ID
Name: ""
# Refer to the S3 bucket we just defined
Location: !Ref CypressReportBucket
Packaging: ZIP
# Remove this if you want to avoid storing build logs in CloudWatch
GroupName: !Sub "/codebuild/${AWS::StackName}-cypress-logs"
StreamName: "ci-log"

Continue with defining the environment of the CodeBuild job. This passes the
input S3 bucket hosting URL so you can override it in the Cypress build execution.

PrivilegedMode: true
Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
Value: !Ref CypressBaseUrl

Now you have to configure a source code repository to download the test artifacts.

In this example, we’ll use GitHub as our source, passed as the CloudFormation Stack input parameterGitHubProjectUrl above. Once you configure AWS to see your GitHub projects, you can execute this CodeBuild project job which will fetch the source from GitHub:

Location: !Ref GitHubProjectUrl
GitCloneDepth: 1

Cypress Execution Options in CodeBuild

To run the build, Cypress must be installed in a CodeBuild virtual machine. We have two options here: running in the machine natively, and running as a Docker container. This post will focus on installing CodeBuild directly in NodeJS in our VM.

Finally, let’s review the build itself. This is a CodeBuild BuildSpec file, embedded in the CloudFormation definition. It will be uploaded to an S3 bucket backing the CodeBuild code.

The key challenges of this build were resolved with some good ole’ Stack Overflowment:

  • running Chromium instead of Chrome (as it is provided as part of the CodeBuild amazon-linux instance),
  • running Chromium headless (without a graphics card) to avoid installing a desktop environment,
  • properly generating the reports (we used Mochawesome as the report generator) and uploading them as build artifacts (not 100% the CodeBuild way, but the CodeBuild reporting engine didn’t work with our reports and this is good enough for our purposes),
  • and passing the baseUrl override into the Cypress launch command.
BuildSpec: |
# important: 0.2 enables some very useful features
version: 0.2
nodejs: 12
# Pre-build - set up the Cypress runtime
- cd cypress-tests
- npm install
- export CONFIG="baseUrl=$CYPRESS_BASE_URL"
- echo $CONFIG
- # NO_COLOR=1 disables ANSI special chars so you can read the output
- # Also: --headless is essential here. We MUST run headless
- # in order to avoid installing an X11 desktop and virtual X graphics
- # context...
- NO_COLOR=1 ./node_modules/.bin/cypress run \
--browser chromium \
--headless \
--config "$CONFIG"
- # You can grab this from the Cypress docs on running Cypress reports
- npx mochawesome-merge "cypress/reports/separate-reports/*.json" > mochawesome.json
- # yes, "marge"
- npx marge mochawesome.json
- # this one is stupid but somehow I can't get codebuild to copy multiple top level paths
- # so I just moved the report dir into place
- mv mochawesome-report cypress/
# these are available in the Build Details tab as the archive
- 'reports/**/*'
- 'screenshots/**/*'
- 'videos/**/*'
- 'mochawesome-report/**/*'
base-directory: 'cypress-tests/cypress'

My advice for those who dare: expect to tweak the options until you get it right. Try running the build script locally with the CYPRESS_BASE_URL set to http://localhost:4200 until it works, then deploy it to CodeBuild.

CodeBuild can be tricky, so we’ve put this project up on a GitHub repo for you to try. You can get all of the code above at

This post was originally written for the Chariot Solutions Blog by Ken Rimple, Chariot’s Director of Training & Mentoring. You can read the original post here.

Starting your next software project? Our software experts are here to help. Read more about our service offerings at Chariot Solutions, or contact us for a quote.

Chariot Solutions is a top IT consulting firm specializing in software and mobile development, and development in the cloud. Visit us at

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store