Amplify the CDK
🛠️

Amplify the CDK

Published
Published September 13, 2021
Author

TL;DR

I'm a huge fan of AWS and think their Amplify initiative is cool but too much magic and too many terminal prompts for me. Honestly, I think the CDK is about as good as you can get for defining your backend. The best part of Amplify is the ability to autogenerate CRUD tables and resolvers with authentication rules from a schema. This is an amazing developer experience and is one of the things that makes tools like Prisma popular so I am glad to see AWS now has this ability. The problem for me is that I want to define my backend with the CDK but also be able to use this functionality. Luckily, Kenneth Winner made cdk-appsync-transformer to give us the best of both worlds! In this post we will take this example project from the Amplify King and replace the Amplify configuration with, what I call, the Amplified CDK implementation.
notion image

Initialize Example Project

Let's clone and configure the example project.
# Clone the example project % git clone [https://github.com/full-stack-serverless/react-chat-app-aws.git](https://github.com/full-stack-serverless/react-chat-app-aws.git)amplify-the-cdk && cd $_ # Set remote to our repository % git remote set-url origin git@github.com:thefinnomenon/amplify-the-cdk.git # Move schema.graphql because we will be reusing it % mv amplify/backend/api/AmplifyChat/schema.graphql src/ # Remove Amplify stuff % rm -rf amplify/ src/graphql/ amplify.json .graphqlconfig.yml

Initialize CDK

Now that have the frontend for this project, we will use the CDK to define the backend.
# Install the CDK CLI % npm install -g aws-cdk # Create CDK directory % mkdir backend && cd $_ # Initialize CDK App % cdk init app --language typescript

Rename Stack

We should give our stack a unique and more descriptive name.
// backend/bin/backend.ts ... new BackendStack(app, 'chat-app-BackendStack', {});

Create Backend

Next, we start defining our backend stack.

Authentication

First, let's define the authentication for the project.

Install Dependencies

% npm i @aws-cdk/aws-cognito@1.120.0 --save-exact
My biggest CDK tip is to always pin the CDK dependencies to the same version using @#.#.# and --save-exact otherwise you almost certainly will end up with some type errors.

Configure UserPool and UserPoolClient

The chat app uses Cognito email and password authentication with email code verification.
// backend/lib/backend-stack.ts import * as cdk from '@aws-cdk/core'; import * as cognito from '@aws-cdk/aws-cognito'; export class BackendStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); /* COGNITO */ const userPool = new cognito.UserPool(this, 'chat-app-UserPool', { selfSignUpEnabled: true, accountRecovery: cognito.AccountRecovery.PHONE_AND_EMAIL, userVerification: { emailStyle: cognito.VerificationEmailStyle.CODE }, autoVerify: { email: true }, standardAttributes: { email: { required: true, mutable: true } } }); const userPoolClient = new cognito.UserPoolClient(this, 'chat-app-UserPoolClient', { userPool, authFlows: { // We really should use SRP instead but in the spirit of not modifying the frontend, we will use this userPassword: true, }, }); new cdk.CfnOutput(this, 'UserPoolId', { value: userPool.userPoolId }); new cdk.CfnOutput(this, 'UserPoolClientId', { value: userPoolClient.userPoolClientId }); } }

Install Dependencies

% npm i cdk-appsync-transformer @aws-cdk/aws-appsync@1.120.0 --save-exact

Fix cdk-appsync-transformer Version

There is currently a pending PR to bump the CDK version used by the transformer but we can bump it ourselves for now (or to match a more recent version).
% cd backend/node_modules/cdk-appsync-transformer/ // Change all "@aws-cdk/*" dependency versions to "~1.120.0" % npm install

Configure API

We will define an AppSync API using the schema.graphql.
// backend/lib/backend-stack.ts ... import * as appsync from '@aws-cdk/aws-appsync'; import { AppSyncTransformer } from 'cdk-appsync-transformer'; ... export class BackendStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { ... /* APPSYNC */ const api = new AppSyncTransformer(this, `chat-app-AppSyncApi}`, { schemaPath: '../src/schema.graphql', authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.USER_POOL, userPoolConfig: { userPool: userPool, defaultAction: appsync.UserPoolDefaultAction.ALLOW, } }, }, xrayEnabled: true, }); new cdk.CfnOutput(this, "AppSyncApiGraphQLUrlOutput", { value: api.appsyncAPI.graphqlUrl }); ... } }

Deploy

Let's deploy our backend
% cdk deploy

Generate Helpers

Now that the backend is deployed, we have two more steps left; generating our configuration file from the CDK outputs and generating the GraphQL operations and types from our schema.

Exports File

