Creating configuration files in S3 with CDK for web applications on AWS

Originally posted on 2022-11-10

TL;DR - Code is at the bottom!

If you host a web interface on AWS it is likely that you’re hosting it on S3. In development if you want TLS support you’ll need to add CloudFront to it as well since S3 static website hosting only supports HTTP.

The issue that I’ve run into is that my web interface needs to know where my API lives. How do I tell my static site where the API is when the location of the API isn’t known until deployment time?

The answer is that you put a configuration file (i.e. config.json) in the same location as your static site. Then you can request the configuration file with a relative path and you don’t need to know anything else ahead of time.

I found a gem in a GitHub issue that I’ve modified to meet my needs and it works really well.

However, if you use this construct along with a BucketDeployment in CDK you’ll notice that sometimes your configuration file just disappears. This happens if the custom resource finishes deploying before the bucket deployment because the bucket deployment cleans up the bucket when it finishes. This behavior can be changed by setting the prune property on the BucketDeployment to false. But that also leaves around any other objects that end up in the bucket which is not what I want.

How do you avoid this? Call .node.addDependency(bucketDeployment) on the AWSCustomResource to force the custom resource to create/update only after the bucket deployment is complete. Like this:

private createConfigFileInS3(bucket: Bucket, api: RestApi, deployment: BucketDeployment) {
    const config = { ApiUrl: api.url }
    let configContents = JSON.stringify(config)

    new AwsCustomResource(this, 'WriteS3ConfigFile', {
            onUpdate: {
                service: 'S3',
                action: 'putObject',
                parameters: {
                    Body: configContents,
                    Bucket: bucket.bucketName,
                    Key: 'config.json',
                },
                physicalResourceId: PhysicalResourceId.of(Date.now().toString())
            },
            policy: AwsCustomResourcePolicy.fromStatements(
                [
                    new iam.PolicyStatement({
                        actions: ['s3:PutObject'],
                        resources: [`${bucket.bucketArn}/config.json`],
                    }),
                ])
        }
    ).node.addDependency(deployment);
}