├── test ├── fleetmgmt-project.test.d.ts ├── fleetmgmt-project.test.ts └── fleetmgmt-project.test.js ├── .gitignore ├── bin ├── fleetmgmt-project.d.ts ├── fleetmgmt-project.ts └── fleetmgmt-project.js ├── figures ├── authors │ ├── Alec.png │ ├── 김진형.JPG │ └── 이승범.jpg ├── sequence_diagram.png ├── system_architecture_msk.png └── system_architecture_msk_English.png ├── lib ├── rule │ ├── rule-keys.json │ ├── key-policy.json │ └── rule-policy.json ├── aws-iot-core-rule-infra-stack.d.ts ├── aws-iot-core-provisioning-infra-stack.d.ts ├── aws-vpc-msk-infra-stack.d.ts ├── device │ ├── device-cc-policy.json │ ├── device-policy.json │ └── provisioning-template.json ├── lambda │ ├── verify-devices-lambda.py │ └── get-bootstrapbrokers-lambda.py ├── aws-vpc-msk-infra-stack.ts ├── aws-iot-core-rule-infra-stack.ts ├── aws-iot-core-provisioning-infra-stack.ts ├── aws-vpc-msk-infra-stack.js ├── aws-iot-core-rule-infra-stack.js └── aws-iot-core-provisioning-infra-stack.js ├── jest.config.js ├── end.sh ├── CODE_OF_CONDUCT.md ├── config ├── config.d.ts ├── config.ts └── config.js ├── .github └── dependabot.yml ├── package.json ├── tsconfig.json ├── LICENSE ├── start.sh ├── cdk.json ├── CONTRIBUTING.md └── README.md /test/fleetmgmt-project.test.d.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cdk.out/ -------------------------------------------------------------------------------- /bin/fleetmgmt-project.d.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | -------------------------------------------------------------------------------- /figures/authors/Alec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/42dot-cdk-fleetmanagement-system/main/figures/authors/Alec.png -------------------------------------------------------------------------------- /figures/authors/김진형.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/42dot-cdk-fleetmanagement-system/main/figures/authors/김진형.JPG -------------------------------------------------------------------------------- /figures/authors/이승범.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/42dot-cdk-fleetmanagement-system/main/figures/authors/이승범.jpg -------------------------------------------------------------------------------- /lib/rule/rule-keys.json: -------------------------------------------------------------------------------- 1 | { 2 | "testRules": [ 3 | "rule1", 4 | "rule2", 5 | "rule3" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /figures/sequence_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/42dot-cdk-fleetmanagement-system/main/figures/sequence_diagram.png -------------------------------------------------------------------------------- /figures/system_architecture_msk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/42dot-cdk-fleetmanagement-system/main/figures/system_architecture_msk.png -------------------------------------------------------------------------------- /figures/system_architecture_msk_English.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/42dot-cdk-fleetmanagement-system/main/figures/system_architecture_msk_English.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /end.sh: -------------------------------------------------------------------------------- 1 | cdk destroy AwsIotCoreRuleInfraStack --require-approval never --force 2 | cdk destroy AwsIotCoreProvisioningInfraStack --require-approval never --force 3 | cdk destroy AwsVpcMskInfraStack --require-approval never --force 4 | 5 | -------------------------------------------------------------------------------- /lib/aws-iot-core-rule-infra-stack.d.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | export declare class AwsIotCoreRuleInfraStack extends Stack { 4 | constructor(scope: Construct, id: string, props?: StackProps); 5 | } 6 | -------------------------------------------------------------------------------- /lib/aws-iot-core-provisioning-infra-stack.d.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | export declare class AwsIotCoreProvisioningInfraStack extends Stack { 4 | constructor(scope: Construct, id: string, props?: StackProps); 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/aws-vpc-msk-infra-stack.d.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps, aws_ec2 as ec2 } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | export declare class AwsVpcMskInfraStack extends Stack { 4 | vpc: ec2.CfnVPC; 5 | constructor(scope: Construct, id: string, props?: StackProps); 6 | subnet_creation(subnet_name: string, subnet_cidr: string): ec2.CfnSubnet; 7 | } 8 | -------------------------------------------------------------------------------- /lib/rule/key-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Id": "msk-encryption-policy", 3 | "Version": "2012-10-17", 4 | "Statement": [ 5 | { 6 | "Sid": "Enable IAM User Permissions", 7 | "Effect": "Allow", 8 | "Principal": { 9 | "AWS": "" 10 | }, 11 | "Action": "kms:*", 12 | "Resource": "*" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /config/config.d.ts: -------------------------------------------------------------------------------- 1 | declare const Config: { 2 | aws: { 3 | account: string; 4 | region: string; 5 | }; 6 | app: { 7 | service: string; 8 | application: string; 9 | environment: string; 10 | }; 11 | s3BucketName: string; 12 | vpc: { 13 | cidr: string; 14 | }; 15 | security_group: string[]; 16 | msk: { 17 | clusterName: string; 18 | }; 19 | }; 20 | export { Config }; 21 | -------------------------------------------------------------------------------- /lib/device/device-cc-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": ["iot:Connect", "iot:RetainPublish"], 7 | "Resource": "*" 8 | }, 9 | { 10 | "Effect": "Allow", 11 | "Action": ["iot:Publish","iot:Receive", "iot:RetainPublish"], 12 | "Resource": [""] 13 | }, 14 | { 15 | "Effect": "Allow", 16 | "Action": "iot:Subscribe", 17 | "Resource": [""] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /lib/device/device-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": "iot:Connect", 7 | "Resource": "*" 8 | }, 9 | { 10 | "Effect": "Allow", 11 | "Action": [ 12 | "iot:Publish", 13 | "iot:Receive", 14 | "iot:RetainPublish" 15 | ], 16 | "Resource": [""] 17 | }, 18 | { 19 | "Effect": "Allow", 20 | "Action": "iot:Subscribe", 21 | "Resource": [""] 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /config/config.ts: -------------------------------------------------------------------------------- 1 | const Config = { 2 | aws: { 3 | account: "<>", 4 | region: "<>", 5 | }, 6 | 7 | app: { 8 | service: 'fleetmgmt', 9 | application: 'iot', 10 | environment: 'dev' 11 | }, 12 | s3BucketName : "cdk-s3-test-bucket", 13 | 14 | // Assume that you have created a VPC with two subnets and a security group 15 | vpc: { 16 | cidr: '10.51' 17 | }, 18 | security_group: ['10.42.0.0/23', 'fleetmgmt'], 19 | 20 | msk: { 21 | clusterName: "cdk-iot-msk-cluster", 22 | } 23 | }; 24 | export { Config }; 25 | -------------------------------------------------------------------------------- /test/fleetmgmt-project.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as CdkTestProject from '../lib/cdk-test-project-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/cdk-test-project-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new CdkTestProject.CdkTestProjectStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.Config = void 0; 4 | const Config = { 5 | aws: { 6 | account: "<>", 7 | region: "<>", 8 | }, 9 | app: { 10 | service: 'fleetmgmt', 11 | application: 'iot', 12 | environment: 'dev' 13 | }, 14 | s3BucketName: "cdk-s3-test-bucket", 15 | // Assume that you have created a VPC with two subnets and a security group 16 | vpc: { 17 | cidr: '10.51' 18 | }, 19 | security_group: ['10.42.0.0/23', 'fleetmgmt'], 20 | msk: { 21 | clusterName: "cdk-iot-msk-cluster", 22 | } 23 | }; 24 | exports.Config = Config; 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fleetmgmt-project", 3 | "version": "0.1.0", 4 | "bin": { 5 | "fleet-management-project": "bin/fleetmgmt-project.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.5.5", 15 | "@types/node": "20.6.3", 16 | "aws-cdk": "2.99.1", 17 | "jest": "^29.7.0", 18 | "ts-jest": "^29.1.1", 19 | "ts-node": "^10.9.1", 20 | "typescript": "~5.2.2" 21 | }, 22 | "dependencies": { 23 | "@aws-sdk/client-kafka": "^3.624.0", 24 | "0g": "^0.0.9", 25 | "aws-cdk-lib": "^2.99.1", 26 | "constructs": "^10.0.0", 27 | "path": "^0.12.7", 28 | "source-map-support": "^0.5.21" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ], 26 | "resolveJsonModule": true, 27 | "esModuleInterop": true 28 | }, 29 | "exclude": [ 30 | "node_modules", 31 | "cdk.out" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /bin/fleetmgmt-project.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import {AwsVpcMskInfraStack} from "../lib/aws-vpc-msk-infra-stack"; 5 | import { Config } from "../config/config"; 6 | import {AwsIotCoreProvisioningInfraStack} from "../lib/aws-iot-core-provisioning-infra-stack"; 7 | import {AwsIotCoreRuleInfraStack} from "../lib/aws-iot-core-rule-infra-stack"; 8 | 9 | const app = new cdk.App(); 10 | 11 | new AwsIotCoreProvisioningInfraStack(app, "AwsIotCoreProvisioningInfraStack", { 12 | env: { 13 | account: Config.aws.account, 14 | region: Config.aws.region, 15 | }, 16 | }); 17 | 18 | new AwsIotCoreRuleInfraStack(app, "AwsIotCoreRuleInfraStack", { 19 | env: { 20 | account: Config.aws.account, 21 | region: Config.aws.region, 22 | }, 23 | }); 24 | 25 | 26 | new AwsVpcMskInfraStack(app, "AwsVpcMskInfraStack", { 27 | env: { 28 | account: Config.aws.account, 29 | region: Config.aws.region 30 | } 31 | }) -------------------------------------------------------------------------------- /lib/device/provisioning-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Parameters": { 3 | "SerialNumber": { 4 | "Type": "String" 5 | }, 6 | "AWS::IoT::Certificate::Id": { 7 | "Type": "String" 8 | } 9 | }, 10 | "Resources": { 11 | "certificate": { 12 | "Properties": { 13 | "CertificateId": { 14 | "Ref": "AWS::IoT::Certificate::Id" 15 | }, 16 | "Status": "Active" 17 | }, 18 | "Type": "AWS::IoT::Certificate" 19 | }, 20 | "policy": { 21 | "Properties": { 22 | "PolicyName": "" 23 | }, 24 | "Type": "AWS::IoT::Policy" 25 | }, 26 | "thing": { 27 | "Type": "AWS::IoT::Thing", 28 | "OverrideSettings": { 29 | "AttributePayload": "MERGE", 30 | "ThingGroups": "DO_NOTHING", 31 | "ThingTypeName": "REPLACE" 32 | }, 33 | "Properties": { 34 | "AttributePayload": {}, 35 | "ThingGroups": [], 36 | "ThingName": { 37 | "Fn::Join": [ 38 | "", 39 | [ 40 | "test-thing-", 41 | { 42 | "Ref": "SerialNumber" 43 | } 44 | ] 45 | ] 46 | } 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /lib/rule/rule-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Action": [ 6 | "ec2:CreateNetworkInterface", 7 | "ec2:CreateNetworkInterfacePermission", 8 | "ec2:DeleteNetworkInterface", 9 | "ec2:DescribeNetworkInterfaces", 10 | "ec2:DescribeSecurityGroups", 11 | "ec2:DescribeSubnets", 12 | "ec2:DescribeVpcAttribute", 13 | "ec2:DescribeVpcs", 14 | "ec2:DescribeSecurityGroups" 15 | ], 16 | "Resource": "*", 17 | "Effect": "Allow" 18 | }, 19 | { 20 | "Effect": "Allow", 21 | "Action": [ 22 | "secretsmanager:DescribeSecret", 23 | "secretsmanager:GetSecretValue" 24 | ], 25 | "Resource": "arn:aws:secretsmanager:ap-northeast-2:1234567890:secret:AmazonMSK_*" 26 | }, 27 | { 28 | "Effect": "Allow", 29 | "Action": [ 30 | "kms:Decrypt", 31 | "kms:Encrypt", 32 | "kms:GenerateDataKey", 33 | "kms:DescribeKey" 34 | ], 35 | "Resource": "*" 36 | } 37 | 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Reset terminal input settings 4 | stty sane 5 | 6 | # Prompt for AWS account_id and region_name 7 | echo "Please provide the following details:" 8 | echo "Enter AWS account_id: " 9 | read account_id 10 | echo "Enter AWS region_name: " 11 | read region_name 12 | 13 | # Function to update the configuration files 14 | update_config_file() { 15 | local file=$1 16 | local account_id=$2 17 | local region_name=$3 18 | 19 | if [ -f "$file" ]; then 20 | sed -i.bak "s/account: \".*\"/account: \"$account_id\"/" "$file" && rm "${file}.bak" 21 | sed -i.bak "s/region: \".*\"/region: \"$region_name\"/" "$file" && rm "${file}.bak" 22 | echo "$file has been updated successfully." 23 | else 24 | echo "Error: $file not found." 25 | fi 26 | } 27 | 28 | # Update config.ts file 29 | update_config_file "config/config.ts" "$account_id" "$region_name" 30 | 31 | # Update config.js file 32 | update_config_file "config/config.js" "$account_id" "$region_name" 33 | 34 | echo "config.ts and config.js have been updated successfully." 35 | cdk bootstrap aws://$account_id/$region_name 36 | echo "deploy cdk stacks" 37 | cdk deploy AwsVpcMskInfraStack --require-approval never 38 | cdk deploy AwsIotCoreProvisioningInfraStack --require-approval never 39 | cdk deploy AwsIotCoreRuleInfraStack --require-approval never -------------------------------------------------------------------------------- /lib/lambda/verify-devices-lambda.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import sys 4 | 5 | # Configure logging 6 | logger = logging.getLogger() 7 | 8 | for h in logger.handlers: 9 | logger.removeHandler(h) 10 | h = logging.StreamHandler(sys.stdout) 11 | 12 | FORMAT = "[%(asctime)s - %(levelname)s - %(filename)s:%(lineno)s - %(funcName)s - %(message)s" 13 | h.setFormatter(logging.Formatter(FORMAT)) 14 | 15 | logger.addHandler(h) 16 | logger.setLevel(logging.INFO) 17 | 18 | SERIAL_STARTSWITH = "297468" 19 | 20 | 21 | def verify_serial(serial_number): 22 | if serial_number.startswith(SERIAL_STARTSWITH): 23 | logger.info("serial_number {} verification succeeded - starts with {}".format(serial_number, SERIAL_STARTSWITH)) 24 | return True 25 | 26 | logger.error("serial_number {} verification failed - does not start with {}".format(serial_number, SERIAL_STARTSWITH)) 27 | return False 28 | 29 | 30 | def lambda_handler(event, context): 31 | response = {'allowProvisioning': False} 32 | logger.info("event: {}".format(json.dumps(event, indent=2))) 33 | 34 | if not "SerialNumber" in event["parameters"]: 35 | logger.error("SerialNumber not provided") 36 | else: 37 | serial_number = event["parameters"]["SerialNumber"] 38 | if verify_serial(serial_number): 39 | response = {'allowProvisioning': True} 40 | 41 | logger.info("response: {}".format(response)) 42 | return response 43 | -------------------------------------------------------------------------------- /lib/lambda/get-bootstrapbrokers-lambda.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import os 3 | import json 4 | import urllib.request 5 | 6 | def send_response(event, context, response_status, response_data): 7 | print("event") 8 | print(event) 9 | print("context") 10 | print(context) 11 | response_body = json.dumps({ 12 | "Status": response_status, 13 | "Reason": "See the details in CloudWatch Log Stream: " + context.log_stream_name, 14 | "PhysicalResourceId": context.log_stream_name, 15 | "StackId": event['StackId'], 16 | "RequestId": event['RequestId'], 17 | "LogicalResourceId": event['LogicalResourceId'], 18 | "Data": response_data 19 | }) 20 | print(response_body) 21 | 22 | response_url = event['ResponseURL'] 23 | data = response_body.encode('utf-8') 24 | headers = {'content-type': '', 'content-length': str(len(data))} 25 | 26 | req = urllib.request.Request(response_url, data, headers, method='PUT') 27 | with urllib.request.urlopen(req) as f: 28 | print("Status code:", f.status) 29 | 30 | def lambda_handler(event, context): 31 | client = boto3.client('kafka') 32 | cluster_arn = os.environ['CLUSTER_ARN'] 33 | 34 | try: 35 | response = client.get_bootstrap_brokers(ClusterArn=cluster_arn) 36 | bootstrap_broker_string = response['BootstrapBrokerStringSaslScram'] 37 | 38 | # CloudFormation에 성공 응답 보내기 39 | send_response(event, context, "SUCCESS", {"BootstrapBrokerString": bootstrap_broker_string}) 40 | except Exception as e: 41 | print(f"Error getting bootstrap brokers: {str(e)}") 42 | # CloudFormation에 실패 응답 보내기 43 | send_response(event, context, "FAILED", {"Error": str(e)}) 44 | -------------------------------------------------------------------------------- /test/fleetmgmt-project.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // import * as cdk from 'aws-cdk-lib'; 3 | // import { Template } from 'aws-cdk-lib/assertions'; 4 | // import * as CdkTestProject from '../lib/fleet-management-project-stack'; 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/fleet-management-project-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new CdkTestProject.CdkTestProjectStack(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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2RrLXRlc3QtcHJvamVjdC50ZXN0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiY2RrLXRlc3QtcHJvamVjdC50ZXN0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQSxzQ0FBc0M7QUFDdEMscURBQXFEO0FBQ3JELG1FQUFtRTtBQUVuRSx1RUFBdUU7QUFDdkUsb0RBQW9EO0FBQ3BELElBQUksQ0FBQyxtQkFBbUIsRUFBRSxHQUFHLEVBQUU7SUFDL0IsK0JBQStCO0lBQy9CLGNBQWM7SUFDZCw4RUFBOEU7SUFDOUUsY0FBYztJQUNkLGdEQUFnRDtJQUVoRCx3REFBd0Q7SUFDeEQsNkJBQTZCO0lBQzdCLFFBQVE7QUFDUixDQUFDLENBQUMsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIGltcG9ydCAqIGFzIGNkayBmcm9tICdhd3MtY2RrLWxpYic7XG4vLyBpbXBvcnQgeyBUZW1wbGF0ZSB9IGZyb20gJ2F3cy1jZGstbGliL2Fzc2VydGlvbnMnO1xuLy8gaW1wb3J0ICogYXMgQ2RrVGVzdFByb2plY3QgZnJvbSAnLi4vbGliL2Nkay10ZXN0LXByb2plY3Qtc3RhY2snO1xuXG4vLyBleGFtcGxlIHRlc3QuIFRvIHJ1biB0aGVzZSB0ZXN0cywgdW5jb21tZW50IHRoaXMgZmlsZSBhbG9uZyB3aXRoIHRoZVxuLy8gZXhhbXBsZSByZXNvdXJjZSBpbiBsaWIvY2RrLXRlc3QtcHJvamVjdC1zdGFjay50c1xudGVzdCgnU1FTIFF1ZXVlIENyZWF0ZWQnLCAoKSA9PiB7XG4vLyAgIGNvbnN0IGFwcCA9IG5ldyBjZGsuQXBwKCk7XG4vLyAgICAgLy8gV0hFTlxuLy8gICBjb25zdCBzdGFjayA9IG5ldyBDZGtUZXN0UHJvamVjdC5DZGtUZXN0UHJvamVjdFN0YWNrKGFwcCwgJ015VGVzdFN0YWNrJyk7XG4vLyAgICAgLy8gVEhFTlxuLy8gICBjb25zdCB0ZW1wbGF0ZSA9IFRlbXBsYXRlLmZyb21TdGFjayhzdGFjayk7XG5cbi8vICAgdGVtcGxhdGUuaGFzUmVzb3VyY2VQcm9wZXJ0aWVzKCdBV1M6OlNRUzo6UXVldWUnLCB7XG4vLyAgICAgVmlzaWJpbGl0eVRpbWVvdXQ6IDMwMFxuLy8gICB9KTtcbn0pO1xuIl19 -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/fleetmgmt-project.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-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 54 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 55 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 56 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 57 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 58 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 59 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 60 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 61 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bin/fleetmgmt-project.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 4 | if (k2 === undefined) k2 = k; 5 | var desc = Object.getOwnPropertyDescriptor(m, k); 6 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 7 | desc = { enumerable: true, get: function() { return m[k]; } }; 8 | } 9 | Object.defineProperty(o, k2, desc); 10 | }) : (function(o, m, k, k2) { 11 | if (k2 === undefined) k2 = k; 12 | o[k2] = m[k]; 13 | })); 14 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 15 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 16 | }) : function(o, v) { 17 | o["default"] = v; 18 | }); 19 | var __importStar = (this && this.__importStar) || function (mod) { 20 | if (mod && mod.__esModule) return mod; 21 | var result = {}; 22 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 23 | __setModuleDefault(result, mod); 24 | return result; 25 | }; 26 | Object.defineProperty(exports, "__esModule", { value: true }); 27 | require("source-map-support/register"); 28 | const cdk = __importStar(require("aws-cdk-lib")); 29 | const aws_vpc_msk_infra_stack_1 = require("../lib/aws-vpc-msk-infra-stack"); 30 | const config_1 = require("../config/config"); 31 | const aws_iot_core_provisioning_infra_stack_1 = require("../lib/aws-iot-core-provisioning-infra-stack"); 32 | const aws_iot_core_rule_infra_stack_1 = require("../lib/aws-iot-core-rule-infra-stack"); 33 | const app = new cdk.App(); 34 | new aws_iot_core_provisioning_infra_stack_1.AwsIotCoreProvisioningInfraStack(app, "AwsIotCoreProvisioningInfraStack", { 35 | env: { 36 | account: config_1.Config.aws.account, 37 | region: config_1.Config.aws.region, 38 | }, 39 | }); 40 | new aws_iot_core_rule_infra_stack_1.AwsIotCoreRuleInfraStack(app, "AwsIotCoreRuleInfraStack", { 41 | env: { 42 | account: config_1.Config.aws.account, 43 | region: config_1.Config.aws.region, 44 | }, 45 | }); 46 | new aws_vpc_msk_infra_stack_1.AwsVpcMskInfraStack(app, "AwsVpcMskInfraStack", { 47 | env: { 48 | account: config_1.Config.aws.account, 49 | region: config_1.Config.aws.region 50 | } 51 | }); 52 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2RrLXRlc3QtcHJvamVjdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImNkay10ZXN0LXByb2plY3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFDQSx1Q0FBcUM7QUFDckMsaURBQW1DO0FBQ25DLDRFQUFtRTtBQUNuRSw2Q0FBMEM7QUFDMUMsd0dBQThGO0FBQzlGLHdGQUE4RTtBQUU5RSxNQUFNLEdBQUcsR0FBRyxJQUFJLEdBQUcsQ0FBQyxHQUFHLEVBQUUsQ0FBQztBQUUxQixJQUFJLHdFQUFnQyxDQUFDLEdBQUcsRUFBRSxrQ0FBa0MsRUFBRTtJQUMxRSxHQUFHLEVBQUU7UUFDRCxPQUFPLEVBQUUsZUFBTSxDQUFDLEdBQUcsQ0FBQyxPQUFPO1FBQzNCLE1BQU0sRUFBRSxlQUFNLENBQUMsR0FBRyxDQUFDLE1BQU07S0FDNUI7Q0FDSixDQUFDLENBQUM7QUFFSCxJQUFJLHdEQUF3QixDQUFDLEdBQUcsRUFBRSwwQkFBMEIsRUFBRTtJQUMxRCxHQUFHLEVBQUU7UUFDRCxPQUFPLEVBQUUsZUFBTSxDQUFDLEdBQUcsQ0FBQyxPQUFPO1FBQzNCLE1BQU0sRUFBRSxlQUFNLENBQUMsR0FBRyxDQUFDLE1BQU07S0FDNUI7Q0FDSixDQUFDLENBQUM7QUFHSCxJQUFJLDZDQUFtQixDQUFDLEdBQUcsRUFBRSxxQkFBcUIsRUFBRTtJQUNoRCxHQUFHLEVBQUU7UUFDRCxPQUFPLEVBQUUsZUFBTSxDQUFDLEdBQUcsQ0FBQyxPQUFPO1FBQzNCLE1BQU0sRUFBRSxlQUFNLENBQUMsR0FBRyxDQUFDLE1BQU07S0FDNUI7Q0FDSixDQUFDLENBQUEiLCJzb3VyY2VzQ29udGVudCI6WyIjIS91c3IvYmluL2VudiBub2RlXG5pbXBvcnQgJ3NvdXJjZS1tYXAtc3VwcG9ydC9yZWdpc3Rlcic7XG5pbXBvcnQgKiBhcyBjZGsgZnJvbSAnYXdzLWNkay1saWInO1xuaW1wb3J0IHtBd3NWcGNNc2tJbmZyYVN0YWNrfSBmcm9tIFwiLi4vbGliL2F3cy12cGMtbXNrLWluZnJhLXN0YWNrXCI7XG5pbXBvcnQgeyBDb25maWcgfSBmcm9tIFwiLi4vY29uZmlnL2NvbmZpZ1wiO1xuaW1wb3J0IHtBd3NJb3RDb3JlUHJvdmlzaW9uaW5nSW5mcmFTdGFja30gZnJvbSBcIi4uL2xpYi9hd3MtaW90LWNvcmUtcHJvdmlzaW9uaW5nLWluZnJhLXN0YWNrXCI7XG5pbXBvcnQge0F3c0lvdENvcmVSdWxlSW5mcmFTdGFja30gZnJvbSBcIi4uL2xpYi9hd3MtaW90LWNvcmUtcnVsZS1pbmZyYS1zdGFja1wiO1xuXG5jb25zdCBhcHAgPSBuZXcgY2RrLkFwcCgpO1xuXG5uZXcgQXdzSW90Q29yZVByb3Zpc2lvbmluZ0luZnJhU3RhY2soYXBwLCBcIkF3c0lvdENvcmVQcm92aXNpb25pbmdJbmZyYVN0YWNrXCIsIHtcbiAgICBlbnY6IHtcbiAgICAgICAgYWNjb3VudDogQ29uZmlnLmF3cy5hY2NvdW50LFxuICAgICAgICByZWdpb246IENvbmZpZy5hd3MucmVnaW9uLFxuICAgIH0sXG59KTtcblxubmV3IEF3c0lvdENvcmVSdWxlSW5mcmFTdGFjayhhcHAsIFwiQXdzSW90Q29yZVJ1bGVJbmZyYVN0YWNrXCIsIHtcbiAgICBlbnY6IHtcbiAgICAgICAgYWNjb3VudDogQ29uZmlnLmF3cy5hY2NvdW50LFxuICAgICAgICByZWdpb246IENvbmZpZy5hd3MucmVnaW9uLFxuICAgIH0sXG59KTtcblxuXG5uZXcgQXdzVnBjTXNrSW5mcmFTdGFjayhhcHAsIFwiQXdzVnBjTXNrSW5mcmFTdGFja1wiLCB7XG4gICAgZW52OiB7XG4gICAgICAgIGFjY291bnQ6IENvbmZpZy5hd3MuYWNjb3VudCxcbiAgICAgICAgcmVnaW9uOiBDb25maWcuYXdzLnJlZ2lvblxuICAgIH1cbn0pIl19 -------------------------------------------------------------------------------- /lib/aws-vpc-msk-infra-stack.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Stack, 3 | StackProps, 4 | aws_ec2 as ec2, 5 | aws_msk as msk, 6 | aws_lambda as lambda, 7 | aws_iam as iam, 8 | CfnOutput, 9 | } from "aws-cdk-lib"; 10 | import { Construct } from "constructs"; 11 | import {Config} from "../config/config"; 12 | import { CustomResource } from 'aws-cdk-lib'; 13 | import path from "path"; 14 | 15 | export class AwsVpcMskInfraStack extends Stack { 16 | vpc: ec2.CfnVPC; 17 | constructor(scope: Construct, id: string, props?: StackProps) { 18 | super(scope, id, props); 19 | 20 | // Create vpc 21 | this.vpc = new ec2.CfnVPC( // TODO: 무엇을 위한 vpc 인지 확인하기 22 | this, "vpc", { 23 | cidrBlock: Config.vpc.cidr + '.0.0/16', 24 | enableDnsHostnames: true, 25 | enableDnsSupport: true, 26 | instanceTenancy: 'default', 27 | tags: [ { key: "Config.app.service" + "-" + Config.app.environment + "-vpc", value: Config.app.service + '-' + Config.app.environment} ] 28 | }); 29 | 30 | new CfnOutput(this, 'vpcId', { 31 | exportName: Config.app.service + '-' + Config.app.environment + '-vpc-Id', 32 | value: this.vpc.ref, 33 | }) 34 | 35 | 36 | // Create two private subnets 37 | const subnet_private01a: ec2.CfnSubnet = this.subnet_creation('private01a', '.96.0/20'); 38 | const subnet_private01b: ec2.CfnSubnet = this.subnet_creation('private01b', '.112.0/20'); 39 | 40 | // Create Security Group 41 | let securityGroup = new ec2.CfnSecurityGroup( // TODO: 무엇을 위한 vpc 인지 확인하기 42 | this, Config.app.service + "-" + Config.app.environment + "-msk-security-group", { 43 | vpcId: this.vpc.ref, 44 | groupDescription: Config.app.service + '-' + Config.app.environment + '-msk-' + Config.msk.clusterName, 45 | groupName: Config.app.service + '-' + Config.app.environment + '-msk-' + Config.msk.clusterName, 46 | securityGroupIngress: [{ 47 | ipProtocol: "TCP", 48 | fromPort: 2181, 49 | toPort: 2181, 50 | cidrIp: Config.security_group[0], 51 | description: Config.security_group[0] 52 | }], 53 | // tags: [{ key: 'Name', value: Config.app.service + '-' + Config.app.environment + '-msk-' + Config.msk.clusterName, }], 54 | }); 55 | 56 | new CfnOutput(this, 'securityGroup', { 57 | exportName: Config.app.service + '-' + Config.app.environment + '-securityGroup-Id', 58 | value: securityGroup.attrGroupId, 59 | }) 60 | 61 | // Create MSK cluster 62 | let mskCluster = new msk.CfnCluster( 63 | this, Config.app.service + "-" + Config.app.environment + "-msk-cluster", { 64 | brokerNodeGroupInfo: { 65 | clientSubnets: [subnet_private01a.ref, subnet_private01b.ref], 66 | instanceType: 'kafka.t3.small', 67 | securityGroups: [securityGroup.ref], 68 | // the properties below are optional 69 | storageInfo: { ebsStorageInfo: { volumeSize: 1 }} 70 | }, 71 | clusterName: Config.app.service + '-' + Config.app.environment + '-msk-' + Config.msk.clusterName, 72 | kafkaVersion: '2.8.1', 73 | numberOfBrokerNodes: 2, 74 | 75 | // the properties below are optional 76 | clientAuthentication: {sasl: {scram: {enabled: true,},},} 77 | }); 78 | 79 | // Create lambda function, to get bootstrapbrokers 80 | const lambdaGetBootstrapBrokers = new lambda.Function( 81 | this, 82 | Config.app.service + "-" + Config.app.environment + "-get-bootstrapbrokers-lambda", 83 | { 84 | handler: 'get-bootstrapbrokers-lambda.lambda_handler', // Python handler format 85 | code: lambda.Code.fromAsset(path.join(__dirname, 'lambda')), // Update the path to your Python code 86 | runtime: lambda.Runtime.PYTHON_3_9, // Change to Python runtime 87 | description : "Lambda for get bootstrapbrokers", 88 | functionName: Config.app.service + "-" + Config.app.environment + "-get-bootstrapbrokers-lambda", 89 | environment: { 90 | CLUSTER_ARN: mskCluster.attrArn, 91 | }, 92 | }); 93 | 94 | lambdaGetBootstrapBrokers.addToRolePolicy(new iam.PolicyStatement({ 95 | actions: ['kafka:GetBootstrapBrokers'], 96 | resources: [mskCluster.attrArn], 97 | })); 98 | 99 | const getBootstrapBrokers = new CustomResource( 100 | this, Config.app.service + "-" + Config.app.environment + 101 | "-get-bootstrapbrokers", 102 | { 103 | serviceToken: lambdaGetBootstrapBrokers.functionArn, 104 | } 105 | ); 106 | 107 | // get bootstrapBroker string 108 | const bootstrapBrokerString = getBootstrapBrokers.getAtt('BootstrapBrokerString').toString(); 109 | 110 | 111 | new CfnOutput(this, 'mskCluster', { 112 | exportName: Config.app.service + '-' + Config.app.environment + '-msk-' + Config.msk.clusterName + "-BootstrapBrokers", 113 | value: bootstrapBrokerString, 114 | }) 115 | } 116 | 117 | subnet_creation(subnet_name: string, subnet_cidr: string): ec2.CfnSubnet 118 | { 119 | const subnet_group = subnet_name.slice(0, -1); 120 | const az = subnet_name.slice(-1); 121 | const subnet = new ec2.CfnSubnet(this, 'subnet' + subnet_name, { 122 | availabilityZone: this.region + az, 123 | cidrBlock: Config.vpc.cidr + subnet_cidr, 124 | vpcId: this.vpc.ref, 125 | tags: [{key: 'Name', value: Config.app.service + '-' + Config.app.environment + '-' + subnet_group + '-' + az}] 126 | }); 127 | 128 | new CfnOutput(this, 'subnet' + subnet_name + 'output', { 129 | exportName: Config.app.service + '-' + Config.app.environment + '-subnet-' + subnet_name, 130 | value: subnet.ref 131 | }) 132 | 133 | return subnet; 134 | } 135 | } -------------------------------------------------------------------------------- /lib/aws-iot-core-rule-infra-stack.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Stack, 3 | StackProps, 4 | aws_iot as iot, 5 | aws_iam as iam, 6 | aws_secretsmanager as secretsmanager, 7 | aws_kms as kms, 8 | Fn, 9 | } from "aws-cdk-lib"; 10 | import { Construct } from "constructs"; 11 | import rulePolicyJson from "./rule/rule-policy.json"; 12 | import { Config } from "../config/config"; 13 | import ruleKeysJson from "./rule/rule-keys.json"; 14 | import keyPolicyJson from "./rule/key-policy.json"; 15 | 16 | 17 | export class AwsIotCoreRuleInfraStack extends Stack { 18 | constructor(scope: Construct, id: string, props?: StackProps) { 19 | super(scope, id, props); 20 | 21 | // Import VPC and Subnet 22 | // const vpc = ec2.Vpc.fromLookup(this, 'vpc', { isDefault: false, tags: { key: "Config.app.service" + "-" + Config.app.environment + "-vpc", value: Config.app.service + '-' + Config.app.environment}}); 23 | const vpcId = Fn.importValue(Config.app.service + '-' + Config.app.environment + '-vpc-Id'); 24 | const securityGroup = Fn.importValue(Config.app.service + '-' + Config.app.environment + '-securityGroup-Id'); 25 | 26 | const subnet_private01a = Fn.importValue(Config.app.service + '-' + Config.app.environment + "-subnet-private01a"); 27 | const subnet_private01b = Fn.importValue(Config.app.service + '-' + Config.app.environment + "-subnet-private01b"); 28 | 29 | const mskCluster_bootstrap_brokers = Fn.importValue(Config.app.service + '-' + Config.app.environment + '-msk-' + Config.msk.clusterName + "-BootstrapBrokers"); 30 | 31 | // For rules in IoT Core, please refer to this https://ap-northeast-2.console.aws.amazon.com/iot/home?region=ap-northeast-2#/rulehub 32 | // For role/policy for rules, please refer to https://docs.aws.amazon.com/iot/latest/developerguide/iot-create-role.html 33 | 34 | // Create role for Rule engine 35 | let roleRuleEngine = new iam.Role( 36 | this, Config.app.service + "-" + Config.app.environment + "-rule-engine-role", { 37 | assumedBy: new iam.ServicePrincipal("iot.amazonaws.com"), 38 | description: "AWS I AM role for IoT rule engine", 39 | roleName: Config.app.service + "-" + Config.app.environment + "-rule-engine-role", 40 | } 41 | ); 42 | 43 | // Create policy for rule engine 44 | let iotCoreRolePolicy = iam.PolicyDocument.fromJson(rulePolicyJson); 45 | 46 | let ruleEnginePolicy = new iam.Policy( 47 | this, 48 | Config.app.service + 49 | "-" + 50 | Config.app.environment + 51 | "-iot-core-role-policy", 52 | { 53 | document: iotCoreRolePolicy, 54 | policyName: "iotCoreRolePolicy", 55 | } 56 | ); 57 | 58 | ruleEnginePolicy.attachToRole(roleRuleEngine) 59 | 60 | //Create Topic Rule Destination for Kafka, replace security group, subnet, and VPC values with your own 61 | let cfnTopicRuleDestination = new iot.CfnTopicRuleDestination( 62 | this, 63 | "MyCfnTopicRuleDestination", 64 | /* all optional props */ { 65 | vpcProperties: { 66 | roleArn: roleRuleEngine.roleArn, 67 | securityGroups: [securityGroup], 68 | subnetIds: [subnet_private01a, subnet_private01b], 69 | vpcId: vpcId, 70 | }, 71 | } 72 | ); 73 | 74 | //CDK Unable to infer the rule destination requires IAM policies. Manually adding dependency 75 | cfnTopicRuleDestination.node.addDependency(ruleEnginePolicy) 76 | 77 | 78 | //Create KMS key for secret encryption 79 | keyPolicyJson.Statement[0].Principal.AWS = "arn:aws:iam::" + Config.aws.account + ":root" 80 | 81 | const key = new kms.CfnKey(this, "Key", { 82 | enabled: true, 83 | enableKeyRotation: false, 84 | keyPolicy: keyPolicyJson, 85 | keySpec: "SYMMETRIC_DEFAULT", 86 | keyUsage: "ENCRYPT_DECRYPT", 87 | }); 88 | 89 | new kms.CfnAlias(this, "KeyAlias", { 90 | aliasName: "alias/" + Config.app.application + "-" + Config.app.environment + "-msk", targetKeyId: key.ref 91 | }); 92 | 93 | //Create AWS Secrets Manager Password for MSK connection 94 | const iotSecret = new secretsmanager.CfnSecret(this, "IoTSecret", { 95 | name: 96 | "AmazonMSK_" + Config.app.application + "-" + Config.app.environment, 97 | kmsKeyId: key.ref, 98 | generateSecretString: { 99 | passwordLength: 20, 100 | excludeCharacters: "]/'", 101 | generateStringKey: "password", 102 | secretStringTemplate: JSON.stringify({username: "test-kafka"}), 103 | }, 104 | }); 105 | 106 | // Get rules from ruleKeysJson 107 | let testRuleKeys = ruleKeysJson.testRules; 108 | 109 | // Create Rules in IoT Core to send to MSK 110 | testRuleKeys.forEach((key) => { 111 | new iot.CfnTopicRule( 112 | this, Config.app.service + "-" + Config.app.environment + `-topic-rule-${key}`, 113 | { 114 | topicRulePayload: { 115 | actions: [ 116 | { 117 | kafka: { 118 | clientProperties: { 119 | acks: "1", 120 | //Replace placeholder Kafka bootstrap Servers with your own 121 | "bootstrap.servers": mskCluster_bootstrap_brokers, 122 | "security.protocol": "SASL_SSL", 123 | "sasl.mechanism": "SCRAM-SHA-512", 124 | "sasl.scram.username": 125 | "${get_secret('AmazonMSK_iot','SecretString','username'," + 126 | `'${roleRuleEngine.roleArn}')}`, 127 | "sasl.scram.password": 128 | "${get_secret('AmazonMSK_iot','SecretString','password'," + 129 | `'${roleRuleEngine.roleArn}')}`, 130 | }, 131 | destinationArn: cfnTopicRuleDestination.attrArn, 132 | topic: `test-msk-topic.${key}` 133 | }, 134 | }, 135 | ], 136 | sql: `SELECT * FROM 'test-rule/${key}'`, 137 | }, 138 | // iot does not allow rule '-' (dash). 139 | ruleName: `test_rule_${key}`, 140 | } 141 | ); 142 | }); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/aws-iot-core-provisioning-infra-stack.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Stack, 3 | StackProps, 4 | RemovalPolicy, 5 | aws_s3_deployment, 6 | aws_lambda as lambda, 7 | aws_iot as iot, 8 | aws_iam as iam, 9 | aws_s3 as s3 10 | } from "aws-cdk-lib"; 11 | import { 12 | AwsCustomResource, 13 | AwsCustomResourcePolicy, 14 | PhysicalResourceId, 15 | } from "aws-cdk-lib/custom-resources"; 16 | import { Construct } from "constructs"; 17 | import testDevicePolicyJson from "./device/device-policy.json"; 18 | import testProvisioningTemplateJson from "./device/provisioning-template.json"; 19 | import * as path from "path"; 20 | import { Config } from "../config/config"; 21 | import testDeviceClaimCertificatePolicyJson from "./device/device-cc-policy.json"; 22 | 23 | export class AwsIotCoreProvisioningInfraStack extends Stack { 24 | constructor(scope: Construct, id: string, props?: StackProps) { 25 | super(scope, id, props); 26 | 27 | // Modify testDevicePolicyJson according to Configs and create device policy for device policy 28 | testDevicePolicyJson.Statement[1].Resource = [ 29 | `arn:aws:iot:${Config.aws.region}:${Config.aws.account}:topic/$aws/rules/*`, 30 | `arn:aws:iot:${Config.aws.region}:${Config.aws.account}:topic/` + '${iot:ClientId}' 31 | ] 32 | testDevicePolicyJson.Statement[2].Resource = [ 33 | `arn:aws:iot:${Config.aws.region}:${Config.aws.account}:topicfilter/` + '${iot:ClientId}' 34 | ] 35 | 36 | let testDevicePolicy = new iot.CfnPolicy( 37 | this, Config.app.service + "-" + Config.app.environment + "device-policy", 38 | { 39 | policyDocument: testDevicePolicyJson, 40 | policyName: Config.app.service + "-" + Config.app.environment + "-device-policy", 41 | } 42 | ); 43 | 44 | 45 | // Create role for pre-provisioning lambda for verification of devices 46 | let rolePreProvisioningLambda = new iam.Role( 47 | this, Config.app.service + "-" + Config.app.environment + "-pre-provisioning-lambda-role", 48 | { 49 | assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), 50 | description: "AWS IAM role for pre-provisioning lambda", 51 | roleName: Config.app.service + "-" + Config.app.environment + "-pre-provisioning-lambda-role", 52 | } 53 | ); 54 | 55 | 56 | // Crate lambda for pre-provisioning hook and add permission for invoke 57 | let lambdaPreProvisioningHook = new lambda.Function( 58 | this, Config.app.service + "-" + Config.app.environment + 59 | "-pre-provisioning-hook-lambda", 60 | { 61 | code: lambda.Code.fromAsset(path.join(__dirname, "lambda")), 62 | handler: "verify-devices-lambda.lambda_handler", 63 | runtime: lambda.Runtime.PYTHON_3_9, 64 | role: rolePreProvisioningLambda, 65 | description: "Lambda for pre-provisioning hook", 66 | functionName: Config.app.service + "-" + Config.app.environment + "-pre-provisioning-hook-lambda", 67 | } 68 | ); 69 | 70 | lambdaPreProvisioningHook.addPermission("InvokePermission", { 71 | principal: new iam.ServicePrincipal("iot.amazonaws.com"), 72 | action: "lambda:InvokeFunction", 73 | }); 74 | 75 | 76 | // Crate role for provisioning templates and add AWSIoTThingsRegistration policy 77 | let roleProvisioning = new iam.Role( 78 | this, Config.app.service + "-" + Config.app.environment + "-provisioning-template-role", 79 | { 80 | assumedBy: new iam.ServicePrincipal("iot.amazonaws.com"), 81 | description: "AWS IAM role for provisioning services", 82 | roleName: Config.app.service + "-" + Config.app.environment + "-provisioning-template-role", 83 | } 84 | ); 85 | 86 | roleProvisioning.addManagedPolicy( 87 | iam.ManagedPolicy.fromAwsManagedPolicyName( 88 | "service-role/AWSIoTThingsRegistration" 89 | ) 90 | ); 91 | 92 | // Create provisioning template 93 | testProvisioningTemplateJson.Resources.policy.Properties.PolicyName = testDevicePolicy.policyName! 94 | 95 | let testProvisioningTemplate = new iot.CfnProvisioningTemplate( 96 | this, Config.app.service + "-" + Config.app.environment + "-provision-template", 97 | { 98 | provisioningRoleArn: roleProvisioning.roleArn, 99 | templateBody: JSON.stringify(testProvisioningTemplateJson), 100 | enabled: true, 101 | preProvisioningHook: { 102 | "payloadVersion": "2020-04-01", 103 | "targetArn": lambdaPreProvisioningHook.functionArn 104 | }, 105 | description: "AWS IoT Provisioning Template", 106 | templateName: Config.app.service + "-" + Config.app.environment + "-provision-template", 107 | } 108 | ); 109 | 110 | // Modify testDeviceClaimCertificatePolicyJson and create vehicle gateway policy for Claim Certificate 111 | let templateTopicCreate = `arn:aws:iot:${Config.aws.region}:${Config.aws.account}:topic/$aws/certificates/create/*` 112 | let templateTopicProvisioning = `arn:aws:iot:${Config.aws.region}:${Config.aws.account}:topic/$aws/provisioning-templates/${testProvisioningTemplate.templateName}/provision/*` 113 | testDeviceClaimCertificatePolicyJson.Statement[1].Resource = [templateTopicCreate, templateTopicProvisioning] 114 | 115 | let templateTopicFilterCreate = `arn:aws:iot:${Config.aws.region}:${Config.aws.account}:topicfilter/$aws/certificates/create/*` 116 | let templateTopicFilterProvisioning = `arn:aws:iot:${Config.aws.region}:${Config.aws.account}:topicfilter/$aws/provisioning-templates/${testProvisioningTemplate.templateName}/provision/*` 117 | testDeviceClaimCertificatePolicyJson.Statement[2].Resource = [templateTopicFilterCreate, templateTopicFilterProvisioning] 118 | 119 | let testDeviceClaimCertificatePolicy = new iot.CfnPolicy( 120 | this, Config.app.service + "-" + Config.app.environment + "-claim-certificate-policy", 121 | { 122 | policyDocument: testDeviceClaimCertificatePolicyJson, 123 | policyName: Config.app.service + "-" + Config.app.environment + "-claim-certificate-policy", 124 | } 125 | ); 126 | 127 | // Create claim certificate by using AwsCustomResource 128 | let createKeysAndCertificateForClaimCertificate = new AwsCustomResource( 129 | this, Config.app.service + "-" + Config.app.environment + "-create-keys-and-certificate-for-claim-certificate", 130 | { 131 | onUpdate: { 132 | service: "Iot", 133 | action: "createKeysAndCertificate", 134 | parameters: {setAsActive: true}, 135 | physicalResourceId: PhysicalResourceId.fromResponse("certificateId"), 136 | outputPaths: ["certificateArn", "certificatePem", "keyPair.PublicKey", "keyPair.PrivateKey"], 137 | }, 138 | policy: AwsCustomResourcePolicy.fromSdkCalls({resources: AwsCustomResourcePolicy.ANY_RESOURCE}), 139 | } 140 | ); 141 | 142 | 143 | // Attach policy to claim certificate 144 | let PolicyPrincipalAttachmentForClaimCertificate = 145 | new iot.CfnPolicyPrincipalAttachment( 146 | this, Config.app.service + "-" + Config.app.environment + "policy-principal-attachment", { 147 | policyName: testDeviceClaimCertificatePolicy.policyName!, 148 | principal: createKeysAndCertificateForClaimCertificate.getResponseField("certificateArn"), 149 | } 150 | ); 151 | 152 | //TODO: Cfn bucket 활용하기 153 | let cdkTestS3Bucket = new s3.Bucket(this, 'cdkTestS3Bucket', { 154 | blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, 155 | versioned: true, 156 | removalPolicy: RemovalPolicy.DESTROY, 157 | autoDeleteObjects: true, 158 | bucketName: Config.app.service + "-" + Config.app.environment + "-" + Config.aws.account + "-" + Config.s3BucketName 159 | }); 160 | 161 | // Save the vehicle-gateway certificates and keys to S3 162 | let keyDeploymentForDeviceClaimCertificate = new aws_s3_deployment.BucketDeployment( 163 | this, Config.app.service + "-" + Config.app.environment + "put-key-to-s3", 164 | { 165 | destinationBucket: cdkTestS3Bucket, 166 | sources: [ 167 | aws_s3_deployment.Source.data( 168 | "claim-certificate/claim.pem", 169 | createKeysAndCertificateForClaimCertificate.getResponseField( 170 | "certificatePem" 171 | ) 172 | ), 173 | aws_s3_deployment.Source.data( 174 | "claim-certificate/claim.public.key", 175 | createKeysAndCertificateForClaimCertificate.getResponseField( 176 | "keyPair.PublicKey" 177 | ) 178 | ), 179 | aws_s3_deployment.Source.data( 180 | "claim-certificate/claim.private.key", 181 | createKeysAndCertificateForClaimCertificate.getResponseField( 182 | "keyPair.PrivateKey" 183 | ) 184 | ), 185 | ], 186 | } 187 | ); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | CASE, the mega-trend keyword for the automotive industry, can be summarized as Connectivity, Autonomous, Sharing, and Electrification. 3 | Among them, connectivity is the foundation that enables cars that are integrated with IT technology to develop into living spaces that are connected to the world and move, and to provide various mobility services. 4 | As the number of connected vehicles in mobility services increases rapidly, fleet management systems (FMS) need to be configured to meet various needs. 5 | In this post, we'll share how 42dot addressed these challenges with AWS IoT Core and AWS CDK. 6 | 7 | ## About 42dot 8 | 42dot is the Global Software Center of Hyundai Motor Group, realizing freedom of movement in a connected world where everything moves by itself through software-defined vehicles (SDVs) based on service-defined and safety-designed values. 9 | 42dot developed AKit, an integrated solution for implementing autonomous driving technology, and TAP!, an integrated platform for autonomous mobility, and obtained the first autonomous driving paid transportation license in Korea. 10 | Currently, it operates an integrated autonomous transportation service in Sangam and Cheonggyecheon, Seoul, where anyone can call not only 42dot's autonomous vehicles but also autonomous vehicles from various companies through the TAP! application. 11 | 42dot is leveraging AWS IoT to easily and securely connect and manage its fleet management system (FMS) vehicle devices to the cloud. It is using AWS CDKs as coded infrastructure tools to automate the deployment of IoT infrastructure for different requirements. 12 | 13 | ## Introduction to AWS IoT Core 14 | AWS IoT Core is an AWS IoT service that allows you to connect and manage IoT devices and integrate with other AWS services. 15 | AWS IoT Core provides the [IoT Device SDK](https://docs.aws.amazon.com/iot/latest/developerguide/iot-sdks.html), and devices developed based on it can easily use IoT Core. 16 | IoT Core is responsible for communicating with devices and plays a central role in AWS IoT services, hence the word "Core". IoT Core provides message routing so that it can be used with storage like S3 and data pipelines like MSK. 17 | You can also utilize services like [Greengrass](https://aws.amazon.com/greengrass/), [FleetWise](https://aws.amazon.com/iot-fleetwise/), and [SiteWise](https://aws.amazon.com/iot-sitewise/) that come with AWS IoT to increase the efficiency of operating and managing IoT devices. 18 | 19 | ## Introduction to CDK 20 | The [AWS Cloud Development Kit](https://aws.amazon.com/cdk/) (hereafter AWS CDK) is an open source software development framework that allows you to define cloud application resources using familiar programming languages. 21 | This approach to managing infrastructure through code is called Infrastructure as a Code, or IaC for short. 22 | The CDK creates the resources by converting all written code into [CloudFormation](https://aws.amazon.com/cloudformation/) templates. 23 | For those looking to implement IaC for the first time on AWS, the CDK provided by AWS is a great first point of departure. 24 | 25 | # Prerequisites 26 | [An AWS Account](https://console.aws.amazon.com/console/home) 27 | 28 | [npm](https://www.npmjs.com/get-npm) 29 | - You can install the CDK via npm. 30 | 31 | [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html), [AWS IoT Core](https://aws.amazon.com/iot-core/getting-started/?nc=sn&loc=5&dn=1) 32 | - This article explains how to apply CDK to build IoT Core infrastructure. For basic usage of CDK and IoT, please refer to the above link. 33 | 34 | [Provisioning of IoT Core](https://docs.aws.amazon.com/iot/latest/developerguide/iot-provision.html) 35 | - Refer to the above link for specific instructions on how to create certificates and policies required to use IoT Core. 36 | 37 | [Setting tsconfig.json to use Json as a document type](https://www.typescriptlang.org/tsconfig#resolveJsonModule) 38 | - Please add the settings guided in the link to `tsconfig.json` in order to use Json files as objects within Typescript. 39 | 40 | # Sample Scenario and System Architecture 41 | ![](figures/system_architecture_msk_English.png) 42 | The figure above shows a simple system architecture utilizing CDK. The behavioral scenario of this architecture is as follows. 43 | 44 | > A device mounted on a vehicle sends vehicle data to the IoT Core using the MQTT protocol. 45 | > After receiving the message, IoT Core forwards the message to MSK (Managed Streaming for Apache Kafka) through the Basic ingest function. 46 | 47 | Basic ingest is one of the Message Routing features provided by IoT Core, which allows you to conveniently send messages to other AWS services without incurring messaging costs. 48 | In this article, we will implement how to create a certificate and register it with IoT Core using the provisioning method of IoT Core, which is the provisioning method by claim, which is the method to use when it is difficult to provision unique credentials. 49 | 50 | ## Infrastructure required to implement the scenario 51 | ### Install CDK and bootstrap 52 | Use th e code below to install the CDK and create a CDK project. 53 | 54 | ``` 55 | git clone https://github.com/aws-samples/42dot-cdk-fleetmanagement-system 56 | cd 42dot-cdk-fleetmanagement-system 57 | npm install 58 | cdk --version 59 | chmod +x start.sh 60 | ./start.sh 61 | ``` 62 | 63 | ### Infrastructure Stacks 64 | To implement the above architecture, we define the following two infrastructure resource stacks 65 | `AwsVpcMskInfraStack` 66 | A stack for declaring VPC, subnet, security group and MSK cluster. 67 | 68 | `AwsIotCoreProvisioningInfraStack` 69 | A stack for implementing a template for the enrollment of AWS IoT Things with claim certificates, the issuance of persistent certificates, and the accompanying provisioning service. 70 | This stack also includes stack for issuing and storing claim certificates. The resulting certificate is shipped with the device firmware and requests the issuance and provisioning of a permanent certificate upon first use. 71 | 72 | `AwsIotCoreRuleInfraStack` 73 | A stack for forwarding and storing messages reported by devices. 74 | This stack also defines an IoT Rule in IoT Core and set it up to forward messages to MSK via Basic ingest. 75 | 76 | 77 | # Walkthrough 78 | 79 | ## Step 1: Creating VPC and MSK Cluster 80 | In `AwsVpcMskInfraStack` 81 | 1. VPC Creation 82 | - Declares a VPC for use with MSK, configuring DNS support and instance tenancy, and assigns it tags based on configuration settings. 83 | 2. Subnets 84 | - Creates two private subnets within the VPC intended for the MSK cluster allocation. 85 | 3. Security Group 86 | - Establishes a security group for the MSK cluster, specifying ingress rules for communication within the cluster. 87 | 4. MSK Cluster 88 | - Utilizes the previously defined subnets and security group to declare the MSK cluster, setting up broker node group information, cluster name, Kafka version, and authentication methods. 89 | 5. Getting Bootstrap Brokers 90 | - Employs an AWS Lambda function to retrieve the bootstrap brokers information from the created MSK cluster. 91 | - This is facilitated by using a CustomResource from the aws-cdk-lib, making it straightforward to obtain this information. 92 | 93 | ## Step 2: Device Policy and Security Setup 94 | In `AwsIotCoreProvisioningInfraStack` 95 | 96 | 1. Device Policy Customization 97 | - Modifies a predefined JSON policy ([`device/device-policy.json`](lib/device/device-policy.json)) based on configuration, creating an IoT device policy that allows topic publishing and subscription. 98 | 2. Lambda Role for Pre-Provisioning 99 | - Establishes an IAM role with permissions for AWS Lambda to verify devices during the pre-provisioning phase. 100 | 101 | ## Step 3: Lambda Function and Provisioning Template 102 | In `AwsIotCoreProvisioningInfraStack` 103 | 104 | 1. Pre-Provisioning Hook Lambda 105 | - Deploys a Lambda function ([`lib/lambda/verify-devices-lambda`](/lib/lambda/verify-devices-lambda.py)) to facilitate device verification, equipped with a specific IAM role and permissions to invoke the function from IoT. 106 | 2. Provisioning Template 107 | - Utilizes provisioning-template.json to define a provisioning template that includes the device policy and a pre-provisioning hook, enabling automated device provisioning. 108 | 109 | ## Step 4: Claim Certificate and Data Storage 110 | In `AwsIotCoreProvisioningInfraStack` 111 | 112 | 1. S3 Bucket for Certificate Storage 113 | - Creates an S3 bucket to securely store claim certificates and keys, configuring it with strict access blocks and auto-deletion policies for security. 114 | 2. Claim Certificate Policy 115 | - Defines and applies a policy to allow certificate claims, ensuring devices can securely communicate with AWS IoT Core. 116 | 3. Certificate Generation and Storage 117 | - Uses AwsCustomResource to call the CreateKeysAndCertificate API, generating a claim certificate and storing it in the S3 bucket. The certificate is then linked to the policy, completing the provisioning process. 118 | 119 | ## Step 5. Declaring message transfer to MSK via AWS IoT Core Rule engine 120 | In `AwsIotCoreRuleInfraStack` 121 | 122 | 1. IAM Role and Policy for Rule Engine: 123 | - Establishes an IAM role for the IoT Rule Engine, allowing it to route messages based on predefined rules. 124 | - A specific policy ([`lib/rule/rule-policy.json`](lib/rule/rule-policy.json)) grants necessary permissions for message forwarding to Amazon MSK. 125 | 2. Configuration of TopicRuleDestination: 126 | - Configures a TopicRuleDestination with security, networking settings (VPC, subnets, and security group), and associates it with the rule engine's IAM role, ensuring messages are securely routed to the correct MSK cluster. 127 | 3. Encryption Keys and Secret Management: 128 | - Utilizes AWS KMS to create encryption keys ([`lib/rule/key-policy.json`](lib/rule/key-policy.json)) for securing message content. 129 | - AWS Secrets Manager stores the MSK credentials, using the KMS key for encryption, to safely handle connection details to MSK. 130 | 4. IoT Core Rules for Message Routing: 131 | - Defines rules ([`lib/rule/rule-keys.json`](lib/rule/rule-keys.json)) in AWS IoT Core for routing messages to MSK topics. 132 | - Each rule specifies the SQL statement for message selection and the action to forward messages to MSK, including topic names and credentials fetched securely from AWS Secrets Manager. 133 | 134 | ## Step 6. Declaring the Stack in your app 135 | Inside [`bin/cdk-test-project`](/bin/cdk-test-project.ts), declare the stacks we declared earlier - `AwsVpcMskInfraStack`, `AwsIotCoreProvisioningInfraStack`, `AwsIoTCoreRuleInfraStack`. 136 | 137 | ## Step 7. Deploy the stack 138 | Having done that, you can try configuring it with the `AWS CloudFormation` template via the `cdk synth` command. 139 | 140 | ``` 141 | cdk synth 142 | ``` 143 | 144 | The `cdk diff` command allows you to compare previously deployed resources with those defined by the current code. 145 | 146 | ``` 147 | cdk diff 148 | ``` 149 | 150 | The `cdk deploy` command deploys the configured resource code to AWS CloudFormation and creates the resources in order. 151 | You can optionally deploy one or all of the stacks. 152 | 153 | ``` 154 | cdk deploy # If there is only one stack 155 | cdk deploy STACK_NAME # If you only want to deploy one specific stack among multiple stacks 156 | cdk deploy --all # Deploy all multiple stacks 157 | ``` 158 | 159 | Now you're all set. When your IoT device sends a message in line with the `test-rule/${key}` topic, 160 | AWS IoT forwards it directly to the `.${key}` topic. 161 | This ability to forward messages to other AWS services through a simple rule definition is called basic ingest. 162 | The advantage of utilizing Basic ingest is that you are not charged for it, and there are no costs associated with forwarding messages to other AWS services. 163 | 164 | 165 | # Cleaning up 166 | To remove all stacks deployed so far, use the following command 167 | 168 | ``` 169 | ./end.sh 170 | ``` 171 | 172 | 173 | # Conclusion 174 | In this post, we've configured the infrastructure from device registration to data storage using IoT Core using CDK. 175 | The CDK is a tool provided by AWS that guarantees high compatibility and can be a fundamental tool for building an AWS service-based architecture as an IaC. 176 | IoT services have many components that need to be prepared in advance in the cloud infrastructure and are sensitive to change history because there are many decisions and changes that need to be made based on the behavior observed by equipment operators and service developers. 177 | Managing this in code allows you to quickly deploy the infrastructure you need based on your environment and reduces human error. 178 | This concludes the post. -------------------------------------------------------------------------------- /lib/aws-vpc-msk-infra-stack.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.AwsVpcMskInfraStack = void 0; 4 | const aws_cdk_lib_1 = require("aws-cdk-lib"); 5 | const config_1 = require("../config/config"); 6 | const custom_resources_1 = require("aws-cdk-lib/custom-resources"); 7 | // import * as sqs from 'aws-cdk-lib/aws-sqs'; 8 | class AwsVpcMskInfraStack extends aws_cdk_lib_1.Stack { 9 | constructor(scope, id, props) { 10 | super(scope, id, props); 11 | // Create vpc 12 | this.vpc = new aws_cdk_lib_1.aws_ec2.CfnVPC(// TODO: 무엇을 위한 vpc 인지 확인하기 13 | this, "vpc", { 14 | cidrBlock: config_1.Config.vpc.cidr + '.0.0/16', 15 | enableDnsHostnames: true, 16 | enableDnsSupport: true, 17 | instanceTenancy: 'default', 18 | tags: [{ key: "Config.app.service" + "-" + config_1.Config.app.environment + "-vpc", value: config_1.Config.app.service + '-' + config_1.Config.app.environment }] 19 | }); 20 | new aws_cdk_lib_1.CfnOutput(this, 'vpcId', { 21 | exportName: config_1.Config.app.service + '-' + config_1.Config.app.environment + '-vpc-Id', 22 | value: this.vpc.ref, 23 | }); 24 | // Create two private subnets 25 | const subnet_private_01_a = this.subnet_creation('private01a', '.96.0/20'); 26 | const subnet_private_01_c = this.subnet_creation('private01c', '.112.0/20'); 27 | // Create Security Group 28 | let securityGroup = new aws_cdk_lib_1.aws_ec2.CfnSecurityGroup(// TODO: 무엇을 위한 vpc 인지 확인하기 29 | this, config_1.Config.app.service + "-" + config_1.Config.app.environment + "-msk-security-group", { 30 | vpcId: this.vpc.ref, 31 | groupDescription: config_1.Config.app.service + '-' + config_1.Config.app.environment + '-msk-' + config_1.Config.msk.clusterName, 32 | groupName: config_1.Config.app.service + '-' + config_1.Config.app.environment + '-msk-' + config_1.Config.msk.clusterName, 33 | securityGroupIngress: [{ 34 | ipProtocol: "TCP", 35 | fromPort: 2181, 36 | toPort: 2181, 37 | cidrIp: config_1.Config.security_group[0], 38 | description: config_1.Config.security_group[0] 39 | }], 40 | // tags: [{ key: 'Name', value: Config.app.service + '-' + Config.app.environment + '-msk-' + Config.msk.clusterName, }], 41 | }); 42 | new aws_cdk_lib_1.CfnOutput(this, 'securityGroup', { 43 | exportName: config_1.Config.app.service + '-' + config_1.Config.app.environment + '-securityGroup-Id', 44 | value: securityGroup.attrGroupId, 45 | }); 46 | // Create MSK cluster 47 | let mskCluster = new aws_cdk_lib_1.aws_msk.CfnCluster(this, config_1.Config.app.service + "-" + config_1.Config.app.environment + "-msk-cluster", { 48 | brokerNodeGroupInfo: { 49 | clientSubnets: [subnet_private_01_a.ref, subnet_private_01_c.ref], 50 | instanceType: 'kafka.t3.small', 51 | securityGroups: [securityGroup.ref], 52 | // the properties below are optional 53 | storageInfo: { ebsStorageInfo: { volumeSize: 1 } } 54 | }, 55 | clusterName: config_1.Config.app.service + '-' + config_1.Config.app.environment + '-msk-' + config_1.Config.msk.clusterName, 56 | kafkaVersion: '2.8.1', 57 | numberOfBrokerNodes: 2, 58 | // the properties below are optional 59 | clientAuthentication: { sasl: { scram: { enabled: true, }, }, } 60 | }); 61 | // TODO: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-kafka/classes/getbootstrapbrokerscommand.html 62 | let getBootStrapBrokers = new custom_resources_1.AwsCustomResource(this, config_1.Config.app.service + "-" + config_1.Config.app.environment + "-get-bootstrap-servers", { 63 | onUpdate: { 64 | service: "client-msk", 65 | action: "GetBootstrapBrokers", 66 | parameters: { ClusterArn: mskCluster.attrArn }, 67 | physicalResourceId: custom_resources_1.PhysicalResourceId.of(mskCluster.attrArn), 68 | }, 69 | policy: custom_resources_1.AwsCustomResourcePolicy.fromSdkCalls({ resources: custom_resources_1.AwsCustomResourcePolicy.ANY_RESOURCE }), 70 | //TODO: 안될 경우 ChatGPT의 조언대로 변경 가능 71 | // policy: AwsCustomResourcePolicy.fromStatements([ 72 | // new iam.PolicyStatement({ 73 | // actions: ['msk:GetBootstrapBrokers'], 74 | // resources: ['*'], 75 | // }), 76 | // ]), 77 | }); 78 | const bootstrapBrokerString = getBootStrapBrokers.getResponseField("BootstrapBrokerString"); 79 | console.log("bootstrapBrokerString: " + bootstrapBrokerString); 80 | // Use AWS SDK to fetch bootstrap brokers 81 | new aws_cdk_lib_1.CfnOutput(this, 'mskCluster', { 82 | exportName: config_1.Config.app.service + '-' + config_1.Config.app.environment + '-msk-' + config_1.Config.msk.clusterName + "-BootstrapBrokers", 83 | value: bootstrapBrokerString, 84 | }); 85 | } 86 | subnet_creation(subnet_name, subnet_cidr) { 87 | const subnet_group = subnet_name.slice(0, -1); 88 | const az = subnet_name.slice(-1); 89 | const subnet = new aws_cdk_lib_1.aws_ec2.CfnSubnet(this, 'subnet' + subnet_name, { 90 | availabilityZone: this.region + az, 91 | cidrBlock: config_1.Config.vpc.cidr + subnet_cidr, 92 | vpcId: this.vpc.ref, 93 | tags: [{ key: 'Name', value: config_1.Config.app.service + '-' + config_1.Config.app.environment + '-' + subnet_group + '-' + az }] 94 | }); 95 | new aws_cdk_lib_1.CfnOutput(this, 'subnet' + subnet_name + 'output', { 96 | exportName: config_1.Config.app.service + '-' + config_1.Config.app.environment + '-subnet-' + subnet_name, 97 | value: subnet.ref 98 | }); 99 | return subnet; 100 | } 101 | } 102 | exports.AwsVpcMskInfraStack = AwsVpcMskInfraStack; 103 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXdzLXZwYy1tc2staW5mcmEtc3RhY2suanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJhd3MtdnBjLW1zay1pbmZyYS1zdGFjay50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSw2Q0FNcUI7QUFFckIsNkNBQXdDO0FBQ3hDLG1FQUE0RztBQUU1Ryw4Q0FBOEM7QUFFOUMsTUFBYSxtQkFBb0IsU0FBUSxtQkFBSztJQUUxQyxZQUFZLEtBQWdCLEVBQUUsRUFBVSxFQUFFLEtBQWtCO1FBQ3hELEtBQUssQ0FBQyxLQUFLLEVBQUUsRUFBRSxFQUFFLEtBQUssQ0FBQyxDQUFDO1FBRXhCLGFBQWE7UUFDYixJQUFJLENBQUMsR0FBRyxHQUFHLElBQUkscUJBQUcsQ0FBQyxNQUFNLENBQUUsMkJBQTJCO1FBQ2xELElBQUksRUFBRSxLQUFLLEVBQUU7WUFDVCxTQUFTLEVBQUUsZUFBTSxDQUFDLEdBQUcsQ0FBQyxJQUFJLEdBQUcsU0FBUztZQUN0QyxrQkFBa0IsRUFBRSxJQUFJO1lBQ3hCLGdCQUFnQixFQUFFLElBQUk7WUFDdEIsZUFBZSxFQUFFLFNBQVM7WUFDMUIsSUFBSSxFQUFFLENBQUUsRUFBRSxHQUFHLEVBQUUsb0JBQW9CLEdBQUcsR0FBRyxHQUFHLGVBQU0sQ0FBQyxHQUFHLENBQUMsV0FBVyxHQUFHLE1BQU0sRUFBRSxLQUFLLEVBQUUsZUFBTSxDQUFDLEdBQUcsQ0FBQyxPQUFPLEdBQUcsR0FBRyxHQUFHLGVBQU0sQ0FBQyxHQUFHLENBQUMsV0FBVyxFQUFDLENBQUU7U0FDM0ksQ0FBQyxDQUFDO1FBRVAsSUFBSSx1QkFBUyxDQUFDLElBQUksRUFBRSxPQUFPLEVBQUU7WUFDekIsVUFBVSxFQUFFLGVBQU0sQ0FBQyxHQUFHLENBQUMsT0FBTyxHQUFHLEdBQUcsR0FBRyxlQUFNLENBQUMsR0FBRyxDQUFDLFdBQVcsR0FBRyxTQUFTO1lBQ3pFLEtBQUssRUFBRSxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUc7U0FDdEIsQ0FBQyxDQUFB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QU0sR0FBRyxJQUFJLHFCQUFHLENBQUMsU0FBUyxDQUFDLElBQUksRUFBRSxRQUFRLEdBQUcsV0FBVyxFQUFFO1lBQzNELGdCQUFnQixFQUFFLElBQUksQ0FBQyxNQUFNLEdBQUcsRUFBRTtZQUNsQyxTQUFTLEVBQUUsZUFBTSxDQUFDLEdBQUcsQ0FBQyxJQUFJLEdBQUcsV0FBVztZQUN4QyxLQUFLLEVBQUUsSUFBSSxDQUFDLEdBQUcsQ0FBQyxHQUFHO1lBQ25CLElBQUksRUFBRSxDQUFDLEVBQUMsR0FBRyxFQUFFLE1BQU0sRUFBRSxLQUFLLEVBQUUsZUFBTSxDQUFDLEdBQUcsQ0FBQyxPQUFPLEdBQUcsR0FBRyxHQUFHLGVBQU0sQ0FBQyxHQUFHLENBQUMsV0FBVyxHQUFHLEdBQUcsR0FBRyxZQUFZLEdBQUcsR0FBRyxHQUFHLEVBQUUsRUFBQyxDQUFDO1NBQ2xILENBQUMsQ0FBQztRQUVILElBQUksdUJBQVMsQ0FBQyxJQUFJLEVBQUUsUUFBUSxHQUFHLFdBQVcsR0FBRyxRQUFRLEVBQUU7WUFDbkQsVUFBVSxFQUFFLGVBQU0sQ0FBQyxHQUFHLENBQUMsT0FBTyxHQUFHLEdBQUcsR0FBRyxlQUFNLENBQUMsR0FBRyxDQUFDLFdBQVcsR0FBRyxVQUFVLEdBQUcsV0FBVztZQUN4RixLQUFLLEVBQUUsTUFBTSxDQUFDLEdBQUc7U0FDcEIsQ0FBQyxDQUFBO1FBRUYsT0FBTyxNQUFNLENBQUM7SUFDbEIsQ0FBQztDQUNKO0FBakhELGtEQWlIQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7XG4gICAgU3RhY2ssXG4gICAgU3RhY2tQcm9wcyxcbiAgICBhd3NfZWMyIGFzIGVjMixcbiAgICBhd3NfbXNrIGFzIG1zayxcbiAgICBDZm5PdXRwdXQsXG59IGZyb20gXCJhd3MtY2RrLWxpYlwiO1xuaW1wb3J0IHsgQ29uc3RydWN0IH0gZnJvbSBcImNvbnN0cnVjdHNcIjtcbmltcG9ydCB7Q29uZmlnfSBmcm9tIFwiLi4vY29uZmlnL2NvbmZpZ1wiO1xuaW1wb3J0IHtBd3NDdXN0b21SZXNvdXJjZSwgQXdzQ3VzdG9tUmVzb3VyY2VQb2xpY3ksIFBoeXNpY2FsUmVzb3VyY2VJZH0gZnJvbSBcImF3cy1jZGstbGliL2N1c3RvbS1yZXNvdXJjZXNcIjtcblxuLy8gaW1wb3J0ICogYXMgc3FzIGZyb20gJ2F3cy1jZGstbGliL2F3cy1zcXMnO1xuXG5leHBvcnQgY2xhc3MgQXdzVnBjTXNrSW5mcmFTdGFjayBleHRlbmRzIFN0YWNrIHtcbiAgICB2cGM6IGVjMi5DZm5WUEM7XG4gICAgY29uc3RydWN0b3Ioc2NvcGU6IENvbnN0cnVjdCwgaWQ6IHN0cmluZywgcHJvcHM/OiBTdGFja1Byb3BzKSB7XG4gICAgICAgIHN1cGVyKHNjb3BlLCBpZCwgcHJvcHMpO1xuXG4gICAgICAgIC8vIENyZWF0ZSB2cGNcbiAgICAgICAgdGhpcy52cGMgPSBuZXcgZWMyLkNmblZQQyggLy8gVE9ETzog66y07JeH7J2EIOychO2VnCB2cGMg7J247KeAIO2ZleyduO2VmOq4sFxuICAgICAgICAgICAgdGhpcywgXCJ2cGNcIiwge1xuICAgICAgICAgICAgICAgIGNpZHJCbG9jazogQ29uZmlnLnZwYy5jaWRyICsgJy4wLjAvMTYnLFxuICAgICAgICAgICAgICAgIGVuYWJsZURuc0hvc3RuYW1lczogdHJ1ZSxcbiAgICAgICAgICAgICAgICBlbmFibGVEbnNTdXBwb3J0OiB0cnVlLFxuICAgICAgICAgICAgICAgIGluc3RhbmNlVGVuYW5jeTogJ2RlZmF1bHQnLFxuICAgICAgICAgICAgICAgIHRhZ3M6IFsgeyBrZXk6IFwiQ29uZmlnLmFwcC5zZXJ2aWNlXCIgKyBcIi1cIiArIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgKyBcIi12cGNcIiwgdmFsdWU6IENvbmZpZy5hcHAuc2VydmljZSArICctJyArIENvbmZpZy5hcHAuZW52aXJvbm1lbnR9IF1cbiAgICAgICAgICAgIH0pO1xuXG4gICAgICAgIG5ldyBDZm5PdXRwdXQodGhpcywgJ3ZwY0lkJywge1xuICAgICAgICAgICAgZXhwb3J0TmFtZTogQ29uZmlnLmFwcC5zZXJ2aWNlICsgJy0nICsgQ29uZmlnLmFwcC5lbnZpcm9ubWVudCArICctdnBjLUlkJyxcbiAgICAgICAgICAgIHZhbHVlOiB0aGlzLnZwYy5yZWYsXG4gICAgICAgIH0pXG5cblxuICAgICAgICAvLyBDcmVhdGUgdHdvIHByaXZhdGUgc3VibmV0c1xuICAgICAgICBjb25zdCBzdWJuZXRfcHJpdmF0ZV8wMV9hOiBlYzIuQ2ZuU3VibmV0ID0gdGhpcy5zdWJuZXRfY3JlYXRpb24oJ3ByaXZhdGUwMWEnLCAnLjk2LjAvMjAnKTtcbiAgICAgICAgY29uc3Qgc3VibmV0X3ByaXZhdGVfMDFfYzogZWMyLkNmblN1Ym5ldCA9IHRoaXMuc3VibmV0X2NyZWF0aW9uKCdwcml2YXRlMDFjJywgJy4xMTIuMC8yMCcpO1xuXG4gICAgICAgIC8vIENyZWF0ZSBTZWN1cml0eSBHcm91cFxuICAgICAgICBsZXQgc2VjdXJpdHlHcm91cCA9IG5ldyBlYzIuQ2ZuU2VjdXJpdHlHcm91cCggLy8gVE9ETzog66y07JeH7J2EIOychO2VnCB2cGMg7J247KeAIO2ZleyduO2VmOq4sFxuICAgICAgICAgICAgdGhpcywgQ29uZmlnLmFwcC5zZXJ2aWNlICsgXCItXCIgKyBDb25maWcuYXBwLmVudmlyb25tZW50ICsgXCItbXNrLXNlY3VyaXR5LWdyb3VwXCIsIHtcbiAgICAgICAgICAgICAgICB2cGNJZDogdGhpcy52cGMucmVmLFxuICAgICAgICAgICAgICAgIGdyb3VwRGVzY3JpcHRpb246IENvbmZpZy5hcHAuc2VydmljZSArICctJyArIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgKyAnLW1zay0nICsgQ29uZmlnLm1zay5jbHVzdGVyTmFtZSxcbiAgICAgICAgICAgICAgICBncm91cE5hbWU6IENvbmZpZy5hcHAuc2VydmljZSArICctJyArIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgKyAnLW1zay0nICsgQ29uZmlnLm1zay5jbHVzdGVyTmFtZSxcbiAgICAgICAgICAgICAgICBzZWN1cml0eUdyb3VwSW5ncmVzczogW3tcbiAgICAgICAgICAgICAgICAgICAgaXBQcm90b2NvbDogXCJUQ1BcIixcbiAgICAgICAgICAgICAgICAgICAgZnJvbVBvcnQ6IDIxODEsXG4gICAgICAgICAgICAgICAgICAgIHRvUG9ydDogMjE4MSxcbiAgICAgICAgICAgICAgICAgICAgY2lkcklwOiBDb25maWcuc2VjdXJpdHlfZ3JvdXBbMF0sXG4gICAgICAgICAgICAgICAgICAgIGRlc2NyaXB0aW9uOiBDb25maWcuc2VjdXJpdHlfZ3JvdXBbMF1cbiAgICAgICAgICAgICAgICB9XSxcbiAgICAgICAgICAgICAgICAvLyB0YWdzOiBbeyBrZXk6ICdOYW1lJywgdmFsdWU6IENvbmZpZy5hcHAuc2VydmljZSArICctJyArIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgKyAnLW1zay0nICsgQ29uZmlnLm1zay5jbHVzdGVyTmFtZSwgfV0sXG4gICAgICAgICAgICB9KTtcblxuICAgICAgICBuZXcgQ2ZuT3V0cHV0KHRoaXMsICdzZWN1cml0eUdyb3VwJywge1xuICAgICAgICAgICAgZXhwb3J0TmFtZTogQ29uZmlnLmFwcC5zZXJ2aWNlICsgJy0nICsgQ29uZmlnLmFwcC5lbnZpcm9ubWVudCArICctc2VjdXJpdHlHcm91cC1JZCcsXG4gICAgICAgICAgICB2YWx1ZTogc2VjdXJpdHlHcm91cC5hdHRyR3JvdXBJZCxcbiAgICAgICAgfSlcblxuICAgICAgICAvLyBDcmVhdGUgTVNLIGNsdXN0ZXJcbiAgICAgICAgbGV0IG1za0NsdXN0ZXIgPSBuZXcgbXNrLkNmbkNsdXN0ZXIoXG4gICAgICAgICAgICB0aGlzLCBDb25maWcuYXBwLnNlcnZpY2UgKyBcIi1cIiArIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgKyBcIi1tc2stY2x1c3RlclwiLCB7XG4gICAgICAgICAgICAgICAgYnJva2VyTm9kZUdyb3VwSW5mbzoge1xuICAgICAgICAgICAgICAgICAgICBjbGllbnRTdWJuZXRzOiBbc3VibmV0X3ByaXZhdGVfMDFfYS5yZWYsIHN1Ym5ldF9wcml2YXRlXzAxX2MucmVmXSxcbiAgICAgICAgICAgICAgICAgICAgaW5zdGFuY2VUeXBlOiAna2Fma2EudDMuc21hbGwnLFxuICAgICAgICAgICAgICAgICAgICBzZWN1cml0eUdyb3VwczogW3NlY3VyaXR5R3JvdXAucmVmXSxcbiAgICAgICAgICAgICAgICAgICAgLy8gdGhlIHByb3BlcnRpZXMgYmVsb3cgYXJlIG9wdGlvbmFsXG4gICAgICAgICAgICAgICAgICAgIHN0b3JhZ2VJbmZvOiB7IGVic1N0b3JhZ2VJbmZvOiB7IHZvbHVtZVNpemU6IDEgfX1cbiAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgICAgIGNsdXN0ZXJOYW1lOiBDb25maWcuYXBwLnNlcnZpY2UgKyAnLScgKyBDb25maWcuYXBwLmVudmlyb25tZW50ICsgJy1tc2stJyArIENvbmZpZy5tc2suY2x1c3Rlck5hbWUsXG4gICAgICAgICAgICAgICAga2Fma2FWZXJzaW9uOiAnMi44LjEnLFxuICAgICAgICAgICAgICAgIG51bWJlck9mQnJva2VyTm9kZXM6IDIsXG5cbiAgICAgICAgICAgICAgICAvLyB0aGUgcHJvcGVydGllcyBiZWxvdyBhcmUgb3B0aW9uYWxcbiAgICAgICAgICAgICAgICBjbGllbnRBdXRoZW50aWNhdGlvbjoge3Nhc2w6IHtzY3JhbToge2VuYWJsZWQ6IHRydWUsfSx9LH1cbiAgICAgICAgICAgIH0pO1xuXG4gICAgICAgIC8vIFRPRE86IGh0dHBzOi8vZG9jcy5hd3MuYW1hem9uLmNvbS9BV1NKYXZhU2NyaXB0U0RLL3YzL2xhdGVzdC9jbGllbnRzL2NsaWVudC1rYWZrYS9jbGFzc2VzL2dldGJvb3RzdHJhcGJyb2tlcnNjb21tYW5kLmh0bWxcbiAgICAgICAgbGV0IGdldEJvb3RTdHJhcEJyb2tlcnMgPSBuZXcgQXdzQ3VzdG9tUmVzb3VyY2UoXG4gICAgICAgICAgICB0aGlzLCBDb25maWcuYXBwLnNlcnZpY2UgKyBcIi1cIiArIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgKyBcIi1nZXQtYm9vdHN0cmFwLXNlcnZlcnNcIixcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgICBvblVwZGF0ZToge1xuICAgICAgICAgICAgICAgICAgICBzZXJ2aWNlOiBcImNsaWVudC1tc2tcIixcbiAgICAgICAgICAgICAgICAgICAgYWN0aW9uOiBcIkdldEJvb3RzdHJhcEJyb2tlcnNcIixcbiAgICAgICAgICAgICAgICAgICAgcGFyYW1ldGVyczoge0NsdXN0ZXJBcm46IG1za0NsdXN0ZXIuYXR0ckFybn0sXG4gICAgICAgICAgICAgICAgICAgIHBoeXNpY2FsUmVzb3VyY2VJZDogUGh5c2ljYWxSZXNvdXJjZUlkLm9mKG1za0NsdXN0ZXIuYXR0ckFybiksXG4gICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgICAgICBwb2xpY3k6IEF3c0N1c3RvbVJlc291cmNlUG9saWN5LmZyb21TZGtDYWxscyh7cmVzb3VyY2VzOiBBd3NDdXN0b21SZXNvdXJjZVBvbGljeS5BTllfUkVTT1VSQ0V9KSxcbiAgICAgICAgICAgICAgICAvL1RPRE86IOyViOuQoCDqsr3smrAgQ2hhdEdQVOydmCDsobDslrjrjIDroZwgIOuzgOqyvSDqsIDriqVcbiAgICAgICAgICAgICAgICAvLyBwb2xpY3k6IEF3c0N1c3RvbVJlc291cmNlUG9saWN5LmZyb21TdGF0ZW1lbnRzKFtcbiAgICAgICAgICAgICAgICAvLyAgICAgbmV3IGlhbS5Qb2xpY3lTdGF0ZW1lbnQoe1xuICAgICAgICAgICAgICAgIC8vICAgICAgICAgYWN0aW9uczogWydtc2s6R2V0Qm9vdHN0cmFwQnJva2VycyddLFxuICAgICAgICAgICAgICAgIC8vICAgICAgICAgcmVzb3VyY2VzOiBbJyonXSxcbiAgICAgICAgICAgICAgICAvLyAgICAgfSksXG4gICAgICAgICAgICAgICAgLy8gXSksXG5cbiAgICAgICAgICAgIH1cbiAgICAgICAgKTtcbiAgICAgICAgY29uc3QgYm9vdHN0cmFwQnJva2VyU3RyaW5nID0gZ2V0Qm9vdFN0cmFwQnJva2Vycy5nZXRSZXNwb25zZUZpZWxkKFwiQm9vdHN0cmFwQnJva2VyU3RyaW5nXCIpO1xuICAgICAgICBjb25zb2xlLmxvZyhcImJvb3RzdHJhcEJyb2tlclN0cmluZzogXCIgKyBib290c3RyYXBCcm9rZXJTdHJpbmcpO1xuXG4gICAgICAgIC8vIFVzZSBBV1MgU0RLIHRvIGZldGNoIGJvb3RzdHJhcCBicm9rZXJzXG5cbiAgICAgICAgbmV3IENmbk91dHB1dCh0aGlzLCAnbXNrQ2x1c3RlcicsIHtcbiAgICAgICAgICAgIGV4cG9ydE5hbWU6IENvbmZpZy5hcHAuc2VydmljZSArICctJyArIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgKyAnLW1zay0nICsgQ29uZmlnLm1zay5jbHVzdGVyTmFtZSArIFwiLUJvb3RzdHJhcEJyb2tlcnNcIixcbiAgICAgICAgICAgIHZhbHVlOiBib290c3RyYXBCcm9rZXJTdHJpbmcsXG4gICAgICAgIH0pXG4gICAgfVxuICAgIHN1Ym5ldF9jcmVhdGlvbihzdWJuZXRfbmFtZTogc3RyaW5nLCBzdWJuZXRfY2lkcjogc3RyaW5nKTogZWMyLkNmblN1Ym5ldFxuICAgIHtcbiAgICAgICAgY29uc3Qgc3VibmV0X2dyb3VwID0gc3VibmV0X25hbWUuc2xpY2UoMCwgLTEpO1xuICAgICAgICBjb25zdCBheiA9IHN1Ym5ldF9uYW1lLnNsaWNlKC0xKTtcbiAgICAgICAgY29uc3Qgc3VibmV0ID0gbmV3IGVjMi5DZm5TdWJuZXQodGhpcywgJ3N1Ym5ldCcgKyBzdWJuZXRfbmFtZSwge1xuICAgICAgICAgICAgYXZhaWxhYmlsaXR5Wm9uZTogdGhpcy5yZWdpb24gKyBheixcbiAgICAgICAgICAgIGNpZHJCbG9jazogQ29uZmlnLnZwYy5jaWRyICsgc3VibmV0X2NpZHIsXG4gICAgICAgICAgICB2cGNJZDogdGhpcy52cGMucmVmLFxuICAgICAgICAgICAgdGFnczogW3trZXk6ICdOYW1lJywgdmFsdWU6IENvbmZpZy5hcHAuc2VydmljZSArICctJyArIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgKyAnLScgKyBzdWJuZXRfZ3JvdXAgKyAnLScgKyBhen1dXG4gICAgICAgIH0pO1xuXG4gICAgICAgIG5ldyBDZm5PdXRwdXQodGhpcywgJ3N1Ym5ldCcgKyBzdWJuZXRfbmFtZSArICdvdXRwdXQnLCB7XG4gICAgICAgICAgICBleHBvcnROYW1lOiBDb25maWcuYXBwLnNlcnZpY2UgKyAnLScgKyBDb25maWcuYXBwLmVudmlyb25tZW50ICsgJy1zdWJuZXQtJyArIHN1Ym5ldF9uYW1lLFxuICAgICAgICAgICAgdmFsdWU6IHN1Ym5ldC5yZWZcbiAgICAgICAgfSlcblxuICAgICAgICByZXR1cm4gc3VibmV0O1xuICAgIH1cbn0iXX0= -------------------------------------------------------------------------------- /lib/aws-iot-core-rule-infra-stack.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.AwsIotCoreRuleInfraStack = void 0; 7 | const aws_cdk_lib_1 = require("aws-cdk-lib"); 8 | const rule_policy_json_1 = __importDefault(require("./rule/rule-policy.json")); 9 | const config_1 = require("../config/config"); 10 | const rule_keys_json_1 = __importDefault(require("./rule/rule-keys.json")); 11 | const key_policy_json_1 = __importDefault(require("./rule/key-policy.json")); 12 | class AwsIotCoreRuleInfraStack extends aws_cdk_lib_1.Stack { 13 | constructor(scope, id, props) { 14 | super(scope, id, props); 15 | // Import VPC and Subnet 16 | // const vpc = ec2.Vpc.fromLookup(this, 'vpc', { isDefault: false, tags: { key: "Config.app.service" + "-" + Config.app.environment + "-vpc", value: Config.app.service + '-' + Config.app.environment}}); 17 | const vpcId = aws_cdk_lib_1.Fn.importValue(config_1.Config.app.service + '-' + config_1.Config.app.environment + '-vpc-Id'); 18 | const securityGroup = aws_cdk_lib_1.Fn.importValue(config_1.Config.app.service + '-' + config_1.Config.app.environment + '-securityGroup-Id'); 19 | const subnet_private02a = aws_cdk_lib_1.Fn.importValue(config_1.Config.app.service + '-' + config_1.Config.app.environment + '-subnet-private02a'); 20 | const subnet_private02b = aws_cdk_lib_1.Fn.importValue(config_1.Config.app.service + '-' + config_1.Config.app.environment + '-subnet-private02b'); 21 | const mskCluster_bootstrap_brokers = aws_cdk_lib_1.Fn.importValue(config_1.Config.app.service + '-' + config_1.Config.app.environment + '-msk-' + config_1.Config.msk.clusterName + "-BootstrapBrokers"); 22 | // For rules in IoT Core, please refer to this https://ap-northeast-2.console.aws.amazon.com/iot/home?region=ap-northeast-2#/rulehub 23 | // For role/policy for rules, please refer to https://docs.aws.amazon.com/iot/latest/developerguide/iot-create-role.html 24 | // Create role for Rule engine 25 | let roleRuleEngine = new aws_cdk_lib_1.aws_iam.Role(this, config_1.Config.app.service + "-" + config_1.Config.app.environment + "-rule-engine-role", { 26 | assumedBy: new aws_cdk_lib_1.aws_iam.ServicePrincipal("iot.amazonaws.com"), 27 | description: "AWS I AM role for IoT rule engine", 28 | roleName: config_1.Config.app.service + "-" + config_1.Config.app.environment + "-rule-engine-role", 29 | }); 30 | // Create policy for rule engine 31 | let iotCoreRolePolicy = aws_cdk_lib_1.aws_iam.PolicyDocument.fromJson(rule_policy_json_1.default); 32 | let ruleEnginePolicy = new aws_cdk_lib_1.aws_iam.Policy(this, config_1.Config.app.service + 33 | "-" + 34 | config_1.Config.app.environment + 35 | "-iot-core-role-policy", { 36 | document: iotCoreRolePolicy, 37 | policyName: "iotCoreRolePolicy", 38 | }); 39 | ruleEnginePolicy.attachToRole(roleRuleEngine); 40 | //Create Topic Rule Destination for Kafka, replace security group, subnet, and VPC values with your own 41 | let cfnTopicRuleDestination = new aws_cdk_lib_1.aws_iot.CfnTopicRuleDestination(this, "MyCfnTopicRuleDestination", 42 | /* all optional props */ { 43 | vpcProperties: { 44 | roleArn: roleRuleEngine.roleArn, 45 | securityGroups: [securityGroup], 46 | subnetIds: [subnet_private02a, subnet_private02b], 47 | vpcId: vpcId, 48 | }, 49 | }); 50 | //CDK Unable to infer the rule destination requires IAM policies. Manually adding dependency 51 | cfnTopicRuleDestination.node.addDependency(ruleEnginePolicy); 52 | //Create KMS key for secret encryption 53 | key_policy_json_1.default.Statement[0].Principal.AWS = "arn:aws:iam::" + config_1.Config.aws.account + ":root"; 54 | const key = new aws_cdk_lib_1.aws_kms.CfnKey(this, "Key", { 55 | enabled: true, 56 | enableKeyRotation: false, 57 | keyPolicy: key_policy_json_1.default, 58 | keySpec: "SYMMETRIC_DEFAULT", 59 | keyUsage: "ENCRYPT_DECRYPT", 60 | }); 61 | new aws_cdk_lib_1.aws_kms.CfnAlias(this, "KeyAlias", { 62 | aliasName: "alias/" + config_1.Config.app.application + "-" + config_1.Config.app.environment + "-msk", targetKeyId: key.ref 63 | }); 64 | //Create AWS Secrets Manager Password for MSK connection 65 | const iotSecret = new aws_cdk_lib_1.aws_secretsmanager.CfnSecret(this, "IoTSecret", { 66 | name: "AmazonMSK_" + config_1.Config.app.application + "-" + config_1.Config.app.environment, 67 | kmsKeyId: key.ref, 68 | generateSecretString: { 69 | passwordLength: 20, 70 | excludeCharacters: "]/'", 71 | generateStringKey: "password", 72 | secretStringTemplate: JSON.stringify({ username: "test-kafka" }), 73 | }, 74 | }); 75 | // Get rules from ruleKeysJson 76 | let testRuleKeys = rule_keys_json_1.default.testRules; 77 | // Create Rules in IoT Core to send to S3 and MSK 78 | testRuleKeys.forEach((key) => { 79 | new aws_cdk_lib_1.aws_iot.CfnTopicRule(this, config_1.Config.app.service + "-" + config_1.Config.app.environment + `-topic-rule-${key}`, { 80 | topicRulePayload: { 81 | actions: [ 82 | { 83 | kafka: { 84 | clientProperties: { 85 | acks: "1", 86 | //Replace placeholder Kafka bootstrap Servers with your own 87 | "bootstrap.servers": mskCluster_bootstrap_brokers, 88 | "security.protocol": "SASL_SSL", 89 | "sasl.mechanism": "SCRAM-SHA-512", 90 | "sasl.scram.username": "${get_secret('AmazonMSK_iot','SecretString','username'," + 91 | `'${roleRuleEngine.roleArn}')}`, 92 | "sasl.scram.password": "${get_secret('AmazonMSK_iot','SecretString','password'," + 93 | `'${roleRuleEngine.roleArn}')}`, 94 | }, 95 | destinationArn: cfnTopicRuleDestination.attrArn, 96 | topic: `test-msk-topic.${key}` 97 | }, 98 | }, 99 | ], 100 | sql: `SELECT * FROM 'test-rule/${key}'`, 101 | }, 102 | // iot does not allow rule '-' (dash). 103 | ruleName: `test_rule_${key}`, 104 | }); 105 | }); 106 | } 107 | } 108 | exports.AwsIotCoreRuleInfraStack = AwsIotCoreRuleInfraStack; 109 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXdzLWlvdC1jb3JlLXJ1bGUtaW5mcmEtc3RhY2suanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJhd3MtaW90LWNvcmUtcnVsZS1pbmZyYS1zdGFjay50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUFBQSw2Q0FTcUI7QUFFckIsK0VBQXFEO0FBQ3JELDZDQUEwQztBQUMxQywyRUFBaUQ7QUFDakQsNkVBQW1EO0FBR25ELE1BQWEsd0JBQXlCLFNBQVEsbUJBQUs7SUFDL0MsWUFBWSxLQUFnQixFQUFFLEVBQVUsRUFBRSxLQUFrQjtRQUN4RCxLQUFLLENBQUMsS0FBSyxFQUFFLEVBQUUsRUFBRSxLQUFLLENBQUMsQ0FBQztRQUV4Qix3QkFBd0I7UUFDeEIsME1BQTBNO1FBQzFNLE1BQU0sS0FBSyxHQUFHLGdCQUFFLENBQUMsV0FBVyxDQUFDLGVBQU0sQ0FBQyxHQUFHLENBQUMsT0FBTyxHQUFHLEdBQUcsR0FBRyxlQUFNLENBQUMsR0FBRyxDQUFDLFdBQVcsR0FBRyxTQUFTLENBQUMsQ0FBQztRQUM1RixNQUFNLGFBQWEsR0FBRyxnQkFBRSxDQUFDLFdBQVcsQ0FBQyxlQUFNLENBQUMsR0FBRyxDQUFDLE9BQU8sR0FBRyxHQUFHLEdBQUcsZUFBTSxDQUFDLEdBQUcsQ0FBQyxXQUFXLEdBQUcsbUJBQW1CLENBQUMsQ0FBQztRQUU5RyxNQUFNLGlCQUFpQixHQUFHLGdCQUFFLENBQUMsV0FBVyxDQUFDLGVBQU0sQ0FBQyxHQUFHLENBQUMsT0FBTyxHQUFHLEdBQUcsR0FBRyxlQUFNLENBQUMsR0FBRyxDQUFDLFdBQVcsR0FBRyxvQkFBb0IsQ0FBQyxDQUFDO1FBQ25ILE1BQU0saUJBQWlCLEdBQUcsZ0JBQUUsQ0FBQyxXQUFXLENBQUMsZUFBTSxDQUFDLEdBQUcsQ0FBQyxPQUFPLEdBQUcsR0FBRyxHQUFHLGVBQU0sQ0FBQyxHQUFHLENBQUMsV0FBVyxHQUFHLG9CQUFvQixDQUFDLENBQUM7UUFFbkgsTUFBTSw0QkFBNEIsR0FBRyxnQkFB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QkFDSjtxQkFDSjtvQkFDRCxHQUFHLEVBQUUsNEJBQTRCLEdBQUcsR0FBRztpQkFDMUM7Z0JBQ0Qsc0NBQXNDO2dCQUN0QyxRQUFRLEVBQUUsYUFBYSxHQUFHLEVBQUU7YUFDL0IsQ0FDSixDQUFDO1FBQ04sQ0FBQyxDQUFDLENBQUM7SUFDUCxDQUFDO0NBQ0o7QUEvSEQsNERBK0hDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHtcbiAgICBTdGFjayxcbiAgICBTdGFja1Byb3BzLFxuICAgIGF3c19pb3QgYXMgaW90LFxuICAgIGF3c19pYW0gYXMgaWFtLFxuICAgIGF3c19zZWNyZXRzbWFuYWdlciBhcyBzZWNyZXRzbWFuYWdlcixcbiAgICBhd3Nfa21zIGFzIGttcyxcbiAgICBhd3NfZWMyIGFzIGVjMixcbiAgICBGbixcbn0gZnJvbSBcImF3cy1jZGstbGliXCI7XG5pbXBvcnQgeyBDb25zdHJ1Y3QgfSBmcm9tIFwiY29uc3RydWN0c1wiO1xuaW1wb3J0IHJ1bGVQb2xpY3lKc29uIGZyb20gXCIuL3J1bGUvcnVsZS1wb2xpY3kuanNvblwiO1xuaW1wb3J0IHsgQ29uZmlnIH0gZnJvbSBcIi4uL2NvbmZpZy9jb25maWdcIjtcbmltcG9ydCBydWxlS2V5c0pzb24gZnJvbSBcIi4vcnVsZS9ydWxlLWtleXMuanNvblwiO1xuaW1wb3J0IGtleVBvbGljeUpzb24gZnJvbSBcIi4vcnVsZS9rZXktcG9saWN5Lmpzb25cIjtcblxuXG5leHBvcnQgY2xhc3MgQXdzSW90Q29yZVJ1bGVJbmZyYVN0YWNrIGV4dGVuZHMgU3RhY2sge1xuICAgIGNvbnN0cnVjdG9yKHNjb3BlOiBDb25zdHJ1Y3QsIGlkOiBzdHJpbmcsIHByb3BzPzogU3RhY2tQcm9wcykge1xuICAgICAgICBzdXBlcihzY29wZSwgaWQsIHByb3BzKTtcblxuICAgICAgICAvLyBJbXBvcnQgVlBDIGFuZCBTdWJuZXRcbiAgICAgICAgLy8gY29uc3QgdnBjID0gZWMyLlZwYy5mcm9tTG9va3VwKHRoaXMsICd2cGMnLCB7IGlzRGVmYXVsdDogZmFsc2UsIHRhZ3M6IHsga2V5OiBcIkNvbmZpZy5hcHAuc2VydmljZVwiICsgXCItXCIgKyBDb25maWcuYXBwLmVudmlyb25tZW50ICsgXCItdnBjXCIsIHZhbHVlOiBDb25maWcuYXBwLnNlcnZpY2UgKyAnLScgKyBDb25maWcuYXBwLmVudmlyb25tZW50fX0pO1xuICAgICAgICBjb25zdCB2cGNJZCA9IEZuLmltcG9ydFZhbHVlKENvbmZpZy5hcHAuc2VydmljZSArICctJyArIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgKyAnLXZwYy1JZCcpO1xuICAgICAgICBjb25zdCBzZWN1cml0eUdyb3VwID0gRm4uaW1wb3J0VmFsdWUoQ29uZmlnLmFwcC5zZXJ2aWNlICsgJy0nICsgQ29uZmlnLmFwcC5lbnZpcm9ubWVudCArICctc2VjdXJpdHlHcm91cC1JZCcpO1xuXG4gICAgICAgIGNvbnN0IHN1Ym5ldF9wcml2YXRlMDJhID0gRm4uaW1wb3J0VmFsdWUoQ29uZmlnLmFwcC5zZXJ2aWNlICsgJy0nICsgQ29uZmlnLmFwcC5lbnZpcm9ubWVudCArICctc3VibmV0LXByaXZhdGUwMmEnKTtcbiAgICAgICAgY29uc3Qgc3VibmV0X3ByaXZhdGUwMmIgPSBGbi5pbXBvcnRWYWx1ZShDb25maWcuYXBwLnNlcnZpY2UgKyAnLScgKyBDb25maWcuYXBwLmVudmlyb25tZW50ICsgJy1zdWJuZXQtcHJpdmF0ZTAyYicpO1xuXG4gICAgICAgIGNvbnN0IG1za0NsdXN0ZXJfYm9vdHN0cmFwX2Jyb2tlcnMgPSBGbi5pbXBvcnRWYWx1ZShDb25maWcuYXBwLnNlcnZpY2UgKyAnLScgKyBDb25maWcuYXBwLmVudmlyb25tZW50ICsgJy1tc2stJyArIENvbmZpZy5tc2suY2x1c3Rlck5hbWUgKyBcIi1Cb290c3RyYXBCcm9rZXJzXCIpO1xuXG4gICAgICAgIC8vIEZvciBydWxlcyBpbiBJb1QgQ29yZSwgcGxlYXNlIHJlZmVyIHRvIHRoaXMgaHR0cHM6Ly9hcC1ub3J0aGVhc3QtMi5jb25zb2xlLmF3cy5hbWF6b24uY29tL2lvdC9ob21lP3JlZ2lvbj1hcC1ub3J0aGVhc3QtMiMvcnVsZWh1YlxuICAgICAgICAvLyBGb3Igcm9sZS9wb2xpY3kgZm9yIHJ1bGVzLCBwbGVhc2UgcmVmZXIgdG8gaHR0cHM6Ly9kb2NzLmF3cy5hbWF6b24uY29tL2lvdC9sYXRlc3QvZGV2ZWxvcGVyZ3VpZGUvaW90LWNyZWF0ZS1yb2xlLmh0bWxcblxuICAgICAgICAvLyAgQ3JlYXRlIHJvbGUgZm9yIFJ1bGUgZW5naW5lXG4gICAgICAgIGxldCByb2xlUnVsZUVuZ2luZSA9IG5ldyBpYW0uUm9sZShcbiAgICAgICAgICAgIHRoaXMsIENvbmZpZy5hcHAuc2VydmljZSArIFwiLVwiICsgQ29uZmlnLmFwcC5lbnZpcm9ubWVudCArIFwiLXJ1bGUtZW5naW5lLXJvbGVcIiwge1xuICAgICAgICAgICAgICAgIGFzc3VtZWRCeTogbmV3IGlhbS5TZXJ2aWNlUHJpbmNpcGFsKFwiaW90LmFtYXpvbmF3cy5jb21cIiksXG4gICAgICAgICAgICAgICAgZGVzY3JpcHRpb246IFwiQVdTIEkgQU0gcm9sZSBmb3IgSW9UIHJ1bGUgZW5naW5lXCIsXG4gICAgICAgICAgICAgICAgcm9sZU5hbWU6IENvbmZpZy5hcHAuc2VydmljZSArIFwiLVwiICsgQ29uZmlnLmFwcC5lbnZpcm9ubWVudCArIFwiLXJ1bGUtZW5naW5lLXJvbGVcIixcbiAgICAgICAgICAgIH1cbiAgICAgICAgKTtcblxuICAgICAgICAvLyBDcmVhdGUgcG9saWN5IGZvciBydWxlIGVuZ2luZVxuICAgICAgICBsZXQgaW90Q29yZVJvbGVQb2xpY3kgPSBpYW0uUG9saWN5RG9jdW1lbnQuZnJvbUpzb24ocnVsZVBvbGljeUpzb24pO1xuXG4gICAgICAgIGxldCBydWxlRW5naW5lUG9saWN5ID0gbmV3IGlhbS5Qb2xpY3koXG4gICAgICAgICAgICB0aGlzLFxuICAgICAgICAgICAgQ29uZmlnLmFwcC5zZXJ2aWNlICtcbiAgICAgICAgICAgIFwiLVwiICtcbiAgICAgICAgICAgIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgK1xuICAgICAgICAgICAgXCItaW90LWNvcmUtcm9sZS1wb2xpY3lcIixcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgICBkb2N1bWVudDogaW90Q29yZVJvbGVQb2xpY3ksXG4gICAgICAgICAgICAgICAgcG9saWN5TmFtZTogXCJpb3RDb3JlUm9sZVBvbGljeVwiLFxuICAgICAgICAgICAgfVxuICAgICAgICApO1xuXG4gICAgICAgIHJ1bGVFbmdpbmVQb2xpY3kuYXR0YWNoVG9Sb2xlKHJvbGVSdWxlRW5naW5lKVxuXG4gICAgICAgIC8vQ3JlYXRlIFRvcGljIFJ1bGUgRGVzdGluYXRpb24gZm9yIEthZmthLCByZXBsYWNlIHNlY3VyaXR5IGdyb3VwLCBzdWJuZXQsIGFuZCBWUEMgdmFsdWVzIHdpdGggeW91ciBvd25cbiAgICAgICAgbGV0IGNmblRvcGljUnVsZURlc3RpbmF0aW9uID0gbmV3IGlvdC5DZm5Ub3BpY1J1bGVEZXN0aW5hdGlvbihcbiAgICAgICAgICAgIHRoaXMsXG4gICAgICAgICAgICBcIk15Q2ZuVG9waWNSdWxlRGVzdGluYXRpb25cIixcbiAgICAgICAgICAgIC8qIGFsbCBvcHRpb25hbCBwcm9wcyAqLyB7XG4gICAgICAgICAgICAgICAgdnBjUHJvcGVydGllczoge1xuICAgICAgICAgICAgICAgICAgICByb2xlQXJuOiByb2xlUnVsZUVuZ2luZS5yb2xlQXJuLFxuICAgICAgICAgICAgICAgICAgICBzZWN1cml0eUdyb3VwczogW3NlY3VyaXR5R3JvdXBdLFxuICAgICAgICAgICAgICAgICAgICBzdWJuZXRJZHM6IFtzdWJuZXRfcHJpdmF0ZTAyYSwgc3VibmV0X3ByaXZhdGUwMmJdLFxuICAgICAgICAgICAgICAgICAgICB2cGNJZDogdnBjSWQsXG4gICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIH1cbiAgICAgICAgKTtcblxuICAgICAgICAvL0NESyBVbmFibGUgdG8gaW5mZXIgdGhlIHJ1bGUgZGVzdGluYXRpb24gcmVxdWlyZXMgSUFNIHBvbGljaWVzLiBNYW51YWxseSBhZGRpbmcgZGVwZW5kZW5jeVxuICAgICAgICBjZm5Ub3BpY1J1bGVEZXN0aW5hdGlvbi5ub2RlLmFkZERlcGVuZGVuY3kocnVsZUVuZ2luZVBvbGljeSlcblxuXG4gICAgICAgIC8vQ3JlYXRlIEtNUyBrZXkgZm9yIHNlY3JldCBlbmNyeXB0aW9uXG4gICAgICAgIGtleVBvbGljeUpzb24uU3RhdGVtZW50WzBdLlByaW5jaXBhbC5BV1MgPSBcImFybjphd3M6aWFtOjpcIiArIENvbmZpZy5hd3MuYWNjb3VudCArIFwiOnJvb3RcIlxuXG4gICAgICAgIGNvbnN0IGtleSA9IG5ldyBrbXMuQ2ZuS2V5KHRoaXMsIFwiS2V5XCIsIHtcbiAgICAgICAgICAgIGVuYWJsZWQ6IHRydWUsXG4gICAgICAgICAgICBlbmFibGVLZXlSb3RhdGlvbjogZmFsc2UsXG4gICAgICAgICAgICBrZXlQb2xpY3k6IGtleVBvbGljeUpzb24sXG4gICAgICAgICAgICBrZXlTcGVjOiBcIlNZTU1FVFJJQ19ERUZBVUxUXCIsXG4gICAgICAgICAgICBrZXlVc2FnZTogXCJFTkNSWVBUX0RFQ1JZUFRcIixcbiAgICAgICAgfSk7XG5cbiAgICAgICAgbmV3IGttcy5DZm5BbGlhcyh0aGlzLCBcIktleUFsaWFzXCIsIHtcbiAgICAgICAgICAgIGFsaWFzTmFtZTogXCJhbGlhcy9cIiArIENvbmZpZy5hcHAuYXBwbGljYXRpb24gKyBcIi1cIiArIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgKyBcIi1tc2tcIiwgdGFyZ2V0S2V5SWQ6IGtleS5yZWZcbiAgICAgICAgfSk7XG5cbiAgICAgICAgLy9DcmVhdGUgQVdTIFNlY3JldHMgTWFuYWdlciBQYXNzd29yZCBmb3IgTVNLIGNvbm5lY3Rpb25cbiAgICAgICAgY29uc3QgaW90U2VjcmV0ID0gbmV3IHNlY3JldHNtYW5hZ2VyLkNmblNlY3JldCh0aGlzLCBcIklvVFNlY3JldFwiLCB7XG4gICAgICAgICAgICBuYW1lOlxuICAgICAgICAgICAgICAgIFwiQW1hem9uTVNLX1wiICsgQ29uZmlnLmFwcC5hcHBsaWNhdGlvbiArIFwiLVwiICsgQ29uZmlnLmFwcC5lbnZpcm9ubWVudCxcbiAgICAgICAgICAgIGttc0tleUlkOiBrZXkucmVmLFxuICAgICAgICAgICAgZ2VuZXJhdGVTZWNyZXRTdHJpbmc6IHtcbiAgICAgICAgICAgICAgICBwYXNzd29yZExlbmd0aDogMjAsXG4gICAgICAgICAgICAgICAgZXhjbHVkZUNoYXJhY3RlcnM6IFwiXS8nXCIsXG4gICAgICAgICAgICAgICAgZ2VuZXJhdGVTdHJpbmdLZXk6IFwicGFzc3dvcmRcIixcbiAgICAgICAgICAgICAgICBzZWNyZXRTdHJpbmdUZW1wbGF0ZTogSlNPTi5zdHJpbmdpZnkoe3VzZXJuYW1lOiBcInRlc3Qta2Fma2FcIn0pLFxuICAgICAgICAgICAgfSxcbiAgICAgICAgfSk7XG5cbiAgICAgICAgLy8gR2V0IHJ1bGVzIGZyb20gcnVsZUtleXNKc29uXG4gICAgICAgIGxldCB0ZXN0UnVsZUtleXMgPSBydWxlS2V5c0pzb24udGVzdFJ1bGVzO1xuXG4gICAgICAgIC8vIENyZWF0ZSBSdWxlcyBpbiBJb1QgQ29yZSB0byBzZW5kIHRvIFMzIGFuZCBNU0tcbiAgICAgICAgdGVzdFJ1bGVLZXlzLmZvckVhY2goKGtleSkgPT4ge1xuICAgICAgICAgICAgbmV3IGlvdC5DZm5Ub3BpY1J1bGUoXG4gICAgICAgICAgICAgICAgdGhpcywgQ29uZmlnLmFwcC5zZXJ2aWNlICsgXCItXCIgKyBDb25maWcuYXBwLmVudmlyb25tZW50ICsgYC10b3BpYy1ydWxlLSR7a2V5fWAsXG4gICAgICAgICAgICAgICAge1xuICAgICAgICAgICAgICAgICAgICB0b3BpY1J1bGVQYXlsb2FkOiB7XG4gICAgICAgICAgICAgICAgICAgICAgICBhY3Rpb25zOiBbXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAge1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBrYWZrYToge1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xpZW50UHJvcGVydGllczoge1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFja3M6IFwiMVwiLFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC8vUmVwbGFjZSBwbGFjZWhvbGRlciBLYWZrYSBib290c3RyYXAgU2VydmVycyB3aXRoIHlvdXIgb3duXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgXCJib290c3RyYXAuc2VydmVyc1wiOiBtc2tDbHVzdGVyX2Jvb3RzdHJhcF9icm9rZXJzLFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFwic2VjdXJpdHkucHJvdG9jb2xcIjogXCJTQVNMX1NTTFwiLFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFwic2FzbC5tZWNoYW5pc21cIjogXCJTQ1JBTS1TSEEtNTEyXCIsXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgXCJzYXNsLnNjcmFtLnVzZXJuYW1lXCI6XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFwiJHtnZXRfc2VjcmV0KCdBbWF6b25NU0tfaW90JywnU2VjcmV0U3RyaW5nJywndXNlcm5hbWUnLFwiICtcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYCcke3JvbGVSdWxlRW5naW5lLnJvbGVBcm59Jyl9YCxcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBcInNhc2wuc2NyYW0ucGFzc3dvcmRcIjpcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgXCIke2dldF9zZWNyZXQoJ0FtYXpvbk1TS19pb3QnLCdTZWNyZXRTdHJpbmcnLCdwYXNzd29yZCcsXCIgK1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBgJyR7cm9sZVJ1bGVFbmdpbmUucm9sZUFybn0nKX1gLFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRlc3RpbmF0aW9uQXJuOiBjZm5Ub3BpY1J1bGVEZXN0aW5hdGlvbi5hdHRyQXJuLFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdG9waWM6IGB0ZXN0LW1zay10b3BpYy4ke2tleX1gXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgICAgICAgICAgICAgIF0sXG4gICAgICAgICAgICAgICAgICAgICAgICBzcWw6IGBTRUxFQ1QgKiBGUk9NICd0ZXN0LXJ1bGUvJHtrZXl9J2AsXG4gICAgICAgICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICAgICAgICAgIC8vIGlvdCBkb2VzIG5vdCBhbGxvdyBydWxlICctJyAoZGFzaCkuXG4gICAgICAgICAgICAgICAgICAgIHJ1bGVOYW1lOiBgdGVzdF9ydWxlXyR7a2V5fWAsXG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgKTtcbiAgICAgICAgfSk7XG4gICAgfVxufVxuIl19 -------------------------------------------------------------------------------- /lib/aws-iot-core-provisioning-infra-stack.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | var __importDefault = (this && this.__importDefault) || function (mod) { 26 | return (mod && mod.__esModule) ? mod : { "default": mod }; 27 | }; 28 | Object.defineProperty(exports, "__esModule", { value: true }); 29 | exports.AwsIotCoreProvisioningInfraStack = void 0; 30 | const aws_cdk_lib_1 = require("aws-cdk-lib"); 31 | const custom_resources_1 = require("aws-cdk-lib/custom-resources"); 32 | const device_policy_json_1 = __importDefault(require("./device/device-policy.json")); 33 | const provisioning_template_json_1 = __importDefault(require("./device/provisioning-template.json")); 34 | const path = __importStar(require("path")); 35 | const config_1 = require("../config/config"); 36 | const device_cc_policy_json_1 = __importDefault(require("./device/device-cc-policy.json")); 37 | class AwsIotCoreProvisioningInfraStack extends aws_cdk_lib_1.Stack { 38 | constructor(scope, id, props) { 39 | super(scope, id, props); 40 | // Modify testDevicePolicyJson according to Configs and create device policy for device policy 41 | device_policy_json_1.default.Statement[1].Resource = [ 42 | `arn:aws:iot:${config_1.Config.aws.region}:${config_1.Config.aws.account}:topic/$aws/rules/*`, 43 | `arn:aws:iot:${config_1.Config.aws.region}:${config_1.Config.aws.account}:topic/` + '${iot:ClientId}' 44 | ]; 45 | device_policy_json_1.default.Statement[2].Resource = [ 46 | `arn:aws:iot:${config_1.Config.aws.region}:${config_1.Config.aws.account}:topicfilter/` + '${iot:ClientId}' 47 | ]; 48 | let testDevicePolicy = new aws_cdk_lib_1.aws_iot.CfnPolicy(this, config_1.Config.app.service + "-" + config_1.Config.app.environment + "device-policy", { 49 | policyDocument: device_policy_json_1.default, 50 | policyName: config_1.Config.app.service + "-" + config_1.Config.app.environment + "-device-policy", 51 | }); 52 | // Create role for pre-provisioning lambda for verification of devices 53 | let rolePreProvisioningLambda = new aws_cdk_lib_1.aws_iam.Role(this, config_1.Config.app.service + "-" + config_1.Config.app.environment + "-pre-provisioning-lambda-role", { 54 | assumedBy: new aws_cdk_lib_1.aws_iam.ServicePrincipal("lambda.amazonaws.com"), 55 | description: "AWS IAM role for pre-provisioning lambda", 56 | roleName: config_1.Config.app.service + "-" + config_1.Config.app.environment + "-pre-provisioning-lambda-role", 57 | }); 58 | // Crate lambda for pre-provisioning hook and add permission for invoke 59 | let lambdaPreProvisioningHook = new aws_cdk_lib_1.aws_lambda.Function(this, config_1.Config.app.service + "-" + config_1.Config.app.environment + 60 | "-pre-provisioning-hook-lambda", { 61 | code: aws_cdk_lib_1.aws_lambda.Code.fromAsset(path.join(__dirname, "device")), 62 | handler: "lambda_function.lambda_handler", 63 | runtime: aws_cdk_lib_1.aws_lambda.Runtime.PYTHON_3_9, 64 | role: rolePreProvisioningLambda, 65 | description: "Lambda for pre-provisioning hook", 66 | functionName: config_1.Config.app.service + "-" + config_1.Config.app.environment + "-pre-provisioning-hook-lambda", 67 | }); 68 | lambdaPreProvisioningHook.addPermission("InvokePermission", { 69 | principal: new aws_cdk_lib_1.aws_iam.ServicePrincipal("iot.amazonaws.com"), 70 | action: "lambda:InvokeFunction", 71 | }); 72 | // Crate role for provisioning templates and add AWSIoTThingsRegistration policy 73 | let roleProvisioning = new aws_cdk_lib_1.aws_iam.Role(this, config_1.Config.app.service + "-" + config_1.Config.app.environment + "-provisioning-template-role", { 74 | assumedBy: new aws_cdk_lib_1.aws_iam.ServicePrincipal("iot.amazonaws.com"), 75 | description: "AWS IAM role for provisioning services", 76 | roleName: config_1.Config.app.service + "-" + config_1.Config.app.environment + "-provisioning-template-role", 77 | }); 78 | roleProvisioning.addManagedPolicy(aws_cdk_lib_1.aws_iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSIoTThingsRegistration")); 79 | // Create provisioning template 80 | provisioning_template_json_1.default.Resources.policy.Properties.PolicyName = testDevicePolicy.policyName; 81 | let testProvisioningTemplate = new aws_cdk_lib_1.aws_iot.CfnProvisioningTemplate(this, config_1.Config.app.service + "-" + config_1.Config.app.environment + "-provision-template", { 82 | provisioningRoleArn: roleProvisioning.roleArn, 83 | templateBody: JSON.stringify(provisioning_template_json_1.default), 84 | enabled: true, 85 | preProvisioningHook: { 86 | "payloadVersion": "2020-04-01", 87 | "targetArn": lambdaPreProvisioningHook.functionArn 88 | }, 89 | description: "AWS IoT Provisioning Template", 90 | templateName: config_1.Config.app.service + "-" + config_1.Config.app.environment + "-provision-template", 91 | }); 92 | // Modify testDeviceClaimCertificatePolicyJson and create vehicle gateway policy for Claim Certificate 93 | let templateTopicCreate = `arn:aws:iot:${config_1.Config.aws.region}:${config_1.Config.aws.account}:topic/$aws/certificates/create/*`; 94 | let templateTopicProvisioning = `arn:aws:iot:${config_1.Config.aws.region}:${config_1.Config.aws.account}:topic/$aws/provisioning-templates/${testProvisioningTemplate.templateName}/provision/*`; 95 | device_cc_policy_json_1.default.Statement[1].Resource = [templateTopicCreate, templateTopicProvisioning]; 96 | let templateTopicFilterCreate = `arn:aws:iot:${config_1.Config.aws.region}:${config_1.Config.aws.account}:topicfilter/$aws/certificates/create/*`; 97 | let templateTopicFilterProvisioning = `arn:aws:iot:${config_1.Config.aws.region}:${config_1.Config.aws.account}:topicfilter/$aws/provisioning-templates/${testProvisioningTemplate.templateName}/provision/*`; 98 | device_cc_policy_json_1.default.Statement[2].Resource = [templateTopicFilterCreate, templateTopicFilterProvisioning]; 99 | let testDeviceClaimCertificatePolicy = new aws_cdk_lib_1.aws_iot.CfnPolicy(this, config_1.Config.app.service + "-" + config_1.Config.app.environment + "-claim-certificate-policy", { 100 | policyDocument: device_cc_policy_json_1.default, 101 | policyName: config_1.Config.app.service + "-" + config_1.Config.app.environment + "-claim-certificate-policy", 102 | }); 103 | // Create claim certificate by using AwsCustomResource 104 | let createKeysAndCertificateForClaimCertificate = new custom_resources_1.AwsCustomResource(this, config_1.Config.app.service + "-" + config_1.Config.app.environment + "-create-keys-and-certificate-for-claim-certificate", { 105 | onUpdate: { 106 | service: "Iot", 107 | action: "createKeysAndCertificate", 108 | parameters: { setAsActive: true }, 109 | physicalResourceId: custom_resources_1.PhysicalResourceId.fromResponse("certificateId"), 110 | outputPaths: ["certificateArn", "certificatePem", "keyPair.PublicKey", "keyPair.PrivateKey"], 111 | }, 112 | policy: custom_resources_1.AwsCustomResourcePolicy.fromSdkCalls({ resources: custom_resources_1.AwsCustomResourcePolicy.ANY_RESOURCE }), 113 | }); 114 | // Attach policy to claim certificate 115 | let PolicyPrincipalAttachmentForClaimCertificate = new aws_cdk_lib_1.aws_iot.CfnPolicyPrincipalAttachment(this, config_1.Config.app.service + "-" + config_1.Config.app.environment + "policy-principal-attachment", { 116 | policyName: testDeviceClaimCertificatePolicy.policyName, 117 | principal: createKeysAndCertificateForClaimCertificate.getResponseField("certificateArn"), 118 | }); 119 | let cdkTestS3Bucket = new aws_cdk_lib_1.aws_s3.Bucket(this, 'cdkTestS3Bucket', { 120 | blockPublicAccess: aws_cdk_lib_1.aws_s3.BlockPublicAccess.BLOCK_ALL, 121 | versioned: true, 122 | removalPolicy: aws_cdk_lib_1.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE, 123 | // autoDeleteObjects: true, 124 | bucketName: config_1.Config.s3BucketName 125 | }); 126 | // Save the vehicle-gateway certificates and keys to S3 127 | let keyDeploymentForDeviceClaimCertificate = new aws_cdk_lib_1.aws_s3_deployment.BucketDeployment(this, config_1.Config.app.service + "-" + config_1.Config.app.environment + "put-key-to-s3", { 128 | destinationBucket: cdkTestS3Bucket, 129 | sources: [ 130 | aws_cdk_lib_1.aws_s3_deployment.Source.data("claim-certificate/claim.pem", createKeysAndCertificateForClaimCertificate.getResponseField("certificatePem")), 131 | aws_cdk_lib_1.aws_s3_deployment.Source.data("claim-certificate/claim.public.key", createKeysAndCertificateForClaimCertificate.getResponseField("keyPair.PublicKey")), 132 | aws_cdk_lib_1.aws_s3_deployment.Source.data("claim-certificate/claim.private.key", createKeysAndCertificateForClaimCertificate.getResponseField("keyPair.PrivateKey")), 133 | ], 134 | }); 135 | } 136 | } 137 | exports.AwsIotCoreProvisioningInfraStack = AwsIotCoreProvisioningInfraStack; 138 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXdzLWlvdC1jb3JlLXByb3Zpc2lvbmluZy1pbmZyYS1zdGFjay5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImF3cy1pb3QtY29yZS1wcm92aXNpb25pbmctaW5mcmEtc3RhY2sudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFBQSw2Q0FTcUI7QUFDckIsbUVBSXNDO0FBRXRDLHFGQUErRDtBQUMvRCxxR0FBK0U7QUFDL0UsMkNBQTZCO0FBQzdCLDZDQUEwQztBQUMxQywyRkFBa0Y7QUFFbEYsTUFBYSxnQ0FBaUMsU0FBUSxtQkFBSztJQUN2RCxZQUFZLEtBQWdCLEVBQUUsRUFBVSxFQUFFLEtBQWtCO1FBQ3hELEtBQUssQ0FBQyxLQUFLLEVBQUUsRUFBRSxFQUFFLEtBQUssQ0FBQyxDQUFDO1FBRXhCLDhGQUE4RjtRQUM5Riw0QkFBb0IsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLENBQUMsUUFBUSxHQUFHO1lBQ3pDLGVBQWUsZUFBTSxDQUFDLEdBQUcsQ0FBQyxNQUFNLElBQUksZUFBTSxDQUFDLEdBQUcsQ0FBQyxPQUFPLHFCQUFxQjtZQUMzRSxlQUFlLGVBQU0sQ0FBQyxHQUFHLENBQUMsTUFBTSxJQUFJLGVBQU0sQ0FBQyxHQUFHLENBQUMsT0FBTyxTQUFTLEdBQUcsaUJBQWlCO1NBQ3RGLENBQUE7UUFDRCw0QkFBb0IsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLENBQUMsUUFBUSxHQUFHO1lBQ3pDLGVBQWUsZUFBTSxDQUFDLEdBQUcsQ0FBQyxNQUFNLElBQUksZUFBTSxDQUFDLEdBQUcsQ0FBQyxPQUFPLGVBQWUsR0FBRyxpQkFBaUI7U0FDNUYsQ0FBQTtRQUVELElBQUksZ0JBQWdCLEdBQUcsSUFBSSxxQkFBRyxDQUFDLFNBQVMsQ0FDcEMsSUFBSSxFQUFFLGVBQU0sQ0FBQyxHQUFHLENBQUMsT0FBTyxHQUFHLEdBQUcsR0FBRyxlQUFNLENBQUMsR0FBRyxDQUFDLFdBQVcsR0FB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b3ZhbFBvbGljeSxcbiAgICBhd3NfczNfZGVwbG95bWVudCxcbiAgICBhd3NfbGFtYmRhIGFzIGxhbWJkYSxcbiAgICBhd3NfaW90IGFzIGlvdCxcbiAgICBhd3NfaWFtIGFzIGlhbSxcbiAgICBhd3NfczMgYXMgczNcbn0gZnJvbSBcImF3cy1jZGstbGliXCI7XG5pbXBvcnQge1xuICAgIEF3c0N1c3RvbVJlc291cmNlLFxuICAgIEF3c0N1c3RvbVJlc291cmNlUG9saWN5LFxuICAgIFBoeXNpY2FsUmVzb3VyY2VJZCxcbn0gZnJvbSBcImF3cy1jZGstbGliL2N1c3RvbS1yZXNvdXJjZXNcIjtcbmltcG9ydCB7IENvbnN0cnVjdCB9IGZyb20gXCJjb25zdHJ1Y3RzXCI7XG5pbXBvcnQgdGVzdERldmljZVBvbGljeUpzb24gZnJvbSBcIi4vZGV2aWNlL2RldmljZS1wb2xpY3kuanNvblwiO1xuaW1wb3J0IHRlc3RQcm92aXNpb25pbmdUZW1wbGF0ZUpzb24gZnJvbSBcIi4vZGV2aWNlL3Byb3Zpc2lvbmluZy10ZW1wbGF0ZS5qc29uXCI7XG5pbXBvcnQgKiBhcyBwYXRoIGZyb20gXCJwYXRoXCI7XG5pbXBvcnQgeyBDb25maWcgfSBmcm9tIFwiLi4vY29uZmlnL2NvbmZpZ1wiO1xuaW1wb3J0IHRlc3REZXZpY2VDbGFpbUNlcnRpZmljYXRlUG9saWN5SnNvbiBmcm9tIFwiLi9kZXZpY2UvZGV2aWNlLWNjLXBvbGljeS5qc29uXCI7XG5cbmV4cG9ydCBjbGFzcyBBd3NJb3RDb3JlUHJvdmlzaW9uaW5nSW5mcmFTdGFjayBleHRlbmRzIFN0YWNrIHtcbiAgICBjb25zdHJ1Y3RvcihzY29wZTogQ29uc3RydWN0LCBpZDogc3RyaW5nLCBwcm9wcz86IFN0YWNrUHJvcHMpIHtcbiAgICAgICAgc3VwZXIoc2NvcGUsIGlkLCBwcm9wcyk7XG5cbiAgICAgICAgLy8gTW9kaWZ5IHRlc3REZXZpY2VQb2xpY3lKc29uIGFjY29yZGluZyB0byBDb25maWdzIGFuZCBjcmVhdGUgZGV2aWNlIHBvbGljeSBmb3IgZGV2aWNlIHBvbGljeVxuICAgICAgICB0ZXN0RGV2aWNlUG9saWN5SnNvbi5TdGF0ZW1lbnRbMV0uUmVzb3VyY2UgPSBbXG4gICAgICAgICAgICBgYXJuOmF3czppb3Q6JHtDb25maWcuYXdzLnJlZ2lvbn06JHtDb25maWcuYXdzLmFjY291bnR9OnRvcGljLyRhd3MvcnVsZXMvKmAsXG4gICAgICAgICAgICBgYXJuOmF3czppb3Q6JHtDb25maWcuYXdzLnJlZ2lvbn06JHtDb25maWcuYXdzLmFjY291bnR9OnRvcGljL2AgKyAnJHtpb3Q6Q2xpZW50SWR9J1xuICAgICAgICBdXG4gICAgICAgIHRlc3REZXZpY2VQb2xpY3lKc29uLlN0YXRlbWVudFsyXS5SZXNvdXJjZSA9IFtcbiAgICAgICAgICAgIGBhcm46YXdzOmlvdDoke0NvbmZpZy5hd3MucmVnaW9ufToke0NvbmZpZy5hd3MuYWNjb3VudH06dG9waWNmaWx0ZXIvYCArICcke2lvdDpDbGllbnRJZH0nXG4gICAgICAgIF1cblxuICAgICAgICBsZXQgdGVzdERldmljZVBvbGljeSA9IG5ldyBpb3QuQ2ZuUG9saWN5KFxuICAgICAgICAgICAgdGhpcywgQ29uZmlnLmFwcC5zZXJ2aWNlICsgXCItXCIgKyBDb25maWcuYXBwLmVudmlyb25tZW50ICsgXCJkZXZpY2UtcG9saWN5XCIsXG4gICAgICAgICAgICB7XG4gICAgICAgICAgICAgICAgcG9saWN5RG9jdW1lbnQ6IHRlc3REZXZpY2VQb2xpY3lKc29uLFxuICAgICAgICAgICAgICAgIHBvbGljeU5hbWU6IENvbmZpZy5hcHAuc2VydmljZSArIFwiLVwiICsgQ29uZmlnLmFwcC5lbnZpcm9ubWVudCArIFwiLWRldmljZS1wb2xpY3lcIixcbiAgICAgICAgICAgIH1cbiAgICAgICAgKTtcblxuXG4gICAgICAgIC8vIENyZWF0ZSByb2xlIGZvciBwcmUtcHJvdmlzaW9uaW5nIGxhbWJkYSBmb3IgdmVyaWZpY2F0aW9uIG9mIGRldmljZXNcbiAgICAgICAgbGV0IHJvbGVQcmVQcm92aXNpb25pbmdMYW1iZGEgPSBuZXcgaWFtLlJvbGUoXG4gICAgICAgICAgICB0aGlzLCBDb25maWcuYXBwLnNlcnZpY2UgKyBcIi1cIiArIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgKyBcIi1wcmUtcHJvdmlzaW9uaW5nLWxhbWJkYS1yb2xlXCIsXG4gICAgICAgICAgICB7XG4gICAgICAgICAgICAgICAgYXNzdW1lZEJ5OiBuZXcgaWFtLlNlcnZpY2VQcmluY2lwYWwoXCJsYW1iZGEuYW1hem9uYXdzLmNvbVwiKSxcbiAgICAgICAgICAgICAgICBkZXNjcmlwdGlvbjogXCJBV1MgSUFNIHJvbGUgZm9yIHByZS1wcm92aXNpb25pbmcgbGFtYmRhXCIsXG4gICAgICAgICAgICAgICAgcm9sZU5hbWU6IENvbmZpZy5hcHAuc2VydmljZSArIFwiLVwiICsgQ29uZmlnLmFwcC5lbnZpcm9ubWVudCArIFwiLXByZS1wcm92aXNpb25pbmctbGFtYmRhLXJvbGVcIixcbiAgICAgICAgICAgIH1cbiAgICAgICAgKTtcblxuXG4gICAgICAgIC8vIENyYXRlIGxhbWJkYSBmb3IgcHJlLXByb3Zpc2lvbmluZyBob29rIGFuZCBhZGQgcGVybWlzc2lvbiBmb3IgaW52b2tlXG4gICAgICAgIGxldCBsYW1iZGFQcmVQcm92aXNpb25pbmdIb29rID0gbmV3IGxhbWJkYS5GdW5jdGlvbihcbiAgICAgICAgICAgIHRoaXMsXG4gICAgICAgICAgICBDb25maWcuYXBwLnNlcnZpY2UgKyBcIi1cIiArIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgK1xuICAgICAgICAgICAgXCItcHJlLXByb3Zpc2lvbmluZy1ob29rLWxhbWJkYVwiLFxuICAgICAgICAgICAge1xuICAgICAgICAgICAgICAgIGNvZGU6IGxhbWJkYS5Db2RlLmZyb21Bc3NldChwYXRoLmpvaW4oX19kaXJuYW1lLCBcImRldmljZVwiKSksXG4gICAgICAgICAgICAgICAgaGFuZGxlcjogXCJsYW1iZGFfZnVuY3Rpb24ubGFtYmRhX2hhbmRsZXJcIixcbiAgICAgICAgICAgICAgICBydW50aW1lOiBsYW1iZGEuUnVudGltZS5QWVRIT05fM185LFxuICAgICAgICAgICAgICAgIHJvbGU6IHJvbGVQcmVQcm92aXNpb25pbmdMYW1iZGEsXG4gICAgICAgICAgICAgICAgZGVzY3JpcHRpb246IFwiTGFtYmRhIGZvciBwcmUtcHJvdmlzaW9uaW5nIGhvb2tcIixcbiAgICAgICAgICAgICAgICBmdW5jdGlvbk5hbWU6IENvbmZpZy5hcHAuc2VydmljZSArIFwiLVwiICsgQ29uZmlnLmFwcC5lbnZpcm9ubWVudCArIFwiLXByZS1wcm92aXNpb25pbmctaG9vay1sYW1iZGFcIixcbiAgICAgICAgICAgIH1cbiAgICAgICAgKTtcblxuICAgICAgICBsYW1iZGFQcmVQcm92aXNpb25pbmdIb29rLmFkZFBlcm1pc3Npb24oXCJJbnZva2VQZXJtaXNzaW9uXCIsIHtcbiAgICAgICAgICAgIHByaW5jaXBhbDogbmV3IGlhbS5TZXJ2aWNlUHJpbmNpcGFsKFwiaW90LmFtYXpvbmF3cy5jb21cIiksXG4gICAgICAgICAgICBhY3Rpb246IFwibGFtYmRhOkludm9rZUZ1bmN0aW9uXCIsXG4gICAgICAgIH0pO1xuXG5cbiAgICAgICAgLy8gQ3JhdGUgcm9sZSBmb3IgcHJvdmlzaW9uaW5nIHRlbXBsYXRlcyBhbmQgYWRkIEFXU0lvVFRoaW5nc1JlZ2lzdHJhdGlvbiBwb2xpY3lcbiAgICAgICAgbGV0IHJvbGVQcm92aXNpb25pbmcgPSBuZXcgaWFtLlJvbGUoXG4gICAgICAgICAgICB0aGlzLCBDb25maWcuYXBwLnNlcnZpY2UgKyBcIi1cIiArIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgKyBcIi1wcm92aXNpb25pbmctdGVtcGxhdGUtcm9sZVwiLFxuICAgICAgICAgICAge1xuICAgICAgICAgICAgICAgIGFzc3VtZWRCeTogbmV3IGlhbS5TZXJ2aWNlUHJpbmNpcGFsKFwiaW90LmFtYXpvbmF3cy5jb21cIiksXG4gICAgICAgICAgICAgICAgZGVzY3JpcHRpb246IFwiQVdTIElBTSByb2xlIGZvciBwcm92aXNpb25pbmcgc2VydmljZXNcIixcbiAgICAgICAgICAgICAgICByb2xlTmFtZTogQ29uZmlnLmFwcC5zZXJ2aWNlICsgXCItXCIgKyBDb25maWcuYXBwLmVudmlyb25tZW50ICsgXCItcHJvdmlzaW9uaW5nLXRlbXBsYXRlLXJvbGVcIixcbiAgICAgICAgICAgIH1cbiAgICAgICAgKTtcblxuICAgICAgICByb2xlUHJvdmlzaW9uaW5nLmFkZE1hbmFnZWRQb2xpY3koXG4gICAgICAgICAgICBpYW0uTWFuYWdlZFBvbGljeS5mcm9tQXdzTWFuYWdlZFBvbGljeU5hbWUoXG4gICAgICAgICAgICAgICAgXCJzZXJ2aWNlLXJvbGUvQVdTSW9UVGhpbmdzUmVnaXN0cmF0aW9uXCJcbiAgICAgICAgICAgIClcbiAgICAgICAgKTtcblxuICAgICAgICAvLyBDcmVhdGUgcHJvdmlzaW9uaW5nIHRlbXBsYXRlXG4gICAgICAgIHRlc3RQcm92aXNpb25pbmdUZW1wbGF0ZUpzb24uUmVzb3VyY2VzLnBvbGljeS5Qcm9wZXJ0aWVzLlBvbGljeU5hbWUgPSB0ZXN0RGV2aWNlUG9saWN5LnBvbGljeU5hbWUhXG5cbiAgICAgICAgbGV0IHRlc3RQcm92aXNpb25pbmdUZW1wbGF0ZSA9IG5ldyBpb3QuQ2ZuUHJvdmlzaW9uaW5nVGVtcGxhdGUoXG4gICAgICAgICAgICB0aGlzLCBDb25maWcuYXBwLnNlcnZpY2UgKyBcIi1cIiArIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgKyBcIi1wcm92aXNpb24tdGVtcGxhdGVcIixcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgICBwcm92aXNpb25pbmdSb2xlQXJuOiByb2xlUHJvdmlzaW9uaW5nLnJvbGVBcm4sXG4gICAgICAgICAgICAgICAgdGVtcGxhdGVCb2R5OiBKU09OLnN0cmluZ2lmeSh0ZXN0UHJvdmlzaW9uaW5nVGVtcGxhdGVKc29uKSxcbiAgICAgICAgICAgICAgICBlbmFibGVkOiB0cnVlLFxuICAgICAgICAgICAgICAgIHByZVByb3Zpc2lvbmluZ0hvb2s6IHtcbiAgICAgICAgICAgICAgICAgICAgXCJwYXlsb2FkVmVyc2lvblwiOiBcIjIwMjAtMDQtMDFcIixcbiAgICAgICAgICAgICAgICAgICAgXCJ0YXJnZXRBcm5cIjogbGFtYmRhUHJlUHJvdmlzaW9uaW5nSG9vay5mdW5jdGlvbkFyblxuICAgICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICAgICAgZGVzY3JpcHRpb246IFwiQVdTIElvVCBQcm92aXNpb25pbmcgVGVtcGxhdGVcIixcbiAgICAgICAgICAgICAgICB0ZW1wbGF0ZU5hbWU6IENvbmZpZy5hcHAuc2VydmljZSArIFwiLVwiICsgQ29uZmlnLmFwcC5lbnZpcm9ubWVudCArIFwiLXByb3Zpc2lvbi10ZW1wbGF0ZVwiLFxuICAgICAgICAgICAgfVxuICAgICAgICApO1xuXG4gICAgICAgIC8vIE1vZGlmeSB0ZXN0RGV2aWNlQ2xhaW1DZXJ0aWZpY2F0ZVBvbGljeUpzb24gYW5kIGNyZWF0ZSB2ZWhpY2xlIGdhdGV3YXkgcG9saWN5IGZvciBDbGFpbSBDZXJ0aWZpY2F0ZVxuICAgICAgICBsZXQgdGVtcGxhdGVUb3BpY0NyZWF0ZSA9IGBhcm46YXdzOmlvdDoke0NvbmZpZy5hd3MucmVnaW9ufToke0NvbmZpZy5hd3MuYWNjb3VudH06dG9waWMvJGF3cy9jZXJ0aWZpY2F0ZXMvY3JlYXRlLypgXG4gICAgICAgIGxldCB0ZW1wbGF0ZVRvcGljUHJvdmlzaW9uaW5nID0gYGFybjphd3M6aW90OiR7Q29uZmlnLmF3cy5yZWdpb259OiR7Q29uZmlnLmF3cy5hY2NvdW50fTp0b3BpYy8kYXdzL3Byb3Zpc2lvbmluZy10ZW1wbGF0ZXMvJHt0ZXN0UHJvdmlzaW9uaW5nVGVtcGxhdGUudGVtcGxhdGVOYW1lfS9wcm92aXNpb24vKmBcbiAgICAgICAgdGVzdERldmljZUNsYWltQ2VydGlmaWNhdGVQb2xpY3lKc29uLlN0YXRlbWVudFsxXS5SZXNvdXJjZSA9IFt0ZW1wbGF0ZVRvcGljQ3JlYXRlLCB0ZW1wbGF0ZVRvcGljUHJvdmlzaW9uaW5nXVxuXG4gICAgICAgIGxldCB0ZW1wbGF0ZVRvcGljRmlsdGVyQ3JlYXRlID0gYGFybjphd3M6aW90OiR7Q29uZmlnLmF3cy5yZWdpb259OiR7Q29uZmlnLmF3cy5hY2NvdW50fTp0b3BpY2ZpbHRlci8kYXdzL2NlcnRpZmljYXRlcy9jcmVhdGUvKmBcbiAgICAgICAgbGV0IHRlbXBsYXRlVG9waWNGaWx0ZXJQcm92aXNpb25pbmcgPSBgYXJuOmF3czppb3Q6JHtDb25maWcuYXdzLnJlZ2lvbn06JHtDb25maWcuYXdzLmFjY291bnR9OnRvcGljZmlsdGVyLyRhd3MvcHJvdmlzaW9uaW5nLXRlbXBsYXRlcy8ke3Rlc3RQcm92aXNpb25pbmdUZW1wbGF0ZS50ZW1wbGF0ZU5hbWV9L3Byb3Zpc2lvbi8qYFxuICAgICAgICB0ZXN0RGV2aWNlQ2xhaW1DZXJ0aWZpY2F0ZVBvbGljeUpzb24uU3RhdGVtZW50WzJdLlJlc291cmNlID0gW3RlbXBsYXRlVG9waWNGaWx0ZXJDcmVhdGUsIHRlbXBsYXRlVG9waWNGaWx0ZXJQcm92aXNpb25pbmddXG5cbiAgICAgICAgbGV0IHRlc3REZXZpY2VDbGFpbUNlcnRpZmljYXRlUG9saWN5ID0gbmV3IGlvdC5DZm5Qb2xpY3koXG4gICAgICAgICAgICB0aGlzLCBDb25maWcuYXBwLnNlcnZpY2UgKyBcIi1cIiArIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgKyBcIi1jbGFpbS1jZXJ0aWZpY2F0ZS1wb2xpY3lcIixcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgICBwb2xpY3lEb2N1bWVudDogdGVzdERldmljZUNsYWltQ2VydGlmaWNhdGVQb2xpY3lKc29uLFxuICAgICAgICAgICAgICAgIHBvbGljeU5hbWU6IENvbmZpZy5hcHAuc2VydmljZSArIFwiLVwiICsgQ29uZmlnLmFwcC5lbnZpcm9ubWVudCArIFwiLWNsYWltLWNlcnRpZmljYXRlLXBvbGljeVwiLFxuICAgICAgICAgICAgfVxuICAgICAgICApO1xuXG4gICAgICAgIC8vIENyZWF0ZSBjbGFpbSBjZXJ0aWZpY2F0ZSBieSB1c2luZyBBd3NDdXN0b21SZXNvdXJjZVxuICAgICAgICBsZXQgY3JlYXRlS2V5c0FuZENlcnRpZmljYXRlRm9yQ2xhaW1DZXJ0aWZpY2F0ZSA9IG5ldyBBd3NDdXN0b21SZXNvdXJjZShcbiAgICAgICAgICAgIHRoaXMsIENvbmZpZy5hcHAuc2VydmljZSArIFwiLVwiICsgQ29uZmlnLmFwcC5lbnZpcm9ubWVudCArIFwiLWNyZWF0ZS1rZXlzLWFuZC1jZXJ0aWZpY2F0ZS1mb3ItY2xhaW0tY2VydGlmaWNhdGVcIixcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgICBvblVwZGF0ZToge1xuICAgICAgICAgICAgICAgICAgICBzZXJ2aWNlOiBcIklvdFwiLFxuICAgICAgICAgICAgICAgICAgICBhY3Rpb246IFwiY3JlYXRlS2V5c0FuZENlcnRpZmljYXRlXCIsXG4gICAgICAgICAgICAgICAgICAgIHBhcmFtZXRlcnM6IHtzZXRBc0FjdGl2ZTogdHJ1ZX0sXG4gICAgICAgICAgICAgICAgICAgIHBoeXNpY2FsUmVzb3VyY2VJZDogUGh5c2ljYWxSZXNvdXJjZUlkLmZyb21SZXNwb25zZShcImNlcnRpZmljYXRlSWRcIiksXG4gICAgICAgICAgICAgICAgICAgIG91dHB1dFBhdGhzOiBbXCJjZXJ0aWZpY2F0ZUFyblwiLCBcImNlcnRpZmljYXRlUGVtXCIsIFwia2V5UGFpci5QdWJsaWNLZXlcIiwgXCJrZXlQYWlyLlByaXZhdGVLZXlcIl0sXG4gICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgICAgICBwb2xpY3k6IEF3c0N1c3RvbVJlc291cmNlUG9saWN5LmZyb21TZGtDYWxscyh7cmVzb3VyY2VzOiBBd3NDdXN0b21SZXNvdXJjZVBvbGljeS5BTllfUkVTT1VSQ0V9KSxcbiAgICAgICAgICAgIH1cbiAgICAgICAgKTtcblxuXG4gICAgICAgIC8vIEF0dGFjaCBwb2xpY3kgdG8gY2xhaW0gY2VydGlmaWNhdGVcbiAgICAgICAgbGV0IFBvbGljeVByaW5jaXBhbEF0dGFjaG1lbnRGb3JDbGFpbUNlcnRpZmljYXRlID1cbiAgICAgICAgICAgIG5ldyBpb3QuQ2ZuUG9saWN5UHJpbmNpcGFsQXR0YWNobWVudChcbiAgICAgICAgICAgICAgICB0aGlzLCBDb25maWcuYXBwLnNlcnZpY2UgKyBcIi1cIiArIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgKyBcInBvbGljeS1wcmluY2lwYWwtYXR0YWNobWVudFwiLCB7XG4gICAgICAgICAgICAgICAgICAgIHBvbGljeU5hbWU6IHRlc3REZXZpY2VDbGFpbUNlcnRpZmljYXRlUG9saWN5LnBvbGljeU5hbWUhLFxuICAgICAgICAgICAgICAgICAgICBwcmluY2lwYWw6IGNyZWF0ZUtleXNBbmRDZXJ0aWZpY2F0ZUZvckNsYWltQ2VydGlmaWNhdGUuZ2V0UmVzcG9uc2VGaWVsZChcImNlcnRpZmljYXRlQXJuXCIpLFxuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICk7XG5cbiAgICAgICAgbGV0IGNka1Rlc3RTM0J1Y2tldCA9IG5ldyBzMy5CdWNrZXQodGhpcywgJ2Nka1Rlc3RTM0J1Y2tldCcsIHtcbiAgICAgICAgICAgIGJsb2NrUHVibGljQWNjZXNzOiBzMy5CbG9ja1B1YmxpY0FjY2Vzcy5CTE9DS19BTEwsXG4gICAgICAgICAgICB2ZXJzaW9uZWQ6IHRydWUsXG4gICAgICAgICAgICByZW1vdmFsUG9saWN5OiBSZW1vdmFsUG9saWN5LlJFVEFJTl9PTl9VUERBVEVfT1JfREVMRVRFLFxuICAgICAgICAgICAgLy8gYXV0b0RlbGV0ZU9iamVjdHM6IHRydWUsXG4gICAgICAgICAgICBidWNrZXROYW1lOiBDb25maWcuczNCdWNrZXROYW1lXG4gICAgICAgIH0pO1xuXG4gICAgICAgIC8vIFNhdmUgdGhlIHZlaGljbGUtZ2F0ZXdheSBjZXJ0aWZpY2F0ZXMgYW5kIGtleXMgdG8gUzNcbiAgICAgICAgbGV0IGtleURlcGxveW1lbnRGb3JEZXZpY2VDbGFpbUNlcnRpZmljYXRlID0gbmV3IGF3c19zM19kZXBsb3ltZW50LkJ1Y2tldERlcGxveW1lbnQoXG4gICAgICAgICAgICB0aGlzLCBDb25maWcuYXBwLnNlcnZpY2UgKyBcIi1cIiArIENvbmZpZy5hcHAuZW52aXJvbm1lbnQgKyBcInB1dC1rZXktdG8tczNcIixcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgICBkZXN0aW5hdGlvbkJ1Y2tldDogY2RrVGVzdFMzQnVja2V0LFxuICAgICAgICAgICAgICAgIHNvdXJjZXM6IFtcbiAgICAgICAgICAgICAgICAgICAgYXdzX3MzX2RlcGxveW1lbnQuU291cmNlLmRhdGEoXG4gICAgICAgICAgICAgICAgICAgICAgICBcImNsYWltLWNlcnRpZmljYXRlL2NsYWltLnBlbVwiLFxuICAgICAgICAgICAgICAgICAgICAgICAgY3JlYXRlS2V5c0FuZENlcnRpZmljYXRlRm9yQ2xhaW1DZXJ0aWZpY2F0ZS5nZXRSZXNwb25zZUZpZWxkKFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIFwiY2VydGlmaWNhdGVQZW1cIlxuICAgICAgICAgICAgICAgICAgICAgICAgKVxuICAgICAgICAgICAgICAgICAgICApLFxuICAgICAgICAgICAgICAgICAgICBhd3NfczNfZGVwbG95bWVudC5Tb3VyY2UuZGF0YShcbiAgICAgICAgICAgICAgICAgICAgICAgIFwiY2xhaW0tY2VydGlmaWNhdGUvY2xhaW0ucHVibGljLmtleVwiLFxuICAgICAgICAgICAgICAgICAgICAgICAgY3JlYXRlS2V5c0FuZENlcnRpZmljYXRlRm9yQ2xhaW1DZXJ0aWZpY2F0ZS5nZXRSZXNwb25zZUZpZWxkKFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIFwia2V5UGFpci5QdWJsaWNLZXlcIlxuICAgICAgICAgICAgICAgICAgICAgICAgKVxuICAgICAgICAgICAgICAgICAgICApLFxuICAgICAgICAgICAgICAgICAgICBhd3NfczNfZGVwbG95bWVudC5Tb3VyY2UuZGF0YShcbiAgICAgICAgICAgICAgICAgICAgICAgIFwiY2xhaW0tY2VydGlmaWNhdGUvY2xhaW0ucHJpdmF0ZS5rZXlcIixcbiAgICAgICAgICAgICAgICAgICAgICAgIGNyZWF0ZUtleXNBbmRDZXJ0aWZpY2F0ZUZvckNsYWltQ2VydGlmaWNhdGUuZ2V0UmVzcG9uc2VGaWVsZChcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBcImtleVBhaXIuUHJpdmF0ZUtleVwiXG4gICAgICAgICAgICAgICAgICAgICAgICApXG4gICAgICAgICAgICAgICAgICAgICksXG4gICAgICAgICAgICAgICAgXSxcbiAgICAgICAgICAgIH1cbiAgICAgICAgKTtcbiAgICB9XG59XG4iXX0= --------------------------------------------------------------------------------