diff --git a/typescript/connect-cdk/.gitignore b/typescript/connect-cdk/.gitignore new file mode 100644 index 0000000000..f60797b6a9 --- /dev/null +++ b/typescript/connect-cdk/.gitignore @@ -0,0 +1,8 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/typescript/connect-cdk/.npmignore b/typescript/connect-cdk/.npmignore new file mode 100644 index 0000000000..c1d6d45dcf --- /dev/null +++ b/typescript/connect-cdk/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/typescript/connect-cdk/README.md b/typescript/connect-cdk/README.md new file mode 100644 index 0000000000..8a0d787c7e --- /dev/null +++ b/typescript/connect-cdk/README.md @@ -0,0 +1,49 @@ +# Amazon Connect fully deployed with CDK + + +--- + +![Stability: Stable](https://img.shields.io/badge/stability-Stable-success.svg?style=for-the-badge) + +> **This is a stable example. It should successfully build out of the box** +> +> This example is built on Construct Libraries marked "Stable" and does not have any infrastructure prerequisites to build. +--- + + +## Overview + +A fully infrastructure-as-code Amazon Connect solution that includes a simple call flow itegrated with Lambda. + +## Solution + +Some of the key services in use: +* Amazon Connect +* AWS Lambda + +### Connect Call Flows +The following Connect flows/modules are found in the `callFlows`: +* `MainFlow.json` -- the main flow + +***Modifying the json can be performed via the Connect user interface, followed by an export operation. Note that string replacements are performed prior to loading the json flows into the resource, they will need to be reset after exporting. This is responsible for ARN and other dependency resolution and is contained to the contact attributes parameters.*** + +## Tagging +The follow tag is set for all taggable resources in `bin/connect_cdk_simple.ts`: +* `application: 'Github Location'` + +## Deploying + +This project contains a Lambda function that use the CDK to deploy. It is written in Python, but des not require any further building or packaging. + +This project uses the Cloud Development Kit (CDK) in Typescript. + +```bash +cdk deploy +``` + +## Cleanup +To cleanup this solution or avoid further or on-going charges + +```bash +cdk destroy +``` \ No newline at end of file diff --git a/typescript/connect-cdk/bin/connect_cdk.ts b/typescript/connect-cdk/bin/connect_cdk.ts new file mode 100644 index 0000000000..7c0c275b95 --- /dev/null +++ b/typescript/connect-cdk/bin/connect_cdk.ts @@ -0,0 +1,8 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { ConnectCdkStack as ConnectCdkStack } from '../lib/connect_cdk-stack'; + +const app = new cdk.App(); + +new ConnectCdkStack(app, 'ConnectCdkStack'); diff --git a/typescript/connect-cdk/callFlows/MainFlow.json b/typescript/connect-cdk/callFlows/MainFlow.json new file mode 100644 index 0000000000..f3136d94d3 --- /dev/null +++ b/typescript/connect-cdk/callFlows/MainFlow.json @@ -0,0 +1,151 @@ +{ + "Version": "2019-10-30", + "StartAction": "d3f90ab2-eb2a-467f-a349-323422ad8bec", + "Metadata": { + "entryPointPosition": { + "x": -10.4, + "y": -28 + }, + "ActionMetadata": { + "51bffcc2-8bda-46a4-8b29-6c327c109cfe": { + "position": { + "x": 271.2, + "y": -29.6 + } + }, + "d3f90ab2-eb2a-467f-a349-323422ad8bec": { + "position": { + "x": 80, + "y": 188.8 + } + }, + "52f73b71-7081-4cf0-b6f6-4202cfba2863": { + "position": { + "x": 512, + "y": 12.8 + } + }, + "6a6556e1-b596-4f5b-b0c1-c5ac5ab965a3": { + "position": { + "x": 1228.8, + "y": 196.8 + } + }, + "049fca85-b0bd-49d6-8517-2943c248cdc3": { + "position": { + "x": 734.4, + "y": -100.8 + }, + "parameters": { + "LambdaFunctionARN": { + "useDynamic": true + } + }, + "dynamicMetadata": {}, + "useDynamic": true + }, + "3e5551e7-b4b8-4051-a438-019dfe5b760b": { + "position": { + "x": 1025.6, + "y": -50.4 + } + } + }, + "Annotations": [], + "name": "MainFlow", + "description": "", + "type": "contactFlow", + "status": "published", + "hash": {} + }, + "Actions": [ + { + "Parameters": { + "FlowAttributes": { + "HelloLambdaArn": { + "Value": "HelloLambdaArnValue" + } + } + }, + "Identifier": "51bffcc2-8bda-46a4-8b29-6c327c109cfe", + "Type": "UpdateFlowAttributes", + "Transitions": { + "NextAction": "52f73b71-7081-4cf0-b6f6-4202cfba2863", + "Errors": [ + { + "NextAction": "6a6556e1-b596-4f5b-b0c1-c5ac5ab965a3", + "ErrorType": "NoMatchingError" + } + ] + } + }, + { + "Parameters": { + "FlowLoggingBehavior": "Enabled" + }, + "Identifier": "d3f90ab2-eb2a-467f-a349-323422ad8bec", + "Type": "UpdateFlowLoggingBehavior", + "Transitions": { + "NextAction": "51bffcc2-8bda-46a4-8b29-6c327c109cfe" + } + }, + { + "Parameters": { + "Text": "Hello" + }, + "Identifier": "52f73b71-7081-4cf0-b6f6-4202cfba2863", + "Type": "MessageParticipant", + "Transitions": { + "NextAction": "049fca85-b0bd-49d6-8517-2943c248cdc3", + "Errors": [ + { + "NextAction": "6a6556e1-b596-4f5b-b0c1-c5ac5ab965a3", + "ErrorType": "NoMatchingError" + } + ] + } + }, + { + "Parameters": {}, + "Identifier": "6a6556e1-b596-4f5b-b0c1-c5ac5ab965a3", + "Type": "DisconnectParticipant", + "Transitions": {} + }, + { + "Parameters": { + "LambdaFunctionARN": "$.FlowAttributes.HelloLambdaArn", + "InvocationTimeLimitSeconds": "3", + "ResponseValidation": { + "ResponseType": "JSON" + } + }, + "Identifier": "049fca85-b0bd-49d6-8517-2943c248cdc3", + "Type": "InvokeLambdaFunction", + "Transitions": { + "NextAction": "3e5551e7-b4b8-4051-a438-019dfe5b760b", + "Errors": [ + { + "NextAction": "6a6556e1-b596-4f5b-b0c1-c5ac5ab965a3", + "ErrorType": "NoMatchingError" + } + ] + } + }, + { + "Parameters": { + "SSML": " $.External.body.response " + }, + "Identifier": "3e5551e7-b4b8-4051-a438-019dfe5b760b", + "Type": "MessageParticipant", + "Transitions": { + "NextAction": "6a6556e1-b596-4f5b-b0c1-c5ac5ab965a3", + "Errors": [ + { + "NextAction": "6a6556e1-b596-4f5b-b0c1-c5ac5ab965a3", + "ErrorType": "NoMatchingError" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/typescript/connect-cdk/cdk.json b/typescript/connect-cdk/cdk.json new file mode 100644 index 0000000000..a5b9c27429 --- /dev/null +++ b/typescript/connect-cdk/cdk.json @@ -0,0 +1,72 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/connect_cdk.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false + } +} diff --git a/typescript/connect-cdk/hellolambda/lambda_function.py b/typescript/connect-cdk/hellolambda/lambda_function.py new file mode 100644 index 0000000000..883daca06d --- /dev/null +++ b/typescript/connect-cdk/hellolambda/lambda_function.py @@ -0,0 +1,20 @@ +import boto3 +from datetime import datetime, timedelta +import logging + +# Establish logging configuration +logger = logging.getLogger() + +def lambda_handler(event, context): + + response = { + + 'response': "the current date is " + datetime.now().strftime("%m-%d-%Y") + } + + logger.info(response) + + return { + 'statusCode': 200, + 'body': response + } \ No newline at end of file diff --git a/typescript/connect-cdk/jest.config.js b/typescript/connect-cdk/jest.config.js new file mode 100644 index 0000000000..08263b8954 --- /dev/null +++ b/typescript/connect-cdk/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; diff --git a/typescript/connect-cdk/lib/connect_cdk-stack.ts b/typescript/connect-cdk/lib/connect_cdk-stack.ts new file mode 100644 index 0000000000..e3abf8382a --- /dev/null +++ b/typescript/connect-cdk/lib/connect_cdk-stack.ts @@ -0,0 +1,88 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as fs from 'fs'; +import * as cr from 'aws-cdk-lib/custom-resources'; + +export class ConnectCdkStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + //generate an aliasID + const connectInstanceName = this.account + cdk.Names.uniqueResourceName(this, { + maxLength: 50 + }); + + // Create an Amazon Connect instance + const connectInstance = new cdk.aws_connect.CfnInstance(this, 'ConnectInstance', { + instanceAlias: connectInstanceName, + identityManagementType: 'CONNECT_MANAGED', + attributes: { + inboundCalls: true, + outboundCalls: false, + contactflowLogs: true + }, + }); + + // Lambda function + const helloLambda = new cdk.aws_lambda.Function(this, 'HelloLambda', { + runtime: cdk.aws_lambda.Runtime.PYTHON_3_12, + handler: 'lambda_function.lambda_handler', + code: cdk.aws_lambda.Code.fromAsset('./hellolambda'), + }); + + // main flow + const connectContactFlow = new cdk.aws_connect.CfnContactFlow(this, 'ConnectPagerContactFlow', { + instanceArn: connectInstance.attrArn, + name: "MainFlow", + type: "CONTACT_FLOW", + content: fs.readFileSync('./callFlows/MainFlow.json', 'utf-8').toString() + .replace("HelloLambdaArnValue", helloLambda.functionArn) + }); + + + // map lambda to connect flows + const connectInstanceFlowLambda = new cdk.aws_connect.CfnIntegrationAssociation(this, 'ConnectInstanceFlowLambda', { + instanceId: connectInstance.attrArn, + integrationArn: helloLambda.functionArn, + integrationType: 'LAMBDA_FUNCTION' + }); + + // claim a number + const connectNumber = new cdk.aws_connect.CfnPhoneNumber(this, 'ConnectPagerPhoneNumber', { + targetArn: connectInstance.attrArn, + countryCode: 'US', + type: 'DID' + }); + + // associate the number using AwsCustomResource, doesn't appear to be any other way + const associateNumber = new cr.AwsCustomResource(this, 'AssociateConnectNumber', { + onUpdate: { + service: 'Connect', + action: 'AssociatePhoneNumberContactFlowCommand', + parameters: { + InstanceId: connectInstance.attrArn, + ContactFlowId: connectContactFlow.attrContactFlowArn, + PhoneNumberId: connectNumber.attrPhoneNumberArn, + }, + physicalResourceId: cr.PhysicalResourceId.of(connectNumber.attrAddress) + }, + onDelete: { + service: 'Connect', + action: 'DisassociatePhoneNumberContactFlowCommand', + parameters: { + InstanceId: connectInstance.attrArn, + ContactFlowId: connectContactFlow.attrContactFlowArn, + PhoneNumberId: connectNumber.attrPhoneNumberArn, + }, + physicalResourceId: cr.PhysicalResourceId.of(connectNumber.attrAddress) + }, + policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ + resources: [connectContactFlow.attrContactFlowArn, connectNumber.attrPhoneNumberArn] + }) + }); + + new cdk.CfnOutput(this, 'connectPhoneNumberOutput', { + value: connectNumber.attrAddress + }); + } +} diff --git a/typescript/connect-cdk/package.json b/typescript/connect-cdk/package.json new file mode 100644 index 0000000000..27da02d6c1 --- /dev/null +++ b/typescript/connect-cdk/package.json @@ -0,0 +1,28 @@ +{ + "name": "connect_cdk_simple", + "version": "0.1.0", + "bin": { + "connect_cdk_simple": "bin/connect_cdk_simple.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^29.5.12", + "@types/node": "20.14.9", + "aws-cdk": "2.157.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.5", + "ts-node": "^10.9.2", + "typescript": "~5.5.3" + }, + "dependencies": { + "aws-cdk-lib": "2.157.0", + "cdk-nag": "^2.30.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + } +} diff --git a/typescript/connect-cdk/test/connect_cdk_simple.test.ts b/typescript/connect-cdk/test/connect_cdk_simple.test.ts new file mode 100644 index 0000000000..b16499b63c --- /dev/null +++ b/typescript/connect-cdk/test/connect_cdk_simple.test.ts @@ -0,0 +1,23 @@ +import * as cdk from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import * as ConnectCdk from '../lib/connect_cdk-stack'; + + +test('Connect Instance and Lambda Function Created', () => { + const app = new cdk.App(); + // WHEN + const stack = new ConnectCdk.ConnectCdkStack(app, 'MyTestStack'); + // THEN + const template = Template.fromStack(stack); + + // Check that Connect instance is created + template.hasResourceProperties('AWS::Connect::Instance', { + IdentityManagementType: 'CONNECT_MANAGED' + }); + + // Check that a Lambda function is created + template.hasResourceProperties('AWS::Lambda::Function', { + Runtime: 'python3.12', + Handler: 'lambda_function.lambda_handler' + }); +}); diff --git a/typescript/connect-cdk/tsconfig.json b/typescript/connect-cdk/tsconfig.json new file mode 100644 index 0000000000..aaa7dc510f --- /dev/null +++ b/typescript/connect-cdk/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +}