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.
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
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!