How to host a serverless static website on AWS with API Gateway
In most cases you will use the combination of CloudFront and S3 to host a static website on AWS. But depending on the requirements you might not be able to use it.
In this Post I’m going to describe how you can use (or abuse) the relatively new S3 Integration in API Gateway to host a serverless static website. You can use it to deploy a Single Page App with React, Vue Angular or any other framework as well.
In short, I am going to use a Rest API to forward requests to an S3 Bucket. API Gateway has an awful lot of features. I am going to configure it to forward the Content-Type of the files in S3. This way the browser will be able to correctly interpret the files and render our website properly.
Sample Website
For illustration I will use a starter vuepress site. But you can also use React, Angular or any other framework.
You can create the starter by running (you need yarn to be installed):
yarn create vuepress-site
This will create the vuepress project and we will leave it mostly untouched. We only need to set the base url as we will use the generated domain by API Gateway which automatically adds /prod
(or the according stage). This is done by adding base: /prod/
to the file src/.vuepress/config.js
.
That’s all for the sample website as this is not the main focus on this post.
API Gateway and S3 Setup
Now comes the interesting part. I am using cdk to setup an S3 Bucket that holds the static website and a Rest API in API Gateway with the according resources that will get the right files through the S3 Proxy integration. To keep it readable the code samples are not complete. But you can find the full code at github.
S3 Bucket:
This part is straight forward. We create a new bucket and deploy our static assets. The cool thing about the BucketDeployment Construct is that it already sets the right Content-Type for us in S3.
const bucketWebsite = new s3.Bucket(this, "WebsiteBucket");// This will deploy the sources to the destination bucket
new s3Deploy.BucketDeployment( this, "deploy-frontend", { sources: [ s3Deploy.Source.asset("../docs/src/.vuepress/dist") ], destinationBucket: bucketWebsite });
API Gateway Rest API:
The Rest API is mostly standard. The most important part here is to define the supported Binary Media Types so that we can forward them from S3 to the Browser.
const api = new apigateway.RestApi( this, "ApiGatewayS3Proxy", { restApiName: "StaticWebsite", // The regional endpoint is faster to deploy as it does not create a CloudFront distribution endpointTypes: [apigateway.EndpointType.REGIONAL],
// We need to configure the supported binary media types so that they are forwarded from S3 through API Gateway to the Browser binaryMediaTypes: [ "application/javascript", "image/png", "image/jpeg", "application/font-woff2", "application/font-woff", "font/woff", "font/woff2", ], });
API Gateway S3 Integration:
In the S3 integration we configure through responseParameters
which headers should be forwarded from S3 to the browser. The code below maps all requests to /
to the index.html
int the S3 Bucket.
const indexPageIntegration = new apigateway.AwsIntegration({ service: "s3", integrationHttpMethod: "GET", path: `${bucket.bucketName}/index.html`, options: { credentialsRole: apiGatewayS3ReadRole, passthroughBehavior: PassthroughBehavior.WHEN_NO_MATCH, integrationResponses: [ { statusCode: "200", responseParameters: { "method.response.header.Content-Type": "integration.response.header.Content-Type", "method.response.header.Timestamp": "integration.response.header.Date" }, }, ]},});const methodOptions: MethodOptions = { methodResponses: [ { statusCode: '200', responseParameters: {"method.response.header.Content-Type": true, "method.response.header.Timestamp": true}}, { statusCode: '400' }, { statusCode: '500' }]};// we add a GET method to the root resource. api.root.addMethod("GET", indexPageIntegration, methodOptions);
We can also configure catchall or proxy+ routes is the name in API Gateway. In our example this is usable for the assets folder. Important part is to define the requestParameters
to forward the path param to S3.
const assetsIntegration = new apigateway.AwsIntegration({ service: "s3", integrationHttpMethod: "GET", path: `${bucket.bucketName}/assets/{path}`, options: { credentialsRole: apiGatewayS3ReadRole, passthroughBehavior: apigateway.PassthroughBehavior.WHEN_NO_MATCH, requestParameters: { "integration.request.path.path": "method.request.path.path" }, integrationResponses: [ { statusCode: "200", responseParameters: { "method.response.header.Content-Type": "integration.response.header.Content-Type", "method.response.header.Timestamp": "integration.response.header.Date" }, }, ]},});const assets = root.addResource("assets");assets.addResource("{path+}")
.addMethod("GET", assetsIntegration, {...methodOptions, requestParameters: {"method.request.path.path": true}});
Evaluation
Pros
- Faster deployment (without CloudFront)
- Can be deployed without any resources in us-east-1 (without CloudFront)
- very flexible and lots of features (Throttling, API Keys, Custom Domain …)
- same API Gateway can be used for Backend routes
- Authentication can be added easily with API Gateway Authorizers ( Cognito Integration, IAM or Custom Authorizer )
Cons
- Compared to CloudFront API Gateway can get very expensive if you receive a lot of requests (millions), so handle with care
- The routing configuration can get quite big depending how your static site is structured.
Summary
We managed to host a serverless static website in AWS without involving CloudFront. For high traffic websites this might not be very useful. But with special requirements it makes sense.
In a production app you will probably add a custom domain and authentication.
I used cdk to define the API but you can use any other supported method (OpenAPI, Smithy).