Last active
January 11, 2024 13:28
-
-
Save mwarman/dc468da1424ec38f8eed279857a4d6a9 to your computer and use it in GitHub Desktop.
React - AWS CloudFront - Continous Deployment with GitHub Actions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## | |
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## | |
# 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