├── .eslintignore ├── documentation └── webhook-event.png ├── config └── deployment.yml ├── .eslintrc ├── package.json ├── src ├── common │ ├── adapters │ │ └── EventBridgeAdapter.js │ └── services │ │ └── OrderEventsService.js └── sendOrderEvent │ └── function.js ├── LICENSE ├── .gitignore ├── README.md └── serverless.yml /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .serverless 4 | .webpack 5 | **/*.json 6 | **/*.yml 7 | **/*.env 8 | **/*.md 9 | **/*.sh 10 | -------------------------------------------------------------------------------- /documentation/webhook-event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverlesspolska/eventbridge-api-destinations-mailchimp/HEAD/documentation/webhook-event.png -------------------------------------------------------------------------------- /config/deployment.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | myProfile: 3 | dev: default 4 | prod: default 5 | myRegion: 6 | dev: eu-west-2 # London 7 | prod: eu-west-2 # London -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | env: 2 | commonjs: true 3 | es6: true 4 | node: true 5 | jest: true 6 | extends: 7 | - airbnb-base 8 | globals: 9 | Atomics: readonly 10 | SharedArrayBuffer: readonly 11 | parserOptions: 12 | ecmaVersion: 2018 13 | rules: 14 | semi: off 15 | line-break-style: off 16 | linebreak-style: off 17 | no-use-before-define: off 18 | import/prefer-default-export: off 19 | comma-dangle: off 20 | no-console: off 21 | import/no-extraneous-dependencies: off -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eb-api-dest-sample", 3 | "version": "1.0.0", 4 | "description": "Sample project to demonstrate how to use Event Bridge API Destinations to integrate with Mailchimp", 5 | "scripts": { 6 | "eslint": "node_modules/.bin/eslint src/**/*.js --ignore-pattern node_modules/" 7 | }, 8 | "author": "Pawel Zubkiewicz", 9 | "license": "MIT", 10 | "devDependencies": { 11 | "aws-sdk": "^2.879.0", 12 | "eslint": "^7.23.0", 13 | "eslint-config-airbnb-base": "^14.2.1", 14 | "eslint-plugin-import": "^2.22.1", 15 | "serverless": "^2.33.1" 16 | }, 17 | "dependencies": {} 18 | } 19 | -------------------------------------------------------------------------------- /src/common/adapters/EventBridgeAdapter.js: -------------------------------------------------------------------------------- 1 | const EventBridge = require('aws-sdk/clients/eventbridge') 2 | 3 | module.exports = class EventBridgeAdapter { 4 | constructor() { 5 | this.ebClient = new EventBridge({ 6 | region: process.env.region 7 | }) 8 | } 9 | 10 | async putEvent(params) { 11 | console.log(`Sending events to EventBridge: ${params.Entries[0].EventBusName}`) 12 | const response = await this.ebClient.putEvents(params).promise() 13 | console.log('Event Bus response', response) 14 | if (response.FailedEntryCount) { 15 | console.log('Error publishing one or more events to EventBridge', params); 16 | throw new Error('Error publishing one or more events to EventBridge'); 17 | } 18 | return response 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/sendOrderEvent/function.js: -------------------------------------------------------------------------------- 1 | const OrderEventsService = require('../common/services/OrderEventsService') 2 | 3 | const handler = async () => { 4 | const ebService = new OrderEventsService() 5 | const order1 = { 6 | orderId: 666, 7 | client: 'Rick Sanchez', 8 | email: 'RickSanchez@rickandmorty.com', 9 | date: '2021-04-05', 10 | paid: 77.77, 11 | product: 'Serverless Course' 12 | } 13 | 14 | const order2 = { 15 | orderId: 123, 16 | client: 'Morty Smith', 17 | email: 'MortySmith@rickandmorty.com', 18 | date: '2021-04-06', 19 | paid: 24.89, 20 | product: 'DataLake Course' 21 | } 22 | 23 | const promise1 = ebService.putEventOrderPurchased(order1) 24 | const promise2 = ebService.putEventOrderPurchased(order2) 25 | await Promise.all([promise1, promise2]) 26 | console.log('Finished') 27 | 28 | return 'done' 29 | } 30 | 31 | module.exports = { handler } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Paweł Zubkiewicz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/common/services/OrderEventsService.js: -------------------------------------------------------------------------------- 1 | const EventBridgeAdapter = require('../adapters/EventBridgeAdapter') 2 | 3 | const EventTypes = { 4 | ORDER_COURSE_SERVERLESS: 'ORDER_COURSE_SERVERLESS', 5 | ORDER_COURSE_DATALAKE: 'ORDER_COURSE_DATALAKE', 6 | } 7 | 8 | module.exports = class OrderEventsService { 9 | constructor(eventBridgeAdapter) { 10 | this.client = eventBridgeAdapter || new EventBridgeAdapter() 11 | } 12 | 13 | async putEventOrderPurchased(order) { 14 | this.order = order 15 | const { eventBusName } = process.env 16 | if (!eventBusName) { 17 | throw new Error('EventBusName not specified') 18 | } 19 | const { tagId, eventType } = this.parametrizeByOrderProduct() 20 | console.log(`Sending ${eventType} event to EventBus`) 21 | const payload = { 22 | mcTagId: tagId, 23 | ...order, 24 | } 25 | const params = { 26 | Entries: [{ 27 | EventBusName: eventBusName, 28 | Source: 'eb-api-dest-sample', 29 | DetailType: eventType, 30 | Detail: JSON.stringify(payload), 31 | }] 32 | } 33 | return this.client.putEvent(params) 34 | } 35 | 36 | // eslint-disable-next-line class-methods-use-this 37 | parametrizeByOrderProduct() { 38 | const { mcTagServerless, mcTagDataLake } = process.env 39 | switch (this.order.product) { 40 | case 'Serverless Course': 41 | return { 42 | tagId: mcTagServerless, 43 | eventType: EventTypes.ORDER_COURSE_SERVERLESS 44 | } 45 | case 'DataLake Course': 46 | return { 47 | tagId: mcTagDataLake, 48 | eventType: EventTypes.ORDER_COURSE_DATALAKE 49 | } 50 | default: 51 | throw new Error(`Unrecognized product ${this.order.product}`) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | #direnv 76 | .envrc 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon EventBridge API Destinations with parametrized endpoint URL and Mailchimp integration example. 2 | Sample project to demonstrate how to use Event Bridge API Destinations to integrate with Mailchimp or any other endpoint on th Internet. This project shows how to use **HTTP Path Parameters** to trigger different endpoints based on message content. 3 | 4 | You can deploy this sample on your AWS account in just few minutes and play with it. Please follow instructions below. 5 | 6 | ## Technologies 7 | This project uses **Node.js** and **Serverless Framework** + **CloudFormation** (which is embedded inside Serverless Framework configuration file `serverless.yml`). 8 | 9 | ## Tutorial 10 | Please read full article at dev.to website explaining contents of this example. 11 | 12 | # How to deploy? 13 | ## Code preparation 14 | 15 | You need to clone this project, and install dependencies: 16 | ``` 17 | git clone https://github.com/serverlesspolska/eventbridge-api-destinations-mailchimp.git 18 | cd eventbridge-api-destinations-mailchimp 19 | npm i 20 | ``` 21 | This requires `node.js` and `npm` be installed on your machine. 22 | 23 | ## Configuration 24 | 25 | As not everyone uses Mailchimp, in the project I defined a second API Destinations `Target` that sends REST requests to `webhook.site` service. This is a free, easy to use web application that will work as our 3rd party endpoint. 26 | 27 | Please go to the [webhook.site](https://webhook.site) and copy **Your unique URL**. Paste that URL into `serverless.yml` config file in line `38`: 28 | ``` 29 | endpoint: # Your Webhook URL 30 | ``` 31 | 32 | By default, project is setup to be deployed in `eu-west-2` - London region - using `default` AWS profile. If you want to change deployment region or profile you may do it in `config/deployment.yml` file. 33 | 34 | ## Solution deployment 35 | 36 | Now we're ready to deploy the project to the `dev` stage using command: 37 | ``` 38 | sls deploy 39 | ``` 40 | (This assumes that you have your `default` AWS profile defined under `~/.aws/credentials`.) 41 | 42 | 43 | # How to run this example? 44 | 45 | After successful deployment (can take few minutes) you can invoke a Lambda function, that will send **two** sample `Order` events to the *EventBus*. 46 | ``` 47 | sls invoke -f sendOrderEvent -l 48 | ``` 49 | As a result, you should see the new request on the `webhook.site` website (your custom URL). That means that API Destinations just called the endpoint responding to the new message on the *EventBus*. 50 | 51 | ### Sample 52 | Here is a sample request received by webhook.site. 53 | ![Event displayed on WebHook website](documentation/webhook-event.png) 54 | 55 | ## Debuging 56 | Mailchimp `Target` is not configured with real IDs and passwords. Therefore each event sent to the EventBus will not be delivered to Mailchimp. 57 | 58 | You can see failed events moved from `EventBus` to a Dead Letter Queue (DLQ) named `dev-eb-api-dest-sample-eventbus-DLQ` in your deployment region. 59 | 60 | # How can I remove this sample from my AWS Account? 61 | To remove this project, simply execute following command in project folder: 62 | ``` 63 | sls remove 64 | ``` -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: eb-api-dest-sample 2 | 3 | configValidationMode: error 4 | disabledDeprecations: 5 | - 'PROVIDER_IAM_SETTINGS' 6 | 7 | provider: 8 | name: aws 9 | stage: ${opt:stage, 'dev'} 10 | runtime: nodejs12.x 11 | profile: ${self:custom.deployment.myProfile.${self:provider.stage}} 12 | region: ${self:custom.deployment.myRegion.${self:provider.stage}} 13 | iamRoleStatements: 14 | - Sid: EventBridgeWrite 15 | Effect: Allow 16 | Action: 17 | - events:PutEvents 18 | Resource: 19 | - !GetAtt EventBus.Arn 20 | logRetentionInDays: 60 21 | lambdaHashingVersion: 20201221 22 | environment: 23 | stage: ${self:provider.stage} 24 | region: ${self:provider.region} 25 | tags: 26 | Application: ${self:service} 27 | Customer: ${self:service}-${self:provider.stage} 28 | Stage: ${self:provider.stage} 29 | stackTags: 30 | ServerlessFramework: 'true' 31 | 32 | custom: 33 | deployment: ${file(config/deployment.yml):deployment} 34 | mailchimp: 35 | endpoint: https://us3.api.mailchimp.com/3.0 # Your Mailchimp URL 36 | list: 1234567890 # Your Mailchimp List ID 37 | webhook: 38 | endpoint: # Your Webhook URL 39 | 40 | functions: 41 | sendOrderEvent: 42 | handler: src/sendOrderEvent/function.handler 43 | description: Send sample event 44 | memorySize: 256 45 | timeout: 2 46 | environment: 47 | eventBusName: !GetAtt EventBus.Name 48 | mcTagServerless: firstTag 49 | mcTagDataLake: secondTag 50 | 51 | package: 52 | patterns: 53 | - src/** 54 | - '!config/**' 55 | - '!*' 56 | 57 | resources: 58 | Description: Event Bridge API Destinations sample application 59 | Resources: 60 | EventBus: 61 | Type: AWS::Events::EventBus 62 | Properties: 63 | Name: ${self:service}-${self:provider.stage} 64 | 65 | TestConnection: 66 | Type: AWS::Events::Connection 67 | Properties: 68 | Name: MyWebhookConnection-${self:provider.stage} 69 | AuthorizationType: BASIC 70 | AuthParameters: 71 | BasicAuthParameters: 72 | Username: "randomUser" 73 | Password: "MailchimpApiPassword" 74 | InvocationHttpParameters: 75 | HeaderParameters: 76 | - Key: MyHeader 77 | Value: MyValue 78 | 79 | TestApiDestination: 80 | Type: AWS::Events::ApiDestination 81 | Properties: 82 | Name: webhook-target-${self:provider.stage} 83 | ConnectionArn: !GetAtt TestConnection.Arn 84 | InvocationEndpoint: ${self:custom.webhook.endpoint}/lists/${self:custom.mailchimp.list}/segments/*/members 85 | HttpMethod: POST 86 | InvocationRateLimitPerSecond: 20 87 | 88 | MailchimpConnection: 89 | Type: AWS::Events::Connection 90 | Properties: 91 | Name: MailchimpConnection-${self:provider.stage} 92 | AuthorizationType: BASIC 93 | AuthParameters: 94 | BasicAuthParameters: 95 | Username: "randomUser" 96 | Password: "Mailchimp API Key" # You can refer secret defined in SSM: ${ssm:mailchimp-api-key~true} 97 | 98 | MailchimpApiDestination: 99 | Type: AWS::Events::ApiDestination 100 | Properties: 101 | Name: mailchimp-tag-${self:provider.stage} 102 | ConnectionArn: !GetAtt MailchimpConnection.Arn 103 | InvocationEndpoint: ${self:custom.mailchimp.endpoint}/lists/${self:custom.mailchimp.list}/segments/*/members 104 | HttpMethod: POST 105 | InvocationRateLimitPerSecond: 20 106 | 107 | ApiDestinationDeliveryRule: 108 | Type: AWS::Events::Rule 109 | Properties: 110 | EventBusName: !Ref EventBus 111 | Name: SendOrderEventsToApiDestinations 112 | EventPattern: 113 | detail-type: 114 | - ORDER_COURSE_SERVERLESS 115 | - ORDER_COURSE_DATALAKE 116 | State: "ENABLED" 117 | Targets: 118 | - Id: MailchimpApiDestination 119 | Arn: !GetAtt MailchimpApiDestination.Arn 120 | RoleArn: !GetAtt ApiDestinationsTargetRole.Arn 121 | DeadLetterConfig: 122 | Arn: !GetAtt DeadLetterQueue.Arn 123 | InputTransformer: 124 | InputPathsMap: 125 | mcTagId: $.detail.mcTagId 126 | email: $.detail.email 127 | InputTemplate: > 128 | { 129 | "email_address": 130 | } 131 | HttpParameters: 132 | PathParameterValues: 133 | - $.detail.mcTagId 134 | 135 | - Id: TestApiDestination 136 | Arn: !GetAtt TestApiDestination.Arn 137 | RoleArn: !GetAtt ApiDestinationsTargetRole.Arn 138 | DeadLetterConfig: 139 | Arn: !GetAtt DeadLetterQueue.Arn 140 | InputTransformer: 141 | InputPathsMap: 142 | orderId: $.detail.orderId 143 | mcTagId: $.detail.mcTagId 144 | client: $.detail.client 145 | email: $.detail.email 146 | date: $.detail.date 147 | paid: $.detail.paid 148 | product: $.detail.product 149 | InputTemplate: > 150 | { 151 | "orderId": , 152 | "mcTagId": , 153 | "client": , 154 | "email": , 155 | "date": , 156 | "paid": , 157 | "product": 158 | } 159 | HttpParameters: 160 | PathParameterValues: 161 | - $.detail.mcTagId 162 | 163 | ApiDestinationsTargetRole: 164 | Type: AWS::IAM::Role 165 | Properties: 166 | RoleName: ${self:provider.stage}-${self:service}-eventbus-api-destinations 167 | AssumeRolePolicyDocument: 168 | Version: '2012-10-17' 169 | Statement: 170 | - Effect: Allow 171 | Principal: 172 | Service: events.amazonaws.com 173 | Action: 'sts:AssumeRole' 174 | Policies: 175 | - PolicyName: AllowApiDestinationsInvoke 176 | PolicyDocument: 177 | Version: '2012-10-17' 178 | Statement: 179 | - Sid: InvokeApiDestination 180 | Effect: Allow 181 | Action: 182 | - events:InvokeApiDestination 183 | Resource: 184 | - !GetAtt TestApiDestination.Arn 185 | - !GetAtt MailchimpApiDestination.Arn 186 | 187 | DeadLetterQueue: 188 | Type: AWS::SQS::Queue 189 | Properties: 190 | QueueName: ${self:provider.stage}-${self:service}-eventbus-DLQ 191 | Tags: 192 | - Key: Application 193 | Value: ${self:service} 194 | - Key: Customer 195 | Value: ${self:service}-${self:provider.stage} 196 | - Key: Stage 197 | Value: ${self:provider.stage} 198 | - Key: StackName 199 | Value: !Ref AWS::StackId 200 | 201 | DeadLetterQueuePolicy: 202 | Type: AWS::SQS::QueuePolicy 203 | Properties: 204 | PolicyDocument: 205 | Statement: 206 | - Sid: OwnerStatement 207 | Action: 208 | - "sqs:*" 209 | Effect: Allow 210 | Resource: !GetAtt DeadLetterQueue.Arn 211 | Principal: 212 | AWS: 213 | - !Sub ${AWS::AccountId} 214 | - Sid: EventRuleDlqStatement 215 | # https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rule-dlq.html#eb-dlq-perms 216 | Action: 217 | - sqs:SendMessage 218 | Effect: Allow 219 | Resource: !GetAtt DeadLetterQueue.Arn 220 | Principal: 221 | Service: events.amazonaws.com 222 | Condition: 223 | ArnEquals: 224 | "aws:SourceArn": !GetAtt ApiDestinationDeliveryRule.Arn 225 | Queues: 226 | - !Ref DeadLetterQueue --------------------------------------------------------------------------------