We will use a script written by Kenneth Winner (thanks again!) to gather the outputs from our CDK stack and format them into the expected Amplify aws-exports.js file.
# In the backend directory % npm i -D aws-sdk
// backend/utilities/generateExports.js /* source: https://github.com/kcwinner/advocacy/blob/master/cdk-amplify-appsync-helpers/generateExports.js */ const fs = require('fs'); const path = require('path'); const AWS = require('aws-sdk'); const REGION = process.env.AWS_REGION || 'us-east-1'; // This have to match the stack name you chose in lib/backend-stack.ts const STACK_NAME = `chat-app-BackendStack`; const cloudformation = new AWS.CloudFormation({ region: REGION }); // These have to match the output names in your stack definition const appsyncURLOutputKey = 'AppSyncApiGraphQLUrlOutput'; const userPoolIDOutputKey = 'UserPoolIdOutput'; const userPoolClientOutputKey = 'UserPoolClientIdOutput'; // Modify the Auth to fit your use case // Configuration reference: https://docs.amplify.aws/lib/client-configuration/configuring-amplify-categories/q/platform/js/#top-level-configurationconst awsmobile = { aws_project_region: REGION, aws_appsync_graphqlEndpoint: '', aws_appsync_region: REGION, aws_appsync_authenticationType: 'AMAZON_COGNITO_USER_POOLS', Auth: { region: REGION, userPoolId: '', userPoolWebClientId: '', authenticationFlowType: 'USER_PASSWORD_AUTH', }, aws_user_pools_id: '', aws_user_pools_web_client_id: '', aws_cognito_region: REGION, }; main(); async function main() { const exportFileName = 'aws-exports.js'; console.log('Generating aws-exports.js'); const describeStackParams = { StackName: STACK_NAME, }; const stackResponse = await cloudformation.describeStacks(describeStackParams).promise(); const stack = stackResponse.Stacks[0]; const appsyncURL = stack.Outputs.find(output => { return output.OutputKey === appsyncURLOutputKey; }); const userPoolId = stack.Outputs.find(output => { return output.OutputKey === userPoolIDOutputKey; }); const userPoolClientId = stack.Outputs.find(output => { return output.OutputKey === userPoolClientOutputKey; }); awsmobile.aws_appsync_graphqlEndpoint = appsyncURL.OutputValue; awsmobile.Auth.userPoolId = userPoolId.OutputValue; awsmobile.Auth.userPoolWebClientId = userPoolClientId.OutputValue; awsmobile.aws_user_pools_id = userPoolId.OutputValue; awsmobile.aws_user_pools_web_client_id = userPoolClientId.OutputValue; const curDir = process.cwd(); const awsExportsPath = path.join(curDir, '../src', exportFileName); const data = `const awsmobile = ${JSON.stringify(awsmobile, null, 4)} export default awsmobile;`.replace(/^ {4}export default awsmobile/gm, 'export default awsmobile'); fs.writeFileSync(awsExportsPath, data); }

Add Script

Let's add a script to generate our exports and another to combine the deploy and generate step.
// backend/package.json { "scripts": { ... "deploy": "cdk deploy && npm run generate:exports", "generate:exports": "node utilities/generateExports.js" } }

Ignore Exports File

Add src/aws-exports.js to .gitignore.

GraphQL Operations & Types

To generate our GraphQL operation & types, we will use Amplify's codegen, but first, we have to create .graphqlconfig.yml.
// .graphqlconfig.yml # schemaPath isn't working for me so run codegen from backend/appsync/. # All the paths are relative to backend/appsync/. projects: ChatApp: schemaPath: schema.graphql includes: - ../../src/graphql/**/*.tsexcludes: [] extensions: amplify: codeGenTarget: typescript generatedFileName: ../../src/graphql/appsync.ts docsFilePath: ../../src/graphql/ frontend: javascript framework: react-native maxDepth: 2 extensions: amplify: version: 3

Add Scripts

Let's add some scripts to automate the generation of these helpers.
// package.json { ... "scripts": { ... "backend:deploy": "cd backend && cdk deploy && cd .. && yarn generate:graphql", "generate:graphql": "cd backend/appsync && amplify codegen", "postinstall": "npm --prefix ./backend install" } }

Deploy & Generate

Now that we have our new deploy script that deploys the backend and generates our helpers, let's call it.
% yarn backend:deploy

Update Frontend

Okay, now all that's left to do is make sure the frontend is working.

Update Dependencies

I had dependency errors when I tried running npm install. The react version wasn't matching the one for the amplify-ui so I ended up just updating them.
// package.json { ... "dependencies": { "@aws-amplify/ui-react": "^1.2.15", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", "aws-amplify": "^3.0.22", "react": "^17.0.2", "react-dom": "^17.0.2", "react-router-dom": "^5.2.0", "react-scripts": "3.4.1" }, ... }

Run App

Now let's start our app and make sure everything is working.
% npm start
notion image

Final Thoughts

I really enjoy this way of defining my backend. Since this example was already implemented with Amplify and the CDK, and now, with the Amplified CDK, you can directly compare the three approaches. Compared to the Amplify approach, the Amplified CDK approach provides more flexibility and explicit control of the configuration. The advantage of using Amplify over this method is that it handles permissions for you but I don't find this too difficult in the CDK. If you compare this approach to the CDK implementation you will see that it greatly reduces the amount of CDK code necessary since you no longer have to manually define the tables and resolvers. Give it a shot and let me know what you think!
Click to rocket boost to the top of the page!