├── .DS_Store ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── appsync-multi-region-api ├── .gitignore ├── .npmignore ├── appsync-api │ ├── resolvers │ │ ├── Mutation.AddTodo.req.vtl │ │ ├── Mutation.AddTodo.resp.vtl │ │ ├── Mutation.AddTodoGlobalSync.req.vtl │ │ ├── Mutation.AddTodoGlobalSync.resp.vtl │ │ ├── Mutation.DeleteTodo.req.vtl │ │ ├── Mutation.DeleteTodo.resp.vtl │ │ ├── Mutation.DeleteTodoGlobalSync.req.vtl │ │ ├── Mutation.DeleteTodoGlobalSync.resp.vtl │ │ ├── Mutation.UpdateTodo.req.vtl │ │ ├── Mutation.UpdateTodo.resp.vtl │ │ ├── Mutation.UpdateTodoGlobalSync.req.vtl │ │ ├── Mutation.UpdateTodoGlobalSync.resp.vtl │ │ ├── Query.GetTodo.req.vtl │ │ ├── Query.GetTodo.resp.vtl │ │ ├── Query.ListTodos.req.vtl │ │ └── Query.ListTodos.resp.vtl │ └── schema.graphql ├── bin │ ├── appsync-multi-region-active-active.js │ └── appsync-multi-region-active-active.ts ├── cdk.json ├── global_appsync_cleanup.sh ├── global_appsync_setup.sh ├── jest.config.js ├── lambdas │ ├── appsync-auth │ │ └── index.js │ ├── appsync-globalapi-router │ │ ├── configs.json │ │ └── index.js │ └── ddb-stream-processor │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json ├── lib │ ├── appsync-multi-region-active-active-routing-stack.js │ ├── appsync-multi-region-active-active-routing-stack.ts │ ├── appsync-multi-region-active-active-stack.js │ ├── appsync-multi-region-active-active-stack.ts │ ├── secondary-appsync-multi-region-active-active-stack.js │ └── secondary-appsync-multi-region-active-active-stack.ts ├── package.json ├── parameters │ ├── globalVariables.js │ └── globalVariables.ts ├── test │ ├── appsync-multi-region-active-active.test.js │ └── appsync-multi-region-active-active.test.ts └── tsconfig.json ├── images ├── appsync-multi-region-active-active.png ├── postman-body-config.png ├── postman-header-config.png ├── postman-mutation-result.png ├── postman-query-result.png ├── postman-subscription-connection.png ├── postman-subscription-headers.png ├── postman-subscription-init-connection.png ├── postman-subscription-notification.png ├── postman-subscription-params.png ├── postman-subscription-query.png ├── postman-subscription-url.png └── sample-todo-app.png └── sample-todo-app ├── .gitignore ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── components ├── Form.js ├── List.js ├── Subscriptions.js └── Todo.js ├── index.css ├── index.js ├── logo.svg ├── reportWebVitals.js └── setupTests.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-multi-region-active-active/d169f0eed31fc279f5965b886d35d903e1f4387d/.DS_Store -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AWS AppSync Multi-Region Active Active Deployment 2 | 3 | This repository contains two applications 4 | 1. [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk/) app that will create a sample GraphQL API for a Todo App deployed in AppSync across two AWS Regions 5 | 6 | 2. A sample React application setup using [AWS Amplify](https://aws.amazon.com/amplify/) that will consume the multi-region active active AppSync API via a single endpoint and showcase how you can query the API endpoint for Queries, Mutations and Subscription 7 | 8 | | :exclamation: This implementation will setup up AppSync in only two AWS regions. If you need to deploy AppSync in 3 or more regions, you will need to follow similar implementation used to deploy in the second region to setup the new regions. | 9 | |-----------------------------------------| 10 | 11 | ### High Level Architecture 12 | 13 | The architecture diagram below shows what the CDK application will be deploying in your AWS account. AppSync will be deployed in two AWS Regions with [Amazon DynamoDB global tables](https://aws.amazon.com/dynamodb/global-tables/) as the datasource. To setup the routing to a single API endpoint for your clients, we will be using [Amazon CloudFront](https://aws.amazon.com/cloudfront/) configured with [AWS Lambda@Edge](https://aws.amazon.com/lambda/edge/) to route traffic to AppSync endpoints based on the routing policy setup in [Amazon Route 53](https://aws.amazon.com/route53/) (for example latency based routing or weighted routing etc.) 14 | 15 | ![AppSync Multi-Region Active Active](images/appsync-multi-region-active-active.png?raw=true "AppSync Multi-Region Active Active") 16 | 17 | Source [AWS Reference Architecture - Multi-Region GraphQL API with CloudFront](https://d1.awsstatic.com/architecture-diagrams/ArchitectureDiagrams/multi-region-graphQL-api-with-cloudfront-ra.pdf) 18 | 19 | 20 | ## Pre-Requisites 21 | To deploy the CDK app, you will need the following: 22 | 23 | 1. Command line interface (CLI) to deploy the CDK. You can use a CLI in your location workstation or [AWS Cloud9](https://aws.amazon.com/cloud9/). Follow the guide [here](https://docs.aws.amazon.com/cloud9/latest/user-guide/create-environment-main.html) to launch a new Cloud9 environment 24 | 25 | 2. CDK version 2 installed. You can use the command below to install CDK. AWS Cloud9 instance have CDK pre-installed. 26 | ```bash 27 | npm install -g aws-cdk 28 | ``` 29 | 30 | 3. You will need [jq](https://stedolan.github.io/jq/) to filter out outputs from CLI. You can download it [here](https://stedolan.github.io/jq/download/) 31 | 32 | 4. The CDK app requires the following global parameters 33 | * A hosted zone in Amazon Route 53. You will need the hosted zone domain name and hosted zone ID 34 | * Custom domains for AppSync in the two regions. This is usually a subdomain of the hosted zone domain name 35 | * AWS Region code for the two AppSync regions (for example us-east-1) 36 | * ARN of the wildcard certificate for the hosted zone domain name created using [Amazon Certificate Manager](https://aws.amazon.com/certificate-manager/) 37 | * DNS of the global API endpoint that will be used by the client. This is usually a subdomain of the hosted zone domain name 38 | * DNS for the Amazon Route 53 routing policy. This is usually a subdomain of the hosted zone domain name 39 | 40 | 41 | To deploy the React app, you will need the following: 42 | 43 | 1. AWS Amplify installed using the command below. 44 | ```bash 45 | npm install -g @aws-amplify/cli 46 | ``` 47 | 48 | ## Setting up the infrastructure 49 | 50 | | :exclamation: These steps have been tested using [AWS Cloud9](https://aws.amazon.com/cloud9/) terminal and it is highly recommended to setup the infrastructure using a Cloud9. You can select either t3.medium (4 GiB RAM + 2 vCPU) or m5.large (8 GiB RAM + 2 vCPU) when creating the Cloud9 instance | 51 | |-----------------------------------------| 52 | 53 | 1. Clone the CDK App and install dependencies 54 | ```bash 55 | git clone https://github.com/aws-samples/aws-appsync-multi-region-active-active.git 56 | cd aws-appsync-multi-region-active-active/appsync-multi-region-api 57 | npm install 58 | ``` 59 | 60 | 2. Update the global parameters. Note, you only need to create the Hosted Zone in Amazon Route 53, you do not need to create the DNS entries for the other domain name requested below. The CDK App will create the DNS entries in Route 53 61 | * Navigate to the file 62 | ```bash 63 | aws-appsync-multi-region-active-active/appsync-multi-region-api/parameters/globalVariables.ts 64 | ``` 65 | * Update the parameter values with the values specific to your API and AWS Account 66 | ```bash 67 | route53HostedZoneName: '', 68 | route53HostedZoneID:'', 69 | primaryRegionAppSyncCustomDomain: '', 70 | secondaryRegionAppSyncCustomDomain: '', 71 | primaryRegion: '', 72 | secondaryRegion: '', 73 | domainCertARN:'', 74 | globalAPIEndpoint: '', 75 | route53RoutingPolicyDomainName: '', 76 | ``` 77 | * Save the changes to the file 78 | 79 | 3. Update the variables required by Lambda@Edge. This is required because you cannot use environment variables for Lambda@Edge. For other options on passing variables to lambda at edge, refer to the blog [here](https://aws.amazon.com/blogs/networking-and-content-delivery/leveraging-external-data-in-lambdaedge/) 80 | * Navigate to the file 81 | ```bash 82 | aws-appsync-multi-region-active-active/appsync-multi-region-api/lambdas/appsync-globalapi-router/configs.json 83 | ``` 84 | * Update the parameter values with the values specific to your API and AWS Account, using the same values provided in the above configuration 85 | ```bash 86 | route53RoutingPolicyDomainName: '', 87 | primaryRegion: '', 88 | primaryRegionAppSyncCustomDomain: '', 89 | secondaryRegion: '', 90 | secondaryRegionAppSyncCustomDomain: '', 91 | ``` 92 | * Save the changes to the file 93 | 94 | 4. Boostrap CDK in the target regions by running the commands below. You will need to specify the primary and secondary region code (e.g. eu-west-1 and us-east-1) 95 | ```bash 96 | cdk bootstrap / / /us-east-1 97 | ``` 98 | 99 | 5. Deploy the CDK stacks providing the value for the primary region code (e.g. eu-west-1) 100 | ```bash 101 | chmod +x global_appsync_setup.sh 102 | ./global_appsync_setup.sh 103 | ``` 104 | 105 | ## Testing your APIs 106 | We will be using [Postman](https://www.postman.com/) to test the API so if not already setup, you can download via the link [here](https://www.postman.com/downloads/) 107 | 108 | ### Testing GraphQL Mutation and Query via the Global API endpoint. Find below the settings on Postman to test out GraphQL Mutation. 109 | 110 | 1. To test GraphQL Mutation, change the request to "Post" request and paste the global API endpoint URL e.g. "https://todoglobalapi.codeforevery.com/graphql" 111 | 2. Click on the "Header" tab and add the key "Authorization" and the token for your Lambda Authorizer which in this case we are using "custom-authorized" 112 | ![Postman Header Configs](images/postman-header-config.png?raw=true "Postman Header Configs") 113 | 3. Click on the "Body" tab and past the GraphQL mutation as shown below. 114 | ![Postman Body Configs](images/postman-body-config.png?raw=true "Postman Body Configs") 115 | 4. You can see the GraphQL response to indicate the mutation is successful as shown below. 116 | ![Postman GraphQL Response](images/postman-mutation-result.png?raw=true "Postman GraphQL Response") 117 | 5. To test GraphQL Query, you only need to change the "Body" with the GraphQL query which will return the results as shown below 118 | ![Postman GraphQL Response](images/postman-query-result.png?raw=true "Postman GraphQL Response") 119 | 120 | ### Testing GraphQL Subscription via the Global API endpoint. Find below the settings on Postman to test out GraphQL Subcription. 121 | 1. Follow the instructions [here](https://blog.postman.com/postman-supports-websocket-apis/) to get started with setting up GraphQL Subscription on Postman 122 | 2. Paste the GraphQL API subscription endpoint as shown below 123 | ![Postman Subscription URL](images/postman-subscription-url.png?raw=true "Postman Subscription URL") 124 | 3. Click on the "Params" tab and provide the "payload" and "header" params. Follow the documentation [here](https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html) on how these values where derived. 125 | ![Postman Subscription Params](images/postman-subscription-params.png?raw=true "Postman Subscription Params") 126 | 4. Click on the "Headers" tab and the key as shown in the diagram below 127 | ![Postman Subscription Headers](images/postman-subscription-headers.png?raw=true "Postman Subscription Headers") 128 | 5. Click on "Connect" button on the right hand corner to establish a subscription connection to one of the AppSync API endpoints. 129 | ![Postman Subscription Connect](images/postman-subscription-connection.png?raw=true "Postman Subscription Connect") 130 | 6. Subscribe to the onAddTodo mutation by adding the query below to the "New Message" section as shown below, then click on "Send" 131 | ```bash 132 | {"id":"abc123","payload":{"data":"{\"query\":\"subscription MySubscription {\\n onAddTodo {\\n id \\n name \\n description\\n priority\\n status\\n }\\n}\\n\",\"variables\":null}","extensions":{"authorization":{"Authorization":"custom-authorized","host":"todoglobalapi.codeforevery.com"}}},"type":"start"} 133 | ``` 134 | ![Postman Subscription Subscribe](images/postman-subscription-query.png?raw=true "Postman Subscription Subscribe") 135 | 7. Send the message show below to initialize the connection. 136 | ![Postman Subscription Init Connect](images/postman-subscription-init-connection.png?raw=true "Postman Subscription Init Connect") 137 | 8. Create a new todo via a GraphQL mutation as described above and you will be able to see a new notification with the details of the new todo as shown below. 138 | ![Postman Subscription Notification](images/postman-subscription-notification.png?raw=true "Postman Subscription Notification") 139 | 140 | 141 | ## Setting up the Sample Todo App 142 | 143 | 1. Navigate to the Todo App directory 144 | ```bash 145 | cd ../sample-todo-app 146 | ``` 147 | 148 | 2. Install dependencies 149 | ```bash 150 | npm install 151 | ``` 152 | 153 | 3. Initialize Amplify and use the default options. You will need to setup AWS Access Keys that will be used by Amplify. 154 | ```bash 155 | amplify init 156 | ``` 157 | 158 | 4. Use Amplify to generate the GraphQL statements for Query, Mutation and Subscription. Replace the API ID with the AppSync API ID from one of the regions. While running the command you will need to specify the `region` the API is deployed and you can generate the code in `javascript`. Leave other options as default. 159 | ```bash 160 | amplify add codegen --apiId 161 | ``` 162 | 163 | 5. Update `aws-exports.js` file with the configuration for the API. The region can be one of the regions where your AppSync endpoint is deployed (not used). See below sample content for the file. 164 | 165 | ```bash 166 | const awsmobile = { 167 | "aws_project_region": "", 168 | "aws_appsync_graphqlEndpoint": "", 169 | "aws_appsync_region": "", 170 | "aws_appsync_authenticationType": "AWS_LAMBDA" 171 | }; 172 | export default awsmobile; 173 | ``` 174 | 175 | 6. Launch the Todo App. You can via the app running by clicking on "Preview" in Cloud9 menu and then "Preview Running Application" 176 | ```bash 177 | npm run start 178 | ``` 179 | 180 | 7. Create new todos and observe new todos created via GraphQL Subscription 181 | ![Sample Todo App](images/sample-todo-app.png?raw=true "Sample Todo App") 182 | 183 | 184 | ## Clean up 185 | Run the command below to clean up the stacks created. 186 | 187 | Note: 188 | * If you get an error around not being able to delete the lambda@edge function and replicas, go to the AWS Cloudformation console and delete the template manually. 189 | * Confirm the DynamoDB tables are deleted from both region and if not you can manually delete it from the console 190 | ```bash 191 | cd aws-appsync-multi-region-active-active/appsync-multi-region-api 192 | chmod +x global_appsync_cleanup.sh 193 | ./global_appsync_cleanup.sh 194 | ``` -------------------------------------------------------------------------------- /appsync-multi-region-api/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | .DS_Store 6 | package-lock.json 7 | 8 | # CDK asset staging directory 9 | .cdk.staging 10 | cdk.out 11 | 12 | #amazon ospo ruleset file 13 | amazon-ospo-ruleset.json 14 | -------------------------------------------------------------------------------- /appsync-multi-region-api/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /appsync-multi-region-api/appsync-api/resolvers/Mutation.AddTodo.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version" : "2017-02-28", 3 | "operation" : "PutItem", 4 | "key" : { 5 | ## If object "id" should come from GraphQL arguments, change to $util.dynamodb.toDynamoDBJson($ctx.args.id) 6 | "id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id), 7 | }, 8 | "attributeValues" : $util.dynamodb.toMapValuesJson($ctx.args.input) 9 | } -------------------------------------------------------------------------------- /appsync-multi-region-api/appsync-api/resolvers/Mutation.AddTodo.resp.vtl: -------------------------------------------------------------------------------- 1 | $util.toJson($ctx.result) -------------------------------------------------------------------------------- /appsync-multi-region-api/appsync-api/resolvers/Mutation.AddTodoGlobalSync.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2017-02-28", 3 | "payload": { 4 | "payload": $util.toJson($context.arguments), 5 | } 6 | } -------------------------------------------------------------------------------- /appsync-multi-region-api/appsync-api/resolvers/Mutation.AddTodoGlobalSync.resp.vtl: -------------------------------------------------------------------------------- 1 | $util.toJson($context.arguments.input) -------------------------------------------------------------------------------- /appsync-multi-region-api/appsync-api/resolvers/Mutation.DeleteTodo.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2017-02-28", 3 | "operation": "DeleteItem", 4 | "key": { 5 | "id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id), 6 | }, 7 | } -------------------------------------------------------------------------------- /appsync-multi-region-api/appsync-api/resolvers/Mutation.DeleteTodo.resp.vtl: -------------------------------------------------------------------------------- 1 | $util.toJson($ctx.result) -------------------------------------------------------------------------------- /appsync-multi-region-api/appsync-api/resolvers/Mutation.DeleteTodoGlobalSync.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2017-02-28", 3 | "payload": { 4 | "payload": $util.toJson($context.arguments), 5 | } 6 | } -------------------------------------------------------------------------------- /appsync-multi-region-api/appsync-api/resolvers/Mutation.DeleteTodoGlobalSync.resp.vtl: -------------------------------------------------------------------------------- 1 | $util.toJson($context.arguments.input) -------------------------------------------------------------------------------- /appsync-multi-region-api/appsync-api/resolvers/Mutation.UpdateTodo.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2017-02-28", 3 | "operation": "UpdateItem", 4 | "key": { 5 | "id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id), 6 | }, 7 | 8 | ## Set up some space to keep track of things we're updating ** 9 | #set( $expNames = {} ) 10 | #set( $expValues = {} ) 11 | #set( $expSet = {} ) 12 | #set( $expAdd = {} ) 13 | #set( $expRemove = [] ) 14 | 15 | ## Iterate through each argument, skipping keys ** 16 | #foreach( $entry in $util.map.copyAndRemoveAllKeys($ctx.args.input, ["id"]).entrySet() ) 17 | #if( $util.isNull($entry.value) ) 18 | ## If the argument is set to "null", then remove that attribute from the item in DynamoDB ** 19 | 20 | #set( $discard = ${expRemove.add("#${entry.key}")} ) 21 | $!{expNames.put("#${entry.key}", "${entry.key}")} 22 | #else 23 | ## Otherwise set (or update) the attribute on the item in DynamoDB ** 24 | 25 | $!{expSet.put("#${entry.key}", ":${entry.key}")} 26 | $!{expNames.put("#${entry.key}", "${entry.key}")} 27 | $!{expValues.put(":${entry.key}", $util.dynamodb.toDynamoDB($entry.value))} 28 | #end 29 | #end 30 | 31 | ## Start building the update expression, starting with attributes we're going to SET ** 32 | #set( $expression = "" ) 33 | #if( !${expSet.isEmpty()} ) 34 | #set( $expression = "SET" ) 35 | #foreach( $entry in $expSet.entrySet() ) 36 | #set( $expression = "${expression} ${entry.key} = ${entry.value}" ) 37 | #if ( $foreach.hasNext ) 38 | #set( $expression = "${expression}," ) 39 | #end 40 | #end 41 | #end 42 | 43 | ## Continue building the update expression, adding attributes we're going to ADD ** 44 | #if( !${expAdd.isEmpty()} ) 45 | #set( $expression = "${expression} ADD" ) 46 | #foreach( $entry in $expAdd.entrySet() ) 47 | #set( $expression = "${expression} ${entry.key} ${entry.value}" ) 48 | #if ( $foreach.hasNext ) 49 | #set( $expression = "${expression}," ) 50 | #end 51 | #end 52 | #end 53 | 54 | ## Continue building the update expression, adding attributes we're going to REMOVE ** 55 | #if( !${expRemove.isEmpty()} ) 56 | #set( $expression = "${expression} REMOVE" ) 57 | 58 | #foreach( $entry in $expRemove ) 59 | #set( $expression = "${expression} ${entry}" ) 60 | #if ( $foreach.hasNext ) 61 | #set( $expression = "${expression}," ) 62 | #end 63 | #end 64 | #end 65 | 66 | ## Finally, write the update expression into the document, along with any expressionNames and expressionValues ** 67 | "update": { 68 | "expression": "${expression}", 69 | #if( !${expNames.isEmpty()} ) 70 | "expressionNames": $utils.toJson($expNames), 71 | #end 72 | #if( !${expValues.isEmpty()} ) 73 | "expressionValues": $utils.toJson($expValues), 74 | #end 75 | }, 76 | 77 | "condition": { 78 | "expression": "attribute_exists(#id)", 79 | "expressionNames": { 80 | "#id": "id", 81 | }, 82 | } 83 | } -------------------------------------------------------------------------------- /appsync-multi-region-api/appsync-api/resolvers/Mutation.UpdateTodo.resp.vtl: -------------------------------------------------------------------------------- 1 | $util.toJson($context.result) -------------------------------------------------------------------------------- /appsync-multi-region-api/appsync-api/resolvers/Mutation.UpdateTodoGlobalSync.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2017-02-28", 3 | "payload": { 4 | "payload": $util.toJson($context.arguments), 5 | } 6 | } -------------------------------------------------------------------------------- /appsync-multi-region-api/appsync-api/resolvers/Mutation.UpdateTodoGlobalSync.resp.vtl: -------------------------------------------------------------------------------- 1 | $util.toJson($context.arguments.input) -------------------------------------------------------------------------------- /appsync-multi-region-api/appsync-api/resolvers/Query.GetTodo.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2017-02-28", 3 | "operation": "GetItem", 4 | "key": { 5 | "id": $util.dynamodb.toDynamoDBJson($ctx.args.id), 6 | } 7 | } -------------------------------------------------------------------------------- /appsync-multi-region-api/appsync-api/resolvers/Query.GetTodo.resp.vtl: -------------------------------------------------------------------------------- 1 | $util.toJson($ctx.result) -------------------------------------------------------------------------------- /appsync-multi-region-api/appsync-api/resolvers/Query.ListTodos.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version" : "2017-02-28", 3 | "operation" : "Scan", 4 | "limit": $util.defaultIfNull(${ctx.args.limit}, 20), 5 | "nextToken": $util.toJson($util.defaultIfNullOrBlank($ctx.args.nextToken, null)) 6 | } -------------------------------------------------------------------------------- /appsync-multi-region-api/appsync-api/resolvers/Query.ListTodos.resp.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "todos": $util.toJson($context.result.items), 3 | "nextToken": $util.toJson($context.result.nextToken) 4 | } -------------------------------------------------------------------------------- /appsync-multi-region-api/appsync-api/schema.graphql: -------------------------------------------------------------------------------- 1 | input AddTodo { 2 | id: ID! 3 | name: String 4 | description: String 5 | priority: Int 6 | status: TodoStatus 7 | } 8 | 9 | input DeleteTodo { 10 | id: ID! 11 | } 12 | 13 | type Mutation { 14 | addTodo(input: AddTodo): Todo 15 | deleteTodo(input: DeleteTodo): Todo 16 | updateTodo(input: UpdateTodo): Todo 17 | addTodoGlobalSync(input: AddTodo): Todo 18 | deleteTodoGlobalSync(input: DeleteTodo): Todo 19 | updateTodoGlobalSync(input: UpdateTodo): Todo 20 | } 21 | 22 | type Query { 23 | listTodos(limit: Int, nextToken: String): TodoConnection 24 | getTodo(id: ID!): Todo 25 | } 26 | 27 | type Subscription { 28 | onAddTodo(input: AddTodo): Todo 29 | @aws_subscribe(mutations: ["addTodoGlobalSync"]) 30 | onDeleteTodo(input: DeleteTodo): Todo 31 | @aws_subscribe(mutations: ["deleteTodoGlobalSync"]) 32 | onUpdateTodo(input: UpdateTodo): Todo 33 | @aws_subscribe(mutations: ["updateTodoGlobalSync"]) 34 | } 35 | 36 | type Todo { 37 | id: ID! 38 | name: String 39 | description: String 40 | priority: Int 41 | status: TodoStatus 42 | } 43 | 44 | type TodoConnection { 45 | todos: [Todo] 46 | nextToken: String 47 | } 48 | 49 | enum TodoStatus { 50 | done 51 | pending 52 | } 53 | 54 | input UpdateTodo { 55 | id: ID! 56 | name: String 57 | description: String 58 | priority: Int 59 | status: TodoStatus 60 | } 61 | 62 | schema { 63 | query: Query 64 | mutation: Mutation 65 | subscription: Subscription 66 | } -------------------------------------------------------------------------------- /appsync-multi-region-api/bin/appsync-multi-region-active-active.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | Object.defineProperty(exports, "__esModule", { value: true }); 4 | require("source-map-support/register"); 5 | const cdk = require("aws-cdk-lib"); 6 | const appsync_multi_region_active_active_stack_1 = require("../lib/appsync-multi-region-active-active-stack"); 7 | const secondary_appsync_multi_region_active_active_stack_1 = require("../lib/secondary-appsync-multi-region-active-active-stack"); 8 | const appsync_multi_region_active_active_routing_stack_1 = require("../lib/appsync-multi-region-active-active-routing-stack"); 9 | const globalVariables_1 = require("../parameters/globalVariables"); 10 | const cdk_nag_1 = require("cdk-nag"); 11 | const aws_cdk_lib_1 = require("aws-cdk-lib"); 12 | const app = new cdk.App(); 13 | aws_cdk_lib_1.Aspects.of(app).add(new cdk_nag_1.AwsSolutionsChecks({ verbose: true })); 14 | new appsync_multi_region_active_active_stack_1.AppsyncMultiRegionActiveActiveStack(app, 'AppsyncMultiRegionActiveActiveStack', { 15 | env: { region: globalVariables_1.globalVariables.primaryRegion }, 16 | primaryRegion: globalVariables_1.globalVariables.primaryRegion, 17 | secondaryRegion: globalVariables_1.globalVariables.secondaryRegion, 18 | appSyncCustomDomain: globalVariables_1.globalVariables.primaryRegionAppSyncCustomDomain, 19 | graphqlAPIDomainNameCertARN: globalVariables_1.globalVariables.domainCertARN, 20 | todoAPIHostedZoneID: globalVariables_1.globalVariables.route53HostedZoneID, 21 | todoAPIHostedZoneName: globalVariables_1.globalVariables.route53HostedZoneName, 22 | }); 23 | new secondary_appsync_multi_region_active_active_stack_1.SecondaryAppsyncMultiRegionActiveActiveStack(app, 'SecondaryAppsyncMultiRegionActiveActiveStack', { 24 | env: { region: globalVariables_1.globalVariables.secondaryRegion }, 25 | primaryRegion: globalVariables_1.globalVariables.primaryRegion, 26 | secondaryRegion: globalVariables_1.globalVariables.secondaryRegion, 27 | appSyncCustomDomain: globalVariables_1.globalVariables.secondaryRegionAppSyncCustomDomain, 28 | graphqlAPIDomainNameCertARN: globalVariables_1.globalVariables.domainCertARN, 29 | todoAPIHostedZoneID: globalVariables_1.globalVariables.route53HostedZoneID, 30 | todoAPIHostedZoneName: globalVariables_1.globalVariables.route53HostedZoneName 31 | }); 32 | new appsync_multi_region_active_active_routing_stack_1.AppsyncMultiRegionActiveActiveRoutingStack(app, 'AppsyncMultiRegionActiveActiveRoutingStack', { 33 | env: { region: 'us-east-1' }, 34 | /* Params below are required to configure the CF distribution however the params for Lambda@edge configuration are kept 35 | within the lambda package /lambdas/appsync-globalapi-router/configs.json. This is the because Lambda@edge does not support env. variables 36 | and given these value don't change frequently, it makes sense to place them within the lambda function vs making a network call which 37 | will introduce latency */ 38 | graphqlAPIDomainNameCertARN: globalVariables_1.globalVariables.domainCertARN, 39 | todoGlobalAPIDomainName: globalVariables_1.globalVariables.globalAPIEndpoint, 40 | regionLatencyRoutingDNS: globalVariables_1.globalVariables.route53RoutingPolicyDomainName, 41 | placeholderCFOrigin: 'aws.amazon.com', 42 | todoAPIHostedZoneID: globalVariables_1.globalVariables.route53HostedZoneID, 43 | todoAPIHostedZoneName: globalVariables_1.globalVariables.route53HostedZoneName, 44 | primaryAppSyncAPIRegionName: globalVariables_1.globalVariables.primaryRegion, 45 | secondaryAppSyncAPIRegionName: globalVariables_1.globalVariables.secondaryRegion, 46 | }); 47 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBwc3luYy1tdWx0aS1yZWdpb24tYWN0aXZlLWFjdGl2ZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImFwcHN5bmMtbXVsdGktcmVnaW9uLWFjdGl2ZS1hY3RpdmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQ0EsdUNBQXFDO0FBQ3JDLG1DQUFtQztBQUNuQyw4R0FBc0c7QUFDdEcsa0lBQXlIO0FBQ3pILDhIQUFxSDtBQUNySCxtRUFBZ0U7QUFDaEUscUNBQTZDO0FBQzdDLDZDQUFzQztBQUV0QyxNQUFNLEdBQUcsR0FBRyxJQUFJLEdBQUcsQ0FBQyxHQUFHLEVBQUUsQ0FBQztBQUUxQixxQkFBTyxDQUFDLEVBQUUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxHQUFHLENBQUMsSUFBSSw0QkFBa0IsQ0FBQyxFQUFFLE9BQU8sRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUE7QUFFOUQsSUFBSSw4RUFBbUMsQ0FBQyxHQUFHLEVBQUUscUNBQXFDLEVBQUU7SUFDbEYsR0FBRyxFQUFFLEVBQUMsTUFBTSxFQUFFLGlDQUFlLENBQUMsYUFBYSxFQUFFO0lBQzdDLGFBQWEsRUFBRSxpQ0FBZSxDQUFDLGFBQWE7SUFDNUMsZUFBZSxFQUFFLGlDQUFlLENBQUMsZUFBZTtJQUNoRCxtQkFBbUIsRUFBRSxpQ0FBZSxDQUFDLGdDQUFnQztJQUNyRSwyQkFBMkIsRUFBRSxpQ0FBZSxDQUFDLGFBQWE7SUFDMUQsbUJBQW1CLEVBQUUsaUNBQWUsQ0FBQyxtQkFBbUI7SUFDeEQscUJBQXFCLEVBQUUsaUNBQWUsQ0FBQyxxQkFBcUI7Q0FFN0QsQ0FBQyxDQUFDO0FBRUgsSUFBSSxpR0FBNEMsQ0FBQyxHQUFHLEVBQUUsOENBQThDLEVBQUU7SUFDcEcsR0FBRyxFQUFFLEVBQUMsTUFBTSxFQUFFLGlDQUFlLENBQUMsZUFBZSxFQUFFO0lBQy9DLGFBQWEsRUFBRSxpQ0FBZSxDQUFDLGFBQWE7SUFDNUMsZUFBZSxFQUFFLGlDQUFlLENBQUMsZUFBZTtJQUNoRCxtQkFBbUIsRUFBRSxpQ0FBZSxDQUFDLGtDQUFrQztJQUN2RSwyQkFBMkIsRUFBRSxpQ0FBZSxDQUFDLGFBQWE7SUFDMUQsbUJBQW1CLEVBQUUsaUNBQWUsQ0FBQyxtQkFBbUI7SUFDeEQscUJBQXFCLEVBQUUsaUNBQWUsQ0FBQyxxQkFBcUI7Q0FDN0QsQ0FBQyxDQUFDO0FBR0gsSUFBSSw2RkFBMEMsQ0FBQyxHQUFHLEVBQUUsNENBQTRDLEVBQUU7SUFDaEcsR0FBRyxFQUFFLEVBQUMsTUFBTSxFQUFFLFdBQVcsRUFBRTtJQUUzQjs7OzZCQUd5QjtJQUV6QiwyQkFBMkIsRUFBRSxpQ0FBZSxDQUFDLGFBQWE7SUFDMUQsdUJBQXVCLEVBQUUsaUNBQWUsQ0FBQyxpQkFBaUI7SUFDMUQsdUJBQXVCLEVBQUUsaUNBQWUsQ0FBQyw4QkFBOEI7SUFDdkUsbUJBQW1CLEVBQUUsZ0JBQWdCO0lBQ3JDLG1CQUFtQixFQUFFLGlDQUFlLENBQUMsbUJBQW1CO0lBQ3hELHFCQUFxQixFQUFFLGlDQUFlLENBQUMscUJBQXFCO0lBQzVELDJCQUEyQixFQUFFLGlDQUFlLENBQUMsYUFBYTtJQUMxRCw2QkFBNkIsRUFBRSxpQ0FBZSxDQUFDLGVBQWU7Q0FFL0QsQ0FBQyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiIyEvdXNyL2Jpbi9lbnYgbm9kZVxuaW1wb3J0ICdzb3VyY2UtbWFwLXN1cHBvcnQvcmVnaXN0ZXInO1xuaW1wb3J0ICogYXMgY2RrIGZyb20gJ2F3cy1jZGstbGliJztcbmltcG9ydCB7IEFwcHN5bmNNdWx0aVJlZ2lvbkFjdGl2ZUFjdGl2ZVN0YWNrIH0gZnJvbSAnLi4vbGliL2FwcHN5bmMtbXVsdGktcmVnaW9uLWFjdGl2ZS1hY3RpdmUtc3RhY2snO1xuaW1wb3J0IHsgU2Vjb25kYXJ5QXBwc3luY011bHRpUmVnaW9uQWN0aXZlQWN0aXZlU3RhY2sgfSBmcm9tICcuLi9saWIvc2Vjb25kYXJ5LWFwcHN5bmMtbXVsdGktcmVnaW9uLWFjdGl2ZS1hY3RpdmUtc3RhY2snO1xuaW1wb3J0IHsgQXBwc3luY011bHRpUmVnaW9uQWN0aXZlQWN0aXZlUm91dGluZ1N0YWNrIH0gZnJvbSAnLi4vbGliL2FwcHN5bmMtbXVsdGktcmVnaW9uLWFjdGl2ZS1hY3RpdmUtcm91dGluZy1zdGFjayc7XG5pbXBvcnQgeyBnbG9iYWxWYXJpYWJsZXMgfSBmcm9tICcuLi9wYXJhbWV0ZXJzL2dsb2JhbFZhcmlhYmxlcyc7XG5pbXBvcnQgeyBBd3NTb2x1dGlvbnNDaGVja3MgfSBmcm9tICdjZGstbmFnJztcbmltcG9ydCB7IEFzcGVjdHMgfSBmcm9tICdhd3MtY2RrLWxpYic7XG5cbmNvbnN0IGFwcCA9IG5ldyBjZGsuQXBwKCk7XG5cbkFzcGVjdHMub2YoYXBwKS5hZGQobmV3IEF3c1NvbHV0aW9uc0NoZWNrcyh7IHZlcmJvc2U6IHRydWUgfSkpXG5cbm5ldyBBcHBzeW5jTXVsdGlSZWdpb25BY3RpdmVBY3RpdmVTdGFjayhhcHAsICdBcHBzeW5jTXVsdGlSZWdpb25BY3RpdmVBY3RpdmVTdGFjaycsIHtcbiAgZW52OiB7cmVnaW9uOiBnbG9iYWxWYXJpYWJsZXMucHJpbWFyeVJlZ2lvbiB9LFxuICBwcmltYXJ5UmVnaW9uOiBnbG9iYWxWYXJpYWJsZXMucHJpbWFyeVJlZ2lvbixcbiAgc2Vjb25kYXJ5UmVnaW9uOiBnbG9iYWxWYXJpYWJsZXMuc2Vjb25kYXJ5UmVnaW9uLFxuICBhcHBTeW5jQ3VzdG9tRG9tYWluOiBnbG9iYWxWYXJpYWJsZXMucHJpbWFyeVJlZ2lvbkFwcFN5bmNDdXN0b21Eb21haW4sXG4gIGdyYXBocWxBUElEb21haW5OYW1lQ2VydEFSTjogZ2xvYmFsVmFyaWFibGVzLmRvbWFpbkNlcnRBUk4sXG4gIHRvZG9BUElIb3N0ZWRab25lSUQ6IGdsb2JhbFZhcmlhYmxlcy5yb3V0ZTUzSG9zdGVkWm9uZUlELFxuICB0b2RvQVBJSG9zdGVkWm9uZU5hbWU6IGdsb2JhbFZhcmlhYmxlcy5yb3V0ZTUzSG9zdGVkWm9uZU5hbWUsXG4gIFxufSk7XG5cbm5ldyBTZWNvbmRhcnlBcHBzeW5jTXVsdGlSZWdpb25BY3RpdmVBY3RpdmVTdGFjayhhcHAsICdTZWNvbmRhcnlBcHBzeW5jTXVsdGlSZWdpb25BY3RpdmVBY3RpdmVTdGFjaycsIHtcbiAgZW52OiB7cmVnaW9uOiBnbG9iYWxWYXJpYWJsZXMuc2Vjb25kYXJ5UmVnaW9uIH0sXG4gIHByaW1hcnlSZWdpb246IGdsb2JhbFZhcmlhYmxlcy5wcmltYXJ5UmVnaW9uLFxuICBzZWNvbmRhcnlSZWdpb246IGdsb2JhbFZhcmlhYmxlcy5zZWNvbmRhcnlSZWdpb24sXG4gIGFwcFN5bmNDdXN0b21Eb21haW46IGdsb2JhbFZhcmlhYmxlcy5zZWNvbmRhcnlSZWdpb25BcHBTeW5jQ3VzdG9tRG9tYWluLFxuICBncmFwaHFsQVBJRG9tYWluTmFtZUNlcnRBUk46IGdsb2JhbFZhcmlhYmxlcy5kb21haW5DZXJ0QVJOLFxuICB0b2RvQVBJSG9zdGVkWm9uZUlEOiBnbG9iYWxWYXJpYWJsZXMucm91dGU1M0hvc3RlZFpvbmVJRCxcbiAgdG9kb0FQSUhvc3RlZFpvbmVOYW1lOiBnbG9iYWxWYXJpYWJsZXMucm91dGU1M0hvc3RlZFpvbmVOYW1lXG59KTtcblxuXG5uZXcgQXBwc3luY011bHRpUmVnaW9uQWN0aXZlQWN0aXZlUm91dGluZ1N0YWNrKGFwcCwgJ0FwcHN5bmNNdWx0aVJlZ2lvbkFjdGl2ZUFjdGl2ZVJvdXRpbmdTdGFjaycsIHtcbiAgZW52OiB7cmVnaW9uOiAndXMtZWFzdC0xJyB9LFxuXG4gIC8qIFBhcmFtcyBiZWxvdyBhcmUgcmVxdWlyZWQgdG8gY29uZmlndXJlIHRoZSBDRiBkaXN0cmlidXRpb24gaG93ZXZlciB0aGUgcGFyYW1zIGZvciBMYW1iZGFAZWRnZSBjb25maWd1cmF0aW9uIGFyZSBrZXB0IFxuICB3aXRoaW4gdGhlIGxhbWJkYSBwYWNrYWdlICAvbGFtYmRhcy9hcHBzeW5jLWdsb2JhbGFwaS1yb3V0ZXIvY29uZmlncy5qc29uLiBUaGlzIGlzIHRoZSBiZWNhdXNlIExhbWJkYUBlZGdlIGRvZXMgbm90IHN1cHBvcnQgZW52LiB2YXJpYWJsZXNcbiAgYW5kIGdpdmVuIHRoZXNlIHZhbHVlIGRvbid0IGNoYW5nZSBmcmVxdWVudGx5LCBpdCBtYWtlcyBzZW5zZSB0byBwbGFjZSB0aGVtIHdpdGhpbiB0aGUgbGFtYmRhIGZ1bmN0aW9uIHZzIG1ha2luZyBhIG5ldHdvcmsgY2FsbCB3aGljaCBcbiAgd2lsbCBpbnRyb2R1Y2UgbGF0ZW5jeSAqL1xuICBcbiAgZ3JhcGhxbEFQSURvbWFpbk5hbWVDZXJ0QVJOOiBnbG9iYWxWYXJpYWJsZXMuZG9tYWluQ2VydEFSTixcbiAgdG9kb0dsb2JhbEFQSURvbWFpbk5hbWU6IGdsb2JhbFZhcmlhYmxlcy5nbG9iYWxBUElFbmRwb2ludCxcbiAgcmVnaW9uTGF0ZW5jeVJvdXRpbmdETlM6IGdsb2JhbFZhcmlhYmxlcy5yb3V0ZTUzUm91dGluZ1BvbGljeURvbWFpbk5hbWUsXG4gIHBsYWNlaG9sZGVyQ0ZPcmlnaW46ICdhd3MuYW1hem9uLmNvbScsIC8vIFRoaXMgcGFyYW1ldGVyIGlzIG9ubHkgcmVxdWlyZWQgYmVjYXVzZSB3ZSBuZWVkIHRvIHNwZWNpZnkgYW4gb3JpZ2luIHNvIHlvdSBjYW4gdXNlIGFueSBkb21haW4gbmFtZSBvZiB5b3VyIGNob2ljZS4gTGFtYmRhQEVkZ2Ugd2lsbCBhbnl3YXkgcm91dGUgdGhlIHRyYWZmaWMgdG8gQXBwU3luYyBhcyB0aGUgb3JpZ2luXG4gIHRvZG9BUElIb3N0ZWRab25lSUQ6IGdsb2JhbFZhcmlhYmxlcy5yb3V0ZTUzSG9zdGVkWm9uZUlELFxuICB0b2RvQVBJSG9zdGVkWm9uZU5hbWU6IGdsb2JhbFZhcmlhYmxlcy5yb3V0ZTUzSG9zdGVkWm9uZU5hbWUsXG4gIHByaW1hcnlBcHBTeW5jQVBJUmVnaW9uTmFtZTogZ2xvYmFsVmFyaWFibGVzLnByaW1hcnlSZWdpb24sXG4gIHNlY29uZGFyeUFwcFN5bmNBUElSZWdpb25OYW1lOiBnbG9iYWxWYXJpYWJsZXMuc2Vjb25kYXJ5UmVnaW9uLFxuXG59KTsiXX0= -------------------------------------------------------------------------------- /appsync-multi-region-api/bin/appsync-multi-region-active-active.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { AppsyncMultiRegionActiveActiveStack } from '../lib/appsync-multi-region-active-active-stack'; 5 | import { SecondaryAppsyncMultiRegionActiveActiveStack } from '../lib/secondary-appsync-multi-region-active-active-stack'; 6 | import { AppsyncMultiRegionActiveActiveRoutingStack } from '../lib/appsync-multi-region-active-active-routing-stack'; 7 | import { globalVariables } from '../parameters/globalVariables'; 8 | 9 | const app = new cdk.App(); 10 | 11 | new AppsyncMultiRegionActiveActiveStack(app, 'AppsyncMultiRegionActiveActiveStack', { 12 | env: {region: globalVariables.primaryRegion }, 13 | primaryRegion: globalVariables.primaryRegion, 14 | secondaryRegion: globalVariables.secondaryRegion, 15 | appSyncCustomDomain: globalVariables.primaryRegionAppSyncCustomDomain, 16 | graphqlAPIDomainNameCertARN: globalVariables.domainCertARN, 17 | todoAPIHostedZoneID: globalVariables.route53HostedZoneID, 18 | todoAPIHostedZoneName: globalVariables.route53HostedZoneName, 19 | 20 | }); 21 | 22 | new SecondaryAppsyncMultiRegionActiveActiveStack(app, 'SecondaryAppsyncMultiRegionActiveActiveStack', { 23 | env: {region: globalVariables.secondaryRegion }, 24 | primaryRegion: globalVariables.primaryRegion, 25 | secondaryRegion: globalVariables.secondaryRegion, 26 | appSyncCustomDomain: globalVariables.secondaryRegionAppSyncCustomDomain, 27 | graphqlAPIDomainNameCertARN: globalVariables.domainCertARN, 28 | todoAPIHostedZoneID: globalVariables.route53HostedZoneID, 29 | todoAPIHostedZoneName: globalVariables.route53HostedZoneName 30 | }); 31 | 32 | 33 | new AppsyncMultiRegionActiveActiveRoutingStack(app, 'AppsyncMultiRegionActiveActiveRoutingStack', { 34 | env: {region: 'us-east-1' }, 35 | 36 | /* Params below are required to configure the CF distribution however the params for Lambda@edge configuration are kept 37 | within the lambda package /lambdas/appsync-globalapi-router/configs.json. This is the because Lambda@edge does not support env. variables 38 | and given these value don't change frequently, it makes sense to place them within the lambda function vs making a network call which 39 | will introduce latency */ 40 | 41 | graphqlAPIDomainNameCertARN: globalVariables.domainCertARN, 42 | todoGlobalAPIDomainName: globalVariables.globalAPIEndpoint, 43 | regionLatencyRoutingDNS: globalVariables.route53RoutingPolicyDomainName, 44 | placeholderCFOrigin: 'aws.amazon.com', // This parameter is only required because we need to specify an origin so you can use any domain name of your choice. Lambda@Edge will anyway route the traffic to AppSync as the origin 45 | todoAPIHostedZoneID: globalVariables.route53HostedZoneID, 46 | todoAPIHostedZoneName: globalVariables.route53HostedZoneName, 47 | primaryAppSyncAPIRegionName: globalVariables.primaryRegion, 48 | secondaryAppSyncAPIRegionName: globalVariables.secondaryRegion, 49 | 50 | }); -------------------------------------------------------------------------------- /appsync-multi-region-api/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/appsync-multi-region-active-active.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 21 | "@aws-cdk/core:stackRelativeExports": true, 22 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 23 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 24 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 25 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/core:checkSecretUsage": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 31 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 32 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 33 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 34 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 35 | "@aws-cdk/core:target-partitions": [ 36 | "aws", 37 | "aws-cn" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /appsync-multi-region-api/global_appsync_cleanup.sh: -------------------------------------------------------------------------------- 1 | # Deploy CDK stacks 2 | cdk destroy AppsyncMultiRegionActiveActiveRoutingStack 3 | cdk destroy SecondaryAppsyncMultiRegionActiveActiveStack 4 | cdk destroy AppsyncMultiRegionActiveActiveStack 5 | 6 | # Done 7 | echo "Stacks successfully destroyed" 8 | -------------------------------------------------------------------------------- /appsync-multi-region-api/global_appsync_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | 5 | # Install Lambda Dependencies 6 | cd lambdas/ddb-stream-processor/ 7 | npm install 8 | 9 | # Build CDK app 10 | cd ../../ 11 | npm run build 12 | 13 | # Install jq 14 | sudo yum install jq -y 15 | 16 | # Deploy CDK stacks 17 | cdk deploy AppsyncMultiRegionActiveActiveStack 18 | export SECONDARY_REGION=$(aws dynamodb describe-table --region $1 --table-name TodoGlobalTable | jq -r '.Table.Replicas[0].RegionName') 19 | export DYNAMODB_STREAM_ARN_IN_SECONDARY_REGION=$(aws dynamodbstreams list-streams --region $SECONDARY_REGION --table-name TodoGlobalTable | jq -r '.Streams[0].StreamArn' 20 | ) 21 | cdk deploy SecondaryAppsyncMultiRegionActiveActiveStack --parameters todoGlobalTableStreamARN=$DYNAMODB_STREAM_ARN_IN_SECONDARY_REGION 22 | cdk deploy AppsyncMultiRegionActiveActiveRoutingStack 23 | 24 | # Done 25 | echo "Stacks successfully deployed" 26 | -------------------------------------------------------------------------------- /appsync-multi-region-api/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /appsync-multi-region-api/lambdas/appsync-auth/index.js: -------------------------------------------------------------------------------- 1 | // Testing 2 | exports.handler = async (event) => { 3 | console.log(`event =`, JSON.stringify(event, null, 2)); 4 | const { authorizationToken } = event 5 | return { 6 | isAuthorized: authorizationToken === 'custom-authorized', 7 | resolverContext: {} 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /appsync-multi-region-api/lambdas/appsync-globalapi-router/configs.json: -------------------------------------------------------------------------------- 1 | { 2 | "route53RoutingPolicyDomainName": "lat-routing.codeforevery.com", 3 | "primaryRegion": "eu-west-1", 4 | "primaryRegionAppSyncCustomDomain": "primary.codeforevery.com", 5 | "secondaryRegion": "ap-southeast-1", 6 | "secondaryRegionAppSyncCustomDomain": "secondary.codeforevery.com" 7 | } 8 | -------------------------------------------------------------------------------- /appsync-multi-region-api/lambdas/appsync-globalapi-router/index.js: -------------------------------------------------------------------------------- 1 | const dns = require('dns'); 2 | const routingConfigs = require('./configs.json') 3 | 4 | let bestOrigin; 5 | let expires = 0; 6 | let TTL = 1; 7 | let DNS_HOST = routingConfigs.route53RoutingPolicyDomainName; 8 | 9 | function getBestRegion() { 10 | console.log("inside resolver"); 11 | const now = Date.now(); 12 | if (now < expires) 13 | return Promise.resolve(bestOrigin); 14 | return new Promise((resolve, reject) => { 15 | dns.resolveCname(DNS_HOST, (err, addr) => { 16 | bestOrigin = addr[0]; 17 | expires = now + TTL; 18 | resolve(bestOrigin); 19 | }); 20 | }); 21 | } 22 | 23 | let regions = []; // use lowercase. 24 | 25 | regions[routingConfigs.primaryRegion] = { "Host": routingConfigs.primaryRegionAppSyncCustomDomain}; 26 | regions[routingConfigs.secondaryRegion] = { "Host": routingConfigs.secondaryRegionAppSyncCustomDomain}; 27 | 28 | function getRegionalSettings(bestRegion){ 29 | return regions[bestRegion]; 30 | } 31 | 32 | exports.handler = async (event, context, callback) => { 33 | const request = event.Records[0].cf.request; 34 | console.log("request-before: "+JSON.stringify(request)); 35 | 36 | let bestRegion = await getBestRegion(); 37 | console.log("best region: "+bestOrigin); 38 | 39 | let map = getRegionalSettings(bestRegion); 40 | console.log("regional settings: "+JSON.stringify(map)); 41 | 42 | let target_domain = map["Host"]; 43 | 44 | console.log("request origin: "+JSON.stringify(request.origin)); 45 | 46 | // Forward GraphQL subscription requests for WebSockets. 47 | request.origin.custom.domainName = target_domain; 48 | 49 | // Forward REST and GraphQL query/mutation requests. 50 | request.headers["host"] = [{ 51 | key: "host", 52 | value: target_domain 53 | }]; 54 | console.log("request-after: "+JSON.stringify(request)); 55 | 56 | console.log(` Request headers set to "${request.headers}"`); 57 | callback(null, request); 58 | }; -------------------------------------------------------------------------------- /appsync-multi-region-api/lambdas/ddb-stream-processor/index.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const gql = require('graphql-tag'); 3 | const graphql = require('graphql'); 4 | const { print } = graphql; 5 | const util = require('util'); 6 | 7 | const addTodoGlobalSync = gql` 8 | mutation AddTodoGlobalSyncMutation($id: ID!, $name: String, $description: String, $priority: Int, $status: TodoStatus) { 9 | addTodoGlobalSync(input: {id: $id, name: $name, description: $description, priority: $priority, status: $status}) { 10 | id 11 | name 12 | description 13 | priority 14 | status 15 | } 16 | } 17 | ` 18 | 19 | const executeMutation = async(id, name, description, priority, status) => { 20 | console.info("Executing Mutation") 21 | const mutation = { 22 | query: print(addTodoGlobalSync), 23 | variables: { 24 | name: name, 25 | id: id, 26 | description: description, 27 | priority: parseInt(priority), 28 | status: status 29 | }, 30 | }; 31 | 32 | try { 33 | console.info("Attempting Axios") 34 | let response = await axios({ 35 | url: process.env.AppSyncAPIEndpoint, 36 | method: 'post', 37 | headers: { 38 | 'Authorization': process.env.AppSyncAPILambdaAuthKey 39 | }, 40 | data: JSON.stringify(mutation) 41 | }); 42 | console.info("Logging Response"); 43 | console.log(response); 44 | } catch (error) { 45 | console.info("Error caught") 46 | throw error; 47 | } 48 | }; 49 | 50 | exports.handler = async(event) => { 51 | for (let record of event.Records) { 52 | console.info("Printing record") 53 | console.info(JSON.stringify(record)) 54 | switch (record.eventName) { 55 | case 'INSERT': 56 | // Grab the data we need from stream... 57 | let id = record.dynamodb.Keys.id.S; 58 | let name = record.dynamodb.NewImage.name.S; 59 | let description = record.dynamodb.NewImage.description.S; 60 | let priority = record.dynamodb.NewImage.priority.N; 61 | let status = record.dynamodb.NewImage.status.S; 62 | // ... and then execute the publish mutation 63 | await executeMutation(id, name, description, priority, status); 64 | break; 65 | case 'UPDATE': 66 | break; 67 | case 'DELETE': 68 | break; 69 | default: 70 | break; 71 | } 72 | } 73 | return { message: `Finished processing ${event.Records.length} records` } 74 | } -------------------------------------------------------------------------------- /appsync-multi-region-api/lambdas/ddb-stream-processor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-appsync-external-mutation", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "author": "Ozioma Uzoegwu", 7 | "license": "MIT-0", 8 | "dependencies": { 9 | "aws-sdk": "^2.1354.0", 10 | "axios": ">=0.21.2", 11 | "cross-fetch": "^3.0.4", 12 | "graphql": "^15.5.0", 13 | "graphql-tag": "^2.10.1" 14 | } 15 | } -------------------------------------------------------------------------------- /appsync-multi-region-api/lib/appsync-multi-region-active-active-routing-stack.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.AppsyncMultiRegionActiveActiveRoutingStack = void 0; 4 | const cdk = require("aws-cdk-lib"); 5 | const cloudfront = require("aws-cdk-lib/aws-cloudfront"); 6 | const origins = require("aws-cdk-lib/aws-cloudfront-origins"); 7 | const lambda = require("aws-cdk-lib/aws-lambda"); 8 | const certificatemaanger = require("aws-cdk-lib/aws-certificatemanager"); 9 | const route53 = require("aws-cdk-lib/aws-route53"); 10 | const path = require("path"); 11 | class AppsyncMultiRegionActiveActiveRoutingStack extends cdk.Stack { 12 | constructor(scope, id, props) { 13 | super(scope, id, props); 14 | const currentGraphqlAPICertARN = (props === null || props === void 0 ? void 0 : props.graphqlAPIDomainNameCertARN) ? props.graphqlAPIDomainNameCertARN : ''; 15 | const graphqlAPIDomainNameCert = certificatemaanger.Certificate.fromCertificateArn(this, 'GraphqlAPIDomainNameCert', currentGraphqlAPICertARN); 16 | const currentTodoGlobalAPIDomainName = (props === null || props === void 0 ? void 0 : props.todoGlobalAPIDomainName) ? props.todoGlobalAPIDomainName : ''; 17 | const currentPlaceholderCFOrigin = (props === null || props === void 0 ? void 0 : props.placeholderCFOrigin) ? props.placeholderCFOrigin : ''; 18 | const currentTodoAPIHostedZoneID = (props === null || props === void 0 ? void 0 : props.todoAPIHostedZoneID) ? props.todoAPIHostedZoneID : ''; 19 | const currentTodoAPIHostedZoneName = (props === null || props === void 0 ? void 0 : props.todoAPIHostedZoneName) ? props.todoAPIHostedZoneName : ''; 20 | const currentRegionLatencyRoutingDNS = (props === null || props === void 0 ? void 0 : props.regionLatencyRoutingDNS) ? props.regionLatencyRoutingDNS : ''; 21 | const currentPrimaryAppSyncAPIRegionName = (props === null || props === void 0 ? void 0 : props.primaryAppSyncAPIRegionName) ? props.primaryAppSyncAPIRegionName : ''; 22 | const currentSecondaryAppSyncAPIRegionName = (props === null || props === void 0 ? void 0 : props.secondaryAppSyncAPIRegionName) ? props.secondaryAppSyncAPIRegionName : ''; 23 | /** LAMBDA @ EDGE FUNCTION FOR THE CLOUDFRONT DISTRIBUTION ROUTING */ 24 | const routingLambda = new lambda.Function(this, 'TodoAPIRoutingLambda', { 25 | runtime: lambda.Runtime.NODEJS_16_X, 26 | handler: 'index.handler', 27 | code: lambda.Code.fromAsset(path.join(path.resolve('./'), '/lambdas/appsync-globalapi-router')), 28 | tracing: lambda.Tracing.ACTIVE 29 | }); 30 | /** CLOUDFRONT DISTRIBUTION FOR THE GLOBAL API ENDPOINT */ 31 | const todoAPICFDist = new cloudfront.Distribution(this, 'TodoAPICFDist', { 32 | defaultBehavior: { 33 | origin: new origins.HttpOrigin(currentPlaceholderCFOrigin), 34 | allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, 35 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.ALLOW_ALL, 36 | compress: true, 37 | smoothStreaming: false, 38 | cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, 39 | originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER, 40 | edgeLambdas: [ 41 | { 42 | functionVersion: routingLambda.currentVersion, 43 | eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, 44 | includeBody: true, 45 | } 46 | ] 47 | }, 48 | certificate: graphqlAPIDomainNameCert, 49 | domainNames: [currentTodoGlobalAPIDomainName] 50 | }); 51 | /** ROUTE 53 CONFIGS FOR THE GLOBAL API ENDPOINT AND APPSYNC CUSTOM DOMAINS */ 52 | const todoAPIDomainNameHostedZone = route53.HostedZone.fromHostedZoneAttributes(this, 'TodoAPIDomainNameHostedZone', { 53 | hostedZoneId: currentTodoAPIHostedZoneID, 54 | zoneName: currentTodoAPIHostedZoneName 55 | }); 56 | const todoGlobalAPIDNSConfig = new route53.CnameRecord(this, 'TodoGlobalAPIDNSConfig', { 57 | recordName: currentTodoGlobalAPIDomainName.split('.')[0], 58 | zone: todoAPIDomainNameHostedZone, 59 | domainName: todoAPICFDist.domainName 60 | }); 61 | /** CREATE LATENCY RECORD SETS */ 62 | const primaryRegionLatencyRoutingConfig = new route53.CfnRecordSet(this, 'PrimaryRegionLatencyRoutingConfig', { 63 | hostedZoneId: currentTodoAPIHostedZoneID, 64 | name: currentRegionLatencyRoutingDNS, 65 | type: route53.RecordType.CNAME, 66 | region: currentPrimaryAppSyncAPIRegionName, 67 | setIdentifier: '1', 68 | resourceRecords: [currentPrimaryAppSyncAPIRegionName], 69 | ttl: '300' 70 | }); 71 | const secondaryRegionLatencyRoutingConfig = new route53.CfnRecordSet(this, 'SecondaryRegionLatencyRoutingConfig', { 72 | hostedZoneId: currentTodoAPIHostedZoneID, 73 | name: currentRegionLatencyRoutingDNS, 74 | type: route53.RecordType.CNAME, 75 | region: currentSecondaryAppSyncAPIRegionName, 76 | setIdentifier: '2', 77 | resourceRecords: [currentSecondaryAppSyncAPIRegionName], 78 | ttl: '300' 79 | }); 80 | } 81 | } 82 | exports.AppsyncMultiRegionActiveActiveRoutingStack = AppsyncMultiRegionActiveActiveRoutingStack; 83 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBwc3luYy1tdWx0aS1yZWdpb24tYWN0aXZlLWFjdGl2ZS1yb3V0aW5nLXN0YWNrLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiYXBwc3luYy1tdWx0aS1yZWdpb24tYWN0aXZlLWFjdGl2ZS1yb3V0aW5nLXN0YWNrLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLG1DQUFtQztBQUVuQyx5REFBeUQ7QUFDekQsOERBQThEO0FBQzlELGlEQUFpRDtBQUNqRCx5RUFBeUU7QUFDekUsbURBQW1EO0FBQ25ELDZCQUE0QjtBQVk1QixNQUFhLDBDQUEyQyxTQUFRLEdBQUcsQ0FBQyxLQUFLO0lBQ3JFLFlBQVksS0FBZ0IsRUFBRSxFQUFVLEVBQUUsS0FBdUQ7UUFDL0YsS0FBSyxDQUFDLEtBQUssRUFBRSxFQUFFLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFFeEIsTUFBTSx3QkFBd0IsR0FBRyxDQUFBLEtBQUssYUFBTCxLQUFLLHVCQUFMLEtBQUssQ0FBRSwyQkFBMkIsRUFBQSxDQUFDLENBQUMsS0FBSyxDQUFDLDJCQUEyQixDQUFBLENBQUMsQ0FBQyxFQUFFLENBQUE7UUFDMUcsTUFBTSx3QkFBd0IsR0FBRyxrQkFBa0IsQ0FBQyxXQUFXLENBQUMsa0JBQWtCLENBQUMsSUFBSSxFQUFDLDBCQUEwQixFQUFFLHdCQUF3QixDQUFDLENBQUE7UUFDN0ksTUFBTSw4QkFBOEIsR0FBRyxDQUFBLEtBQUssYUFBTCxLQUFLLHVCQUFMLEtBQUssQ0FBRSx1QkFBdUIsRUFBQSxDQUFDLENBQUMsS0FBSyxDQUFDLHVCQUF1QixDQUFBLENBQUMsQ0FBQyxFQUFFLENBQUE7UUFDeEcsTUFBTSwwQkFBMEIsR0FBRyxDQUFBLEtBQUssYUFBTCxLQUFLLHVCQUFMLEtBQUssQ0FBRSxtQkFBbUIsRUFBQSxDQUFDLENBQUMsS0FBSyxDQUFDLG1CQUFtQixDQUFBLENBQUMsQ0FBQyxFQUFFLENBQUE7UUFDNUYsTUFBTSwwQkFBMEIsR0FBRyxDQUFBLEtBQUssYUFB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LENBQUMsVUFBVTtTQUNyQyxDQUFDLENBQUE7UUFFRixpQ0FBaUM7UUFDakMsTUFBTSxpQ0FBaUMsR0FBRyxJQUFJLE9BQU8sQ0FBQyxZQUFZLENBQUMsSUFBSSxFQUFFLG1DQUFtQyxFQUFDO1lBQ3pHLFlBQVksRUFBRSwwQkFBMEI7WUFDeEMsSUFBSSxFQUFFLDhCQUE4QjtZQUNwQyxJQUFJLEVBQUUsT0FBTyxDQUFDLFVBQVUsQ0FBQyxLQUFLO1lBQzlCLE1BQU0sRUFBRSxrQ0FBa0M7WUFDMUMsYUFBYSxFQUFFLEdBQUc7WUFDbEIsZUFBZSxFQUFFLENBQUMsa0NBQWtDLENBQUM7WUFDckQsR0FBRyxFQUFFLEtBQUs7U0FDYixDQUFDLENBQUE7UUFFRixNQUFNLG1DQUFtQyxHQUFHLElBQUksT0FBTyxDQUFDLFlBQVksQ0FBQyxJQUFJLEVBQUUscUNBQXFDLEVBQUM7WUFDL0csWUFBWSxFQUFFLDBCQUEwQjtZQUN4QyxJQUFJLEVBQUUsOEJBQThCO1lBQ3BDLElBQUksRUFBRSxPQUFPLENBQUMsVUFBVSxDQUFDLEtBQUs7WUFDOUIsTUFBTSxFQUFFLG9DQUFvQztZQUM1QyxhQUFhLEVBQUUsR0FBRztZQUNsQixlQUFlLEVBQUUsQ0FBQyxvQ0FBb0MsQ0FBQztZQUN2RCxHQUFHLEVBQUUsS0FBSztTQUNYLENBQUMsQ0FBQTtJQUNKLENBQUM7Q0FDSjtBQTdFRCxnR0E2RUMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBjZGsgZnJvbSAnYXdzLWNkay1saWInO1xuaW1wb3J0IHsgQ29uc3RydWN0IH0gZnJvbSAnY29uc3RydWN0cyc7XG5pbXBvcnQgKiBhcyBjbG91ZGZyb250IGZyb20gJ2F3cy1jZGstbGliL2F3cy1jbG91ZGZyb250JztcbmltcG9ydCAqIGFzIG9yaWdpbnMgZnJvbSAnYXdzLWNkay1saWIvYXdzLWNsb3VkZnJvbnQtb3JpZ2lucyc7XG5pbXBvcnQgKiBhcyBsYW1iZGEgZnJvbSAnYXdzLWNkay1saWIvYXdzLWxhbWJkYSc7XG5pbXBvcnQgKiBhcyBjZXJ0aWZpY2F0ZW1hYW5nZXIgZnJvbSAnYXdzLWNkay1saWIvYXdzLWNlcnRpZmljYXRlbWFuYWdlcic7XG5pbXBvcnQgKiBhcyByb3V0ZTUzIGZyb20gJ2F3cy1jZGstbGliL2F3cy1yb3V0ZTUzJztcbmltcG9ydCAqIGFzIHBhdGggZnJvbSAncGF0aCdcblxuaW50ZXJmYWNlIEFwcFN5bmNNdWx0aVJlZ2lvbkFjdGl2ZUFjdGl2ZVJvdXRpbmdTdGFja1Byb3BzIGV4dGVuZHMgY2RrLlN0YWNrUHJvcHMge1xuICAgIGdyYXBocWxBUElEb21haW5OYW1lQ2VydEFSTj86IHN0cmluZyxcbiAgICB0b2RvR2xvYmFsQVBJRG9tYWluTmFtZT86IHN0cmluZyxcbiAgICBwbGFjZWhvbGRlckNGT3JpZ2luOiBzdHJpbmcsXG4gICAgdG9kb0FQSUhvc3RlZFpvbmVJRD86IHN0cmluZyxcbiAgICB0b2RvQVBJSG9zdGVkWm9uZU5hbWU/OiBzdHJpbmcsXG4gICAgcmVnaW9uTGF0ZW5jeVJvdXRpbmdETlM/OiBzdHJpbmcsXG4gICAgcHJpbWFyeUFwcFN5bmNBUElSZWdpb25OYW1lPzogc3RyaW5nLFxuICAgIHNlY29uZGFyeUFwcFN5bmNBUElSZWdpb25OYW1lPzogc3RyaW5nLFxufVxuZXhwb3J0IGNsYXNzIEFwcHN5bmNNdWx0aVJlZ2lvbkFjdGl2ZUFjdGl2ZVJvdXRpbmdTdGFjayBleHRlbmRzIGNkay5TdGFjayB7XG4gICAgY29uc3RydWN0b3Ioc2NvcGU6IENvbnN0cnVjdCwgaWQ6IHN0cmluZywgcHJvcHM/OiBBcHBTeW5jTXVsdGlSZWdpb25BY3RpdmVBY3RpdmVSb3V0aW5nU3RhY2tQcm9wcykge1xuICAgICAgc3VwZXIoc2NvcGUsIGlkLCBwcm9wcyk7XG5cbiAgICAgIGNvbnN0IGN1cnJlbnRHcmFwaHFsQVBJQ2VydEFSTiA9IHByb3BzPy5ncmFwaHFsQVBJRG9tYWluTmFtZUNlcnRBUk4/IHByb3BzLmdyYXBocWxBUElEb21haW5OYW1lQ2VydEFSTjogJydcbiAgICAgIGNvbnN0IGdyYXBocWxBUElEb21haW5OYW1lQ2VydCA9IGNlcnRpZmljYXRlbWFhbmdlci5DZXJ0aWZpY2F0ZS5mcm9tQ2VydGlmaWNhdGVBcm4odGhpcywnR3JhcGhxbEFQSURvbWFpbk5hbWVDZXJ0JywgY3VycmVudEdyYXBocWxBUElDZXJ0QVJOKVxuICAgICAgY29uc3QgY3VycmVudFRvZG9HbG9iYWxBUElEb21haW5OYW1lID0gcHJvcHM/LnRvZG9HbG9iYWxBUElEb21haW5OYW1lPyBwcm9wcy50b2RvR2xvYmFsQVBJRG9tYWluTmFtZTogJydcbiAgICAgIGNvbnN0IGN1cnJlbnRQbGFjZWhvbGRlckNGT3JpZ2luID0gcHJvcHM/LnBsYWNlaG9sZGVyQ0ZPcmlnaW4/IHByb3BzLnBsYWNlaG9sZGVyQ0ZPcmlnaW46ICcnXG4gICAgICBjb25zdCBjdXJyZW50VG9kb0FQSUhvc3RlZFpvbmVJRCA9IHByb3BzPy50b2RvQVBJSG9zdGVkWm9uZUlEPyBwcm9wcy50b2RvQVBJSG9zdGVkWm9uZUlEOicnXG4gICAgICBjb25zdCBjdXJyZW50VG9kb0FQSUhvc3RlZFpvbmVOYW1lID0gcHJvcHM/LnRvZG9BUElIb3N0ZWRab25lTmFtZT8gcHJvcHMudG9kb0FQSUhvc3RlZFpvbmVOYW1lOicnXG4gICAgICBjb25zdCBjdXJyZW50UmVnaW9uTGF0ZW5jeVJvdXRpbmdETlMgPSBwcm9wcz8ucmVnaW9uTGF0ZW5jeVJvdXRpbmdETlM/IHByb3BzLnJlZ2lvbkxhdGVuY3lSb3V0aW5nRE5TOicnXG4gICAgICBjb25zdCBjdXJyZW50UHJpbWFyeUFwcFN5bmNBUElSZWdpb25OYW1lID0gcHJvcHM/LnByaW1hcnlBcHBTeW5jQVBJUmVnaW9uTmFtZT8gcHJvcHMucHJpbWFyeUFwcFN5bmNBUElSZWdpb25OYW1lOicnXG4gICAgICBjb25zdCBjdXJyZW50U2Vjb25kYXJ5QXBwU3luY0FQSVJlZ2lvbk5hbWUgPSBwcm9wcz8uc2Vjb25kYXJ5QXBwU3luY0FQSVJlZ2lvbk5hbWU/IHByb3BzLnNlY29uZGFyeUFwcFN5bmNBUElSZWdpb25OYW1lOicnXG4gICAgICBcbiAgICAgIC8qKiBMQU1CREEgQCBFREdFIEZVTkNUSU9OIEZPUiBUSEUgQ0xPVURGUk9OVCBESVNUUklCVVRJT04gUk9VVElORyAqL1xuICAgICAgY29uc3Qgcm91dGluZ0xhbWJkYSA9IG5ldyBsYW1iZGEuRnVuY3Rpb24odGhpcywgJ1RvZG9BUElSb3V0aW5nTGFtYmRhJyx7XG4gICAgICAgIHJ1bnRpbWU6IGxhbWJkYS5SdW50aW1lLk5PREVKU18xNl9YLFxuICAgICAgICBoYW5kbGVyOiAnaW5kZXguaGFuZGxlcicsXG4gICAgICAgIGNvZGU6IGxhbWJkYS5Db2RlLmZyb21Bc3NldChwYXRoLmpvaW4ocGF0aC5yZXNvbHZlKCcuLycpLCcvbGFtYmRhcy9hcHBzeW5jLWdsb2JhbGFwaS1yb3V0ZXInKSksXG4gICAgICAgIHRyYWNpbmc6IGxhbWJkYS5UcmFjaW5nLkFDVElWRVxuICAgICAgfSk7XG5cbiAgICAgIC8qKiBDTE9VREZST05UIERJU1RSSUJVVElPTiBGT1IgVEhFIEdMT0JBTCBBUEkgRU5EUE9JTlQgKi9cbiAgICAgIGNvbnN0IHRvZG9BUElDRkRpc3QgPSBuZXcgY2xvdWRmcm9udC5EaXN0cmlidXRpb24odGhpcywnVG9kb0FQSUNGRGlzdCcse1xuICAgICAgICBkZWZhdWx0QmVoYXZpb3I6IHtcbiAgICAgICAgICAgIG9yaWdpbjogbmV3IG9yaWdpbnMuSHR0cE9yaWdpbihjdXJyZW50UGxhY2Vob2xkZXJDRk9yaWdpbiksXG4gICAgICAgICAgICBhbGxvd2VkTWV0aG9kczogY2xvdWRmcm9udC5BbGxvd2VkTWV0aG9kcy5BTExPV19BTEwsXG4gICAgICAgICAgICB2aWV3ZXJQcm90b2NvbFBvbGljeTogY2xvdWRmcm9udC5WaWV3ZXJQcm90b2NvbFBvbGljeS5BTExPV19BTEwsXG4gICAgICAgICAgICBjb21wcmVzczogdHJ1ZSxcbiAgICAgICAgICAgIHNtb290aFN0cmVhbWluZzogZmFsc2UsXG4gICAgICAgICAgICBjYWNoZVBvbGljeTpjbG91ZGZyb250LkNhY2hlUG9saWN5LkNBQ0hJTkdfRElTQUJMRUQsXG4gICAgICAgICAgICBvcmlnaW5SZXF1ZXN0UG9saWN5OiBjbG91ZGZyb250Lk9yaWdpblJlcXVlc3RQb2xpY3kuQUxMX1ZJRVdFUixcbiAgICAgICAgICAgIGVkZ2VMYW1iZGFzOiBbXG4gICAgICAgICAgICAgICAge1xuICAgICAgICAgICAgICAgICAgICBmdW5jdGlvblZlcnNpb246IHJvdXRpbmdMYW1iZGEuY3VycmVudFZlcnNpb24sXG4gICAgICAgICAgICAgICAgICAgIGV2ZW50VHlwZTogY2xvdWRmcm9udC5MYW1iZGFFZGdlRXZlbnRUeXBlLk9SSUdJTl9SRVFVRVNULFxuICAgICAgICAgICAgICAgICAgICBpbmNsdWRlQm9keTogdHJ1ZSxcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICBdXG4gICAgICAgIH0sXG4gICAgICAgIGNlcnRpZmljYXRlOiBncmFwaHFsQVBJRG9tYWluTmFtZUNlcnQsXG4gICAgICAgIGRvbWFpbk5hbWVzOiBbY3VycmVudFRvZG9HbG9iYWxBUElEb21haW5OYW1lXVxuICAgICAgfSk7XG5cbiAgICAgIC8qKiBST1VURSA1MyBDT05GSUdTIEZPUiBUSEUgR0xPQkFMIEFQSSBFTkRQT0lOVCBBTkQgQVBQU1lOQyBDVVNUT00gRE9NQUlOUyAqL1xuICAgICAgY29uc3QgdG9kb0FQSURvbWFpbk5hbWVIb3N0ZWRab25lID0gcm91dGU1My5Ib3N0ZWRab25lLmZyb21Ib3N0ZWRab25lQXR0cmlidXRlcyh0aGlzLCAnVG9kb0FQSURvbWFpbk5hbWVIb3N0ZWRab25lJywge1xuICAgICAgICBob3N0ZWRab25lSWQ6IGN1cnJlbnRUb2RvQVBJSG9zdGVkWm9uZUlELFxuICAgICAgICB6b25lTmFtZTogY3VycmVudFRvZG9BUElIb3N0ZWRab25lTmFtZVxuICAgICAgfSk7XG5cbiAgICAgIGNvbnN0IHRvZG9HbG9iYWxBUElETlNDb25maWcgPSBuZXcgcm91dGU1My5DbmFtZVJlY29yZCh0aGlzLCAnVG9kb0dsb2JhbEFQSUROU0NvbmZpZycse1xuICAgICAgICByZWNvcmROYW1lOiBjdXJyZW50VG9kb0dsb2JhbEFQSURvbWFpbk5hbWUuc3BsaXQoJy4nKVswXSxcbiAgICAgICAgem9uZTogdG9kb0FQSURvbWFpbk5hbWVIb3N0ZWRab25lLFxuICAgICAgICBkb21haW5OYW1lOiB0b2RvQVBJQ0ZEaXN0LmRvbWFpbk5hbWVcbiAgICAgIH0pXG5cbiAgICAgIC8qKiBDUkVBVEUgTEFURU5DWSBSRUNPUkQgU0VUUyAqL1xuICAgICAgY29uc3QgcHJpbWFyeVJlZ2lvbkxhdGVuY3lSb3V0aW5nQ29uZmlnID0gbmV3IHJvdXRlNTMuQ2ZuUmVjb3JkU2V0KHRoaXMsICdQcmltYXJ5UmVnaW9uTGF0ZW5jeVJvdXRpbmdDb25maWcnLHtcbiAgICAgICAgICBob3N0ZWRab25lSWQ6IGN1cnJlbnRUb2RvQVBJSG9zdGVkWm9uZUlELFxuICAgICAgICAgIG5hbWU6IGN1cnJlbnRSZWdpb25MYXRlbmN5Um91dGluZ0ROUyxcbiAgICAgICAgICB0eXBlOiByb3V0ZTUzLlJlY29yZFR5cGUuQ05BTUUsXG4gICAgICAgICAgcmVnaW9uOiBjdXJyZW50UHJpbWFyeUFwcFN5bmNBUElSZWdpb25OYW1lLFxuICAgICAgICAgIHNldElkZW50aWZpZXI6ICcxJyxcbiAgICAgICAgICByZXNvdXJjZVJlY29yZHM6IFtjdXJyZW50UHJpbWFyeUFwcFN5bmNBUElSZWdpb25OYW1lXSxcbiAgICAgICAgICB0dGw6ICczMDAnXG4gICAgICB9KVxuXG4gICAgICBjb25zdCBzZWNvbmRhcnlSZWdpb25MYXRlbmN5Um91dGluZ0NvbmZpZyA9IG5ldyByb3V0ZTUzLkNmblJlY29yZFNldCh0aGlzLCAnU2Vjb25kYXJ5UmVnaW9uTGF0ZW5jeVJvdXRpbmdDb25maWcnLHtcbiAgICAgICAgaG9zdGVkWm9uZUlkOiBjdXJyZW50VG9kb0FQSUhvc3RlZFpvbmVJRCxcbiAgICAgICAgbmFtZTogY3VycmVudFJlZ2lvbkxhdGVuY3lSb3V0aW5nRE5TLFxuICAgICAgICB0eXBlOiByb3V0ZTUzLlJlY29yZFR5cGUuQ05BTUUsXG4gICAgICAgIHJlZ2lvbjogY3VycmVudFNlY29uZGFyeUFwcFN5bmNBUElSZWdpb25OYW1lLFxuICAgICAgICBzZXRJZGVudGlmaWVyOiAnMicsXG4gICAgICAgIHJlc291cmNlUmVjb3JkczogW2N1cnJlbnRTZWNvbmRhcnlBcHBTeW5jQVBJUmVnaW9uTmFtZV0sXG4gICAgICAgIHR0bDogJzMwMCdcbiAgICAgIH0pXG4gICAgfVxufSJdfQ== -------------------------------------------------------------------------------- /appsync-multi-region-api/lib/appsync-multi-region-active-active-routing-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; 4 | import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'; 5 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 6 | import * as certificatemaanger from 'aws-cdk-lib/aws-certificatemanager'; 7 | import * as route53 from 'aws-cdk-lib/aws-route53'; 8 | import * as path from 'path' 9 | 10 | interface AppSyncMultiRegionActiveActiveRoutingStackProps extends cdk.StackProps { 11 | graphqlAPIDomainNameCertARN?: string, 12 | todoGlobalAPIDomainName?: string, 13 | placeholderCFOrigin: string, 14 | todoAPIHostedZoneID?: string, 15 | todoAPIHostedZoneName?: string, 16 | regionLatencyRoutingDNS?: string, 17 | primaryAppSyncAPIRegionName?: string, 18 | secondaryAppSyncAPIRegionName?: string, 19 | } 20 | export class AppsyncMultiRegionActiveActiveRoutingStack extends cdk.Stack { 21 | constructor(scope: Construct, id: string, props?: AppSyncMultiRegionActiveActiveRoutingStackProps) { 22 | super(scope, id, props); 23 | 24 | const currentGraphqlAPICertARN = props?.graphqlAPIDomainNameCertARN? props.graphqlAPIDomainNameCertARN: '' 25 | const graphqlAPIDomainNameCert = certificatemaanger.Certificate.fromCertificateArn(this,'GraphqlAPIDomainNameCert', currentGraphqlAPICertARN) 26 | const currentTodoGlobalAPIDomainName = props?.todoGlobalAPIDomainName? props.todoGlobalAPIDomainName: '' 27 | const currentPlaceholderCFOrigin = props?.placeholderCFOrigin? props.placeholderCFOrigin: '' 28 | const currentTodoAPIHostedZoneID = props?.todoAPIHostedZoneID? props.todoAPIHostedZoneID:'' 29 | const currentTodoAPIHostedZoneName = props?.todoAPIHostedZoneName? props.todoAPIHostedZoneName:'' 30 | const currentRegionLatencyRoutingDNS = props?.regionLatencyRoutingDNS? props.regionLatencyRoutingDNS:'' 31 | const currentPrimaryAppSyncAPIRegionName = props?.primaryAppSyncAPIRegionName? props.primaryAppSyncAPIRegionName:'' 32 | const currentSecondaryAppSyncAPIRegionName = props?.secondaryAppSyncAPIRegionName? props.secondaryAppSyncAPIRegionName:'' 33 | 34 | /** LAMBDA @ EDGE FUNCTION FOR THE CLOUDFRONT DISTRIBUTION ROUTING */ 35 | const routingLambda = new lambda.Function(this, 'TodoAPIRoutingLambda',{ 36 | runtime: lambda.Runtime.NODEJS_16_X, 37 | handler: 'index.handler', 38 | code: lambda.Code.fromAsset(path.join(path.resolve('./'),'/lambdas/appsync-globalapi-router')), 39 | tracing: lambda.Tracing.ACTIVE 40 | }); 41 | 42 | /** CLOUDFRONT DISTRIBUTION FOR THE GLOBAL API ENDPOINT */ 43 | const todoAPICFDist = new cloudfront.Distribution(this,'TodoAPICFDist',{ 44 | defaultBehavior: { 45 | origin: new origins.HttpOrigin(currentPlaceholderCFOrigin), 46 | allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, 47 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.ALLOW_ALL, 48 | compress: true, 49 | smoothStreaming: false, 50 | cachePolicy:cloudfront.CachePolicy.CACHING_DISABLED, 51 | originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER, 52 | edgeLambdas: [ 53 | { 54 | functionVersion: routingLambda.currentVersion, 55 | eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, 56 | includeBody: true, 57 | } 58 | ] 59 | }, 60 | certificate: graphqlAPIDomainNameCert, 61 | domainNames: [currentTodoGlobalAPIDomainName] 62 | }); 63 | 64 | /** ROUTE 53 CONFIGS FOR THE GLOBAL API ENDPOINT AND APPSYNC CUSTOM DOMAINS */ 65 | const todoAPIDomainNameHostedZone = route53.HostedZone.fromHostedZoneAttributes(this, 'TodoAPIDomainNameHostedZone', { 66 | hostedZoneId: currentTodoAPIHostedZoneID, 67 | zoneName: currentTodoAPIHostedZoneName 68 | }); 69 | 70 | const todoGlobalAPIDNSConfig = new route53.CnameRecord(this, 'TodoGlobalAPIDNSConfig',{ 71 | recordName: currentTodoGlobalAPIDomainName.split('.')[0], 72 | zone: todoAPIDomainNameHostedZone, 73 | domainName: todoAPICFDist.domainName 74 | }) 75 | 76 | /** CREATE LATENCY RECORD SETS */ 77 | const primaryRegionLatencyRoutingConfig = new route53.CfnRecordSet(this, 'PrimaryRegionLatencyRoutingConfig',{ 78 | hostedZoneId: currentTodoAPIHostedZoneID, 79 | name: currentRegionLatencyRoutingDNS, 80 | type: route53.RecordType.CNAME, 81 | region: currentPrimaryAppSyncAPIRegionName, 82 | setIdentifier: '1', 83 | resourceRecords: [currentPrimaryAppSyncAPIRegionName], 84 | ttl: '300' 85 | }) 86 | 87 | const secondaryRegionLatencyRoutingConfig = new route53.CfnRecordSet(this, 'SecondaryRegionLatencyRoutingConfig',{ 88 | hostedZoneId: currentTodoAPIHostedZoneID, 89 | name: currentRegionLatencyRoutingDNS, 90 | type: route53.RecordType.CNAME, 91 | region: currentSecondaryAppSyncAPIRegionName, 92 | setIdentifier: '2', 93 | resourceRecords: [currentSecondaryAppSyncAPIRegionName], 94 | ttl: '300' 95 | }) 96 | } 97 | } -------------------------------------------------------------------------------- /appsync-multi-region-api/lib/appsync-multi-region-active-active-stack.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.AppsyncMultiRegionActiveActiveStack = void 0; 4 | const cdk = require("aws-cdk-lib"); 5 | const lambda = require("aws-cdk-lib/aws-lambda"); 6 | const aws_lambda_event_sources_1 = require("aws-cdk-lib/aws-lambda-event-sources"); 7 | const dynamodb = require("aws-cdk-lib/aws-dynamodb"); 8 | const iam = require("aws-cdk-lib/aws-iam"); 9 | const appsync = require("@aws-cdk/aws-appsync-alpha"); 10 | const certificatemaanger = require("aws-cdk-lib/aws-certificatemanager"); 11 | const route53 = require("aws-cdk-lib/aws-route53"); 12 | const path = require("path"); 13 | class AppsyncMultiRegionActiveActiveStack extends cdk.Stack { 14 | constructor(scope, id, props) { 15 | super(scope, id, props); 16 | const currentSecondaryRegion = (props === null || props === void 0 ? void 0 : props.secondaryRegion) ? props.secondaryRegion : ''; 17 | const currentGraphqlAPICertARN = (props === null || props === void 0 ? void 0 : props.graphqlAPIDomainNameCertARN) ? props.graphqlAPIDomainNameCertARN : ''; 18 | const currentAppSyncCustomDomain = (props === null || props === void 0 ? void 0 : props.appSyncCustomDomain) ? props.appSyncCustomDomain : ''; 19 | const currentTodoAPIHostedZoneID = (props === null || props === void 0 ? void 0 : props.todoAPIHostedZoneID) ? props.todoAPIHostedZoneID : ''; 20 | const currentTodoAPIHostedZoneName = (props === null || props === void 0 ? void 0 : props.todoAPIHostedZoneName) ? props.todoAPIHostedZoneName : ''; 21 | /** DYNAMODB GLOBAL TABLE */ 22 | const todoGlobalTable = new dynamodb.Table(this, 'TodoGlobalTable', { 23 | partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, 24 | replicationRegions: [currentSecondaryRegion], 25 | tableName: "TodoGlobalTable", 26 | stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES, 27 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 28 | encryption: dynamodb.TableEncryption.AWS_MANAGED 29 | }); 30 | /** APPSYNC LAMBDA AUTHORIZER */ 31 | const appSyncLambdaAuth = new lambda.Function(this, 'AppSyncLambdaAuth', { 32 | runtime: lambda.Runtime.NODEJS_16_X, 33 | handler: 'index.handler', 34 | code: lambda.Code.fromAsset(path.join(path.resolve('./'), '/lambdas/appsync-auth')), 35 | tracing: lambda.Tracing.ACTIVE 36 | }); 37 | /** APPSYNC LOG CONFIG */ 38 | const logConfig = { 39 | excludeVerboseContent: false, 40 | fieldLogLevel: appsync.FieldLogLevel.ALL, 41 | }; 42 | /** APPSYNC API */ 43 | const todoGraphQLAPI = new appsync.GraphqlApi(this, 'TodoGraphQLAPI', { 44 | name: 'TodoGraphQLAPI', 45 | schema: appsync.Schema.fromAsset(path.join(path.resolve('./'), '/appsync-api/schema.graphql')), 46 | authorizationConfig: { 47 | defaultAuthorization: { 48 | authorizationType: appsync.AuthorizationType.LAMBDA, 49 | lambdaAuthorizerConfig: { 50 | handler: appSyncLambdaAuth 51 | } 52 | } 53 | }, 54 | logConfig, 55 | xrayEnabled: true, 56 | }); 57 | const todoDynamoDBDataSource = todoGraphQLAPI.addDynamoDbDataSource('TodoDynamoDBDataSource', todoGlobalTable); 58 | const todoNoneDataSource = todoGraphQLAPI.addNoneDataSource('TodoNoneDataSource'); 59 | /** APPSYNC RESOLVERS */ 60 | todoDynamoDBDataSource.createResolver({ 61 | typeName: 'Mutation', 62 | fieldName: 'addTodo', 63 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.AddTodo.req.vtl'), 64 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.AddTodo.resp.vtl') 65 | }); 66 | todoDynamoDBDataSource.createResolver({ 67 | typeName: 'Mutation', 68 | fieldName: 'updateTodo', 69 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.UpdateTodo.req.vtl'), 70 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.UpdateTodo.resp.vtl') 71 | }); 72 | todoDynamoDBDataSource.createResolver({ 73 | typeName: 'Mutation', 74 | fieldName: 'deleteTodo', 75 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.DeleteTodo.req.vtl'), 76 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.DeleteTodo.resp.vtl') 77 | }); 78 | todoNoneDataSource.createResolver({ 79 | typeName: 'Mutation', 80 | fieldName: 'addTodoGlobalSync', 81 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.AddTodoGlobalSync.req.vtl'), 82 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.AddTodoGlobalSync.resp.vtl') 83 | }); 84 | todoNoneDataSource.createResolver({ 85 | typeName: 'Mutation', 86 | fieldName: 'deleteTodoGlobalSync', 87 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.DeleteTodoGlobalSync.req.vtl'), 88 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.DeleteTodoGlobalSync.resp.vtl') 89 | }); 90 | todoNoneDataSource.createResolver({ 91 | typeName: 'Mutation', 92 | fieldName: 'updateTodoGlobalSync', 93 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.UpdateTodoGlobalSync.req.vtl'), 94 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.UpdateTodoGlobalSync.resp.vtl') 95 | }); 96 | todoDynamoDBDataSource.createResolver({ 97 | typeName: 'Query', 98 | fieldName: 'getTodo', 99 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Query.GetTodo.req.vtl'), 100 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Query.GetTodo.resp.vtl') 101 | }); 102 | todoDynamoDBDataSource.createResolver({ 103 | typeName: 'Query', 104 | fieldName: 'listTodos', 105 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Query.ListTodos.req.vtl'), 106 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Query.ListTodos.resp.vtl') 107 | }); 108 | /** CONFIGURE CUSTOM DOMAIN */ 109 | const graphqlAPIDomainNameCert = certificatemaanger.Certificate.fromCertificateArn(this, 'GraphqlAPIDomainNameCert', currentGraphqlAPICertARN); 110 | const graphqlAPICustomDomain = new cdk.aws_appsync.CfnDomainName(this, 'GraphqlAPICustomDomain', { 111 | certificateArn: graphqlAPIDomainNameCert.certificateArn, 112 | domainName: currentAppSyncCustomDomain 113 | }); 114 | const appSyncCustomDomainAssoc = new cdk.aws_appsync.CfnDomainNameApiAssociation(this, 'AppSyncCustomDomainAssoc', { 115 | apiId: todoGraphQLAPI.apiId, 116 | domainName: graphqlAPICustomDomain.domainName 117 | }); 118 | appSyncCustomDomainAssoc.addDependsOn(graphqlAPICustomDomain); 119 | //Adding Route 53 Records for the custom domain 120 | const todoAPIDomainNameHostedZone = route53.HostedZone.fromHostedZoneAttributes(this, 'TodoAPIDomainNameHostedZone', { 121 | hostedZoneId: currentTodoAPIHostedZoneID, 122 | zoneName: currentTodoAPIHostedZoneName 123 | }); 124 | const appSyncDNSConfigs = new route53.CnameRecord(this, 'AppSyncDNSConfigs', { 125 | recordName: currentAppSyncCustomDomain.split('.')[0], 126 | zone: todoAPIDomainNameHostedZone, 127 | domainName: graphqlAPICustomDomain.attrAppSyncDomainName 128 | }); 129 | /** LAMBDA STREAM PROCESSOR EXECUTION ROLE */ 130 | const todoDDStreamLambdaExecRole = new iam.Role(this, 'TodoDDStreamLambdaExecRole', { 131 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), 132 | managedPolicies: [ 133 | iam.ManagedPolicy.fromAwsManagedPolicyName('AWSAppSyncInvokeFullAccess'), 134 | iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLogsFullAccess') 135 | ] 136 | }); 137 | /** LAMBDA STREAM PROCESSOR */ 138 | const todoDDStreamLambda = new lambda.Function(this, 'TodoDDStreamLambda', { 139 | runtime: lambda.Runtime.NODEJS_16_X, 140 | handler: 'index.handler', 141 | code: lambda.Code.fromAsset(path.join(path.resolve('./'), '/lambdas/ddb-stream-processor')), 142 | role: todoDDStreamLambdaExecRole, 143 | environment: { 144 | 'AppSyncAPIEndpoint': todoGraphQLAPI.graphqlUrl, 145 | 'AppSyncAPILambdaAuthKey': 'custom-authorized' 146 | }, 147 | tracing: lambda.Tracing.ACTIVE 148 | }); 149 | /** ADD DYNAMO DB STREAM AS EVENT SOURCE TO LAMBDA */ 150 | todoDDStreamLambda.addEventSource(new aws_lambda_event_sources_1.DynamoEventSource(todoGlobalTable, { 151 | startingPosition: lambda.StartingPosition.TRIM_HORIZON, 152 | })); 153 | /** OUTPUT STACK VALUES */ 154 | const todoTableStreamARN = (todoGlobalTable === null || todoGlobalTable === void 0 ? void 0 : todoGlobalTable.tableStreamArn) ? todoGlobalTable.tableStreamArn : ''; 155 | new cdk.CfnOutput(this, 'API URL', { value: todoGraphQLAPI.graphqlUrl }); 156 | new cdk.CfnOutput(this, 'TODO TABLE STREAM ARN', { value: todoTableStreamARN }); 157 | } 158 | } 159 | exports.AppsyncMultiRegionActiveActiveStack = AppsyncMultiRegionActiveActiveStack; 160 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBwc3luYy1tdWx0aS1yZWdpb24tYWN0aXZlLWFjdGl2ZS1zdGFjay5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImFwcHN5bmMtbXVsdGktcmVnaW9uLWFjdGl2ZS1hY3RpdmUtc3RhY2sudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsbUNBQW1DO0FBRW5DLGlEQUFpRDtBQUNqRCxtRkFBeUU7QUFDekUscURBQXFEO0FBQ3JELDJDQUEyQztBQUMzQyxzREFBc0Q7QUFDdEQseUVBQXlFO0FBQ3pFLG1EQUFtRDtBQUNuRCw2QkFBNkI7QUFXN0IsTUFBYSxtQ0FBb0MsU0FBUSxHQUFHLENBQUMsS0FBSztJQUNoRSxZQUFZLEtBQWdCLEVBQUUsRUFBVSxFQUFFLEtBQWdEO1FBQ3hGLEtBQUssQ0FBQyxLQUFLLEVBQUUsRUFBRSxFQUFFLEtBQUssQ0FBQyxDQUFDO1FBRXhCLE1BQU0sc0JBQXNCLEdBQUcsQ0FBQSxLQUFLLGFBQUwsS0FBSyx1QkFBTCxLQUFLLENBQUUsZUFBZSxFQUFBLENBQUMsQ0FBQyxLQUFLLENBQUMsZUFBZSxDQUFBLENBQUMsQ0FBQyxFQUFFLENBQUM7UUFDakYsTUFBTSx3QkFBd0IsR0FBRyxDQUFBLEtBQUssYUFBTCxLQUFLLHVCQUFMLEtBQUssQ0FBRSwyQkFBMkIsRUFBQSxDQUFDLENBQUMsS0FBSyxDQUFDLDJCQUEyQixDQUFBLENBQUMsQ0FBQyxFQUFFLENBQUE7UUFDMUcsTUFBTSwwQkFBMEIsR0FBRyxDQUFBLEtBQUssYUFBTCxLQUFLLHVCQUFMLEtBQUssQ0FBRSxtQkFBbUIsRUFBQSxDQUFDLENBQUMsS0FBSyxDQUFDLG1CQUFtQixDQUFBLENBQUMsQ0FBQSxFQUFFLENBQUE7UUFDM0YsTUFBTSwwQkFBMEIsR0FBRyxDQUFBLEtBQUssYUFBTCxLQUFLLHVCQUFMLEtBQUssQ0FBRSxtQkFBbUIsRUFBQSxDQUFDLENBQUMsS0FBSyxDQUFDLG1CQUFtQixDQUFBLENBQUMsQ0FBQSxFQUFFLENBQUE7UUFD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Q2hDLFdBQVcsRUFBRTtnQkFDWCxvQkFBb0IsRUFBRSxjQUFjLENBQUMsVUFBVTtnQkFDL0MseUJBQXlCLEVBQUUsbUJBQW1CO2FBQy9DO1lBQ0QsT0FBTyxFQUFFLE1BQU0sQ0FBQyxPQUFPLENBQUMsTUFBTTtTQUMvQixDQUFDLENBQUM7UUFFSCxzREFBc0Q7UUFDdEQsa0JBQWtCLENBQUMsY0FBYyxDQUFDLElBQUksNENBQWlCLENBQUMsZUFBZSxFQUFDO1lBQ3RFLGdCQUFnQixFQUFFLE1BQU0sQ0FBQyxnQkFBZ0IsQ0FBQyxZQUFZO1NBQ3ZELENBQUMsQ0FBQyxDQUFBO1FBRUgsMEJBQTBCO1FBQzFCLE1BQU0sa0JBQWtCLEdBQUcsQ0FBQSxlQUFlLGFBQWYsZUFBZSx1QkFBZixlQUFlLENBQUUsY0FBYyxFQUFBLENBQUMsQ0FBQyxlQUFlLENBQUMsY0FBYyxDQUFBLENBQUMsQ0FBQSxFQUFFLENBQUE7UUFFN0YsSUFBSSxHQUFHLENBQUMsU0FBUyxDQUFDLElBQUksRUFBQyxTQUFTLEVBQUUsRUFBQyxLQUFLLEVBQUUsY0FBYyxDQUFDLFVBQVUsRUFBQyxDQUFDLENBQUE7UUFDckUsSUFBSSxHQUFHLENBQUMsU0FBUyxDQUFDLElBQUksRUFBQyx1QkFBdUIsRUFBRSxFQUFDLEtBQUssRUFBQyxrQkFBa0IsRUFBQyxDQUFDLENBQUE7SUFDN0UsQ0FBQztDQUNGO0FBbkxELGtGQW1MQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCAqIGFzIGNkayBmcm9tICdhd3MtY2RrLWxpYic7XG5pbXBvcnQgeyBDb25zdHJ1Y3QgfSBmcm9tICdjb25zdHJ1Y3RzJztcbmltcG9ydCAqIGFzIGxhbWJkYSBmcm9tICdhd3MtY2RrLWxpYi9hd3MtbGFtYmRhJztcbmltcG9ydCB7IER5bmFtb0V2ZW50U291cmNlIH0gZnJvbSAnYXdzLWNkay1saWIvYXdzLWxhbWJkYS1ldmVudC1zb3VyY2VzJztcbmltcG9ydCAqIGFzIGR5bmFtb2RiIGZyb20gJ2F3cy1jZGstbGliL2F3cy1keW5hbW9kYic7XG5pbXBvcnQgKiBhcyBpYW0gZnJvbSAnYXdzLWNkay1saWIvYXdzLWlhbSc7XG5pbXBvcnQgKiBhcyBhcHBzeW5jIGZyb20gJ0Bhd3MtY2RrL2F3cy1hcHBzeW5jLWFscGhhJztcbmltcG9ydCAqIGFzIGNlcnRpZmljYXRlbWFhbmdlciBmcm9tICdhd3MtY2RrLWxpYi9hd3MtY2VydGlmaWNhdGVtYW5hZ2VyJztcbmltcG9ydCAqIGFzIHJvdXRlNTMgZnJvbSAnYXdzLWNkay1saWIvYXdzLXJvdXRlNTMnO1xuaW1wb3J0ICogYXMgcGF0aCBmcm9tICdwYXRoJztcblxuaW50ZXJmYWNlIEFwcFN5bmNNdWx0aVJlZ2lvbkFjdGl2ZUFjdGl2ZVN0YWNrUHJvcHMgZXh0ZW5kcyBjZGsuU3RhY2tQcm9wcyB7XG4gIHByaW1hcnlSZWdpb24/OiBzdHJpbmcsXG4gIHNlY29uZGFyeVJlZ2lvbj86IHN0cmluZyxcbiAgYXBwU3luY0N1c3RvbURvbWFpbj86IHN0cmluZyxcbiAgZ3JhcGhxbEFQSURvbWFpbk5hbWVDZXJ0QVJOPzogc3RyaW5nLFxuICB0b2RvQVBJSG9zdGVkWm9uZUlEPzogc3RyaW5nLFxuICB0b2RvQVBJSG9zdGVkWm9uZU5hbWU/OiBzdHJpbmdcbn1cblxuZXhwb3J0IGNsYXNzIEFwcHN5bmNNdWx0aVJlZ2lvbkFjdGl2ZUFjdGl2ZVN0YWNrIGV4dGVuZHMgY2RrLlN0YWNrIHtcbiAgY29uc3RydWN0b3Ioc2NvcGU6IENvbnN0cnVjdCwgaWQ6IHN0cmluZywgcHJvcHM/OiBBcHBTeW5jTXVsdGlSZWdpb25BY3RpdmVBY3RpdmVTdGFja1Byb3BzKSB7XG4gICAgc3VwZXIoc2NvcGUsIGlkLCBwcm9wcyk7XG4gICAgXG4gICAgY29uc3QgY3VycmVudFNlY29uZGFyeVJlZ2lvbiA9IHByb3BzPy5zZWNvbmRhcnlSZWdpb24/IHByb3BzLnNlY29uZGFyeVJlZ2lvbjogJyc7XG4gICAgY29uc3QgY3VycmVudEdyYXBocWxBUElDZXJ0QVJOID0gcHJvcHM/LmdyYXBocWxBUElEb21haW5OYW1lQ2VydEFSTj8gcHJvcHMuZ3JhcGhxbEFQSURvbWFpbk5hbWVDZXJ0QVJOOiAnJ1xuICAgIGNvbnN0IGN1cnJlbnRBcHBTeW5jQ3VzdG9tRG9tYWluID0gcHJvcHM/LmFwcFN5bmNDdXN0b21Eb21haW4/IHByb3BzLmFwcFN5bmNDdXN0b21Eb21haW46JydcbiAgICBjb25zdCBjdXJyZW50VG9kb0FQSUhvc3RlZFpvbmVJRCA9IHByb3BzPy50b2RvQVBJSG9zdGVkWm9uZUlEPyBwcm9wcy50b2RvQVBJSG9zdGVkWm9uZUlEOicnXG4gICAgY29uc3QgY3VycmVudFRvZG9BUElIb3N0ZWRab25lTmFtZSA9IHByb3BzPy50b2RvQVBJSG9zdGVkWm9uZU5hbWU/IHByb3BzLnRvZG9BUElIb3N0ZWRab25lTmFtZTonJ1xuXG5cbiAgICAvKiogRFlOQU1PREIgR0xPQkFMIFRBQkxFICovXG4gICAgY29uc3QgdG9kb0dsb2JhbFRhYmxlID0gbmV3IGR5bmFtb2RiLlRhYmxlKHRoaXMsICdUb2RvR2xvYmFsVGFibGUnLCB7XG4gICAgICBwYXJ0aXRpb25LZXk6IHsgbmFtZTogJ2lkJywgdHlwZTogZHluYW1vZGIuQXR0cmlidXRlVHlwZS5TVFJJTkcgfSxcbiAgICAgIHJlcGxpY2F0aW9uUmVnaW9uczogW2N1cnJlbnRTZWNvbmRhcnlSZWdpb25dLFxuICAgICAgdGFibGVOYW1lOiBcIlRvZG9HbG9iYWxUYWJsZVwiLFxuICAgICAgc3RyZWFtOiBkeW5hbW9kYi5TdHJlYW1WaWV3VHlwZS5ORVdfQU5EX09MRF9JTUFHRVMsXG4gICAgICBiaWxsaW5nTW9kZTogZHluYW1vZGIuQmlsbGluZ01vZGUuUEFZX1BFUl9SRVFVRVNULFxuICAgICAgZW5jcnlwdGlvbjogZHluYW1vZGIuVGFibGVFbmNyeXB0aW9uLkFXU19NQU5BR0VEXG4gICAgfSk7XG4gICAgXG4gICAgLyoqIEFQUFNZTkMgTEFNQkRBIEFVVEhPUklaRVIgKi9cbiAgICBjb25zdCBhcHBTeW5jTGFtYmRhQXV0aCA9IG5ldyBsYW1iZGEuRnVuY3Rpb24odGhpcywgJ0FwcFN5bmNMYW1iZGFBdXRoJywge1xuICAgICAgcnVudGltZTogbGFtYmRhLlJ1bnRpbWUuTk9ERUpTXzE2X1gsXG4gICAgICBoYW5kbGVyOiAnaW5kZXguaGFuZGxlcicsXG4gICAgICBjb2RlOiBsYW1iZGEuQ29kZS5mcm9tQXNzZXQocGF0aC5qb2luKHBhdGgucmVzb2x2ZSgnLi8nKSwnL2xhbWJkYXMvYXBwc3luYy1hdXRoJykpLFxuICAgICAgdHJhY2luZzogbGFtYmRhLlRyYWNpbmcuQUNUSVZFXG4gICAgfSk7XG5cbiAgICAvKiogQVBQU1lOQyBMT0cgQ09ORklHICovICAgIFxuICAgIGNvbnN0IGxvZ0NvbmZpZzogYXBwc3luYy5Mb2dDb25maWcgPSB7XG4gICAgICBleGNsdWRlVmVyYm9zZUNvbnRlbnQ6IGZhbHNlLFxuICAgICAgZmllbGRMb2dMZXZlbDogYXBwc3luYy5GaWVsZExvZ0xldmVsLkFMTCxcbiAgICB9OyAgXG5cbiAgICAvKiogQVBQU1lOQyBBUEkgKi9cbiAgICBjb25zdCB0b2RvR3JhcGhRTEFQSSA9IG5ldyBhcHBzeW5jLkdyYXBocWxBcGkodGhpcywnVG9kb0dyYXBoUUxBUEknLCB7XG4gICAgICBuYW1lOiAnVG9kb0dyYXBoUUxBUEknLFxuICAgICAgc2NoZW1hOiBhcHBzeW5jLlNjaGVtYS5mcm9tQXNzZXQocGF0aC5qb2luKHBhdGgucmVzb2x2ZSgnLi8nKSwnL2FwcHN5bmMtYXBpL3NjaGVtYS5ncmFwaHFsJykpLFxuICAgICAgYXV0aG9yaXphdGlvbkNvbmZpZzoge1xuICAgICAgICBkZWZhdWx0QXV0aG9yaXphdGlvbjoge1xuICAgICAgICAgIGF1dGhvcml6YXRpb25UeXBlOiBhcHBzeW5jLkF1dGhvcml6YXRpb25UeXBlLkxBTUJEQSxcbiAgICAgICAgICBsYW1iZGFBdXRob3JpemVyQ29uZmlnOiB7XG4gICAgICAgICAgICBoYW5kbGVyOiBhcHBTeW5jTGFtYmRhQXV0aFxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfSxcbiAgICAgIGxvZ0NvbmZpZyxcbiAgICAgIHhyYXlFbmFibGVkOiB0cnVlLFxuICAgIH0pO1xuXG4gICAgY29uc3QgdG9kb0R5bmFtb0RCRGF0YVNvdXJjZSA9IHRvZG9HcmFwaFFMQVBJLmFkZER5bmFtb0RiRGF0YVNvdXJjZSgnVG9kb0R5bmFtb0RCRGF0YVNvdXJjZScsIHRvZG9HbG9iYWxUYWJsZSk7XG4gICAgY29uc3QgdG9kb05vbmVEYXRhU291cmNlID0gdG9kb0dyYXBoUUxBUEkuYWRkTm9uZURhdGFTb3VyY2UoJ1RvZG9Ob25lRGF0YVNvdXJjZScpXG5cbiAgICAvKiogQVBQU1lOQyBSRVNPTFZFUlMgKi9cbiAgICB0b2RvRHluYW1vREJEYXRhU291cmNlLmNyZWF0ZVJlc29sdmVyKHtcbiAgICAgIHR5cGVOYW1lOiAnTXV0YXRpb24nLFxuICAgICAgZmllbGROYW1lOiAnYWRkVG9kbycsXG4gICAgICByZXF1ZXN0TWFwcGluZ1RlbXBsYXRlOiBhcHBzeW5jLk1hcHBpbmdUZW1wbGF0ZS5mcm9tRmlsZSgnYXBwc3luYy1hcGkvcmVzb2x2ZXJzL011dGF0aW9uLkFkZFRvZG8ucmVxLnZ0bCcpLFxuICAgICAgcmVzcG9uc2VNYXBwaW5nVGVtcGxhdGU6IGFwcHN5bmMuTWFwcGluZ1RlbXBsYXRlLmZyb21GaWxlKCdhcHBzeW5jLWFwaS9yZXNvbHZlcnMvTXV0YXRpb24uQWRkVG9kby5yZXNwLnZ0bCcpXG4gICAgfSk7XG5cbiAgICB0b2RvRHluYW1vREJEYXRhU291cmNlLmNyZWF0ZVJlc29sdmVyKHtcbiAgICAgIHR5cGVOYW1lOiAnTXV0YXRpb24nLFxuICAgICAgZmllbGROYW1lOiAndXBkYXRlVG9kbycsXG4gICAgICByZXF1ZXN0TWFwcGluZ1RlbXBsYXRlOiBhcHBzeW5jLk1hcHBpbmdUZW1wbGF0ZS5mcm9tRmlsZSgnYXBwc3luYy1hcGkvcmVzb2x2ZXJzL011dGF0aW9uLlVwZGF0ZVRvZG8ucmVxLnZ0bCcpLFxuICAgICAgcmVzcG9uc2VNYXBwaW5nVGVtcGxhdGU6IGFwcHN5bmMuTWFwcGluZ1RlbXBsYXRlLmZyb21GaWxlKCdhcHBzeW5jLWFwaS9yZXNvbHZlcnMvTXV0YXRpb24uVXBkYXRlVG9kby5yZXNwLnZ0bCcpXG4gICAgfSk7XG5cbiAgICB0b2RvRHluYW1vREJEYXRhU291cmNlLmNyZWF0ZVJlc29sdmVyKHtcbiAgICAgIHR5cGVOYW1lOiAnTXV0YXRpb24nLFxuICAgICAgZmllbGROYW1lOiAnZGVsZXRlVG9kbycsXG4gICAgICByZXF1ZXN0TWFwcGluZ1RlbXBsYXRlOiBhcHBzeW5jLk1hcHBpbmdUZW1wbGF0ZS5mcm9tRmlsZSgnYXBwc3luYy1hcGkvcmVzb2x2ZXJzL011dGF0aW9uLkRlbGV0ZVRvZG8ucmVxLnZ0bCcpLFxuICAgICAgcmVzcG9uc2VNYXBwaW5nVGVtcGxhdGU6IGFwcHN5bmMuTWFwcGluZ1RlbXBsYXRlLmZyb21GaWxlKCdhcHBzeW5jLWFwaS9yZXNvbHZlcnMvTXV0YXRpb24uRGVsZXRlVG9kby5yZXNwLnZ0bCcpXG4gICAgfSk7XG5cbiAgICB0b2RvTm9uZURhdGFTb3VyY2UuY3JlYXRlUmVzb2x2ZXIoe1xuICAgICAgdHlwZU5hbWU6ICdNdXRhdGlvbicsXG4gICAgICBmaWVsZE5hbWU6ICdhZGRUb2RvR2xvYmFsU3luYycsXG4gICAgICByZXF1ZXN0TWFwcGluZ1RlbXBsYXRlOiBhcHBzeW5jLk1hcHBpbmdUZW1wbGF0ZS5mcm9tRmlsZSgnYXBwc3luYy1hcGkvcmVzb2x2ZXJzL011dGF0aW9uLkFkZFRvZG9HbG9iYWxTeW5jLnJlcS52dGwnKSxcbiAgICAgIHJlc3BvbnNlTWFwcGluZ1RlbXBsYXRlOiBhcHBzeW5jLk1hcHBpbmdUZW1wbGF0ZS5mcm9tRmlsZSgnYXBwc3luYy1hcGkvcmVzb2x2ZXJzL011dGF0aW9uLkFkZFRvZG9HbG9iYWxTeW5jLnJlc3AudnRsJylcbiAgICB9KTtcblxuICAgIHRvZG9Ob25lRGF0YVNvdXJjZS5jcmVhdGVSZXNvbHZlcih7XG4gICAgICB0eXBlTmFtZTogJ011dGF0aW9uJyxcbiAgICAgIGZpZWxkTmFtZTogJ2RlbGV0ZVRvZG9HbG9iYWxTeW5jJyxcbiAgICAgIHJlcXVlc3RNYXBwaW5nVGVtcGxhdGU6IGFwcHN5bmMuTWFwcGluZ1RlbXBsYXRlLmZyb21GaWxlKCdhcHBzeW5jLWFwaS9yZXNvbHZlcnMvTXV0YXRpb24uRGVsZXRlVG9kb0dsb2JhbFN5bmMucmVxLnZ0bCcpLFxuICAgICAgcmVzcG9uc2VNYXBwaW5nVGVtcGxhdGU6IGFwcHN5bmMuTWFwcGluZ1RlbXBsYXRlLmZyb21GaWxlKCdhcHBzeW5jLWFwaS9yZXNvbHZlcnMvTXV0YXRpb24uRGVsZXRlVG9kb0dsb2JhbFN5bmMucmVzcC52dGwnKVxuICAgIH0pO1xuXG4gICAgdG9kb05vbmVEYXRhU291cmNlLmNyZWF0ZVJlc29sdmVyKHtcbiAgICAgIHR5cGVOYW1lOiAnTXV0YXRpb24nLFxuICAgICAgZmllbGROYW1lOiAndXBkYXRlVG9kb0dsb2JhbFN5bmMnLFxuICAgICAgcmVxdWVzdE1hcHBpbmdUZW1wbGF0ZTogYXBwc3luYy5NYXBwaW5nVGVtcGxhdGUuZnJvbUZpbGUoJ2FwcHN5bmMtYXBpL3Jlc29sdmVycy9NdXRhdGlvbi5VcGRhdGVUb2RvR2xvYmFsU3luYy5yZXEudnRsJyksXG4gICAgICByZXNwb25zZU1hcHBpbmdUZW1wbGF0ZTogYXBwc3luYy5NYXBwaW5nVGVtcGxhdGUuZnJvbUZpbGUoJ2FwcHN5bmMtYXBpL3Jlc29sdmVycy9NdXRhdGlvbi5VcGRhdGVUb2RvR2xvYmFsU3luYy5yZXNwLnZ0bCcpXG4gICAgfSk7XG5cbiAgICB0b2RvRHluYW1vREJEYXRhU291cmNlLmNyZWF0ZVJlc29sdmVyKHtcbiAgICAgIHR5cGVOYW1lOiAnUXVlcnknLFxuICAgICAgZmllbGROYW1lOiAnZ2V0VG9kbycsXG4gICAgICByZXF1ZXN0TWFwcGluZ1RlbXBsYXRlOiBhcHBzeW5jLk1hcHBpbmdUZW1wbGF0ZS5mcm9tRmlsZSgnYXBwc3luYy1hcGkvcmVzb2x2ZXJzL1F1ZXJ5LkdldFRvZG8ucmVxLnZ0bCcpLFxuICAgICAgcmVzcG9uc2VNYXBwaW5nVGVtcGxhdGU6IGFwcHN5bmMuTWFwcGluZ1RlbXBsYXRlLmZyb21GaWxlKCdhcHBzeW5jLWFwaS9yZXNvbHZlcnMvUXVlcnkuR2V0VG9kby5yZXNwLnZ0bCcpXG4gICAgfSk7XG5cbiAgICB0b2RvRHluYW1vREJEYXRhU291cmNlLmNyZWF0ZVJlc29sdmVyKHtcbiAgICAgIHR5cGVOYW1lOiAnUXVlcnknLFxuICAgICAgZmllbGROYW1lOiAnbGlzdFRvZG9zJyxcbiAgICAgIHJlcXVlc3RNYXBwaW5nVGVtcGxhdGU6IGFwcHN5bmMuTWFwcGluZ1RlbXBsYXRlLmZyb21GaWxlKCdhcHBzeW5jLWFwaS9yZXNvbHZlcnMvUXVlcnkuTGlzdFRvZG9zLnJlcS52dGwnKSxcbiAgICAgIHJlc3BvbnNlTWFwcGluZ1RlbXBsYXRlOiBhcHBzeW5jLk1hcHBpbmdUZW1wbGF0ZS5mcm9tRmlsZSgnYXBwc3luYy1hcGkvcmVzb2x2ZXJzL1F1ZXJ5Lkxpc3RUb2Rvcy5yZXNwLnZ0bCcpXG4gICAgfSk7XG5cbiAgICAvKiogIENPTkZJR1VSRSBDVVNUT00gRE9NQUlOICovXG4gICAgY29uc3QgZ3JhcGhxbEFQSURvbWFpbk5hbWVDZXJ0ID0gY2VydGlmaWNhdGVtYWFuZ2VyLkNlcnRpZmljYXRlLmZyb21DZXJ0aWZpY2F0ZUFybih0aGlzLCdHcmFwaHFsQVBJRG9tYWluTmFtZUNlcnQnLCBjdXJyZW50R3JhcGhxbEFQSUNlcnRBUk4pXG4gICAgY29uc3QgZ3JhcGhxbEFQSUN1c3RvbURvbWFpbiA9IG5ldyBjZGsuYXdzX2FwcHN5bmMuQ2ZuRG9tYWluTmFtZShcbiAgICAgIHRoaXMsXG4gICAgICAnR3JhcGhxbEFQSUN1c3RvbURvbWFpbicsXG4gICAgICB7XG4gICAgICAgIGNlcnRpZmljYXRlQXJuOiBncmFwaHFsQVBJRG9tYWluTmFtZUNlcnQuY2VydGlmaWNhdGVBcm4sXG4gICAgICAgIGRvbWFpbk5hbWU6IGN1cnJlbnRBcHBTeW5jQ3VzdG9tRG9tYWluXG4gICAgICB9XG4gICAgKTtcblxuICAgIGNvbnN0IGFwcFN5bmNDdXN0b21Eb21haW5Bc3NvYyA9IG5ldyBjZGsuYXdzX2FwcHN5bmMuQ2ZuRG9tYWluTmFtZUFwaUFzc29jaWF0aW9uKFxuICAgICAgdGhpcywgXG4gICAgICAnQXBwU3luY0N1c3RvbURvbWFpbkFzc29jJyxcbiAgICAgIHtcbiAgICAgICAgYXBpSWQ6IHRvZG9HcmFwaFFMQVBJLmFwaUlkLFxuICAgICAgICBkb21haW5OYW1lOiBncmFwaHFsQVBJQ3VzdG9tRG9tYWluLmRvbWFpbk5hbWVcbiAgICAgIH1cbiAgICApO1xuXG4gICAgYXBwU3luY0N1c3RvbURvbWFpbkFzc29jLmFkZERlcGVuZHNPbihncmFwaHFsQVBJQ3VzdG9tRG9tYWluKTtcblxuICAgIC8vQWRkaW5nIFJvdXRlIDUzIFJlY29yZHMgZm9yIHRoZSBjdXN0b20gZG9tYWluXG4gICAgY29uc3QgdG9kb0FQSURvbWFpbk5hbWVIb3N0ZWRab25lID0gcm91dGU1My5Ib3N0ZWRab25lLmZyb21Ib3N0ZWRab25lQXR0cmlidXRlcyh0aGlzLCAnVG9kb0FQSURvbWFpbk5hbWVIb3N0ZWRab25lJywge1xuICAgICAgaG9zdGVkWm9uZUlkOiBjdXJyZW50VG9kb0FQSUhvc3RlZFpvbmVJRCxcbiAgICAgIHpvbmVOYW1lOiBjdXJyZW50VG9kb0FQSUhvc3RlZFpvbmVOYW1lXG4gICAgfSk7XG5cbiAgICBjb25zdCBhcHBTeW5jRE5TQ29uZmlncyA9IG5ldyByb3V0ZTUzLkNuYW1lUmVjb3JkKHRoaXMsICdBcHBTeW5jRE5TQ29uZmlncycse1xuICAgICAgcmVjb3JkTmFtZTogY3VycmVudEFwcFN5bmNDdXN0b21Eb21haW4uc3BsaXQoJy4nKVswXSxcbiAgICAgIHpvbmU6IHRvZG9BUElEb21haW5OYW1lSG9zdGVkWm9uZSxcbiAgICAgIGRvbWFpbk5hbWU6IGdyYXBocWxBUElDdXN0b21Eb21haW4uYXR0ckFwcFN5bmNEb21haW5OYW1lXG4gICAgfSlcblxuXG4gICAgLyoqICBMQU1CREEgU1RSRUFNIFBST0NFU1NPUiBFWEVDVVRJT04gUk9MRSAqL1xuICAgIGNvbnN0IHRvZG9ERFN0cmVhbUxhbWJkYUV4ZWNSb2xlID0gbmV3IGlhbS5Sb2xlKHRoaXMsJ1RvZG9ERFN0cmVhbUxhbWJkYUV4ZWNSb2xlJyx7XG4gICAgICBhc3N1bWVkQnk6IG5ldyBpYW0uU2VydmljZVByaW5jaXBhbCgnbGFtYmRhLmFtYXpvbmF3cy5jb20nKSxcbiAgICAgIG1hbmFnZWRQb2xpY2llczogW1xuICAgICAgICBpYW0uTWFuYWdlZFBvbGljeS5mcm9tQXdzTWFuYWdlZFBvbGljeU5hbWUoJ0FXU0FwcFN5bmNJbnZva2VGdWxsQWNjZXNzJyksXG4gICAgICAgIGlhbS5NYW5hZ2VkUG9saWN5LmZyb21Bd3NNYW5hZ2VkUG9saWN5TmFtZSgnQ2xvdWRXYXRjaExvZ3NGdWxsQWNjZXNzJylcbiAgICAgIF1cbiAgICB9KTtcblxuICAgIC8qKiAgTEFNQkRBIFNUUkVBTSBQUk9DRVNTT1IgKi9cbiAgICBjb25zdCB0b2RvRERTdHJlYW1MYW1iZGEgPSBuZXcgbGFtYmRhLkZ1bmN0aW9uKHRoaXMsICdUb2RvRERTdHJlYW1MYW1iZGEnLCB7XG4gICAgICBydW50aW1lOiBsYW1iZGEuUnVudGltZS5OT0RFSlNfMTZfWCxcbiAgICAgIGhhbmRsZXI6ICdpbmRleC5oYW5kbGVyJyxcbiAgICAgIGNvZGU6IGxhbWJkYS5Db2RlLmZyb21Bc3NldChwYXRoLmpvaW4ocGF0aC5yZXNvbHZlKCcuLycpLCcvbGFtYmRhcy9kZGItc3RyZWFtLXByb2Nlc3NvcicpKSxcbiAgICAgIHJvbGU6IHRvZG9ERFN0cmVhbUxhbWJkYUV4ZWNSb2xlLFxuICAgICAgZW52aXJvbm1lbnQ6IHtcbiAgICAgICAgJ0FwcFN5bmNBUElFbmRwb2ludCc6IHRvZG9HcmFwaFFMQVBJLmdyYXBocWxVcmwsXG4gICAgICAgICdBcHBTeW5jQVBJTGFtYmRhQXV0aEtleSc6ICdjdXN0b20tYXV0aG9yaXplZCdcbiAgICAgIH0sXG4gICAgICB0cmFjaW5nOiBsYW1iZGEuVHJhY2luZy5BQ1RJVkVcbiAgICB9KTtcblxuICAgIC8qKiAgQUREIERZTkFNTyBEQiBTVFJFQU0gQVMgRVZFTlQgU09VUkNFIFRPIExBTUJEQSAqL1xuICAgIHRvZG9ERFN0cmVhbUxhbWJkYS5hZGRFdmVudFNvdXJjZShuZXcgRHluYW1vRXZlbnRTb3VyY2UodG9kb0dsb2JhbFRhYmxlLHtcbiAgICAgIHN0YXJ0aW5nUG9zaXRpb246IGxhbWJkYS5TdGFydGluZ1Bvc2l0aW9uLlRSSU1fSE9SSVpPTixcbiAgICB9KSlcblxuICAgIC8qKiBPVVRQVVQgU1RBQ0sgVkFMVUVTICovXG4gICAgY29uc3QgdG9kb1RhYmxlU3RyZWFtQVJOID0gdG9kb0dsb2JhbFRhYmxlPy50YWJsZVN0cmVhbUFybj8gdG9kb0dsb2JhbFRhYmxlLnRhYmxlU3RyZWFtQXJuOicnXG5cbiAgICBuZXcgY2RrLkNmbk91dHB1dCh0aGlzLCdBUEkgVVJMJywge3ZhbHVlOiB0b2RvR3JhcGhRTEFQSS5ncmFwaHFsVXJsfSlcbiAgICBuZXcgY2RrLkNmbk91dHB1dCh0aGlzLCdUT0RPIFRBQkxFIFNUUkVBTSBBUk4nLCB7dmFsdWU6dG9kb1RhYmxlU3RyZWFtQVJOfSlcbiAgfVxufVxuIl19 -------------------------------------------------------------------------------- /appsync-multi-region-api/lib/appsync-multi-region-active-active-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 4 | import { DynamoEventSource } from 'aws-cdk-lib/aws-lambda-event-sources'; 5 | import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; 6 | import * as iam from 'aws-cdk-lib/aws-iam'; 7 | import * as appsync from '@aws-cdk/aws-appsync-alpha'; 8 | import * as certificatemaanger from 'aws-cdk-lib/aws-certificatemanager'; 9 | import * as route53 from 'aws-cdk-lib/aws-route53'; 10 | import * as path from 'path'; 11 | 12 | interface AppSyncMultiRegionActiveActiveStackProps extends cdk.StackProps { 13 | primaryRegion?: string, 14 | secondaryRegion?: string, 15 | appSyncCustomDomain?: string, 16 | graphqlAPIDomainNameCertARN?: string, 17 | todoAPIHostedZoneID?: string, 18 | todoAPIHostedZoneName?: string 19 | } 20 | 21 | export class AppsyncMultiRegionActiveActiveStack extends cdk.Stack { 22 | constructor(scope: Construct, id: string, props?: AppSyncMultiRegionActiveActiveStackProps) { 23 | super(scope, id, props); 24 | 25 | const currentSecondaryRegion = props?.secondaryRegion? props.secondaryRegion: ''; 26 | const currentGraphqlAPICertARN = props?.graphqlAPIDomainNameCertARN? props.graphqlAPIDomainNameCertARN: '' 27 | const currentAppSyncCustomDomain = props?.appSyncCustomDomain? props.appSyncCustomDomain:'' 28 | const currentTodoAPIHostedZoneID = props?.todoAPIHostedZoneID? props.todoAPIHostedZoneID:'' 29 | const currentTodoAPIHostedZoneName = props?.todoAPIHostedZoneName? props.todoAPIHostedZoneName:'' 30 | 31 | 32 | /** DYNAMODB GLOBAL TABLE */ 33 | const todoGlobalTable = new dynamodb.Table(this, 'TodoGlobalTable', { 34 | partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, 35 | replicationRegions: [currentSecondaryRegion], 36 | tableName: "TodoGlobalTable", 37 | stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES, 38 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 39 | encryption: dynamodb.TableEncryption.AWS_MANAGED 40 | }); 41 | 42 | /** APPSYNC LAMBDA AUTHORIZER */ 43 | const appSyncLambdaAuth = new lambda.Function(this, 'AppSyncLambdaAuth', { 44 | runtime: lambda.Runtime.NODEJS_16_X, 45 | handler: 'index.handler', 46 | code: lambda.Code.fromAsset(path.join(path.resolve('./'),'/lambdas/appsync-auth')), 47 | tracing: lambda.Tracing.ACTIVE 48 | }); 49 | 50 | /** APPSYNC LOG CONFIG */ 51 | const logConfig: appsync.LogConfig = { 52 | excludeVerboseContent: false, 53 | fieldLogLevel: appsync.FieldLogLevel.ALL, 54 | }; 55 | 56 | /** APPSYNC API */ 57 | const todoGraphQLAPI = new appsync.GraphqlApi(this,'TodoGraphQLAPI', { 58 | name: 'TodoGraphQLAPI', 59 | schema: appsync.Schema.fromAsset(path.join(path.resolve('./'),'/appsync-api/schema.graphql')), 60 | authorizationConfig: { 61 | defaultAuthorization: { 62 | authorizationType: appsync.AuthorizationType.LAMBDA, 63 | lambdaAuthorizerConfig: { 64 | handler: appSyncLambdaAuth 65 | } 66 | } 67 | }, 68 | logConfig, 69 | xrayEnabled: true, 70 | }); 71 | 72 | const todoDynamoDBDataSource = todoGraphQLAPI.addDynamoDbDataSource('TodoDynamoDBDataSource', todoGlobalTable); 73 | const todoNoneDataSource = todoGraphQLAPI.addNoneDataSource('TodoNoneDataSource') 74 | 75 | /** APPSYNC RESOLVERS */ 76 | todoDynamoDBDataSource.createResolver({ 77 | typeName: 'Mutation', 78 | fieldName: 'addTodo', 79 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.AddTodo.req.vtl'), 80 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.AddTodo.resp.vtl') 81 | }); 82 | 83 | todoDynamoDBDataSource.createResolver({ 84 | typeName: 'Mutation', 85 | fieldName: 'updateTodo', 86 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.UpdateTodo.req.vtl'), 87 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.UpdateTodo.resp.vtl') 88 | }); 89 | 90 | todoDynamoDBDataSource.createResolver({ 91 | typeName: 'Mutation', 92 | fieldName: 'deleteTodo', 93 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.DeleteTodo.req.vtl'), 94 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.DeleteTodo.resp.vtl') 95 | }); 96 | 97 | todoNoneDataSource.createResolver({ 98 | typeName: 'Mutation', 99 | fieldName: 'addTodoGlobalSync', 100 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.AddTodoGlobalSync.req.vtl'), 101 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.AddTodoGlobalSync.resp.vtl') 102 | }); 103 | 104 | todoNoneDataSource.createResolver({ 105 | typeName: 'Mutation', 106 | fieldName: 'deleteTodoGlobalSync', 107 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.DeleteTodoGlobalSync.req.vtl'), 108 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.DeleteTodoGlobalSync.resp.vtl') 109 | }); 110 | 111 | todoNoneDataSource.createResolver({ 112 | typeName: 'Mutation', 113 | fieldName: 'updateTodoGlobalSync', 114 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.UpdateTodoGlobalSync.req.vtl'), 115 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.UpdateTodoGlobalSync.resp.vtl') 116 | }); 117 | 118 | todoDynamoDBDataSource.createResolver({ 119 | typeName: 'Query', 120 | fieldName: 'getTodo', 121 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Query.GetTodo.req.vtl'), 122 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Query.GetTodo.resp.vtl') 123 | }); 124 | 125 | todoDynamoDBDataSource.createResolver({ 126 | typeName: 'Query', 127 | fieldName: 'listTodos', 128 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Query.ListTodos.req.vtl'), 129 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Query.ListTodos.resp.vtl') 130 | }); 131 | 132 | /** CONFIGURE CUSTOM DOMAIN */ 133 | const graphqlAPIDomainNameCert = certificatemaanger.Certificate.fromCertificateArn(this,'GraphqlAPIDomainNameCert', currentGraphqlAPICertARN) 134 | const graphqlAPICustomDomain = new cdk.aws_appsync.CfnDomainName( 135 | this, 136 | 'GraphqlAPICustomDomain', 137 | { 138 | certificateArn: graphqlAPIDomainNameCert.certificateArn, 139 | domainName: currentAppSyncCustomDomain 140 | } 141 | ); 142 | 143 | const appSyncCustomDomainAssoc = new cdk.aws_appsync.CfnDomainNameApiAssociation( 144 | this, 145 | 'AppSyncCustomDomainAssoc', 146 | { 147 | apiId: todoGraphQLAPI.apiId, 148 | domainName: graphqlAPICustomDomain.domainName 149 | } 150 | ); 151 | 152 | appSyncCustomDomainAssoc.addDependsOn(graphqlAPICustomDomain); 153 | 154 | //Adding Route 53 Records for the custom domain 155 | const todoAPIDomainNameHostedZone = route53.HostedZone.fromHostedZoneAttributes(this, 'TodoAPIDomainNameHostedZone', { 156 | hostedZoneId: currentTodoAPIHostedZoneID, 157 | zoneName: currentTodoAPIHostedZoneName 158 | }); 159 | 160 | const appSyncDNSConfigs = new route53.CnameRecord(this, 'AppSyncDNSConfigs',{ 161 | recordName: currentAppSyncCustomDomain.split('.')[0], 162 | zone: todoAPIDomainNameHostedZone, 163 | domainName: graphqlAPICustomDomain.attrAppSyncDomainName 164 | }) 165 | 166 | 167 | /** LAMBDA STREAM PROCESSOR EXECUTION ROLE */ 168 | const todoDDStreamLambdaExecRole = new iam.Role(this,'TodoDDStreamLambdaExecRole',{ 169 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), 170 | managedPolicies: [ 171 | iam.ManagedPolicy.fromAwsManagedPolicyName('AWSAppSyncInvokeFullAccess'), 172 | iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLogsFullAccess') 173 | ] 174 | }); 175 | 176 | /** LAMBDA STREAM PROCESSOR */ 177 | const todoDDStreamLambda = new lambda.Function(this, 'TodoDDStreamLambda', { 178 | runtime: lambda.Runtime.NODEJS_16_X, 179 | handler: 'index.handler', 180 | code: lambda.Code.fromAsset(path.join(path.resolve('./'),'/lambdas/ddb-stream-processor')), 181 | role: todoDDStreamLambdaExecRole, 182 | environment: { 183 | 'AppSyncAPIEndpoint': todoGraphQLAPI.graphqlUrl, 184 | 'AppSyncAPILambdaAuthKey': 'custom-authorized' 185 | }, 186 | tracing: lambda.Tracing.ACTIVE 187 | }); 188 | 189 | /** ADD DYNAMO DB STREAM AS EVENT SOURCE TO LAMBDA */ 190 | todoDDStreamLambda.addEventSource(new DynamoEventSource(todoGlobalTable,{ 191 | startingPosition: lambda.StartingPosition.TRIM_HORIZON, 192 | })) 193 | 194 | /** OUTPUT STACK VALUES */ 195 | const todoTableStreamARN = todoGlobalTable?.tableStreamArn? todoGlobalTable.tableStreamArn:'' 196 | 197 | new cdk.CfnOutput(this,'API URL', {value: todoGraphQLAPI.graphqlUrl}) 198 | new cdk.CfnOutput(this,'TODO TABLE STREAM ARN', {value:todoTableStreamARN}) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /appsync-multi-region-api/lib/secondary-appsync-multi-region-active-active-stack.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.SecondaryAppsyncMultiRegionActiveActiveStack = void 0; 4 | const cdk = require("aws-cdk-lib"); 5 | const lambda = require("aws-cdk-lib/aws-lambda"); 6 | const aws_lambda_event_sources_1 = require("aws-cdk-lib/aws-lambda-event-sources"); 7 | const dynamodb = require("aws-cdk-lib/aws-dynamodb"); 8 | const iam = require("aws-cdk-lib/aws-iam"); 9 | const appsync = require("@aws-cdk/aws-appsync-alpha"); 10 | const certificatemaanger = require("aws-cdk-lib/aws-certificatemanager"); 11 | const route53 = require("aws-cdk-lib/aws-route53"); 12 | const path = require("path"); 13 | class SecondaryAppsyncMultiRegionActiveActiveStack extends cdk.Stack { 14 | constructor(scope, id, props) { 15 | super(scope, id, props); 16 | const currentAppSyncCustomDomain = (props === null || props === void 0 ? void 0 : props.appSyncCustomDomain) ? props.appSyncCustomDomain : ''; 17 | const currentGraphqlAPICertARN = (props === null || props === void 0 ? void 0 : props.graphqlAPIDomainNameCertARN) ? props.graphqlAPIDomainNameCertARN : ''; 18 | const currentTodoAPIHostedZoneID = (props === null || props === void 0 ? void 0 : props.todoAPIHostedZoneID) ? props.todoAPIHostedZoneID : ''; 19 | const currentTodoAPIHostedZoneName = (props === null || props === void 0 ? void 0 : props.todoAPIHostedZoneName) ? props.todoAPIHostedZoneName : ''; 20 | /** PARAMETER FOR TODO GLOBAL TABLE NAME */ 21 | const todoGlobalTableStreamARN = new cdk.CfnParameter(this, "todoGlobalTableStreamARN", { 22 | type: "String", 23 | description: "The ARN of the Todo Global Table Stream." 24 | }); 25 | /** DYNAMO DB TABLE */ 26 | const todoGlobalTable = dynamodb.Table.fromTableAttributes(this, 'TodoGlobalTable', { 27 | tableName: "TodoGlobalTable", 28 | tableStreamArn: todoGlobalTableStreamARN.valueAsString 29 | }); 30 | /** APPSYNC LAMBDA AUTHORIZER */ 31 | const appSyncLambdaAuth = new lambda.Function(this, 'AppSyncLambdaAuth', { 32 | runtime: lambda.Runtime.NODEJS_16_X, 33 | handler: 'index.handler', 34 | code: lambda.Code.fromAsset(path.join(path.resolve('./'), '/lambdas/appsync-auth')), 35 | tracing: lambda.Tracing.ACTIVE 36 | }); 37 | /** APPSYNC LOG CONFIG */ 38 | const logConfig = { 39 | excludeVerboseContent: false, 40 | fieldLogLevel: appsync.FieldLogLevel.ALL, 41 | }; 42 | /** APPSYNC API */ 43 | const todoGraphQLAPI = new appsync.GraphqlApi(this, 'TodoGraphQLAPI', { 44 | name: 'TodoGraphQLAPI', 45 | schema: appsync.Schema.fromAsset(path.join(path.resolve('./'), '/appsync-api/schema.graphql')), 46 | authorizationConfig: { 47 | defaultAuthorization: { 48 | authorizationType: appsync.AuthorizationType.LAMBDA, 49 | lambdaAuthorizerConfig: { 50 | handler: appSyncLambdaAuth 51 | } 52 | } 53 | }, 54 | logConfig, 55 | xrayEnabled: true, 56 | }); 57 | const todoDynamoDBDataSource = todoGraphQLAPI.addDynamoDbDataSource('TodoDynamoDBDataSource', todoGlobalTable); 58 | const todoNoneDataSource = todoGraphQLAPI.addNoneDataSource('TodoNoneDataSource'); 59 | /** APPSYNC RESOLVERS */ 60 | todoDynamoDBDataSource.createResolver({ 61 | typeName: 'Mutation', 62 | fieldName: 'addTodo', 63 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.AddTodo.req.vtl'), 64 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.AddTodo.resp.vtl') 65 | }); 66 | todoDynamoDBDataSource.createResolver({ 67 | typeName: 'Mutation', 68 | fieldName: 'updateTodo', 69 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.UpdateTodo.req.vtl'), 70 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.UpdateTodo.resp.vtl') 71 | }); 72 | todoDynamoDBDataSource.createResolver({ 73 | typeName: 'Mutation', 74 | fieldName: 'deleteTodo', 75 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.DeleteTodo.req.vtl'), 76 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.DeleteTodo.resp.vtl') 77 | }); 78 | todoNoneDataSource.createResolver({ 79 | typeName: 'Mutation', 80 | fieldName: 'addTodoGlobalSync', 81 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.AddTodoGlobalSync.req.vtl'), 82 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.AddTodoGlobalSync.resp.vtl') 83 | }); 84 | todoNoneDataSource.createResolver({ 85 | typeName: 'Mutation', 86 | fieldName: 'deleteTodoGlobalSync', 87 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.DeleteTodoGlobalSync.req.vtl'), 88 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.DeleteTodoGlobalSync.resp.vtl') 89 | }); 90 | todoNoneDataSource.createResolver({ 91 | typeName: 'Mutation', 92 | fieldName: 'updateTodoGlobalSync', 93 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.UpdateTodoGlobalSync.req.vtl'), 94 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.UpdateTodoGlobalSync.resp.vtl') 95 | }); 96 | todoDynamoDBDataSource.createResolver({ 97 | typeName: 'Query', 98 | fieldName: 'getTodo', 99 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Query.GetTodo.req.vtl'), 100 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Query.GetTodo.resp.vtl') 101 | }); 102 | todoDynamoDBDataSource.createResolver({ 103 | typeName: 'Query', 104 | fieldName: 'listTodos', 105 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Query.ListTodos.req.vtl'), 106 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Query.ListTodos.resp.vtl') 107 | }); 108 | /** CONFIGURE CUSTOM DOMAIN */ 109 | const graphqlAPIDomainNameCert = certificatemaanger.Certificate.fromCertificateArn(this, 'GraphqlAPIDomainNameCert', currentGraphqlAPICertARN); 110 | const graphqlAPICustomDomain = new cdk.aws_appsync.CfnDomainName(this, 'GraphqlAPICustomDomain', { 111 | certificateArn: graphqlAPIDomainNameCert.certificateArn, 112 | domainName: currentAppSyncCustomDomain 113 | }); 114 | const appSyncCustomDomainAssoc = new cdk.aws_appsync.CfnDomainNameApiAssociation(this, 'AppSyncCustomDomainAssoc', { 115 | apiId: todoGraphQLAPI.apiId, 116 | domainName: graphqlAPICustomDomain.domainName 117 | }); 118 | appSyncCustomDomainAssoc.addDependsOn(graphqlAPICustomDomain); 119 | //Adding Route 53 Records for the custom domain 120 | const todoAPIDomainNameHostedZone = route53.HostedZone.fromHostedZoneAttributes(this, 'TodoAPIDomainNameHostedZone', { 121 | hostedZoneId: currentTodoAPIHostedZoneID, 122 | zoneName: currentTodoAPIHostedZoneName 123 | }); 124 | const appSyncDNSConfigs = new route53.CnameRecord(this, 'AppSyncDNSConfigs', { 125 | recordName: currentAppSyncCustomDomain.split('.')[0], 126 | zone: todoAPIDomainNameHostedZone, 127 | domainName: graphqlAPICustomDomain.attrAppSyncDomainName 128 | }); 129 | /** LAMBDA STREAM PROCESSOR EXECUTION ROLE */ 130 | const todoDDStreamLambdaExecRole = new iam.Role(this, 'TodoDDStreamLambdaExecRole', { 131 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), 132 | managedPolicies: [ 133 | iam.ManagedPolicy.fromAwsManagedPolicyName('AWSAppSyncInvokeFullAccess'), 134 | iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLogsFullAccess') 135 | ] 136 | }); 137 | /** LAMBDA STREAM PROCESSOR */ 138 | const todoDDStreamLambda = new lambda.Function(this, 'TodoDDStreamLambda', { 139 | runtime: lambda.Runtime.NODEJS_16_X, 140 | handler: 'index.handler', 141 | code: lambda.Code.fromAsset(path.join(path.resolve('./'), '/lambdas/ddb-stream-processor')), 142 | role: todoDDStreamLambdaExecRole, 143 | environment: { 144 | 'AppSyncAPIEndpoint': todoGraphQLAPI.graphqlUrl, 145 | 'AppSyncAPILambdaAuthKey': 'custom-authorized' 146 | }, 147 | tracing: lambda.Tracing.ACTIVE 148 | }); 149 | /** ADD DYNAMO DB STREAM AS EVENT SOURCE TO LAMBDA */ 150 | todoDDStreamLambda.addEventSource(new aws_lambda_event_sources_1.DynamoEventSource(todoGlobalTable, { 151 | startingPosition: lambda.StartingPosition.TRIM_HORIZON, 152 | })); 153 | /** OUTPUT STACK VALUES */ 154 | new cdk.CfnOutput(this, 'API URL', { value: todoGraphQLAPI.graphqlUrl }); 155 | new cdk.CfnOutput(this, 'TODO TABLE ARN', { value: todoGlobalTable.tableArn }); 156 | new cdk.CfnOutput(this, 'LAMBDA ARN', { value: todoDDStreamLambda.functionArn }); 157 | } 158 | } 159 | exports.SecondaryAppsyncMultiRegionActiveActiveStack = SecondaryAppsyncMultiRegionActiveActiveStack; 160 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2Vjb25kYXJ5LWFwcHN5bmMtbXVsdGktcmVnaW9uLWFjdGl2ZS1hY3RpdmUtc3RhY2suanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJzZWNvbmRhcnktYXBwc3luYy1tdWx0aS1yZWdpb24tYWN0aXZlLWFjdGl2ZS1zdGFjay50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSxtQ0FBbUM7QUFFbkMsaURBQWlEO0FBQ2pELG1GQUF5RTtBQUN6RSxxREFBcUQ7QUFDckQsMkNBQTJDO0FBQzNDLHNEQUFzRDtBQUN0RCx5RUFBeUU7QUFDekUsbURBQW1EO0FBQ25ELDZCQUE0QjtBQVU1QixNQUFhLDRDQUE2QyxTQUFRLEdBQUcsQ0FBQyxLQUFLO0lBQ3ZFLFlBQVksS0FBZ0IsRUFBRSxFQUFVLEVBQUUsS0FBeUQ7UUFDakcsS0FBSyxDQUFDLEtBQUssRUFBRSxFQUFFLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFFeEIsTUFBTSwwQkFBMEIsR0FBRyxDQUFBLEtBQUssYUFBTCxLQUFLLHVCQUFMLEtBQUssQ0FBRSxtQkFBbUIsRUFBQSxDQUFDLENBQUMsS0FBSyxDQUFDLG1CQUFtQixDQUFBLENBQUMsQ0FBQSxFQUFFLENBQUE7UUFDM0YsTUFBTSx3QkFBd0IsR0FBRyxDQUFBLEtBQUssYUFBTCxLQUFLLHVCQUFMLEtBQUssQ0FBRSwyQkFBMkIsRUFBQSxDQUFDLENBQUMsS0FBSyxDQUFDLDJCQUEyQixDQUFBLENBQUMsQ0FBQyxFQUFFLENBQUE7UUFDMUcsTUFBTSwwQkFBMEIsR0FBRyxDQUFBLEtBQUssYUFBTCxLQUFLLHVCQUFMLEtBQUssQ0FBRSxtQkFBbUIsRUFBQSxDQUFDLENBQUMsS0FBSyxDQUFDLG1CQUFtQixDQUFBLENBQUMsQ0FBQSxFQUFFLENBQUE7UUFDM0YsTUFBTSw0QkFBNEIsR0FBRyxDQUFBLEtBQUssYUFBTCxLQUFLLHVCQUFMLEtBQUssQ0FBRSxxQkFBcUIsRUFBQSxDQUFDLENBQUMsS0FBSyxDQUFDLHFCQUFxQixDQUFB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LENBQUMsY0FBYyxDQUFDLElBQUksNENBQWlCLENBQUMsZUFBZSxFQUFDO1lBQ3RFLGdCQUFnQixFQUFFLE1BQU0sQ0FBQyxnQkFBZ0IsQ0FBQyxZQUFZO1NBQ3ZELENBQUMsQ0FBQyxDQUFBO1FBRUgsMEJBQTBCO1FBQzFCLElBQUksR0FBRyxDQUFDLFNBQVMsQ0FBQyxJQUFJLEVBQUMsU0FBUyxFQUFFLEVBQUMsS0FBSyxFQUFDLGNBQWMsQ0FBQyxVQUFVLEVBQUMsQ0FBQyxDQUFBO1FBQ3BFLElBQUksR0FBRyxDQUFDLFNBQVMsQ0FBQyxJQUFJLEVBQUMsZ0JBQWdCLEVBQUUsRUFBQyxLQUFLLEVBQUMsZUFBZSxDQUFDLFFBQVEsRUFBQyxDQUFDLENBQUE7UUFDMUUsSUFBSSxHQUFHLENBQUMsU0FBUyxDQUFDLElBQUksRUFBQyxZQUFZLEVBQUUsRUFBQyxLQUFLLEVBQUMsa0JBQWtCLENBQUMsV0FBVyxFQUFDLENBQUMsQ0FBQTtJQUU5RSxDQUFDO0NBQ0o7QUFsTEQsb0dBa0xDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgY2RrIGZyb20gJ2F3cy1jZGstbGliJztcbmltcG9ydCB7IENvbnN0cnVjdCB9IGZyb20gJ2NvbnN0cnVjdHMnO1xuaW1wb3J0ICogYXMgbGFtYmRhIGZyb20gJ2F3cy1jZGstbGliL2F3cy1sYW1iZGEnO1xuaW1wb3J0IHsgRHluYW1vRXZlbnRTb3VyY2UgfSBmcm9tICdhd3MtY2RrLWxpYi9hd3MtbGFtYmRhLWV2ZW50LXNvdXJjZXMnO1xuaW1wb3J0ICogYXMgZHluYW1vZGIgZnJvbSAnYXdzLWNkay1saWIvYXdzLWR5bmFtb2RiJztcbmltcG9ydCAqIGFzIGlhbSBmcm9tICdhd3MtY2RrLWxpYi9hd3MtaWFtJztcbmltcG9ydCAqIGFzIGFwcHN5bmMgZnJvbSAnQGF3cy1jZGsvYXdzLWFwcHN5bmMtYWxwaGEnO1xuaW1wb3J0ICogYXMgY2VydGlmaWNhdGVtYWFuZ2VyIGZyb20gJ2F3cy1jZGstbGliL2F3cy1jZXJ0aWZpY2F0ZW1hbmFnZXInO1xuaW1wb3J0ICogYXMgcm91dGU1MyBmcm9tICdhd3MtY2RrLWxpYi9hd3Mtcm91dGU1Myc7XG5pbXBvcnQgKiBhcyBwYXRoIGZyb20gJ3BhdGgnXG5cbmludGVyZmFjZSBTZWNvbmRhcnlBcHBTeW5jTXVsdGlSZWdpb25BY3RpdmVBY3RpdmVTdGFja1Byb3BzIGV4dGVuZHMgY2RrLlN0YWNrUHJvcHMge1xuICAgIHByaW1hcnlSZWdpb24/OiBzdHJpbmcsXG4gICAgc2Vjb25kYXJ5UmVnaW9uPzogc3RyaW5nLFxuICAgIGFwcFN5bmNDdXN0b21Eb21haW4/OiBzdHJpbmcsXG4gICAgZ3JhcGhxbEFQSURvbWFpbk5hbWVDZXJ0QVJOPzogc3RyaW5nLFxuICAgIHRvZG9BUElIb3N0ZWRab25lSUQ/OiBzdHJpbmcsXG4gICAgdG9kb0FQSUhvc3RlZFpvbmVOYW1lPzogc3RyaW5nXG59XG5leHBvcnQgY2xhc3MgU2Vjb25kYXJ5QXBwc3luY011bHRpUmVnaW9uQWN0aXZlQWN0aXZlU3RhY2sgZXh0ZW5kcyBjZGsuU3RhY2sge1xuICAgIGNvbnN0cnVjdG9yKHNjb3BlOiBDb25zdHJ1Y3QsIGlkOiBzdHJpbmcsIHByb3BzPzogU2Vjb25kYXJ5QXBwU3luY011bHRpUmVnaW9uQWN0aXZlQWN0aXZlU3RhY2tQcm9wcykge1xuICAgICAgc3VwZXIoc2NvcGUsIGlkLCBwcm9wcyk7XG4gICAgICBcbiAgICAgIGNvbnN0IGN1cnJlbnRBcHBTeW5jQ3VzdG9tRG9tYWluID0gcHJvcHM/LmFwcFN5bmNDdXN0b21Eb21haW4/IHByb3BzLmFwcFN5bmNDdXN0b21Eb21haW46JydcbiAgICAgIGNvbnN0IGN1cnJlbnRHcmFwaHFsQVBJQ2VydEFSTiA9IHByb3BzPy5ncmFwaHFsQVBJRG9tYWluTmFtZUNlcnRBUk4/IHByb3BzLmdyYXBocWxBUElEb21haW5OYW1lQ2VydEFSTjogJydcbiAgICAgIGNvbnN0IGN1cnJlbnRUb2RvQVBJSG9zdGVkWm9uZUlEID0gcHJvcHM/LnRvZG9BUElIb3N0ZWRab25lSUQ/IHByb3BzLnRvZG9BUElIb3N0ZWRab25lSUQ6JydcbiAgICAgIGNvbnN0IGN1cnJlbnRUb2RvQVBJSG9zdGVkWm9uZU5hbWUgPSBwcm9wcz8udG9kb0FQSUhvc3RlZFpvbmVOYW1lPyBwcm9wcy50b2RvQVBJSG9zdGVkWm9uZU5hbWU6JydcblxuICAgICAgLyoqIFBBUkFNRVRFUiBGT1IgVE9ETyBHTE9CQUwgVEFCTEUgTkFNRSAqL1xuICAgICAgY29uc3QgdG9kb0dsb2JhbFRhYmxlU3RyZWFtQVJOID0gbmV3IGNkay5DZm5QYXJhbWV0ZXIodGhpcywgXCJ0b2RvR2xvYmFsVGFibGVTdHJlYW1BUk5cIiwge1xuICAgICAgICB0eXBlOiBcIlN0cmluZ1wiLFxuICAgICAgICBkZXNjcmlwdGlvbjogXCJUaGUgQVJOIG9mIHRoZSBUb2RvIEdsb2JhbCBUYWJsZSBTdHJlYW0uXCJcbiAgICAgIH0pO1xuXG4gICAgICAvKiogRFlOQU1PIERCIFRBQkxFICovXG4gICAgICBjb25zdCB0b2RvR2xvYmFsVGFibGUgPSBkeW5hbW9kYi5UYWJsZS5mcm9tVGFibGVBdHRyaWJ1dGVzKHRoaXMsICdUb2RvR2xvYmFsVGFibGUnLCB7XG4gICAgICAgIHRhYmxlTmFtZTogXCJUb2RvR2xvYmFsVGFibGVcIixcbiAgICAgICAgdGFibGVTdHJlYW1Bcm46IHRvZG9HbG9iYWxUYWJsZVN0cmVhbUFSTi52YWx1ZUFzU3RyaW5nXG4gICAgICB9KTtcblxuICAgICAgLyoqIEFQUFNZTkMgTEFNQkRBIEFVVEhPUklaRVIgKi9cbiAgICAgIGNvbnN0IGFwcFN5bmNMYW1iZGFBdXRoID0gbmV3IGxhbWJkYS5GdW5jdGlvbih0aGlzLCAnQXBwU3luY0xhbWJkYUF1dGgnLCB7XG4gICAgICAgIHJ1bnRpbWU6IGxhbWJkYS5SdW50aW1lLk5PREVKU18xNl9YLFxuICAgICAgICBoYW5kbGVyOiAnaW5kZXguaGFuZGxlcicsXG4gICAgICAgIGNvZGU6IGxhbWJkYS5Db2RlLmZyb21Bc3NldChwYXRoLmpvaW4ocGF0aC5yZXNvbHZlKCcuLycpLCcvbGFtYmRhcy9hcHBzeW5jLWF1dGgnKSksXG4gICAgICAgIHRyYWNpbmc6IGxhbWJkYS5UcmFjaW5nLkFDVElWRVxuICAgICAgfSk7XG5cbiAgICAgIC8qKiBBUFBTWU5DIExPRyBDT05GSUcgKi8gICAgXG4gICAgICBjb25zdCBsb2dDb25maWc6IGFwcHN5bmMuTG9nQ29uZmlnID0ge1xuICAgICAgICBleGNsdWRlVmVyYm9zZUNvbnRlbnQ6IGZhbHNlLFxuICAgICAgICBmaWVsZExvZ0xldmVsOiBhcHBzeW5jLkZpZWxkTG9nTGV2ZWwuQUxMLFxuICAgICAgfTsgIFxuICBcbiAgICAgIC8qKiBBUFBTWU5DIEFQSSAqL1xuICAgICAgY29uc3QgdG9kb0dyYXBoUUxBUEkgPSBuZXcgYXBwc3luYy5HcmFwaHFsQXBpKHRoaXMsJ1RvZG9HcmFwaFFMQVBJJywge1xuICAgICAgICBuYW1lOiAnVG9kb0dyYXBoUUxBUEknLFxuICAgICAgICBzY2hlbWE6IGFwcHN5bmMuU2NoZW1hLmZyb21Bc3NldChwYXRoLmpvaW4ocGF0aC5yZXNvbHZlKCcuLycpLCcvYXBwc3luYy1hcGkvc2NoZW1hLmdyYXBocWwnKSksXG4gICAgICAgIGF1dGhvcml6YXRpb25Db25maWc6IHtcbiAgICAgICAgICBkZWZhdWx0QXV0aG9yaXphdGlvbjoge1xuICAgICAgICAgICAgYXV0aG9yaXphdGlvblR5cGU6IGFwcHN5bmMuQXV0aG9yaXphdGlvblR5cGUuTEFNQkRBLFxuICAgICAgICAgICAgbGFtYmRhQXV0aG9yaXplckNvbmZpZzoge1xuICAgICAgICAgICAgICBoYW5kbGVyOiBhcHBTeW5jTGFtYmRhQXV0aFxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAgbG9nQ29uZmlnLFxuICAgICAgICB4cmF5RW5hYmxlZDogdHJ1ZSxcbiAgICAgIH0pO1xuXG4gICAgICBjb25zdCB0b2RvRHluYW1vREJEYXRhU291cmNlID0gdG9kb0dyYXBoUUxBUEkuYWRkRHluYW1vRGJEYXRhU291cmNlKCdUb2RvRHluYW1vREJEYXRhU291cmNlJywgdG9kb0dsb2JhbFRhYmxlKTtcbiAgICAgIGNvbnN0IHRvZG9Ob25lRGF0YVNvdXJjZSA9IHRvZG9HcmFwaFFMQVBJLmFkZE5vbmVEYXRhU291cmNlKCdUb2RvTm9uZURhdGFTb3VyY2UnKVxuXG4gICAgICAvKiogQVBQU1lOQyBSRVNPTFZFUlMgKi9cbiAgICAgIHRvZG9EeW5hbW9EQkRhdGFTb3VyY2UuY3JlYXRlUmVzb2x2ZXIoe1xuICAgICAgICB0eXBlTmFtZTogJ011dGF0aW9uJyxcbiAgICAgICAgZmllbGROYW1lOiAnYWRkVG9kbycsXG4gICAgICAgIHJlcXVlc3RNYXBwaW5nVGVtcGxhdGU6IGFwcHN5bmMuTWFwcGluZ1RlbXBsYXRlLmZyb21GaWxlKCdhcHBzeW5jLWFwaS9yZXNvbHZlcnMvTXV0YXRpb24uQWRkVG9kby5yZXEudnRsJyksXG4gICAgICAgIHJlc3BvbnNlTWFwcGluZ1RlbXBsYXRlOiBhcHBzeW5jLk1hcHBpbmdUZW1wbGF0ZS5mcm9tRmlsZSgnYXBwc3luYy1hcGkvcmVzb2x2ZXJzL011dGF0aW9uLkFkZFRvZG8ucmVzcC52dGwnKVxuICAgICAgfSk7XG5cbiAgICAgIHRvZG9EeW5hbW9EQkRhdGFTb3VyY2UuY3JlYXRlUmVzb2x2ZXIoe1xuICAgICAgICB0eXBlTmFtZTogJ011dGF0aW9uJyxcbiAgICAgICAgZmllbGROYW1lOiAndXBkYXRlVG9kbycsXG4gICAgICAgIHJlcXVlc3RNYXBwaW5nVGVtcGxhdGU6IGFwcHN5bmMuTWFwcGluZ1RlbXBsYXRlLmZyb21GaWxlKCdhcHBzeW5jLWFwaS9yZXNvbHZlcnMvTXV0YXRpb24uVXBkYXRlVG9kby5yZXEudnRsJyksXG4gICAgICAgIHJlc3BvbnNlTWFwcGluZ1RlbXBsYXRlOiBhcHBzeW5jLk1hcHBpbmdUZW1wbGF0ZS5mcm9tRmlsZSgnYXBwc3luYy1hcGkvcmVzb2x2ZXJzL011dGF0aW9uLlVwZGF0ZVRvZG8ucmVzcC52dGwnKVxuICAgICAgfSk7XG4gIFxuICAgICAgdG9kb0R5bmFtb0RCRGF0YVNvdXJjZS5jcmVhdGVSZXNvbHZlcih7XG4gICAgICAgIHR5cGVOYW1lOiAnTXV0YXRpb24nLFxuICAgICAgICBmaWVsZE5hbWU6ICdkZWxldGVUb2RvJyxcbiAgICAgICAgcmVxdWVzdE1hcHBpbmdUZW1wbGF0ZTogYXBwc3luYy5NYXBwaW5nVGVtcGxhdGUuZnJvbUZpbGUoJ2FwcHN5bmMtYXBpL3Jlc29sdmVycy9NdXRhdGlvbi5EZWxldGVUb2RvLnJlcS52dGwnKSxcbiAgICAgICAgcmVzcG9uc2VNYXBwaW5nVGVtcGxhdGU6IGFwcHN5bmMuTWFwcGluZ1RlbXBsYXRlLmZyb21GaWxlKCdhcHBzeW5jLWFwaS9yZXNvbHZlcnMvTXV0YXRpb24uRGVsZXRlVG9kby5yZXNwLnZ0bCcpXG4gICAgICB9KTtcblxuICAgICAgdG9kb05vbmVEYXRhU291cmNlLmNyZWF0ZVJlc29sdmVyKHtcbiAgICAgICAgdHlwZU5hbWU6ICdNdXRhdGlvbicsXG4gICAgICAgIGZpZWxkTmFtZTogJ2FkZFRvZG9HbG9iYWxTeW5jJyxcbiAgICAgICAgcmVxdWVzdE1hcHBpbmdUZW1wbGF0ZTogYXBwc3luYy5NYXBwaW5nVGVtcGxhdGUuZnJvbUZpbGUoJ2FwcHN5bmMtYXBpL3Jlc29sdmVycy9NdXRhdGlvbi5BZGRUb2RvR2xvYmFsU3luYy5yZXEudnRsJyksXG4gICAgICAgIHJlc3BvbnNlTWFwcGluZ1RlbXBsYXRlOiBhcHBzeW5jLk1hcHBpbmdUZW1wbGF0ZS5mcm9tRmlsZSgnYXBwc3luYy1hcGkvcmVzb2x2ZXJzL011dGF0aW9uLkFkZFRvZG9HbG9iYWxTeW5jLnJlc3AudnRsJylcbiAgICAgIH0pO1xuICBcbiAgICAgIHRvZG9Ob25lRGF0YVNvdXJjZS5jcmVhdGVSZXNvbHZlcih7XG4gICAgICAgIHR5cGVOYW1lOiAnTXV0YXRpb24nLFxuICAgICAgICBmaWVsZE5hbWU6ICdkZWxldGVUb2RvR2xvYmFsU3luYycsXG4gICAgICAgIHJlcXVlc3RNYXBwaW5nVGVtcGxhdGU6IGFwcHN5bmMuTWFwcGluZ1RlbXBsYXRlLmZyb21GaWxlKCdhcHBzeW5jLWFwaS9yZXNvbHZlcnMvTXV0YXRpb24uRGVsZXRlVG9kb0dsb2JhbFN5bmMucmVxLnZ0bCcpLFxuICAgICAgICByZXNwb25zZU1hcHBpbmdUZW1wbGF0ZTogYXBwc3luYy5NYXBwaW5nVGVtcGxhdGUuZnJvbUZpbGUoJ2FwcHN5bmMtYXBpL3Jlc29sdmVycy9NdXRhdGlvbi5EZWxldGVUb2RvR2xvYmFsU3luYy5yZXNwLnZ0bCcpXG4gICAgICB9KTtcblxuICAgICAgdG9kb05vbmVEYXRhU291cmNlLmNyZWF0ZVJlc29sdmVyKHtcbiAgICAgICAgdHlwZU5hbWU6ICdNdXRhdGlvbicsXG4gICAgICAgIGZpZWxkTmFtZTogJ3VwZGF0ZVRvZG9HbG9iYWxTeW5jJyxcbiAgICAgICAgcmVxdWVzdE1hcHBpbmdUZW1wbGF0ZTogYXBwc3luYy5NYXBwaW5nVGVtcGxhdGUuZnJvbUZpbGUoJ2FwcHN5bmMtYXBpL3Jlc29sdmVycy9NdXRhdGlvbi5VcGRhdGVUb2RvR2xvYmFsU3luYy5yZXEudnRsJyksXG4gICAgICAgIHJlc3BvbnNlTWFwcGluZ1RlbXBsYXRlOiBhcHBzeW5jLk1hcHBpbmdUZW1wbGF0ZS5mcm9tRmlsZSgnYXBwc3luYy1hcGkvcmVzb2x2ZXJzL011dGF0aW9uLlVwZGF0ZVRvZG9HbG9iYWxTeW5jLnJlc3AudnRsJylcbiAgICAgIH0pO1xuICBcbiAgICAgIHRvZG9EeW5hbW9EQkRhdGFTb3VyY2UuY3JlYXRlUmVzb2x2ZXIoe1xuICAgICAgICB0eXBlTmFtZTogJ1F1ZXJ5JyxcbiAgICAgICAgZmllbGROYW1lOiAnZ2V0VG9kbycsXG4gICAgICAgIHJlcXVlc3RNYXBwaW5nVGVtcGxhdGU6IGFwcHN5bmMuTWFwcGluZ1RlbXBsYXRlLmZyb21GaWxlKCdhcHBzeW5jLWFwaS9yZXNvbHZlcnMvUXVlcnkuR2V0VG9kby5yZXEudnRsJyksXG4gICAgICAgIHJlc3BvbnNlTWFwcGluZ1RlbXBsYXRlOiBhcHBzeW5jLk1hcHBpbmdUZW1wbGF0ZS5mcm9tRmlsZSgnYXBwc3luYy1hcGkvcmVzb2x2ZXJzL1F1ZXJ5LkdldFRvZG8ucmVzcC52dGwnKVxuICAgICAgfSk7XG4gIFxuICAgICAgdG9kb0R5bmFtb0RCRGF0YVNvdXJjZS5jcmVhdGVSZXNvbHZlcih7XG4gICAgICAgIHR5cGVOYW1lOiAnUXVlcnknLFxuICAgICAgICBmaWVsZE5hbWU6ICdsaXN0VG9kb3MnLFxuICAgICAgICByZXF1ZXN0TWFwcGluZ1RlbXBsYXRlOiBhcHBzeW5jLk1hcHBpbmdUZW1wbGF0ZS5mcm9tRmlsZSgnYXBwc3luYy1hcGkvcmVzb2x2ZXJzL1F1ZXJ5Lkxpc3RUb2Rvcy5yZXEudnRsJyksXG4gICAgICAgIHJlc3BvbnNlTWFwcGluZ1RlbXBsYXRlOiBhcHBzeW5jLk1hcHBpbmdUZW1wbGF0ZS5mcm9tRmlsZSgnYXBwc3luYy1hcGkvcmVzb2x2ZXJzL1F1ZXJ5Lkxpc3RUb2Rvcy5yZXNwLnZ0bCcpXG4gICAgICB9KTtcblxuICAgICAgLyoqICBDT05GSUdVUkUgQ1VTVE9NIERPTUFJTiAqL1xuICAgICAgY29uc3QgZ3JhcGhxbEFQSURvbWFpbk5hbWVDZXJ0ID0gY2VydGlmaWNhdGVtYWFuZ2VyLkNlcnRpZmljYXRlLmZyb21DZXJ0aWZpY2F0ZUFybih0aGlzLCdHcmFwaHFsQVBJRG9tYWluTmFtZUNlcnQnLCBjdXJyZW50R3JhcGhxbEFQSUNlcnRBUk4pXG4gICAgICBjb25zdCBncmFwaHFsQVBJQ3VzdG9tRG9tYWluID0gbmV3IGNkay5hd3NfYXBwc3luYy5DZm5Eb21haW5OYW1lKFxuICAgICAgICB0aGlzLFxuICAgICAgICAnR3JhcGhxbEFQSUN1c3RvbURvbWFpbicsXG4gICAgICAgIHtcbiAgICAgICAgICBjZXJ0aWZpY2F0ZUFybjogZ3JhcGhxbEFQSURvbWFpbk5hbWVDZXJ0LmNlcnRpZmljYXRlQXJuLFxuICAgICAgICAgIGRvbWFpbk5hbWU6IGN1cnJlbnRBcHBTeW5jQ3VzdG9tRG9tYWluXG4gICAgICAgIH1cbiAgICAgICk7XG5cbiAgICAgIGNvbnN0IGFwcFN5bmNDdXN0b21Eb21haW5Bc3NvYyA9IG5ldyBjZGsuYXdzX2FwcHN5bmMuQ2ZuRG9tYWluTmFtZUFwaUFzc29jaWF0aW9uKFxuICAgICAgICB0aGlzLCBcbiAgICAgICAgJ0FwcFN5bmNDdXN0b21Eb21haW5Bc3NvYycsXG4gICAgICAgIHtcbiAgICAgICAgICBhcGlJZDogdG9kb0dyYXBoUUxBUEkuYXBpSWQsXG4gICAgICAgICAgZG9tYWluTmFtZTogZ3JhcGhxbEFQSUN1c3RvbURvbWFpbi5kb21haW5OYW1lXG4gICAgICAgIH1cbiAgICAgICk7XG5cbiAgICAgIGFwcFN5bmNDdXN0b21Eb21haW5Bc3NvYy5hZGREZXBlbmRzT24oZ3JhcGhxbEFQSUN1c3RvbURvbWFpbik7XG5cbiAgICAgIC8vQWRkaW5nIFJvdXRlIDUzIFJlY29yZHMgZm9yIHRoZSBjdXN0b20gZG9tYWluXG4gICAgICBjb25zdCB0b2RvQVBJRG9tYWluTmFtZUhvc3RlZFpvbmUgPSByb3V0ZTUzLkhvc3RlZFpvbmUuZnJvbUhvc3RlZFpvbmVBdHRyaWJ1dGVzKHRoaXMsICdUb2RvQVBJRG9tYWluTmFtZUhvc3RlZFpvbmUnLCB7XG4gICAgICAgIGhvc3RlZFpvbmVJZDogY3VycmVudFRvZG9BUElIb3N0ZWRab25lSUQsXG4gICAgICAgIHpvbmVOYW1lOiBjdXJyZW50VG9kb0FQSUhvc3RlZFpvbmVOYW1lXG4gICAgICB9KTtcbiAgXG4gICAgICBjb25zdCBhcHBTeW5jRE5TQ29uZmlncyA9IG5ldyByb3V0ZTUzLkNuYW1lUmVjb3JkKHRoaXMsICdBcHBTeW5jRE5TQ29uZmlncycse1xuICAgICAgICByZWNvcmROYW1lOiBjdXJyZW50QXBwU3luY0N1c3RvbURvbWFpbi5zcGxpdCgnLicpWzBdLFxuICAgICAgICB6b25lOiB0b2RvQVBJRG9tYWluTmFtZUhvc3RlZFpvbmUsXG4gICAgICAgIGRvbWFpbk5hbWU6IGdyYXBocWxBUElDdXN0b21Eb21haW4uYXR0ckFwcFN5bmNEb21haW5OYW1lXG4gICAgICB9KVxuXG4gICAgICAvKiogIExBTUJEQSBTVFJFQU0gUFJPQ0VTU09SIEVYRUNVVElPTiBST0xFICovXG4gICAgICBjb25zdCB0b2RvRERTdHJlYW1MYW1iZGFFeGVjUm9sZSA9IG5ldyBpYW0uUm9sZSh0aGlzLCdUb2RvRERTdHJlYW1MYW1iZGFFeGVjUm9sZScse1xuICAgICAgICBhc3N1bWVkQnk6IG5ldyBpYW0uU2VydmljZVByaW5jaXBhbCgnbGFtYmRhLmFtYXpvbmF3cy5jb20nKSxcbiAgICAgICAgbWFuYWdlZFBvbGljaWVzOiBbXG4gICAgICAgICAgaWFtLk1hbmFnZWRQb2xpY3kuZnJvbUF3c01hbmFnZWRQb2xpY3lOYW1lKCdBV1NBcHBTeW5jSW52b2tlRnVsbEFjY2VzcycpLFxuICAgICAgICAgIGlhbS5NYW5hZ2VkUG9saWN5LmZyb21Bd3NNYW5hZ2VkUG9saWN5TmFtZSgnQ2xvdWRXYXRjaExvZ3NGdWxsQWNjZXNzJylcbiAgICAgICAgXVxuICAgICAgfSlcblxuICAgICAgLyoqICBMQU1CREEgU1RSRUFNIFBST0NFU1NPUiAqL1xuICAgICAgY29uc3QgdG9kb0REU3RyZWFtTGFtYmRhID0gbmV3IGxhbWJkYS5GdW5jdGlvbih0aGlzLCAnVG9kb0REU3RyZWFtTGFtYmRhJywge1xuICAgICAgICBydW50aW1lOiBsYW1iZGEuUnVudGltZS5OT0RFSlNfMTZfWCxcbiAgICAgICAgaGFuZGxlcjogJ2luZGV4LmhhbmRsZXInLFxuICAgICAgICBjb2RlOiBsYW1iZGEuQ29kZS5mcm9tQXNzZXQocGF0aC5qb2luKHBhdGgucmVzb2x2ZSgnLi8nKSwnL2xhbWJkYXMvZGRiLXN0cmVhbS1wcm9jZXNzb3InKSksXG4gICAgICAgIHJvbGU6IHRvZG9ERFN0cmVhbUxhbWJkYUV4ZWNSb2xlLFxuICAgICAgICBlbnZpcm9ubWVudDoge1xuICAgICAgICAgICdBcHBTeW5jQVBJRW5kcG9pbnQnOiB0b2RvR3JhcGhRTEFQSS5ncmFwaHFsVXJsLFxuICAgICAgICAgICdBcHBTeW5jQVBJTGFtYmRhQXV0aEtleSc6ICdjdXN0b20tYXV0aG9yaXplZCdcbiAgICAgICAgfSxcbiAgICAgICAgdHJhY2luZzogbGFtYmRhLlRyYWNpbmcuQUNUSVZFXG4gICAgICB9KTtcbiAgXG4gICAgICAvKiogIEFERCBEWU5BTU8gREIgU1RSRUFNIEFTIEVWRU5UIFNPVVJDRSBUTyBMQU1CREEgKi9cbiAgICAgIHRvZG9ERFN0cmVhbUxhbWJkYS5hZGRFdmVudFNvdXJjZShuZXcgRHluYW1vRXZlbnRTb3VyY2UodG9kb0dsb2JhbFRhYmxlLHtcbiAgICAgICAgc3RhcnRpbmdQb3NpdGlvbjogbGFtYmRhLlN0YXJ0aW5nUG9zaXRpb24uVFJJTV9IT1JJWk9OLFxuICAgICAgfSkpXG4gIFxuICAgICAgLyoqIE9VVFBVVCBTVEFDSyBWQUxVRVMgKi9cbiAgICAgIG5ldyBjZGsuQ2ZuT3V0cHV0KHRoaXMsJ0FQSSBVUkwnLCB7dmFsdWU6dG9kb0dyYXBoUUxBUEkuZ3JhcGhxbFVybH0pXG4gICAgICBuZXcgY2RrLkNmbk91dHB1dCh0aGlzLCdUT0RPIFRBQkxFIEFSTicsIHt2YWx1ZTp0b2RvR2xvYmFsVGFibGUudGFibGVBcm59KVxuICAgICAgbmV3IGNkay5DZm5PdXRwdXQodGhpcywnTEFNQkRBIEFSTicsIHt2YWx1ZTp0b2RvRERTdHJlYW1MYW1iZGEuZnVuY3Rpb25Bcm59KVxuICAgICAgICBcbiAgICB9XG59Il19 -------------------------------------------------------------------------------- /appsync-multi-region-api/lib/secondary-appsync-multi-region-active-active-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 4 | import { DynamoEventSource } from 'aws-cdk-lib/aws-lambda-event-sources'; 5 | import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; 6 | import * as iam from 'aws-cdk-lib/aws-iam'; 7 | import * as appsync from '@aws-cdk/aws-appsync-alpha'; 8 | import * as certificatemaanger from 'aws-cdk-lib/aws-certificatemanager'; 9 | import * as route53 from 'aws-cdk-lib/aws-route53'; 10 | import * as path from 'path' 11 | 12 | interface SecondaryAppSyncMultiRegionActiveActiveStackProps extends cdk.StackProps { 13 | primaryRegion?: string, 14 | secondaryRegion?: string, 15 | appSyncCustomDomain?: string, 16 | graphqlAPIDomainNameCertARN?: string, 17 | todoAPIHostedZoneID?: string, 18 | todoAPIHostedZoneName?: string 19 | } 20 | export class SecondaryAppsyncMultiRegionActiveActiveStack extends cdk.Stack { 21 | constructor(scope: Construct, id: string, props?: SecondaryAppSyncMultiRegionActiveActiveStackProps) { 22 | super(scope, id, props); 23 | 24 | const currentAppSyncCustomDomain = props?.appSyncCustomDomain? props.appSyncCustomDomain:'' 25 | const currentGraphqlAPICertARN = props?.graphqlAPIDomainNameCertARN? props.graphqlAPIDomainNameCertARN: '' 26 | const currentTodoAPIHostedZoneID = props?.todoAPIHostedZoneID? props.todoAPIHostedZoneID:'' 27 | const currentTodoAPIHostedZoneName = props?.todoAPIHostedZoneName? props.todoAPIHostedZoneName:'' 28 | 29 | /** PARAMETER FOR TODO GLOBAL TABLE NAME */ 30 | const todoGlobalTableStreamARN = new cdk.CfnParameter(this, "todoGlobalTableStreamARN", { 31 | type: "String", 32 | description: "The ARN of the Todo Global Table Stream." 33 | }); 34 | 35 | /** DYNAMO DB TABLE */ 36 | const todoGlobalTable = dynamodb.Table.fromTableAttributes(this, 'TodoGlobalTable', { 37 | tableName: "TodoGlobalTable", 38 | tableStreamArn: todoGlobalTableStreamARN.valueAsString 39 | }); 40 | 41 | /** APPSYNC LAMBDA AUTHORIZER */ 42 | const appSyncLambdaAuth = new lambda.Function(this, 'AppSyncLambdaAuth', { 43 | runtime: lambda.Runtime.NODEJS_16_X, 44 | handler: 'index.handler', 45 | code: lambda.Code.fromAsset(path.join(path.resolve('./'),'/lambdas/appsync-auth')), 46 | tracing: lambda.Tracing.ACTIVE 47 | }); 48 | 49 | /** APPSYNC LOG CONFIG */ 50 | const logConfig: appsync.LogConfig = { 51 | excludeVerboseContent: false, 52 | fieldLogLevel: appsync.FieldLogLevel.ALL, 53 | }; 54 | 55 | /** APPSYNC API */ 56 | const todoGraphQLAPI = new appsync.GraphqlApi(this,'TodoGraphQLAPI', { 57 | name: 'TodoGraphQLAPI', 58 | schema: appsync.Schema.fromAsset(path.join(path.resolve('./'),'/appsync-api/schema.graphql')), 59 | authorizationConfig: { 60 | defaultAuthorization: { 61 | authorizationType: appsync.AuthorizationType.LAMBDA, 62 | lambdaAuthorizerConfig: { 63 | handler: appSyncLambdaAuth 64 | } 65 | } 66 | }, 67 | logConfig, 68 | xrayEnabled: true, 69 | }); 70 | 71 | const todoDynamoDBDataSource = todoGraphQLAPI.addDynamoDbDataSource('TodoDynamoDBDataSource', todoGlobalTable); 72 | const todoNoneDataSource = todoGraphQLAPI.addNoneDataSource('TodoNoneDataSource') 73 | 74 | /** APPSYNC RESOLVERS */ 75 | todoDynamoDBDataSource.createResolver({ 76 | typeName: 'Mutation', 77 | fieldName: 'addTodo', 78 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.AddTodo.req.vtl'), 79 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.AddTodo.resp.vtl') 80 | }); 81 | 82 | todoDynamoDBDataSource.createResolver({ 83 | typeName: 'Mutation', 84 | fieldName: 'updateTodo', 85 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.UpdateTodo.req.vtl'), 86 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.UpdateTodo.resp.vtl') 87 | }); 88 | 89 | todoDynamoDBDataSource.createResolver({ 90 | typeName: 'Mutation', 91 | fieldName: 'deleteTodo', 92 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.DeleteTodo.req.vtl'), 93 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.DeleteTodo.resp.vtl') 94 | }); 95 | 96 | todoNoneDataSource.createResolver({ 97 | typeName: 'Mutation', 98 | fieldName: 'addTodoGlobalSync', 99 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.AddTodoGlobalSync.req.vtl'), 100 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.AddTodoGlobalSync.resp.vtl') 101 | }); 102 | 103 | todoNoneDataSource.createResolver({ 104 | typeName: 'Mutation', 105 | fieldName: 'deleteTodoGlobalSync', 106 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.DeleteTodoGlobalSync.req.vtl'), 107 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.DeleteTodoGlobalSync.resp.vtl') 108 | }); 109 | 110 | todoNoneDataSource.createResolver({ 111 | typeName: 'Mutation', 112 | fieldName: 'updateTodoGlobalSync', 113 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.UpdateTodoGlobalSync.req.vtl'), 114 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Mutation.UpdateTodoGlobalSync.resp.vtl') 115 | }); 116 | 117 | todoDynamoDBDataSource.createResolver({ 118 | typeName: 'Query', 119 | fieldName: 'getTodo', 120 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Query.GetTodo.req.vtl'), 121 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Query.GetTodo.resp.vtl') 122 | }); 123 | 124 | todoDynamoDBDataSource.createResolver({ 125 | typeName: 'Query', 126 | fieldName: 'listTodos', 127 | requestMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Query.ListTodos.req.vtl'), 128 | responseMappingTemplate: appsync.MappingTemplate.fromFile('appsync-api/resolvers/Query.ListTodos.resp.vtl') 129 | }); 130 | 131 | /** CONFIGURE CUSTOM DOMAIN */ 132 | const graphqlAPIDomainNameCert = certificatemaanger.Certificate.fromCertificateArn(this,'GraphqlAPIDomainNameCert', currentGraphqlAPICertARN) 133 | const graphqlAPICustomDomain = new cdk.aws_appsync.CfnDomainName( 134 | this, 135 | 'GraphqlAPICustomDomain', 136 | { 137 | certificateArn: graphqlAPIDomainNameCert.certificateArn, 138 | domainName: currentAppSyncCustomDomain 139 | } 140 | ); 141 | 142 | const appSyncCustomDomainAssoc = new cdk.aws_appsync.CfnDomainNameApiAssociation( 143 | this, 144 | 'AppSyncCustomDomainAssoc', 145 | { 146 | apiId: todoGraphQLAPI.apiId, 147 | domainName: graphqlAPICustomDomain.domainName 148 | } 149 | ); 150 | 151 | appSyncCustomDomainAssoc.addDependsOn(graphqlAPICustomDomain); 152 | 153 | //Adding Route 53 Records for the custom domain 154 | const todoAPIDomainNameHostedZone = route53.HostedZone.fromHostedZoneAttributes(this, 'TodoAPIDomainNameHostedZone', { 155 | hostedZoneId: currentTodoAPIHostedZoneID, 156 | zoneName: currentTodoAPIHostedZoneName 157 | }); 158 | 159 | const appSyncDNSConfigs = new route53.CnameRecord(this, 'AppSyncDNSConfigs',{ 160 | recordName: currentAppSyncCustomDomain.split('.')[0], 161 | zone: todoAPIDomainNameHostedZone, 162 | domainName: graphqlAPICustomDomain.attrAppSyncDomainName 163 | }) 164 | 165 | /** LAMBDA STREAM PROCESSOR EXECUTION ROLE */ 166 | const todoDDStreamLambdaExecRole = new iam.Role(this,'TodoDDStreamLambdaExecRole',{ 167 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), 168 | managedPolicies: [ 169 | iam.ManagedPolicy.fromAwsManagedPolicyName('AWSAppSyncInvokeFullAccess'), 170 | iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLogsFullAccess') 171 | ] 172 | }) 173 | 174 | /** LAMBDA STREAM PROCESSOR */ 175 | const todoDDStreamLambda = new lambda.Function(this, 'TodoDDStreamLambda', { 176 | runtime: lambda.Runtime.NODEJS_16_X, 177 | handler: 'index.handler', 178 | code: lambda.Code.fromAsset(path.join(path.resolve('./'),'/lambdas/ddb-stream-processor')), 179 | role: todoDDStreamLambdaExecRole, 180 | environment: { 181 | 'AppSyncAPIEndpoint': todoGraphQLAPI.graphqlUrl, 182 | 'AppSyncAPILambdaAuthKey': 'custom-authorized' 183 | }, 184 | tracing: lambda.Tracing.ACTIVE 185 | }); 186 | 187 | /** ADD DYNAMO DB STREAM AS EVENT SOURCE TO LAMBDA */ 188 | todoDDStreamLambda.addEventSource(new DynamoEventSource(todoGlobalTable,{ 189 | startingPosition: lambda.StartingPosition.TRIM_HORIZON, 190 | })) 191 | 192 | /** OUTPUT STACK VALUES */ 193 | new cdk.CfnOutput(this,'API URL', {value:todoGraphQLAPI.graphqlUrl}) 194 | new cdk.CfnOutput(this,'TODO TABLE ARN', {value:todoGlobalTable.tableArn}) 195 | new cdk.CfnOutput(this,'LAMBDA ARN', {value:todoDDStreamLambda.functionArn}) 196 | 197 | } 198 | } -------------------------------------------------------------------------------- /appsync-multi-region-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appsync-multi-region-active-active", 3 | "version": "0.1.0", 4 | "bin": { 5 | "appsync-multi-region-active-active": "bin/appsync-multi-region-active-active.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^27.5.2", 15 | "@types/node": "^10.17.27", 16 | "@types/prettier": "2.6.0", 17 | "aws-cdk": "2.40.0", 18 | "jest": "^27.5.1", 19 | "ts-jest": "^27.1.4", 20 | "ts-node": "^10.9.1", 21 | "typescript": "~3.9.7" 22 | }, 23 | "dependencies": { 24 | "@aws-cdk/aws-appsync-alpha": "^2.40.0-alpha.0", 25 | "constructs": "^10.0.0", 26 | "source-map-support": "^0.5.21" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /appsync-multi-region-api/parameters/globalVariables.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.globalVariables = void 0; 4 | exports.globalVariables = { 5 | route53HostedZoneName: '', 6 | route53HostedZoneID: '', 7 | primaryRegionAppSyncCustomDomain: '', 8 | secondaryRegionAppSyncCustomDomain: '', 9 | primaryRegion: '', 10 | secondaryRegion: '', 11 | domainCertARN: '', 12 | globalAPIEndpoint: '', 13 | route53RoutingPolicyDomainName: '', 14 | }; 15 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ2xvYmFsVmFyaWFibGVzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZ2xvYmFsVmFyaWFibGVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFhLFFBQUEsZUFBZSxHQUFHO0lBQzNCLHFCQUFxQixFQUFFLGVBQWU7SUFDdEMsbUJBQW1CLEVBQUMseUJBQXlCO0lBQzdDLGdDQUFnQyxFQUFFLHVCQUF1QjtJQUN6RCxrQ0FBa0MsRUFBRSx5QkFBeUI7SUFDN0QsYUFBYSxFQUFFLGVBQWU7SUFDOUIsZUFBZSxFQUFFLGVBQWU7SUFDaEMsYUFBYSxFQUFDLG1CQUFtQjtJQUNqQyxpQkFBaUIsRUFBRSxpQ0FBaUM7SUFDcEQsOEJBQThCLEVBQUUsNkJBQTZCO0NBQ2hFLENBQUEiLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgY29uc3QgZ2xvYmFsVmFyaWFibGVzID0ge1xuICAgIHJvdXRlNTNIb3N0ZWRab25lTmFtZTogJzxleGFtcGxlLmNvbT4nLFxuICAgIHJvdXRlNTNIb3N0ZWRab25lSUQ6JzxYWFhYWFhYWFhYWFhYWFhYWFhYWFg+JyxcbiAgICBwcmltYXJ5UmVnaW9uQXBwU3luY0N1c3RvbURvbWFpbjogJzxwcmltYXJ5LmV4YW1wbGUuY29tPicsXG4gICAgc2Vjb25kYXJ5UmVnaW9uQXBwU3luY0N1c3RvbURvbWFpbjogJzxzZWNvbmRhcnkuZXhhbXBsZS5jb20+JyxcbiAgICBwcmltYXJ5UmVnaW9uOiAnPHJlZ2lvbl9jb2RlPicsXG4gICAgc2Vjb25kYXJ5UmVnaW9uOiAnPHJlZ2lvbl9jb2RlPicsXG4gICAgZG9tYWluQ2VydEFSTjonPGNlcnRpZmljYXRlX2Fybj4nLFxuICAgIGdsb2JhbEFQSUVuZHBvaW50OiAnPGdsb2JhbGFwaWVuZHBvaW50LmV4YW1wbGUuY29tPicsXG4gICAgcm91dGU1M1JvdXRpbmdQb2xpY3lEb21haW5OYW1lOiAnPHJvdXRpbmdwb2xpY3kuZXhhbXBsZS5jb20+Jyxcbn0iXX0= -------------------------------------------------------------------------------- /appsync-multi-region-api/parameters/globalVariables.ts: -------------------------------------------------------------------------------- 1 | export const globalVariables = { 2 | route53HostedZoneName: '', 3 | route53HostedZoneID:'', 4 | primaryRegionAppSyncCustomDomain: '', 5 | secondaryRegionAppSyncCustomDomain: '', 6 | primaryRegion: '', 7 | secondaryRegion: '', 8 | domainCertARN:'', 9 | globalAPIEndpoint: '', 10 | route53RoutingPolicyDomainName: '', 11 | } -------------------------------------------------------------------------------- /appsync-multi-region-api/test/appsync-multi-region-active-active.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // import * as cdk from 'aws-cdk-lib'; 3 | // import { Template } from 'aws-cdk-lib/assertions'; 4 | // import * as AppsyncMultiRegionActiveActive from '../lib/appsync-multi-region-active-active-stack'; 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/appsync-multi-region-active-active-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new AppsyncMultiRegionActiveActive.AppsyncMultiRegionActiveActiveStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | // template.hasResourceProperties('AWS::SQS::Queue', { 14 | // VisibilityTimeout: 300 15 | // }); 16 | }); 17 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBwc3luYy1tdWx0aS1yZWdpb24tYWN0aXZlLWFjdGl2ZS50ZXN0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiYXBwc3luYy1tdWx0aS1yZWdpb24tYWN0aXZlLWFjdGl2ZS50ZXN0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQSxzQ0FBc0M7QUFDdEMscURBQXFEO0FBQ3JELHFHQUFxRztBQUVyRyx1RUFBdUU7QUFDdkUsc0VBQXNFO0FBQ3RFLElBQUksQ0FBQyxtQkFBbUIsRUFBRSxHQUFHLEVBQUU7SUFDL0IsK0JBQStCO0lBQy9CLGNBQWM7SUFDZCw4R0FBOEc7SUFDOUcsY0FBYztJQUNkLGdEQUFnRDtJQUVoRCx3REFBd0Q7SUFDeEQsNkJBQTZCO0lBQzdCLFFBQVE7QUFDUixDQUFDLENBQUMsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIGltcG9ydCAqIGFzIGNkayBmcm9tICdhd3MtY2RrLWxpYic7XG4vLyBpbXBvcnQgeyBUZW1wbGF0ZSB9IGZyb20gJ2F3cy1jZGstbGliL2Fzc2VydGlvbnMnO1xuLy8gaW1wb3J0ICogYXMgQXBwc3luY011bHRpUmVnaW9uQWN0aXZlQWN0aXZlIGZyb20gJy4uL2xpYi9hcHBzeW5jLW11bHRpLXJlZ2lvbi1hY3RpdmUtYWN0aXZlLXN0YWNrJztcblxuLy8gZXhhbXBsZSB0ZXN0LiBUbyBydW4gdGhlc2UgdGVzdHMsIHVuY29tbWVudCB0aGlzIGZpbGUgYWxvbmcgd2l0aCB0aGVcbi8vIGV4YW1wbGUgcmVzb3VyY2UgaW4gbGliL2FwcHN5bmMtbXVsdGktcmVnaW9uLWFjdGl2ZS1hY3RpdmUtc3RhY2sudHNcbnRlc3QoJ1NRUyBRdWV1ZSBDcmVhdGVkJywgKCkgPT4ge1xuLy8gICBjb25zdCBhcHAgPSBuZXcgY2RrLkFwcCgpO1xuLy8gICAgIC8vIFdIRU5cbi8vICAgY29uc3Qgc3RhY2sgPSBuZXcgQXBwc3luY011bHRpUmVnaW9uQWN0aXZlQWN0aXZlLkFwcHN5bmNNdWx0aVJlZ2lvbkFjdGl2ZUFjdGl2ZVN0YWNrKGFwcCwgJ015VGVzdFN0YWNrJyk7XG4vLyAgICAgLy8gVEhFTlxuLy8gICBjb25zdCB0ZW1wbGF0ZSA9IFRlbXBsYXRlLmZyb21TdGFjayhzdGFjayk7XG5cbi8vICAgdGVtcGxhdGUuaGFzUmVzb3VyY2VQcm9wZXJ0aWVzKCdBV1M6OlNRUzo6UXVldWUnLCB7XG4vLyAgICAgVmlzaWJpbGl0eVRpbWVvdXQ6IDMwMFxuLy8gICB9KTtcbn0pO1xuIl19 -------------------------------------------------------------------------------- /appsync-multi-region-api/test/appsync-multi-region-active-active.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as AppsyncMultiRegionActiveActive from '../lib/appsync-multi-region-active-active-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/appsync-multi-region-active-active-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new AppsyncMultiRegionActiveActive.AppsyncMultiRegionActiveActiveStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /appsync-multi-region-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2018" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /images/appsync-multi-region-active-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-multi-region-active-active/d169f0eed31fc279f5965b886d35d903e1f4387d/images/appsync-multi-region-active-active.png -------------------------------------------------------------------------------- /images/postman-body-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-multi-region-active-active/d169f0eed31fc279f5965b886d35d903e1f4387d/images/postman-body-config.png -------------------------------------------------------------------------------- /images/postman-header-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-multi-region-active-active/d169f0eed31fc279f5965b886d35d903e1f4387d/images/postman-header-config.png -------------------------------------------------------------------------------- /images/postman-mutation-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-multi-region-active-active/d169f0eed31fc279f5965b886d35d903e1f4387d/images/postman-mutation-result.png -------------------------------------------------------------------------------- /images/postman-query-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-multi-region-active-active/d169f0eed31fc279f5965b886d35d903e1f4387d/images/postman-query-result.png -------------------------------------------------------------------------------- /images/postman-subscription-connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-multi-region-active-active/d169f0eed31fc279f5965b886d35d903e1f4387d/images/postman-subscription-connection.png -------------------------------------------------------------------------------- /images/postman-subscription-headers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-multi-region-active-active/d169f0eed31fc279f5965b886d35d903e1f4387d/images/postman-subscription-headers.png -------------------------------------------------------------------------------- /images/postman-subscription-init-connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-multi-region-active-active/d169f0eed31fc279f5965b886d35d903e1f4387d/images/postman-subscription-init-connection.png -------------------------------------------------------------------------------- /images/postman-subscription-notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-multi-region-active-active/d169f0eed31fc279f5965b886d35d903e1f4387d/images/postman-subscription-notification.png -------------------------------------------------------------------------------- /images/postman-subscription-params.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-multi-region-active-active/d169f0eed31fc279f5965b886d35d903e1f4387d/images/postman-subscription-params.png -------------------------------------------------------------------------------- /images/postman-subscription-query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-multi-region-active-active/d169f0eed31fc279f5965b886d35d903e1f4387d/images/postman-subscription-query.png -------------------------------------------------------------------------------- /images/postman-subscription-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-multi-region-active-active/d169f0eed31fc279f5965b886d35d903e1f4387d/images/postman-subscription-url.png -------------------------------------------------------------------------------- /images/sample-todo-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-multi-region-active-active/d169f0eed31fc279f5965b886d35d903e1f4387d/images/sample-todo-app.png -------------------------------------------------------------------------------- /sample-todo-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | #amplify-do-not-edit-begin 26 | amplify/\#current-cloud-backend 27 | amplify/.config/local-* 28 | amplify/logs 29 | amplify/mock-data 30 | amplify/backend/amplify-meta.json 31 | amplify/backend/.temp 32 | build/ 33 | dist/ 34 | node_modules/ 35 | aws-exports.js 36 | awsconfiguration.json 37 | amplifyconfiguration.json 38 | amplifyconfiguration.dart 39 | amplify-build-config.json 40 | amplify-gradle-config.json 41 | amplifytools.xcconfig 42 | .secret-* 43 | **.sample 44 | #amplify-do-not-edit-end 45 | 46 | #amazon ospo ruleset file 47 | amazon-ospo-ruleset.json 48 | .graphqlconfig.yml -------------------------------------------------------------------------------- /sample-todo-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todoapp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@aws-amplify/ui-react": "^3.5.8", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "aws-amplify": "^4.3.39", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-icons": "^3.11.0", 14 | "react-scripts": "5.0.1", 15 | "web-vitals": "^2.1.4" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } -------------------------------------------------------------------------------- /sample-todo-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-multi-region-active-active/d169f0eed31fc279f5965b886d35d903e1f4387d/sample-todo-app/public/favicon.ico -------------------------------------------------------------------------------- /sample-todo-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /sample-todo-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-multi-region-active-active/d169f0eed31fc279f5965b886d35d903e1f4387d/sample-todo-app/public/logo192.png -------------------------------------------------------------------------------- /sample-todo-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-multi-region-active-active/d169f0eed31fc279f5965b886d35d903e1f4387d/sample-todo-app/public/logo512.png -------------------------------------------------------------------------------- /sample-todo-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /sample-todo-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /sample-todo-app/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sample-todo-app/src/App.js: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import { List } from './components/List'; 3 | 4 | 5 | function App() { 6 | 7 | return ( 8 |
9 | 10 |
11 | 12 | ); 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /sample-todo-app/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /sample-todo-app/src/components/Form.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | TextField, 4 | SelectField, 5 | TextAreaField, 6 | Text, 7 | Heading 8 | } from '@aws-amplify/ui-react'; 9 | import { addTodo } from '../graphql/mutations' 10 | import { API } from 'aws-amplify' 11 | import '@aws-amplify/ui-react/styles.css'; 12 | 13 | 14 | export const Form = (props) => { 15 | 16 | const handleSubmit = async (e) => { 17 | e.preventDefault() 18 | const { target } = e; 19 | 20 | try { 21 | const { data } = await API.graphql({ 22 | query: addTodo, 23 | variables: { 24 | input: { 25 | id: Math.floor(Math.random() * 10000), 26 | name: target.todoName.value, 27 | description: target.description.value, 28 | priority: parseInt(target.priority.value), 29 | status: target.status.value 30 | } 31 | }, 32 | authToken: "custom-authorized" 33 | }) 34 | 35 | props.setTodoData((curTodoData) => { 36 | return [...curTodoData, data.addTodo] 37 | 38 | }) 39 | //to clean the input after submit 40 | target.todoName.value = '' 41 | target.description.value = '' 42 | target.priority.value = '' 43 | target.status.value = 'Select Status' 44 | } catch (error) { 45 | console.log(error) 46 | } 47 | } 48 | 49 | return ( 50 | <> 51 | My Todos 52 |
53 | 54 | 59 | Name 60 | } 61 | placeholder='Add a name' 62 | name='todoName' 63 | 64 | variation="primary" 65 | /> 66 | 71 | Description 72 | } 73 | placeholder='Add a description' 74 | name='description' 75 | 76 | variation="primary" 77 | /> 78 | Priority } name="priority" id="priority" > 79 | 80 | 81 | 82 | 83 | 84 | 85 | Status } name="status" id="status"> 86 | 87 | 88 | 89 | 90 | 93 | 94 | 95 | ); 96 | } 97 | 98 | -------------------------------------------------------------------------------- /sample-todo-app/src/components/List.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Form } from './Form'; 3 | import { Todo } from './Todo'; 4 | import { Heading, Card, ScrollView } from '@aws-amplify/ui-react'; 5 | import '@aws-amplify/ui-react/styles.css'; 6 | import { deleteTodo } from '../graphql/mutations'; 7 | import { API } from 'aws-amplify'; 8 | import { Subscriptions } from './Subscriptions' 9 | 10 | 11 | //container for the formular and the table 12 | export const List = () => { 13 | const [todoData, setTodoData] = useState([]) 14 | const handleTodoDelete = async (todoId) => { 15 | 16 | const newTodoData = todoData.filter((todo) => todo.id !== todoId) 17 | 18 | try { 19 | await API.graphql({ 20 | query: deleteTodo, 21 | variables: { 22 | input: { 23 | id: todoId 24 | } 25 | }, 26 | authToken: "custom-authorized" 27 | }) 28 | setTodoData(newTodoData) 29 | } 30 | catch (error) { 31 | console.log(error) 32 | } 33 | } 34 | 35 | return ( 36 |
37 | < div id="header" > AWS AppSync Multi-Region Active-Active Demo
38 |
39 | 40 |
42 | 43 | 47 | 48 | 49 |
50 |
51 | 52 | 53 | 54 |
55 | 56 | 57 | 58 | ); 59 | } 60 | 61 | -------------------------------------------------------------------------------- /sample-todo-app/src/components/Subscriptions.js: -------------------------------------------------------------------------------- 1 | import { onAddTodo } from '../graphql/subscriptions' 2 | import { API } from 'aws-amplify' 3 | import { useState, useEffect } from 'react'; 4 | import { Heading, Alert } from '@aws-amplify/ui-react'; 5 | import '@aws-amplify/ui-react/styles.css'; 6 | import { CONNECTION_STATE_CHANGE } from '@aws-amplify/pubsub'; 7 | import { Hub } from 'aws-amplify'; 8 | import { Badge } from '@aws-amplify/ui-react'; 9 | 10 | export const Subscriptions = () => { 11 | 12 | const [newTodoSubscriptionMessage, setNewTodoSubscriptionMessage] = useState([]) 13 | const [subscriptionVar, setSubscriptionVar] = useState() 14 | const [connectionVar, setConnectionVar] = useState('Disconnected') 15 | 16 | useEffect(() => { 17 | Hub.listen('api', (data) => { 18 | const { payload } = data; 19 | if (payload.event === CONNECTION_STATE_CHANGE) { 20 | setConnectionVar(payload.data.connectionState); 21 | console.log(connectionVar); 22 | } 23 | }); 24 | }, [connectionVar]); 25 | 26 | const handleTodoSubscription = async () => { 27 | try { 28 | const res = await API.graphql({ 29 | query: onAddTodo, 30 | authToken: "custom-authorized" 31 | }) 32 | .subscribe({ 33 | next: newTodoSubMessage => { 34 | const todoDetails = { 35 | name: newTodoSubMessage.value.data.onAddTodo.name, 36 | description: newTodoSubMessage.value.data.onAddTodo.description, 37 | priority: newTodoSubMessage.value.data.onAddTodo.priority, 38 | createdAt: newTodoSubMessage.value.data.onAddTodo.createdAt, 39 | timeReceived: new Date().toJSON(), 40 | createdInRegion: newTodoSubMessage.value.data.onAddTodo.createdInRegion, 41 | status: newTodoSubMessage.value.data.onAddTodo.status, 42 | } 43 | 44 | setNewTodoSubscriptionMessage((curTodoMessage) => { 45 | return [...curTodoMessage, JSON.stringify(todoDetails)] 46 | }) 47 | } 48 | }) 49 | 50 | setSubscriptionVar(res) 51 | 52 | } catch (error) { 53 | console.log(error) 54 | } 55 | } 56 | 57 | const handleTodoUnSubscription = async () => { 58 | try { 59 | subscriptionVar.unsubscribe() 60 | setSubscriptionVar(false) 61 | } catch (error) { 62 | console.log(error) 63 | } 64 | } 65 | 66 | return ( 67 |
68 | Todo Notifications 69 | {connectionVar === "Connected" ? {connectionVar} : {connectionVar}} 70 | {subscriptionVar ? : 71 | } 72 |

Subscribe to the new todos

73 |
74 | {newTodoSubscriptionMessage ? newTodoSubscriptionMessage.map((row) => (
{row}
)) : ''} 75 |
76 |
77 | 78 | ); 79 | } 80 | 81 | 82 | -------------------------------------------------------------------------------- /sample-todo-app/src/components/Todo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Table, 4 | TableCell, 5 | TableBody, 6 | TableHead, 7 | TableRow, 8 | } from '@aws-amplify/ui-react'; 9 | import { BsXOctagonFill } from "react-icons/bs"; 10 | import { useEffect } from 'react'; 11 | import { API } from 'aws-amplify' 12 | import { listTodos } from '../graphql/queries' 13 | 14 | export const Todo = (props,) => { 15 | const { todoData } = props; 16 | useEffect(() => { 17 | try { 18 | const fetchTodos = async () => { 19 | const res = await API.graphql({ 20 | query: listTodos, 21 | authToken: "custom-authorized" 22 | }) 23 | return res.data.listTodos.todos 24 | } 25 | 26 | fetchTodos().then(todos => props.setTodoData(todos)) 27 | 28 | } catch (error) { 29 | console.log(error) 30 | } 31 | 32 | }, []) 33 | 34 | return ( 35 | 40 | 41 | 42 | Name 43 | Description 44 | Priority 45 | Created At 46 | Created In 47 | Status 48 | Remove 49 | 50 | 51 | 52 | {todoData.map((row) => ( 53 | 57 | {row.name} 58 | {row.description} 59 | {row.priority} 60 | {row.createdAt} 61 | {row.createdInRegion} 62 | {row.status} 63 | 64 | props.removeTodo(row.id)} 66 | className='delete-icon' 67 | cursor="pointer" 68 | /> 69 | 70 | 71 | ))} 72 | 73 |
74 | ) 75 | 76 | } 77 | -------------------------------------------------------------------------------- /sample-todo-app/src/index.css: -------------------------------------------------------------------------------- 1 | :root, 2 | [data-amplify-theme] { 3 | --amplify-green: hsl(130, 43%, 46%); 4 | } 5 | 6 | #app { 7 | margin: 2% 2%; 8 | } 9 | 10 | 11 | .main-card { 12 | max-height: 40%; 13 | width: 49%; 14 | margin: 0.5% 0.5%; 15 | float: left; 16 | } 17 | 18 | .sub-card { 19 | float: right; 20 | max-height: 40%; 21 | width: 49%; 22 | margin: 0.5% 0.5%; 23 | } 24 | 25 | #header { 26 | background: var(--amplify-green); 27 | height: 60px; 28 | text-align: center; 29 | } 30 | 31 | form { 32 | margin: 1% 27%; 33 | text-align: left; 34 | } 35 | 36 | .button { 37 | cursor: pointer; 38 | width: 100%; 39 | background: var(--amplify-green); 40 | border: none; 41 | font-weight: 700; 42 | font-size: larger; 43 | padding: 10px; 44 | color: white; 45 | margin: 20px 0; 46 | border-radius: 5px; 47 | } 48 | 49 | 50 | .button-sub { 51 | cursor: pointer; 52 | padding: 10px; 53 | font-weight: 700; 54 | font-size: larger; 55 | border-color: var(--amplify-green); 56 | float: right; 57 | background: white; 58 | margin: 20px 0; 59 | border-radius: 5px; 60 | margin: 20px 10px; 61 | } -------------------------------------------------------------------------------- /sample-todo-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import config from './aws-exports' 6 | import Amplify from 'aws-amplify' 7 | 8 | Amplify.configure(config) 9 | 10 | const root = ReactDOM.createRoot(document.getElementById('root')); 11 | root.render( 12 | 13 | 14 | 15 | ); 16 | 17 | -------------------------------------------------------------------------------- /sample-todo-app/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample-todo-app/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /sample-todo-app/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | --------------------------------------------------------------------------------