AWS CDK and Amplify runtime-config

August 25, 2022

Hi,

Seamless integration of AWS CDK and Amplify apps used to be very cumbersome! With a runtime-config for the Amplify frontend React app, it's now much easier. Here I would like to introduce you to the idea of a runtime-config.

In my fullstack projects, I regularly use AWS CDK as backend. AppSync as a GraphQL implementation is the interface between the frontend and backend. The frontend is usually a React SPA (Single Page Application) hosted in an S3 bucket. I use AWS Cognito to manage and authenticate the users and usually configure the frontend React app using AWS Amplify.

Idea runtime-config

The runtime-config allows you to configure Amplify after the build phase on runtime. The dist folder of the SPA is given a file like runtime-config.json in the public folder which fetched ruding the runtime of the app. Here you see an example for the runtime-config.json:

{
  "region": "eu-central-1",
  "identityPoolId": "eu-central-1:cda9c404-0e74-439d-b40c-90204a0e1234",
  "userPoolId": "eu-central-1_Uv0E91234",
  "userPoolWebClientId": "1t6jbsr5b7utg6c9urhj51234",
  "appSyncGraphqlEndpoint": "https://wr2cf4zklfbt3pxw26bik12345.appsync-api.eu-central-1.amazonaws.com/graphql"
}

The runtime-config is then dynamically loaded in the React app via useEffect and fetch:

useEffect(() => {
    fetch('/runtime-config.json')
      .then((response) => response.json())
      .then((runtimeContext) => {
        runtimeContext.region &&
          runtimeContext.userPoolId &&
          runtimeContext.userPoolWebClientId &&
          runtimeContext.identityPoolId &&
          Amplify.configure({
            aws_project_region: runtimeContext.region,
            aws_cognito_identity_pool_id: runtimeContext.identityPoolId,
            aws_cognito_region: runtimeContext.region,
            aws_user_pools_id: runtimeContext.userPoolId,
            aws_user_pools_web_client_id: runtimeContext.userPoolWebClientId,
            aws_appsync_graphqlEndpoint: runtimeContext.appSyncGraphqlEndpoint,
            aws_appsync_region: runtimeContext.region,
            aws_appsync_authenticationType: 'AMAZON_COGNITO_USER_POOLS',
            Auth: {
              region: runtimeContext.region,
              userPoolId: runtimeContext.userPoolId,
              userPoolWebClientId: runtimeContext.userPoolWebClientId,
              identityPoolId: runtimeContext.identityPoolId,
            },
          });
      })
      .catch((e) => console.log(e));
  }, []);

As you can see, a fetch to load the runtime-config.json is executed initially. After that Amplify is configured with the extracted properties.

You can also use HTML window variables to set the Amplify parameters. However, I prefer the fetch solution presented here because it is potentially more responsive to a missing runtime-config.json or single missing properties. Also, window variables should be avoided as they get global access to the DOM.

Workflows

The typical workflow without the runtime-config to build and deploy the React app sometimes went like this:

  • curl and store current endpoints like user pool id, AppSynch endpoint and more.
  • build Amplify config file
  • build react app
  • cdk deploy react dist folder to S3

Build pipeline workflow with runtime-config:

  • build react app
  • cdk deploy react dist folder and runtime config to S3

CDK example

The complete code is available in my GitHub Senjuns project.

const userPool = new cognito.UserPool(...)
...
const identityPool = new cognito.CfnIdentityPool(...)
...

const dashboard = new StaticWebsite(this, 'dashboard', {
    build: '../dashboard/build',
    recordName: 'dashboard',
    domainName: props.domainName,
    runtimeOptions: {
        jsonPayload: {
            region: core.Stack.of(this).region,
            identityPoolId: identityPool.ref,
            userPoolId: userPool.userPoolId,
            userPoolWebClientId: userPoolWebClient.userPoolClientId,
            appSyncGraphqlEndpoint: graphqlUrl.stringValue,
        },
    },
});

The StaticWebsite is a simple L3 CDK construct with an S3 static website bucket as the main resource. You can see more details here. But the interesting details are in the runtimeOptions object. There the endpoints for the runtime config for Amplify are stored. Behind this is the S3 Bucket Deployment Construct which transfers the endpoints via s3deploy.Source.jsonData(...) into the JSON file runtime-config.json:

const DEFAULT_RUNTIME_CONFIG_FILENAME = 'runtime-config.json';

...

new s3deploy.BucketDeployment(this, 'BucketDeployment', {
    sources: [
    s3deploy.Source.asset(props.build),
    ...(props.runtimeOptions
        ? [
        s3deploy.Source.jsonData(
            props.runtimeOptions?.jsonFileName ||
                DEFAULT_RUNTIME_CONFIG_FILENAME,
            props.runtimeOptions?.jsonPayload,
        ),
        ]
        : []),
    ],
    distribution,
    destinationBucket: siteBucket,
});

This is a cool CDK integration :) ! Just giving the BucketDeployment Construct the two parameters like the React dist and the runtime-config is a pretty smart idea.

Workaround with nested stack outputs

During my work with the runtime-config I encountered a problem. It is not possible to use CDK outputs from a nested stack for runtime-config. But there is a workaround using AWS Systems Manager parameters:

const graphqlUrl = new ssm.StringParameter(this, 'GraphqlUrl', {
    parameterName: 'GraphqlUrl',
    stringValue: appSyncTransformer.appsyncAPI.graphqlUrl,
});

...

const dashboard = new StaticWebsite(this, 'dashboard', {
    build: '../dashboard/build',
    recordName: 'dashboard',
    domainName: props.domainName,
    runtimeOptions: {
    jsonPayload: {
        region: core.Stack.of(this).region,
        identityPoolId: identityPool.ref,
        userPoolId: userPool.userPoolId,
        userPoolWebClientId: userPoolWebClient.userPoolClientId,
        appSyncGraphqlEndpoint: graphqlUrl.stringValue,
    },
    },
});

Cool, right? The nested stack output is simply stored in an SSM string parameter and can then be read later. Thanks to Adrian Dimech for the great workaround 🙏.

Conclusion

AWS CDK and Amplify are a powerful combination. With the runtime-config presented here, this combination feels much better! I copied this solution from aws-prototyping-sdk. There are some interesting AWS CDK constructs being developed in this repo. So you should definitely check it out!

Thanks to the DeepL translater (free version) for helping with translating to english and saving me tons of time :).

I love to work on Open Source projects. A lot of my stuff you can already use on https://github.com/mmuller88 . If you like my work there and my blog posts, please consider supporting me on:

Buy me a Ko-Fi

OR

Buy me a Ko-Fi

And don't forget to visit my site

martinmueller.dev

Tagged in eng2022awscdk

Share