Skip to content

Instantly share code, notes, and snippets.

@mwarman
Last active January 11, 2024 13:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mwarman/dc468da1424ec38f8eed279857a4d6a9 to your computer and use it in GitHub Desktop.
Save mwarman/dc468da1424ec38f8eed279857a4d6a9 to your computer and use it in GitHub Desktop.
React - AWS CloudFront - Continous Deployment with GitHub Actions
##
# GitHub Action workflow that continously deploys a React application to AWS
# when changes are pushed to the 'main' branch of the repository.
##
name: Deploy
on:
push:
branches:
- main
tags:
- dev
# Allow at most 1 workflow to run at any given time
concurrency:
group: ${{ github.workflow }}
# Workflow environment variables
# Some are sourced from GitHub Actions variables/secrets
env:
APP_NAME: app.example.com
AWS_CFN_STACK_NAME: ui-example-resources-dev
AWS_CFN_TEMPLATE: template.yml
AWS_ENV_CODE: dev
AWS_REGION: ${{ vars.AWS_REGION }}
# The AWS IAM Role ARN that allowing GitHub Actions to obtain AWS STS tokens
AWS_ROLE_ARN: ${{ vars.AWS_ROLE_ARN_DEV }}
# The React .env file for the target environment
ENV_FILE: ${{ secrets.ENV_DEV }}
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
timeout-minutes: 20
# GitHub token permissions needed to integrate with AWS
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js Environment
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: npm
- name: Install Dependencies
run: npm ci
# Create .env file for the React (CRA) build
# Appends build attributes so that they may be used within the app
- name: Create Environment Configuration
run: |
echo "${{ env.ENV_FILE }}" > .env
echo "REACT_APP_BUILD_DATE=$(date +'%Y-%m-%d')" >> .env
echo "REACT_APP_BUILD_TIME=$(date +'%H:%M:%S%z')" >> .env
echo "REACT_APP_BUILD_TS=$(date +'%Y-%m-%dT%H:%M:%S%z')" >> .env
echo "REACT_APP_BUILD_COMMIT_SHA=${{ github.sha }}" >> .env
echo "REACT_APP_BUILD_ENV_CODE=${{ env.AWS_ENV_CODE }}" >> .env
echo "REACT_APP_BUILD_WORKFLOW_NAME=${{ github.workflow }}" >> .env
echo "REACT_APP_BUILD_WORKFLOW_RUN_NUMBER=${{ github.run_number }}" >> .env
echo "REACT_APP_BUILD_WORKFLOW_RUN_ATTEMPT=${{ github.run_attempt }}" >> .env
- name: Build
run: npm run build
# Obtain AWS STS token and configures AWS CLI for use in subsequent steps
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ env.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
# Use AWS CLI to deploy the 'template.yml' CloudFormation Stack
# Prefer CLI to GitHub action because more parameters available in CLI
- name: Deploy AWS CloudFormation Stack
run: |-
aws cloudformation deploy \
--stack-name ${{ env.AWS_CFN_STACK_NAME }} \
--template-file ${{ env.AWS_CFN_TEMPLATE }} \
--parameter-overrides EnvironmentCode=${{ env.AWS_ENV_CODE }} \
--tags App=${{ env.APP_NAME }} Env=${{ env.AWS_ENV_CODE }} OU=engineering Owner='Matthew Warman'
# Use AWS CLI to fetch CloudFormation stack outputs
# Store stack outputs as GitHub Actions step outputs for use in subsequent steps
- name: Get CloudFormation Stack Outputs
id: cloudformation
run: |-
APP_BUCKET_NAME=$(
aws cloudformation describe-stacks \
--stack-name ${{ env.AWS_CFN_STACK_NAME }} \
--query "Stacks[0].Outputs[?OutputKey=='AppBucketName'].OutputValue | [0]"
)
echo "APP_BUCKET_NAME=$APP_BUCKET_NAME" >> "$GITHUB_OUTPUT"
# Deploy the React application to the S3 bucket named in CloudFormation outputs
- name: Deploy to AWS S3
run: |
aws s3 sync build s3://${{ steps.cloudformation.outputs.APP_BUCKET_NAME }} --delete
##
# AWS CloudFormation Template to provision resources for a React SPA component
##
Description: Example UI component resources
Parameters:
EnvironmentCode:
Type: String
Description: Select an Environment
AllowedValues:
- dev
- qa
- prod
Default: dev
ConstraintDescription: Must select a valid environment
Mappings:
EnvironmentAttributeMap:
dev:
CertificateArn: arn:aws:acm:us-east-1:012345678912:certificate/3d110b0f-8b3d-4ddc-bbd8-fab08ae6f030
CloudFrontOAID: E2U9SKLVDD8TPE
HostedZone: dev.example.com
qa:
CertificateArn: arn:aws:acm:us-east-1:012345678912:certificate/5cd1bce7-1323-4625-a49e-5e72d1cff7eb
CloudFrontOAID: E322H9D7WOKWXZ
HostedZone: qa.example.com
prod:
CertificateArn: arn:aws:acm:us-east-1:012345678912:certificate/fc25a13b-0c9f-4c79-a20f-a13f5d2245b6
CloudFrontOAID: EVMQ2O0M1MS7Q
HostedZone: example.com
Resources:
##
# S3 Bucket for the React App
##
BucketApp:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub
- 'example-ui-app.${HostedZone}-${AWS::Region}-${AWS::AccountId}'
- HostedZone: !FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, HostedZone]
##
# Bucket Policy allows access from AWS CloudFront
##
BucketPolicyApp:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref BucketApp
PolicyDocument:
Statement:
- Action:
- s3:GetObject
Effect: Allow
Resource: !Join
- ''
- - 'arn:aws:s3:::'
- !Ref BucketApp
- '/*'
Principal:
AWS: !Sub
- 'arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOAID}'
- CloudFrontOAID:
!FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, CloudFrontOAID]
##
# CloudFront Distribution for the React App - SPA errors and behaviors
##
DistributionUi:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Comment: !Sub 'Example UI SPA (${EnvironmentCode})'
CustomErrorResponses:
- ErrorCode: 404
ResponsePagePath: '/index.html'
ResponseCode: 200
- ErrorCode: 403
ResponsePagePath: '/index.html'
ResponseCode: 200
DefaultCacheBehavior:
AllowedMethods:
- GET
- HEAD
- OPTIONS
DefaultTTL: 60
ForwardedValues:
Cookies:
Forward: none
QueryString: false
TargetOriginId: S3-APP
ViewerProtocolPolicy: redirect-to-https
DefaultRootObject: index.html
Enabled: true
HttpVersion: http2
Origins:
- DomainName: !GetAtt BucketApp.DomainName
Id: S3-APP
S3OriginConfig:
OriginAccessIdentity: !Sub
- 'origin-access-identity/cloudfront/${CloudFrontOAID}'
- CloudFrontOAID:
!FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, CloudFrontOAID]
PriceClass: PriceClass_100
##
# CloudFront Distribution for complete, full-stack APP - routing for API and UI
##
DistributionApp:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Comment: !Sub 'Example UI App (${EnvironmentCode})'
Aliases:
- !Sub
- 'example.${HostedZone}'
- HostedZone: !FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, HostedZone]
CacheBehaviors:
- AllowedMethods:
- DELETE
- GET
- HEAD
- OPTIONS
- PATCH
- POST
- PUT
DefaultTTL: 0
ForwardedValues:
Cookies:
Forward: none
Headers:
- Accept
- Authorization
- Content-Type
- X-Requested-With
QueryString: true
MaxTTL: 0
MinTTL: 0
PathPattern: /api*
TargetOriginId: CUSTOM-API
ViewerProtocolPolicy: redirect-to-https
DefaultCacheBehavior:
AllowedMethods:
- GET
- HEAD
- OPTIONS
DefaultTTL: 60
ForwardedValues:
Cookies:
Forward: none
QueryString: false
TargetOriginId: CUSTOM-UI
ViewerProtocolPolicy: redirect-to-https
DefaultRootObject: index.html
Enabled: true
HttpVersion: http2
Origins:
- CustomOriginConfig:
HTTPPort: 80
HTTPSPort: 443
OriginProtocolPolicy: https-only
OriginSSLProtocols:
- SSLv3
- TLSv1
- TLSv1.1
- TLSv1.2
DomainName: !GetAtt DistributionUi.DomainName
Id: CUSTOM-UI
- CustomOriginConfig:
HTTPPort: 80
HTTPSPort: 443
OriginProtocolPolicy: https-only
OriginSSLProtocols:
- SSLv3
- TLSv1
- TLSv1.1
- TLSv1.2
DomainName: !Sub
- 'api.${HostedZone}'
- HostedZone: !FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, HostedZone]
Id: CUSTOM-API
PriceClass: PriceClass_100
ViewerCertificate:
AcmCertificateArn:
!FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, CertificateArn]
SslSupportMethod: sni-only
##
# Route53 DNS for the 'App' CloudFront Distribution
##
RecordSetAppA:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneName: !Sub
- '${HostedZone}.'
- HostedZone: !FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, HostedZone]
Name: !Sub
- 'example.${HostedZone}'
- HostedZone: !FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, HostedZone]
Type: A
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW4
DNSName: !GetAtt DistributionApp.DomainName
##
# Route53 DNS for the 'App' CloudFront Distribution
##
RecordSetAppAAAA:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneName: !Sub
- '${HostedZone}.'
- HostedZone: !FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, HostedZone]
Name: !Sub
- 'example.${HostedZone}'
- HostedZone: !FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, HostedZone]
Type: AAAA
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW4
DNSName: !GetAtt DistributionApp.DomainName
Outputs:
AppBucketName:
Description: The application S3 bucket name
Value: !Ref BucketApp
DomainName:
Description: The application domain name
Value: !Ref RecordSetAppA
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment