AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > cqrs-pattern-stepfunction Sample SAM Template for cqrs-pattern-stepfunction # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: Function: Timeout: 3 #Parameters: # VPCName: # Description: The name of the VPC being created. # Type: String # Default: "CQRS Pattern Step Functions VPC" # AppName: # Description: "Application Name" # Type: String # Default: "CQRS Pattern Step Functions Demo" # DBClusterName: # Description: "Aurora RDS cluster name" # Type: String # Default: "cqrsstepfunctionsrdscluster" # DatabaseName: # Description: "Aurora RDS database name" # Type: String # Default: "cqrsstepfunctionsdb" # stateMachineName: # Description: State Machine Name # Type: String # Default: "CQRSPatternStepFunction" # eventBusName: # Description: Event Bus Name # Type: String # Default: "CQRSPatternEventBus" # logGroupName: # Description: Step Functions Log Group Name # Type: String # Default: "CQRSPatternStepFunctionLogGroup" # DBMasterUserName: # AllowedPattern: "[a-zA-Z0-9_]+" # ConstraintDescription: must be between 1 to 16 alphanumeric characters. # Description: The database admin account user name, between 1 to 16 alphanumeric characters. # MaxLength: '16' # MinLength: '1' # Type: String # Default: "admin_user" Mappings: GeneralConfig: VPC: VPCName: "CQRS Pattern Step Functions VPC" SecretsManager: AppName: "CQRS Pattern Step Functions Demo" Database: DBClusterName: "cqrsstepfunctionsrdscluster" DatabaseName: "cqrsstepfunctionsdb" MasterUserName: "admin_user" StateMachine: StateMachineName: "CQRSPatternStepFunction" EventBus: EventBusName: "CQRSPatternEventBus" LogGroup: LogGroupName: "CQRSPatternStepFunctionLogGroup" SubnetConfig: VPC: CIDR: "10.0.0.0/16" Public0: CIDR: "10.0.0.0/24" Public1: CIDR: "10.0.1.0/24" Private0: CIDR: "10.0.2.0/24" Private1: CIDR: "10.0.3.0/24" # This mapping accounts for the scenario when certain AZs # are not available to use (this differs on a per account # per customer basis). E.g., if the 'b' AZ is not available # in a specific region in one's account then updating the # list contained in the mapping below here will allow a # different AZ to be chosen. AZRegions: ap-northeast-1: AZs: ["a", "b"] ap-northeast-2: AZs: ["a", "b"] ap-south-1: AZs: ["a", "b"] ap-southeast-1: AZs: ["a", "b"] ap-southeast-2: AZs: ["a", "b"] ca-central-1: AZs: ["a", "b"] eu-central-1: AZs: ["a", "b"] eu-west-1: AZs: ["a", "b"] eu-west-2: AZs: ["a", "b"] sa-east-1: AZs: ["a", "b"] us-east-1: AZs: ["a", "b"] us-east-2: AZs: ["a", "b"] us-west-1: AZs: ["a", "b"] us-west-2: AZs: ["a", "b"] Resources: DBSubnetGroup: Type: AWS::RDS::DBSubnetGroup Properties: DBSubnetGroupDescription: CloudFormation managed DB subnet group. SubnetIds: - !Ref PrivateSubnet0 - !Ref PrivateSubnet1 DBSecret: Type: AWS::SecretsManager::Secret Properties: Name: "AuroraUserSecret" Description: RDS database auto-generated user password GenerateSecretString: SecretStringTemplate: !Sub - '{"username": "${DBMasterUserName}"}' - DBMasterUserName: !FindInMap [ "GeneralConfig", "Database", "MasterUserName" ] GenerateStringKey: "password" PasswordLength: 30 ExcludeCharacters: '"@/\' Tags: - Key: AppName Value: !FindInMap [ "GeneralConfig", "SecretsManager", "AppName" ] RDSCluster: Type: AWS::RDS::DBCluster DeletionPolicy: Delete Properties: DBClusterIdentifier: !FindInMap [ "GeneralConfig", "Database", "DBClusterName" ] MasterUsername: !Join ['', ['{{resolve:secretsmanager:', !Ref DBSecret, ':SecretString:username}}' ]] MasterUserPassword: !Join ['', ['{{resolve:secretsmanager:', !Ref DBSecret, ':SecretString:password}}' ]] DatabaseName: !FindInMap [ "GeneralConfig", "Database", "DatabaseName" ] Engine: aurora-mysql EngineVersion: 8.0.mysql_aurora.3.02.1 ServerlessV2ScalingConfiguration: MaxCapacity: 4 MinCapacity: 0.5 DBSubnetGroupName: Ref: DBSubnetGroup VpcSecurityGroupIds: - !Ref DBSecurityGroup RDSInstance: Type: AWS::RDS::DBInstance Properties: DBInstanceClass: db.serverless Engine: aurora-mysql EngineVersion: 8.0.mysql_aurora.3.02.1 AutoMinorVersionUpgrade: true PubliclyAccessible: false DBClusterIdentifier: Ref: RDSCluster DBSubnetGroupName: Ref: DBSubnetGroup LambdaDBAccessSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Lambda DB Access VpcId: Ref: "VPC" DBSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Aurora Serverless v2 Access VpcId: Ref: "VPC" SecurityGroupIngress: - IpProtocol: tcp FromPort: 3306 ToPort: 3306 SourceSecurityGroupId: !Ref LambdaDBAccessSecurityGroup VPC: Type: "AWS::EC2::VPC" Properties: EnableDnsSupport: "true" EnableDnsHostnames: "true" CidrBlock: Fn::FindInMap: - "SubnetConfig" - "VPC" - "CIDR" Tags: - Key: "Application" Value: Ref: "AWS::StackName" - Key: "Network" Value: "Public" - Key: "Name" Value: !FindInMap [ "GeneralConfig", "VPC", "VPCName" ] PublicSubnet0: Type: "AWS::EC2::Subnet" Properties: VpcId: Ref: "VPC" AvailabilityZone: Fn::Sub: - "${AWS::Region}${AZ}" - AZ: !Select [ 0, !FindInMap [ "AZRegions", !Ref "AWS::Region", "AZs" ] ] CidrBlock: Fn::FindInMap: - "SubnetConfig" - "Public0" - "CIDR" MapPublicIpOnLaunch: "true" Tags: - Key: "Application" Value: Ref: "AWS::StackName" - Key: "Network" Value: "Public" - Key: "Name" Value: !Join - '' - - !FindInMap [ "GeneralConfig", "VPC", "VPCName" ] - '-public-' - !Select [ 0, !FindInMap [ "AZRegions", !Ref "AWS::Region", "AZs" ] ] PublicSubnet1: Type: "AWS::EC2::Subnet" Properties: VpcId: Ref: "VPC" AvailabilityZone: Fn::Sub: - "${AWS::Region}${AZ}" - AZ: !Select [ 1, !FindInMap [ "AZRegions", !Ref "AWS::Region", "AZs" ] ] CidrBlock: Fn::FindInMap: - "SubnetConfig" - "Public1" - "CIDR" MapPublicIpOnLaunch: "true" Tags: - Key: "Application" Value: Ref: "AWS::StackName" - Key: "Network" Value: "Public" - Key: "Name" Value: !Join - '' - - !FindInMap [ "GeneralConfig", "VPC", "VPCName" ] - '-public-' - !Select [ 1, !FindInMap [ "AZRegions", !Ref "AWS::Region", "AZs" ] ] PrivateSubnet0: Type: "AWS::EC2::Subnet" Properties: VpcId: Ref: "VPC" AvailabilityZone: Fn::Sub: - "${AWS::Region}${AZ}" - AZ: !Select [ 0, !FindInMap [ "AZRegions", !Ref "AWS::Region", "AZs" ] ] CidrBlock: Fn::FindInMap: - "SubnetConfig" - "Private0" - "CIDR" Tags: - Key: "Application" Value: Ref: "AWS::StackName" - Key: "Network" Value: "Private" - Key: "Name" Value: !Join - '' - - !FindInMap [ "GeneralConfig", "VPC", "VPCName" ] - '-private-' - !Select [ 0, !FindInMap [ "AZRegions", !Ref "AWS::Region", "AZs" ] ] PrivateSubnet1: Type: "AWS::EC2::Subnet" Properties: VpcId: Ref: "VPC" AvailabilityZone: Fn::Sub: - "${AWS::Region}${AZ}" - AZ: !Select [ 1, !FindInMap [ "AZRegions", !Ref "AWS::Region", "AZs" ] ] CidrBlock: Fn::FindInMap: - "SubnetConfig" - "Private1" - "CIDR" Tags: - Key: "Application" Value: Ref: "AWS::StackName" - Key: "Network" Value: "Private" - Key: "Name" Value: !Join - '' - - !FindInMap [ "GeneralConfig", "VPC", "VPCName" ] - '-private-' - !Select [ 1, !FindInMap [ "AZRegions", !Ref "AWS::Region", "AZs" ] ] InternetGateway: Type: "AWS::EC2::InternetGateway" Properties: Tags: - Key: "Application" Value: Ref: "AWS::StackName" - Key: "Network" Value: "Public" - Key: "Name" Value: !Join - '' - - !FindInMap [ "GeneralConfig", "VPC", "VPCName" ] - '-IGW' GatewayToInternet: Type: "AWS::EC2::VPCGatewayAttachment" Properties: VpcId: Ref: "VPC" InternetGatewayId: Ref: "InternetGateway" PublicRouteTable: Type: "AWS::EC2::RouteTable" Properties: VpcId: Ref: "VPC" Tags: - Key: "Application" Value: Ref: "AWS::StackName" - Key: "Network" Value: "Public" - Key: "Name" Value: !Join - '' - - !FindInMap [ "GeneralConfig", "VPC", "VPCName" ] - '-public-route-table' PublicRoute: Type: "AWS::EC2::Route" DependsOn: "GatewayToInternet" Properties: RouteTableId: Ref: "PublicRouteTable" DestinationCidrBlock: "0.0.0.0/0" GatewayId: Ref: "InternetGateway" PublicSubnetRouteTableAssociation0: Type: "AWS::EC2::SubnetRouteTableAssociation" Properties: SubnetId: Ref: "PublicSubnet0" RouteTableId: Ref: "PublicRouteTable" PublicSubnetRouteTableAssociation1: Type: "AWS::EC2::SubnetRouteTableAssociation" Properties: SubnetId: Ref: "PublicSubnet1" RouteTableId: Ref: "PublicRouteTable" PublicNetworkAcl: Type: "AWS::EC2::NetworkAcl" Properties: VpcId: Ref: "VPC" Tags: - Key: "Application" Value: Ref: "AWS::StackName" - Key: "Network" Value: "Public" - Key: "Name" Value: !Join - '' - - !FindInMap [ "GeneralConfig", "VPC", "VPCName" ] - '-public-nacl' InboundHTTPPublicNetworkAclEntry: Type: "AWS::EC2::NetworkAclEntry" Properties: NetworkAclId: Ref: "PublicNetworkAcl" RuleNumber: "100" Protocol: "-1" RuleAction: "allow" Egress: "false" CidrBlock: "0.0.0.0/0" PortRange: From: "0" To: "65535" OutboundPublicNetworkAclEntry: Type: "AWS::EC2::NetworkAclEntry" Properties: NetworkAclId: Ref: "PublicNetworkAcl" RuleNumber: "100" Protocol: "-1" RuleAction: "allow" Egress: "true" CidrBlock: "0.0.0.0/0" PortRange: From: "0" To: "65535" PublicSubnetNetworkAclAssociation0: Type: "AWS::EC2::SubnetNetworkAclAssociation" Properties: SubnetId: Ref: "PublicSubnet0" NetworkAclId: Ref: "PublicNetworkAcl" PublicSubnetNetworkAclAssociation1: Type: "AWS::EC2::SubnetNetworkAclAssociation" Properties: SubnetId: Ref: "PublicSubnet1" NetworkAclId: Ref: "PublicNetworkAcl" ElasticIP0: Type: "AWS::EC2::EIP" Properties: Domain: "vpc" ElasticIP1: Type: "AWS::EC2::EIP" Properties: Domain: "vpc" NATGateway0: Type: "AWS::EC2::NatGateway" Properties: AllocationId: Fn::GetAtt: - "ElasticIP0" - "AllocationId" SubnetId: Ref: "PublicSubnet0" NATGateway1: Type: "AWS::EC2::NatGateway" Properties: AllocationId: Fn::GetAtt: - "ElasticIP1" - "AllocationId" SubnetId: Ref: "PublicSubnet1" PrivateRouteTable0: Type: "AWS::EC2::RouteTable" Properties: VpcId: Ref: "VPC" Tags: - Key: "Name" Value: !Join - '' - - !FindInMap [ "GeneralConfig", "VPC", "VPCName" ] - '-private-route-table-0' PrivateRouteTable1: Type: "AWS::EC2::RouteTable" Properties: VpcId: Ref: "VPC" Tags: - Key: "Name" Value: !Join - '' - - !FindInMap [ "GeneralConfig", "VPC", "VPCName" ] - '-private-route-table-1' PrivateRouteToInternet0: Type: "AWS::EC2::Route" Properties: RouteTableId: Ref: "PrivateRouteTable0" DestinationCidrBlock: "0.0.0.0/0" NatGatewayId: Ref: "NATGateway0" PrivateRouteToInternet1: Type: "AWS::EC2::Route" Properties: RouteTableId: Ref: "PrivateRouteTable1" DestinationCidrBlock: "0.0.0.0/0" NatGatewayId: Ref: "NATGateway1" PrivateSubnetRouteTableAssociation0: Type: "AWS::EC2::SubnetRouteTableAssociation" Properties: SubnetId: Ref: "PrivateSubnet0" RouteTableId: Ref: "PrivateRouteTable0" PrivateSubnetRouteTableAssociation1: Type: "AWS::EC2::SubnetRouteTableAssociation" Properties: SubnetId: Ref: "PrivateSubnet1" RouteTableId: Ref: "PrivateRouteTable1" VPC: Type: "AWS::EC2::VPC" Properties: EnableDnsSupport: "true" EnableDnsHostnames: "true" CidrBlock: Fn::FindInMap: - "SubnetConfig" - "VPC" - "CIDR" Tags: - Key: "Application" Value: Ref: "AWS::StackName" - Key: "Network" Value: "Public" - Key: "Name" Value: !FindInMap [ "GeneralConfig", "VPC", "VPCName" ] ProcessOrderTableRecords: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: CodeUri: function/process-order-table-records/ Handler: app.handler Runtime: nodejs16.x Timeout: 15 VpcConfig: SecurityGroupIds: - !Ref LambdaDBAccessSecurityGroup SubnetIds: - !Ref PrivateSubnet0 - !Ref PrivateSubnet1 Architectures: - x86_64 Environment: Variables: SECRET_NAME: "AuroraUserSecret" AURORA_ENDPOINT: !GetAtt 'RDSCluster.Endpoint.Address' DB_NAME: !FindInMap [ "GeneralConfig", "Database", "DatabaseName" ] Policies: - AWSSecretsManagerGetSecretValuePolicy: SecretArn: !Ref DBSecret Events: Stream: Type: DynamoDB Properties: Stream: !GetAtt OrdersTable.StreamArn BatchSize: 1 MaximumRetryAttempts: 2 MaximumRecordAgeInSeconds: 60 StartingPosition: TRIM_HORIZON DependsOn: RDSInstance QueryItemSalesReport: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: CodeUri: function/query-item-sales-report/ Handler: app.handler Runtime: nodejs16.x Timeout: 15 VpcConfig: SecurityGroupIds: - !Ref LambdaDBAccessSecurityGroup SubnetIds: - !Ref PrivateSubnet0 - !Ref PrivateSubnet1 Architectures: - x86_64 Environment: Variables: SECRET_NAME: "AuroraUserSecret" AURORA_ENDPOINT: !GetAtt 'RDSCluster.Endpoint.Address' DB_NAME: !FindInMap [ "GeneralConfig", "Database", "DatabaseName" ] Policies: - AWSSecretsManagerGetSecretValuePolicy: SecretArn: !Ref DBSecret QueryMonthlySalesByItem: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: CodeUri: function/query-monthly-sales-by-item/ Handler: app.handler Runtime: nodejs16.x Timeout: 15 VpcConfig: SecurityGroupIds: - !Ref LambdaDBAccessSecurityGroup SubnetIds: - !Ref PrivateSubnet0 - !Ref PrivateSubnet1 Architectures: - x86_64 Environment: Variables: SECRET_NAME: "AuroraUserSecret" AURORA_ENDPOINT: !GetAtt 'RDSCluster.Endpoint.Address' DB_NAME: !FindInMap [ "GeneralConfig", "Database", "DatabaseName" ] Policies: - AWSSecretsManagerGetSecretValuePolicy: SecretArn: !Ref DBSecret InitializeDatabase: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: CodeUri: function/initialize-database/ Handler: app.handler Runtime: nodejs16.x Timeout: 120 VpcConfig: SecurityGroupIds: - !Ref LambdaDBAccessSecurityGroup SubnetIds: - !Ref PrivateSubnet0 - !Ref PrivateSubnet1 Architectures: - x86_64 Events: EBTrigger: Type: EventBridgeRule Properties: EventBusName: !FindInMap [ "GeneralConfig", "EventBus", "EventBusName" ] Pattern: source: - orderPipeline detail-type: - initializeDatabase Environment: Variables: SECRET_NAME: "AuroraUserSecret" AURORA_ENDPOINT: !GetAtt 'RDSCluster.Endpoint.Address' DB_NAME: !FindInMap [ "GeneralConfig", "Database", "DatabaseName" ] Policies: - AWSSecretsManagerGetSecretValuePolicy: SecretArn: !Ref DBSecret DependsOn: RDSInstance InitializeDatabaseCustomAction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: CodeUri: function/initialize-database-custom-action/ Handler: app.handler Runtime: nodejs16.x Timeout: 10 Architectures: - x86_64 Environment: Variables: EVENTBUS_NAME: !Ref CQRSEventBus Policies: - EventBridgePutEventsPolicy: EventBusName: !FindInMap [ "GeneralConfig", "EventBus", "EventBusName" ] DependsOn: - InitializeDatabase - CQRSEventBus - InitializeDatabaseEBTrigger - InitializeDatabaseEBTriggerPermission InitializeDatabaseCustomResource: Type: "Custom::CustomResource" Properties: ServiceToken: !GetAtt InitializeDatabaseCustomAction.Arn DependsOn: InitializeDatabaseCustomAction OrdersTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: orderid AttributeType: S KeySchema: - AttributeName: orderid KeyType: HASH ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 StreamSpecification: StreamViewType: NEW_AND_OLD_IMAGES CQRSEventBus: Type: AWS::Events::EventBus Properties: Name: !FindInMap [ "GeneralConfig", "EventBus", "EventBusName" ] CQRSStateMachine: Type: AWS::Serverless::StateMachine Properties: Name: !FindInMap [ "GeneralConfig", "StateMachine", "StateMachineName" ] Type: EXPRESS DefinitionUri: statemachine/statemachine.asl.json Tracing: Enabled: true Events: EBTrigger: Type: EventBridgeRule Properties: EventBusName: !FindInMap [ "GeneralConfig", "EventBus", "EventBusName" ] Pattern: source: - orderPipeline detail-type: - orderCommand - orderQuery Logging: Destinations: - CloudWatchLogsLogGroup: LogGroupArn: !GetAtt CQRSStateMachineLogGroup.Arn IncludeExecutionData: true Level: 'ALL' DefinitionSubstitutions: DynamoDBTableName: !Ref OrdersTable EventBridgeBusName: !FindInMap [ "GeneralConfig", "EventBus", "EventBusName" ] QueryItemSalesReportFunctionArn: !GetAtt QueryItemSalesReport.Arn QueryMonthlySalesByItemFunctionArn: !GetAtt QueryMonthlySalesByItem.Arn Policies: - DynamoDBWritePolicy: TableName: !Ref OrdersTable - EventBridgePutEventsPolicy: EventBusName: !FindInMap [ "GeneralConfig", "EventBus", "EventBusName" ] - LambdaInvokePolicy: FunctionName: !Ref QueryItemSalesReport - LambdaInvokePolicy: FunctionName: !Ref QueryMonthlySalesByItem - Statement: - Sid: CQRSCloudWatchLogDeliveryPolicy Effect: Allow Action: - ssm:DescribeParameters - logs:CreateLogDelivery - logs:GetLogDelivery - logs:UpdateLogDelivery - logs:DeleteLogDelivery - logs:ListLogDeliveries - logs:PutLogEvents - logs:PutResourcePolicy - logs:DescribeResourcePolicies - logs:DescribeLogGroups Resource: '*' CQRSStateMachineLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !FindInMap [ "GeneralConfig", "LogGroup", "LogGroupName" ] Outputs: # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function # Find out more about other implicit resources you can reference within SAM # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api OrdersTableName: Description: "DynamoDB Table Name for Orders" Value: !Ref OrdersTable StateMachineName: Description: "Step Functions Workflow Name" Value: !GetAtt CQRSStateMachine.Name EventBridgeARN: Description: "EventBridge Event Bus ARN" Value: !GetAtt CQRSEventBus.Arn