AWSTemplateFormatVersion: '2010-09-09' Description: 'Device Data Generator (qs-1tsuk2plc)' Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: 'Job Configuration' Parameters: - GenerationState - GenerationInterval - TotalDevices - DevicesPerWorker - RegionalVoltage - MinLoad - MaxLoad - LateArrivalSimulate - LateArrivalPercent - LateArrivalMinuteOffset - Label: default: 'Timestream Configuration' Parameters: - TimestreamCreation - TimestreamDatabaseName - TimestreamTableName - Label: default: 'Deployment Configuration' Parameters: - QSS3BucketName - QSS3KeyPrefix ParameterLabels: GenerationState: default: 'What is the desired state generating device readings?' GenerationInterval: default: 'How frequently do you want to generate device readings?' TotalDevices: default: 'For how many devices do you want to generate readings at each interval?' DevicesPerWorker: default: 'How many devices should each Lambda worker handle?' RegionalVoltage: default: 'What is the voltage for which to base the device readings?' MinLoad: default: 'What is the minimum contract load KW for a device?' MaxLoad: default: 'What is the maximum contract load KW for a device?' LateArrivalSimulate: default: 'Do you want to simulate late arriving data?' LateArrivalPercent: default: 'What percent of device readings to you want to arrive late?' LateArrivalMinuteOffset: default: 'By how many minutes do you want for late records to arrive?' TimestreamCreation: default: 'Do you want to use an existing Timestream database and table or create new ones?' TimestreamDatabaseName: default: 'Name of the new or existing Timestream database' TimestreamTableName: default: 'Name of the new or existing Timestream table' QSS3BucketName: default: 'What is the name of the bucket in which the Quick Start assets are hosted?' QSS3KeyPrefix: default: 'What is the prefix (directory) in which the Qkick Start assets are located?' Parameters: GenerationState: Description: 'Change to DISABLE at any time to stop generation of device readings' Type: String Default: 'ENABLED' AllowedValues: - 'ENABLED' - 'DISABLED' GenerationInterval: Description: 'Minutes per interval, must be greater than 5' Type: Number Default: 5 MinValue: 5 ConstraintDescription: 'GenerationInterval must contain a numeric value greater than 5' TotalDevices: Description: 'Number of devices to generate readings at each interval' Type: Number Default: 50000 DevicesPerWorker: Description: 'A higher number means less Lambda workers invoked, but longer time to generate readings' Type: Number Default: 10000 RegionalVoltage: Description: 'Number of volts for country' Type: Number Default: 220 MinLoad: Description: 'Minimum contract load KW' Type: Number Default: 5 MinValue: 0 MaxValue: 100 MaxLoad: Description: 'Maximum contract load KW' Type: Number Default: 20 MinValue: 0 MaxValue: 100 LateArrivalSimulate: Description: 'Change to ENABLED at any time to simlate late arriving device readings' Type: String Default: 'DISABLED' AllowedValues: - 'ENABLED' - 'DISABLED' LateArrivalPercent: Description: 'Percent of late arriving device readings' Type: Number Default: 10 MinValue: 1 MaxValue: 100 LateArrivalMinuteOffset: Description: 'Delay in minutes for late arriving device readings' Type: Number Default: 1440 MinValue: 1 MaxValue: 43200 TimestreamCreation: Default: 'Use Existing' Type: String AllowedValues: - 'Create New' - 'Use Existing' TimestreamDatabaseName: Type: String Default: 'devices' MinLength: 3 MaxLength: 256 TimestreamTableName: Type: String Default: 'readings' MinLength: 3 MaxLength: 256 QSS3BucketName: Description: 'Keep the default bucket unless you are customizing the template or assets in a new location. Changing the name updates code references to point to a new Quick Start location. This name can include numbers, lowercase letters, uppercase letters, and hyphens, but do not start or end with a hyphen (-). See https://aws-quickstart.github.io/option1.html.' Type: String Default: 'aws-quickstart' AllowedPattern: '^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$' ConstraintDescription: 'The Quick Start bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-).' QSS3KeyPrefix: Description: 'Keep the default prefix unless you are customizing the template or asset location. Changing this prefix updates code references to point to a new Quick Start location. This prefix can include numbers, lowercase letters, uppercase letters, hyphens (-), and forward slashes (/). End with a forward slash. See https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html and https://aws-quickstart.github.io/option1.html.' Type: String Default: 'quickstart-aws-utility-meter-data-generator/' AllowedPattern: '^[0-9a-zA-Z-/]*$' ConstraintDescription: 'The Quick Start S3 key prefix can include numbers, lowercase letters, uppercase letters, hyphens (-), and forward slashes (/). The prefix should end with a forward slash (/).' Conditions: createTimestream: !Equals [ !Ref TimestreamCreation, 'Create New'] UsingDefaultBucket: !Equals [ !Ref QSS3BucketName, 'aws-quickstart' ] Resources: TimestreamDatabase: Condition: createTimestream Type: AWS::Timestream::Database Properties: DatabaseName: !Ref TimestreamDatabaseName TimestreamTable: Condition: createTimestream Type: AWS::Timestream::Table Properties: DatabaseName: !Ref TimestreamDatabase TableName: !Ref TimestreamTableName RetentionProperties: MemoryStoreRetentionPeriodInHours: 168 MagneticStoreRetentionPeriodInDays: 30 DdgBucket: Type: AWS::S3::Bucket DeletionPolicy: Delete S3CopyLambdaRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: Effect: 'Allow' Action: - 'sts:AssumeRole' Principal: Service: - lambda.amazonaws.com S3CopyLambdaPolicy: Metadata: cfn-lint: config: ignore_checks: - E9101 ignore_reasons: E9101: 'AbortMultipartUpload is an s3 action and can not be renamed' Type: AWS::IAM::Policy Properties: Roles: - !Ref S3CopyLambdaRole PolicyName: !Sub '${AWS::Region}-${S3CopyLambdaFunction}-policy' PolicyDocument: Version: '2012-10-17' Statement: - Effect: 'Allow' Action: - 's3:GetObject' Resource: - !Sub 'arn:${AWS::Partition}:s3:::${QSS3BucketName}/*' - Effect: 'Allow' Action: - 's3:ListBucket' - 's3:GetBucketLocation' - 's3:ListBucketMultipartUploads' Resource: !Sub '${DdgBucket.Arn}' - Effect: 'Allow' Action: - 's3:CopyObject' - 's3:PutObject' - 's3:PutObjectAcl' - 's3:DeleteObject' - 's3:DeleteObjects' - 's3:CreateMultipartUpload' - 's3:AbortMultipartUpload' - 's3:ListMultipartUploadParts' Resource: - !Sub '${DdgBucket.Arn}/*' - Effect: 'Allow' Action: - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: - !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${S3CopyLambdaFunction}' - !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${S3CopyLambdaFunction}:*' - !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${S3CopyLambdaFunction}:*:*' S3CopyInvoke: Type: AWS::CloudFormation::CustomResource Version: '1.0' DependsOn: S3CopyLambdaPolicy Properties: ServiceToken: !GetAtt S3CopyLambdaFunction.Arn SourceBucket: !Ref QSS3BucketName SourcePrefix: !Ref QSS3KeyPrefix DestBucket: !Ref DdgBucket Objects: !Join - ', ' - - 'assets/lambda/s3_copy_function/lambda.zip' - 'assets/lambda/orchestrator_function/lambda.zip' - 'assets/lambda/worker_function/lambda.zip' S3CopyLambdaFunction: Type: AWS::Lambda::Function Properties: Role: !GetAtt S3CopyLambdaRole.Arn Runtime: python3.9 MemorySize: 512 Timeout: 120 ReservedConcurrentExecutions: 1 Handler: 'lambda_function.lambda_handler' Code: S3Bucket: !If [ UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName ] S3Key: !Sub '${QSS3KeyPrefix}assets/lambda/s3_copy_function/lambda.zip' S3CopyLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub '/aws/lambda/${S3CopyLambdaFunction}' RetentionInDays: 90 WorkerSQSQueue: Type: AWS::SQS::Queue Properties: VisibilityTimeout: 3600 MessageRetentionPeriod: 28800 SQSLambdaTrigger: Type: AWS::Lambda::EventSourceMapping DependsOn: WorkerLambdaPolicy Properties: BatchSize: 1 EventSourceArn: !GetAtt WorkerSQSQueue.Arn FunctionName: !GetAtt WorkerLambdaFunction.Arn OrechestratorScheduledRule: Type: AWS::Events::Rule Properties: Description: !Join [ ' ', [ 'Launch', !Ref OrchestratorLambdaFunction, 'every', !Ref GenerationInterval, 'minutes' ] ] ScheduleExpression: !Join [ '', [ 'rate(', !Ref GenerationInterval, ' minutes)' ] ] State: !Ref GenerationState Targets: - Arn: !GetAtt OrchestratorLambdaFunction.Arn Id: 'TargetFunctionV1' PermissionForEventsToInvokeOrchestratorLambda: Type: AWS::Lambda::Permission Properties: FunctionName: !Ref OrchestratorLambdaFunction Action: 'lambda:InvokeFunction' Principal: 'events.amazonaws.com' SourceArn: !GetAtt OrechestratorScheduledRule.Arn WorkerLambdaRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: Effect: 'Allow' Action: - 'sts:AssumeRole' Principal: Service: - lambda.amazonaws.com Version: '2012-10-17' WorkerLambdaFunction: Type: AWS::Lambda::Function DependsOn: S3CopyInvoke Properties: Role: !GetAtt WorkerLambdaRole.Arn Runtime: python3.9 MemorySize: 128 Timeout: 900 Environment: Variables: TIMESTREAM_DATABASE: !If [createTimestream, !Ref TimestreamDatabase, !Ref TimestreamDatabaseName] TIMESTREAM_TABLE: !If [createTimestream, !GetAtt TimestreamTable.Name, !Ref TimestreamTableName] REGION_VOLTAGE: !Ref RegionalVoltage MIN_LOAD: !Ref MinLoad MAX_LOAD: !Ref MaxLoad LATE_ARRIVAL_SIMULATE: !Ref LateArrivalSimulate LATE_ARRIVAL_PERCENT: !Ref LateArrivalPercent LATE_ARRIVAL_MINUTE_OFFSET: !Ref LateArrivalMinuteOffset Handler: 'lambda_function.lambda_handler' Code: S3Bucket: !Ref DdgBucket S3Key: 'assets/lambda/worker_function/lambda.zip' WorkerLogGroup: Type: 'AWS::Logs::LogGroup' Properties: LogGroupName: !Sub '/aws/lambda/${WorkerLambdaFunction}' RetentionInDays: 90 WorkerLambdaPolicy: Type: AWS::IAM::Policy Properties: Roles: - !Ref WorkerLambdaRole PolicyName: !Sub '${AWS::Region}-${WorkerLambdaFunction}-policy' PolicyDocument: Version: '2012-10-17' Statement: - Effect: 'Allow' Action: - 'sqs:ReceiveMessage' - 'sqs:DeleteMessage' - 'sqs:GetQueueAttributes' Resource: !GetAtt WorkerSQSQueue.Arn - Effect: Allow Action: - logs:CreateLogStream - logs:PutLogEvents Resource: - !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${WorkerLambdaFunction}' - !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${WorkerLambdaFunction}:*' - !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${WorkerLambdaFunction}:*:*' - Effect: 'Allow' Action: - 'timestream:DescribeEndpoints' Resource: '*' - Effect: 'Allow' Action: - 'timestream:WriteRecords' Resource: !If [ createTimestream, !GetAtt TimestreamTable.Arn, !Join [ '/', [ !Join [ ':', ['arn' , 'aws', 'timestream', !Ref AWS::Region, !Ref AWS::AccountId, 'database'] ] , !Ref TimestreamDatabaseName, 'table', !Ref TimestreamTableName ] ] ] OrchestratorLambdaRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: Effect: 'Allow' Action: - 'sts:AssumeRole' Principal: Service: - lambda.amazonaws.com Version: '2012-10-17' OrchestratorLambdaFunction: Type: AWS::Lambda::Function DependsOn: S3CopyInvoke Properties: Role: !GetAtt OrchestratorLambdaRole.Arn Runtime: python3.9 MemorySize: 128 Timeout: 120 Environment: Variables: WORKER_QUEUE_URL: !Ref WorkerSQSQueue DEVICE_COUNT: !Ref TotalDevices RECORDS_PER_WORKER: !Ref DevicesPerWorker GENERATION_INTERVAL_MINUTES: !Ref GenerationInterval Handler: 'lambda_function.lambda_handler' Code: S3Bucket: !Ref DdgBucket S3Key: 'assets/lambda/orchestrator_function/lambda.zip' OrchestratorLogGroup: Type: 'AWS::Logs::LogGroup' Properties: LogGroupName: !Sub '/aws/lambda/${OrchestratorLambdaFunction}' RetentionInDays: 90 OrchestratorLambdaPolicy: Type: AWS::IAM::Policy Properties: Roles: - !Ref OrchestratorLambdaRole PolicyName: !Sub '${AWS::Region}-${OrchestratorLambdaFunction}-policy' PolicyDocument: Version: '2012-10-17' Statement: - Effect: 'Allow' Action: - 'sqs:SendMessage' - 'sqs:GetQueueAttributes' Resource: !GetAtt WorkerSQSQueue.Arn - Effect: Allow Action: - logs:CreateLogStream - logs:PutLogEvents Resource: - !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${OrchestratorLambdaFunction}' - !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${OrchestratorLambdaFunction}:*' - !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${OrchestratorLambdaFunction}:*:*' Outputs: TimestreamDatabaseARN: Description: 'ARN of Timestream database' Value: !If [ createTimestream, !GetAtt TimestreamDatabase.Arn, !Join [ '/', [ !Join [ ':', ['arn' , 'aws', 'timestream', !Ref AWS::Region, !Ref AWS::AccountId, 'database'] ] , !Ref TimestreamDatabaseName ] ] ] Export: Name: !Join [ ':', [ !Ref AWS::StackName, 'TimestreamDatabaseARN' ] ] TimestreamTableARN: Description: 'ARN of Timestream table' Value: !If [ createTimestream, !GetAtt TimestreamTable.Arn, !Join [ '/', [ !Join [ ':', ['arn' , 'aws', 'timestream', !Ref AWS::Region, !Ref AWS::AccountId, 'database'] ] , !Ref TimestreamDatabaseName, 'table', !Ref TimestreamTableName ] ] ] Export: Name: !Join [ ':', [ !Ref AWS::StackName, 'TimestreamTableARN' ] ] TimestreamDatabaseName: Description: 'Name of Timestream database' Value: !If [createTimestream, !Ref TimestreamDatabase, !Ref TimestreamDatabaseName] Export: Name: !Join [ ':', [ !Ref AWS::StackName, 'TimestreamDatabaseName' ] ] TimestreamTableName: Description: 'Name of Timestream table' Value: !If [createTimestream, !GetAtt TimestreamTable.Name, !Ref TimestreamTableName] Export: Name: !Join [ ':', [ !Ref AWS::StackName, 'TimestreamTableName' ] ]