Deploy any client-side Single Page Application (SPA) on AWS from Github with CI/CD.
Supported frameworks:
AWS resources:
You need an AWS account to create and deploy the required resources for the site on AWS.
Before you begin, make sure you have the following:
Node.js and npm: Ensure you have Node.js (v18 or later) and npm installed.
AWS CLI: Install and configure the AWS Command Line Interface.
AWS CDK: Install the AWS CDK globally
npm install -g aws-cdk
Before deploying, bootstrap your AWS environment:
cdk bootstrap aws://your-aws-account-id/us-east-1
This package uses the npm
package manager and is an ES6+ Module.
Navigate to your project directory and install the package and its required dependencies.
Your package.json
must also contain tsx
and this specific version of aws-cdk-lib
:
npm i tsx [email protected] @thunderso/cdk-spa --save-dev
Login into the AWS console and note the Account ID
. You will need it in the configuration step.
Run the following command to automatically create the required CDK stack entrypoint at stack/index.ts
.
npx cdk-spa-init
You should adapt the file to your project's needs.
[!NOTE] Use different filenames such as
production.ts
andtesting.ts
for environments.
//stack/index.ts
import { App } from "aws-cdk-lib";
import { SPAStack, type SPAProps } from "@thunderso/cdk-spa";
const appStackProps: SPAProps = {
env: {
account: 'your-account-id',
region: 'us-east-1'
},
application: 'your-application-id',
service: 'your-service-id',
environment: 'production',
// Your Github repository url contains https://github.com/<owner>/<repo>
sourceProps: {
owner: 'your-github-username',
repo: 'your-repo-name',
branchOrRef: 'main',
rootdir: '' // supports monorepos. e.g. frontend/
},
buildProps: {
outputdir: 'dist/' // the build output directory with static files and assets
}
};
new SPAStack(new App(), `${appStackProps.application}-${appStackProps.service}-${appStackProps.environment}-stack`, appStackProps);
By running the following script, the CDK stack will be deployed to AWS.
npx cdk deploy --require-approval never --all --app="npx tsx stack/index.ts"
If you want to destroy the stack and all its resources (including storage, e.g., access logs), run the following script:
npx cdk destroy --require-approval never --all --app="npx tsx stack/index.ts"
In your GitHub repository, add a new workflow file under .github/workflows/deploy.yml
with the following content:
name: Deploy SPA to AWS
on:
push:
branches:
- main # or the branch you want to deploy from
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Deploy to AWS
run: |
npx cdk deploy --require-approval never --all --app="npx tsx stack/index.ts"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: 'us-east-1' # or your preferred region
Add AWS_ACCESS_KEY_ID
and AWS_SECRET_ACCESS_KEY
as repository secrets in GitHub. These should be the access key and secret for an IAM user with permissions to deploy your stack.
This is required to create DNS records for the domain to make the app publicly available on that domain. On the hosted zone details you should see the Hosted zone ID
of the hosted zone.
us-east-1
(global) and validate it, if you don't have one yet. This is required to provide the app via HTTPS on the public internet. Take note of the displayed ARN
for the certificate.
[!IMPORTANT] The certificate must be issued in
us-east-1
(global) regardless of the region used for the app itself as it will be attached to the CloudFront distribution which works globally.
// stack/index.ts
const appStackProps: SPAProps = {
// ... other props
// Optional: Domain settings
// - create a hosted zone for your domain in Route53
// - issue a global tls certificate in us-east-1 in AWS ACM
domain: 'sub.example.com',
hostedZoneId: 'XXXXXXXXXXXXXXX',
globalCertificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/abcd1234-abcd-1234-abcd-1234abcd1234',
};
If you prefer to use AWS CodePipeline and CodeBuild for automatic deployment instead of Github Actions, you can enable this by providing a GitHub Personal Access Token stored in AWS Secrets Manager.
Create a Github Personal Access Token for your Github account. This token must be kept secure.
Here's how to create a GitHub Personal Access Token (PAT):
repo
: Full control of private repositories.admin:repo_hook
: Full control of repository hooks.[!NOTE] Copy the token immediately and store it securely. You won't be able to see it again. If you lose it, you'll need to generate a new one.
Create a Secrets Manager secret as plaintext
with the Personal Access Token you created earlier. Note the ARN
of the secret. E.g. arn:aws:secretsmanager:<REGION_NAME>:<ACCOUNT_ID>:secret:<secret-name>
.
Use the AWS CLI to create a new secret in AWS Secrets Manager:
aws secretsmanager create-secret --name your-secret-name --secret-string your-token
{
"ARN": "arn:aws:secretsmanager:us-east-1:665186350000:secret:your-secret-name-XXXXXX",
"Name": "your-secret-name",
"VersionId": "b1a532d2-4434-42a3-9283-41581be07455"
}
Take note of the ARN.
[!IMPORTANT] Storing secrets in AWS Secrets Manager will incur a cost (around $0.40 per month).
// stack/index.ts
const appStackProps: SPAProps = {
// ... other props
buildProps: {
runtime: 'nodejs',
runtime_version: '20',
installcmd: 'npm ci',
buildcmd: 'npm run build',
outputdir: 'dist/',
},
githubAccessTokenArn: 'arn:aws:secretsmanager:us-east-1:665186350000:secret:your-secret-name-XXXXXX',
};
When using Pipeline mode, runtime
, runtime_version
, installcmd
and buildcmd
are mandatory for CodeBuild to function.
runtime
and runtime_version
supports all CodeBuild runtime versions
If you have a custom CodeBuild buildspec.yml
file for your app, provide relative path to the file.
version: 0.2
phases:
install:
commands:
- npm ci
build:
commands:
- npm run build
artifacts:
files:
- '**/*'
base-directory: 'dist/'
// stack/index.ts
const appStackProps: SPAProps = {
// ... other props
buildSpecFilePath: 'buildspec.yml',
githubAccessTokenArn: 'arn:aws:secretsmanager:us-east-1:0123456789000:secret:your-secret-name-XXXXX',
};
When you have a buildspec.yml
, the buildProps
configuration is not required.
When using the Pipeline mode, you can provide build environment variables to AWS CodeBuild.
Create a parameter in SSM Parameter Store:
aws ssm put-parameter --name "/my-app/API_URL" --type "String" --value "https://api.example.com"
aws ssm put-parameter --name "/my-app/API_KEY" --type "SecureString" --value "your-secret-api-key"
Pass environment variables to your build, for example, to inject configuration or secrets.
// stack/index.ts
const appStackProps: SPAProps = {
// ... other props
buildEnvironmentVariables: [
{ key: 'API_URL', resource: '/my-app/API_URL' },
{ key: 'API_KEY', resource: '/my-app/API_KEY' },
],
githubAccessTokenArn: 'arn:aws:secretsmanager:us-east-1:0123456789000:secret:your-secret-name-XXXXX',
};
The library automatically adds the necessary permissions to the CodeBuild project's role to read parameters from SSM Parameter Store.
[!NOTE] Be cautious when using environment variables. Ensure that any API keys or secrets included are safe to expose publicly.
When deploying web applications, especially Single Page Applications (SPAs), configuring URL handling is crucial for both user experience and search engine optimization (SEO). The terms redirect and rewrite refer to different methods of handling HTTP requests.
This library uses Lambda@Edge to configure redirects and rewrites.
A redirect is an HTTP response that instructs the client's browser to navigate to a different URL. This involves a round-trip to the server and results in the browser updating the address bar to the new URL.
HTTP Status Codes: This library uses 301 (Moved Permanently)
.
// stack/index.ts
const appStackProps: SPAProps = {
// ... other props
redirects: [
{
// static
source: '/home',
destination: '/'
},
{
// wildcard
source: '/guide/*',
destination: '/docs/*'
},
{
// placeholders
source: '/blog/:year/:month',
destination: '/:year/:month'
},
],
};
A URL rewrite modifies the URL path internally on the server without changing the URL in the client's browser. The client remains unaware of the rewrite.
// stack/index.ts
const appStackProps: SPAProps = {
// ... other props
rewrites: [
{
source: '/app/*',
destination: '/index.html',
},
{
source: '/profile/:username',
destination: '/user/:username',
},
],
};
Lambda@Edge enables you to customize HTTP response headers for your application by executing lightweight Lambda functions at AWS CloudFront edge locations.
This allows you to modify headers dynamically based on request paths, enhancing security, performance, and user experience.
For example, you can set caching policies with Cache-Control
, enforce security with Strict-Transport-Security
, or manage cross-origin requests with Access-Control-Allow-Origin
. The configuration supports wildcards and placeholders for flexible path matching, ensuring precise control over header application.
// stack/index.ts
const appStackProps: SPAProps = {
// ... other props
headers: [
{
path: '/*',
name: 'Cache-Control',
value: 'public, max-age=864000',
},
{
path: '/api/*',
name: 'Cache-Control',
value: 'max-age=0, no-cache, no-store, must-revalidate',
},
{
path: '/blog/*',
name: 'Cache-Control',
value: 'public, max-age=31536000',
},
{
path: '/**',
name: 'Access-Control-Allow-Origin',
value: 'https://www.foo.com',
},
{
path: '/**',
name: 'Referrer-Policy',
value: 'same-origin',
},
],
};
The header path must be a relative path without the domain. It will be matched with all custom domains attached to your site.
You can use wildcards to match arbitrary request paths.
Path | Effect |
---|---|
/* |
Only the root directory paths. |
/** |
All request paths, including the root path and all sub-paths |
/blog/* |
Matches /blog/ , /blog/latest-post/ , and all other paths under /blog/ |
/**/* |
Matches /blog/ , /assets/ , and all other paths with at least two slashes. |
The CDK-SPA library provides sensible defaults which you can override using the configuration above.
Header | Default Value |
---|---|
X-Frame-Options | DENY |
Referrer-Policy | strict-origin-when-cross-origin |
X-Content-Type-Options | nosniff |
Strict-Transport-Security | max-age=31536000 includeSubDomains |
Content-Security-Policy | default-src 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self' data: |
X-XSS-Protection | 1; mode=block |
Header | Default Value |
---|---|
Access-Control-Allow-Origin | * |
Access-Control-Allow-Credentials | false |
Access-Control-Allow-Methods | GET, HEAD, OPTIONS |
Access-Control-Allow-Headers | * |
You can specify a custom error page to handle 404 Not Found
errors by setting the errorPagePath
property. This path should be relative to your application's output directory.
Example Configuration:
const appStackProps: SPAProps = {
// ... other props
// Optional: Custom error page
errorPagePath: '/404.html', // Relative to the output directory. Defaults to '/index.html'.
};
You can fine-tune CloudFront's caching behavior by specifying which headers
, cookies
, and query parameters
to include or exclude in the cache key. This allows you to control how CloudFront caches content and forwards requests to the origin, improving cache efficiency and ensuring dynamic content is handled correctly.
// stack/index.ts
const appStackProps: SPAProps = {
// ... other props
// Customize cache behavior
allowHeaders: ['Accept-Language', 'User-Agent'],
allowCookies: ['session-*', 'user-preferences'],
allowQueryParams: ['lang', 'theme'],
// Or, to exclude specific query parameters
// denyQueryParams: ['utm_source', 'utm_medium', 'fbclid'],
};
allowHeaders
: An array of header names to include in the cache key and forward to the origin.allowCookies
: An array of cookie names to include in the cache key and forward to the origin.allowQueryParams
: An array of query parameter names to include in the cache key and forward to the origin.denyQueryParams
: An array of query parameter names to exclude from the cache key and not forward to the origin.If neither allowQueryParams
nor denyQueryParams
are specified, all query parameters are ignored in caching and not forwarded to the origin.
[!NOTE] The
allowQueryParams
anddenyQueryParams
properties are mutually exclusive. If both are provided, denyQueryParams will be ignored.
AWS_ACCESS_KEY_ID
, AWS_SECRET_ACCESS_KEY
) are set as secrets in your GitHub repository.outputdir
) is correctly specified and that all assets are being uploaded to S3.rootdir
and outputdir
are correctly set in your configuration. The library appends rootdir
and outputdir
to construct the correct directory path where your static assets are located.For further assistance, consult the AWS documentation or raise an issue in the GitHub repository.