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 distributionendpointTypes: [apigateway.EndpointType.REGIONAL],binaryMediaTypes: [ "application/javascript", "image/png", "image/jpeg", "application/font-woff2", "application/font-woff", "font/woff", "font/woff2", ], });// We need to configure the supported binary media types so that they are forwarded from S3 through API Gateway to the Browser
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
- 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 )
- 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.
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).