Host Frontend with AWS API Gateway-S3 integration
Configure multiple frontends to be hosted on a single S3 bucket exposed through API Gateway using CloudFormation
Context
I want to be able to use a single API Gateway to direct traffic to multiple frontend applications stored in a central S3 bucket.
I want an approach to define the API path in API Gateway that is scalable and without having to create the paths manually.
Key Concepts
API Gateway Resource Configurations
In the paths defined in the API Gateway's API, we have
/{fe-target}
and/{fe-target}/static/{subfolder+}
.The common thought would be "Why not just use proxy resource?"
Other than potential increased in security concerns of path transversal of gaining unintended access to other folders/files in the S3 bucket
On a more functionality focus point of view, you cannot create child resource under a resource with a greedy path
Using parameter i.e. resource defined in curly braces, you allow the flexibility of taking different values while setting the boundaries of resources to access for the frontend static resources.
For example, the Invoke URL of my API Gateway is
https://example.execute-api.ap-southeast-1.amazonaws.com/portals
I have multiple frontend static resources stored in my S3 bucketsample-s3-bucket
,web1
andweb2
The Invoke URL to access
web1
will behttps://example.execute-api.ap-southeast-1.amazonaws.com/portals/web1
The path override used is
sample-s3-bucket/{fe-target}/index.html
To map the value of
{fe-target}
from the method request, the URL path parameter will need to be defined with Path parameter,method.request.path.fe-target
under the Integration request settings
React App Build
Due to way we are storing each frontend production build folder in S3 bucket, we can say that the frontend is not hosted at the root URL.
By default, the React App is build assuming the frontend is hosted on a root URL.
Leveraging on the advanced configurations of building React applications, the React frontend app will be built correctly as though they are hosted at a non-root URL. This is because Create React app will substitute the
%PUBLIC_URL%
with a correct absolute path whennpm run build
is executed
BUILD_PATH
: This variable is to specify a new path for Create React App to output assets. This should be specified as a path relative to the root of your project.PUBLIC_URL
: Create React App assumes your application is hosted at the serving web server's root; this variable to force assets to be referenced verbatim to the URL you provide (hostname included). This may be particularly useful when using a CDN to host your application.
Environment Setup
AWS Resources
Using AWS rain
command to create the boilerplate and define the following AWS resources to be provisioned using CloudFormation(CF):
S3 bucket and its bucket policy
API Gateway with REST API type
IAM role to be used by API Gateway
Please refer to this link for the CloudFormation template to provision the required AWS resources.
Sample Web Application
I will be leveraging on the Official AWS sample React CORS-friendly Single Page Application with some modifications made to show different images.
Creating production build
I will be building 2 production builds with 2 names for PUBLIC_URL
and BUILD_PATH
, i.e. web1
and web2
respectively. The steps below are for in the context of creating a production build for web1
.
In order for NPM to build the production build as intended, you will need to first define a dotenv file,
.env
containing the following environment variablesBUILD_PATH
PUBLIC_URL
# Sample .dotenv file when running for value `web1`
BUILD_PATH='web1'
PUBLIC_URL='web1/'
Install the required dependencies with
npm install
Run
npm run build
to create a production build. It should show similar output as shown below- You can see that the build has been created with the project being built assuming it is hosted at the specified PUBLIC_URL path
% npm run build
> react-cors-spa@1.0.0 build
> react-scripts build
Creating an optimized production build...
Browserslist: caniuse-lite is outdated. Please run:
npx update-browserslist-db@latest
Why you should do it regularly: https://github.com/browserslist/update-db#readme
Browserslist: caniuse-lite is outdated. Please run:
npx update-browserslist-db@latest
Why you should do it regularly: https://github.com/browserslist/update-db#readme
Compiled successfully.
File sizes after gzip:
48.52 kB web1/static/js/main.31d56e89.js
673 B web1/static/css/main.4a727023.css
The project was built assuming it is hosted at web1/.
You can control this with the homepage field in your package.json.
The web1 folder is ready to be deployed.
Find out more about deployment here:
https://cra.link/deployment
- You should have a similar structure from your production build
% tree web1
web1
├── asset-manifest.json
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
├── robots.txt
└── static
├── css
│ ├── main.4a727023.css
│ └── main.4a727023.css.map
├── js
│ ├── main.31d56e89.js
│ ├── main.31d56e89.js.LICENSE.txt
│ └── main.31d56e89.js.map
└── media
├── logo.6ce24c58023cc2f8fd88fe9d219db6c6.svg
├── logoCloudFront.2ea0429935d363011374.png
└── logoS3.aaefa311c47140b02bed.png
5 directories, 15 files
Final Setup & Testing
Upload the production build folder,
web1
into the provisioned S3 bucket- In my case, I will be uploading
web1
andweb2
into the S3 bucket
- In my case, I will be uploading
- Deploy the API to a new stage for the API Gateway
- Now, test accessing the different frontends by using the Invoke URL for the API Gateway!
Caveats and Considerations
This approach will only work if all the frontend production build project structure are all the same.
- This is considering that we are leveraging on the path override feature in the Integration request setting to access the intended resource in the S3 bucket.
If you would like to have more intuitive URLs for your API users, consider using custom domain names for your API Gateway.