--- #----------------------------------------------------------------------------------------------------------------------- # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # with the License. A copy of the License is located at # # http://www.apache.org/licenses/LICENSE-2.0 # # or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions # and limitations under the License. #----------------------------------------------------------------------------------------------------------------------- AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: CDF Command & Control Service Globals: Api: OpenApiVersion: 3.0.1 Parameters: Environment: Description: Name of environment. Used to name the created resources. Type: String MinLength: 1 ApplicationConfigurationOverride: Description: This allows you to override any configuration application configuration. Must be a JSON formatted string. Any configurations contained in this will override the configurations found and merged from the config files. Type: String TemplateSnippetS3UriBase: Description: | S3 uri of directory where template snippets are stored for the account. Type: String MinLength: 1 AuthType: Description: Authorization type to apply to the API gateway endpoints Type: String Default: None AllowedValues: - None - Private - Cognito - LambdaRequest - LambdaToken - ApiKey - IAM MinLength: 1 ApiGatewayDefinitionTemplate: Description: | Name of the API Gateway Cloudformation definition along with the authorization method to use. Use one of the provided templates to implement no auth, private, api key, lambda request, lamdba token, or Cognito auth, or modify one to meet your own authentization requirements. The template must exist within the provided TemplateSnippetS3UriBase location. Type: String MinLength: 1 VpcId: Description: ID of VPC to deploy the API into. Only required if AuthType = 'Private'. Type: String Default: 'N/A' CDFSecurityGroupId: Description: ID of an existing CDF security group to deploy the API into. Only required if AuthType = 'Private'. Type: String PrivateSubNetIds: Description: Comma delimited list of private subnetIds to deploy the API into. Only required if AuthType = 'Private'. Type: CommaDelimitedList PrivateApiGatewayVPCEndpoint: Description: VPC endpoint. Only required if AuthType = 'Private'. Type: String CognitoUserPoolArn: Description: Cognito user pool arn. Only required if AuthType is set to 'Cognito'. Type: String Default: 'N/A' AuthorizerFunctionArn: Description: Lambda authorizer function arn. Only required if AuthType is set to 'LambdaRequest' or 'LambdaToken'. Type: String Default: 'N/A' BucketName: Description: Name of bucket to store command information. Type: String MinLength: 1 AssetLibraryFunctionName: Description: AssetLibrary REST API function name Type: String Default: '' ProvisioningFunctionName: Description: Provisioning REST API function name Type: String Default: '' CustomResourceLambdaArn: Description: Custom resource lambda arn Type: String MinLength: 1 KmsKeyId: Description: The KMS key ID used to encrypt DynamoDB. Type: String ProvisionedConcurrentExecutions: Description: The no. of desired concurrent executions to provision. Set to 0 to disable. Type: Number Default: 0 ApplyAutoscaling: Description: If true, will apply auto-scaling as defined in `./cfn-autoscaling.yml' Type: String Default: false AllowedValues: - true - false MinLength: 1 EnableApiGatewayAccessLogs: Description: Enables API gateway Access Logging, defaults to false if not specified. Type: String Default: 'false' AllowedValues: - 'true' - 'false' MinLength: 1 Conditions: DeployInVPC: !Not [!Equals [!Ref VpcId, 'N/A']] DeployWithLambdaAuth: !Or [!Equals [!Ref AuthType, 'LambdaRequest'], !Equals [!Ref AuthType, 'LambdaToken']] DeployWithProvisionedCapacity: !Not [!Equals [!Ref ProvisionedConcurrentExecutions, '0']] KmsKeyIdProvided: !Not [!Equals [!Ref KmsKeyId, '']] EnableApiGatewayAccessLogs: !Equals [!Ref EnableApiGatewayAccessLogs, 'true'] Resources: ApiGatewayApi: 'Fn::Transform': Name: 'AWS::Include' Parameters: Location: !Sub '${TemplateSnippetS3UriBase}${ApiGatewayDefinitionTemplate}' DependsOn: - RESTLambdaFunction ApiGatewayAuthorizerInvokeRole: Condition: DeployWithLambdaAuth 'Fn::Transform': Name: 'AWS::Include' Parameters: Location: !Sub '${TemplateSnippetS3UriBase}cfn-role-lambdaRequestAuthInvokerRole.yaml' DependsOn: - RESTLambdaFunction ApiGatewayAccessLogGroup: Condition: EnableApiGatewayAccessLogs Type: 'AWS::Logs::LogGroup' Properties: LogGroupName: !Sub 'cdf-commandandcontrol-apigatewayaccesslogs-${Environment}' KmsPolicy: Type: AWS::IAM::ManagedPolicy Condition: KmsKeyIdProvided Properties: Description: 'cdf-commandandcontrol policy for accessing KMS' Path: '/cdf/commandandcontrol/' PolicyDocument: Version: '2012-10-17' Statement: - Action: - kms:Decrypt - kms:GenerateDataKey Effect: Allow Resource: !Sub 'arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/${KmsKeyId}' ApplicationPolicies: Type: 'AWS::IAM::ManagedPolicy' Metadata: cfn_nag: rules_to_suppress: - id: W13 reason: 'Lambda provided permissions to manage resources in iot core' Properties: Description: 'cdf-commandandcontrol application policies' Path: '/' PolicyDocument: Version: '2012-10-17' Statement: - Sid: 'iotgroups' Action: - 'iot:ListThingsInThingGroup' Effect: Allow Resource: - !Sub 'arn:aws:iot:${AWS::Region}:${AWS::AccountId}:thinggroup/*' - Sid: 'iotjobs' Action: - 'iot:AssociateTargetsWithJob' - 'iot:CreateJob' - 'iot:CreateThingGroup' - 'iot:DescribeJob' - 'iot:DescribeJobExecution' - 'iot:GetJobDocument' - 'iot:ListJobExecutionsForJob' - 'iot:ListJobExecutionsForThing' Effect: Allow Resource: - !Sub 'arn:aws:iot:${AWS::Region}:${AWS::AccountId}:job/*' - !Sub 'arn:aws:iot:${AWS::Region}:${AWS::AccountId}:thing/*' - !Sub 'arn:aws:iot:${AWS::Region}:${AWS::AccountId}:thinggroup/*' - Sid: 'iottopic' Action: - 'iot:Publish' Effect: Allow Resource: - !Sub 'arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/cmd/cdf/cac/*' - Sid: 'iotshadow' Action: - iot:GetThingShadow - iot:UpdateThingShadow Effect: Allow Resource: - !Sub 'arn:aws:iot:${AWS::Region}:${AWS::AccountId}:thing/*' - Sid: 'dynamodb' Action: - 'dynamodb:BatchGetItem' - 'dynamodb:GetItem' - 'dynamodb:Query' - 'dynamodb:Scan' - 'dynamodb:BatchWriteItem' - 'dynamodb:PutItem' - 'dynamodb:UpdateItem' - 'dynamodb:DeleteItem' Effect: Allow Resource: !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/cdf-commandandcontrol-*' - Sid: 'iampassrole' Action: - 'iam:PassRole' Effect: Allow Resource: !Sub 'arn:aws:iam::${AWS::AccountId}:role/cdf-commandandcontrol-*' - Sid: 's3bucket' Action: - 's3:ListBucket' Effect: Allow Resource: - !Sub 'arn:aws:s3:::${BucketName}' - Sid: 's3objects' Action: - 's3:Get*' - 's3:Put*' Effect: Allow Resource: - !Sub 'arn:aws:s3:::${BucketName}/*' - Sid: lambda Action: - 'lambda:Invoke*' Effect: Allow Resource: - !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${AssetLibraryFunctionName}' - !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${ProvisioningFunctionName}' - Sid: sqs Action: - sqs:SendMessage - sqs:ReceiveMessage - sqs:DeleteMessage - sqs:GetQueueAttributes Effect: Allow Resource: - !GetAtt MessagesQueue.Arn - !GetAtt CommandsQueue.Arn - !GetAtt DlqQueue.Arn LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: Effect: Allow Principal: Service: - lambda.amazonaws.com - iot.amazonaws.com Action: sts:AssumeRole Path: '/' ManagedPolicyArns: - !Ref ApplicationPolicies - !If [KmsKeyIdProvided, !Ref KmsPolicy, !Ref 'AWS::NoValue'] - arn:aws:iam::aws:policy/AWSLambdaExecute - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole DependsOn: - ApplicationPolicies RESTLambdaFunction: Type: AWS::Serverless::Function Properties: FunctionName: !Sub 'cdf-commandandcontrol-rest-${Environment}' CodeUri: ../bundle.zip Handler: lambda_rest_handler.handler MemorySize: 512 Role: !GetAtt LambdaExecutionRole.Arn Runtime: nodejs16.x Timeout: 29 AutoPublishAlias: live DeploymentPreference: Type: AllAtOnce ProvisionedConcurrencyConfig: Fn::If: - DeployWithProvisionedCapacity - ProvisionedConcurrentExecutions: !Ref ProvisionedConcurrentExecutions - Ref: AWS::NoValue Environment: Variables: APP_CONFIG_DIR: 'config' APP_CONFIG: !Ref ApplicationConfigurationOverride AWS_ACCOUNTID: !Ref AWS::AccountId AWS_DYNAMODB_TABLE: !Ref DataTable AWS_IOT_ENDPOINT: !GetAtt IotEndpoint.address AWS_S3_BUCKET: !Ref BucketName AWS_S3_ROLE_ARN: !Sub '${LambdaExecutionRole.Arn}' AWS_SQS_QUEUES_MESSAGES_QUEUEURL: !Ref MessagesQueue AWS_SQS_QUEUES_COMMANDS_QUEUEURL: !Ref CommandsQueue Tracing: Active VpcConfig: Fn::If: - DeployInVPC - SubnetIds: !Ref PrivateSubNetIds SecurityGroupIds: - !Ref CDFSecurityGroupId - Ref: AWS::NoValue Events: ProxyApiRoot: Type: Api Properties: RestApiId: !Ref ApiGatewayApi Path: / Method: ANY ProxyApiGreedy: Type: Api Properties: RestApiId: !Ref ApiGatewayApi Path: /{proxy+} Method: ANY TopicCommandResponseLambdaFunction: Type: AWS::Serverless::Function Properties: FunctionName: !Sub 'cdf-commandandcontrol-topic-${Environment}' CodeUri: ../bundle.zip Handler: lambda_topic_command_response_handler.handler MemorySize: 512 Role: !GetAtt LambdaExecutionRole.Arn Runtime: nodejs16.x Timeout: 30 AutoPublishAlias: live DeploymentPreference: Type: AllAtOnce ProvisionedConcurrencyConfig: Fn::If: - DeployWithProvisionedCapacity - ProvisionedConcurrentExecutions: !Ref ProvisionedConcurrentExecutions - Ref: AWS::NoValue Environment: Variables: APP_CONFIG_DIR: 'config' APP_CONFIG: !Ref ApplicationConfigurationOverride AWS_ACCOUNTID: !Ref AWS::AccountId AWS_S3_ROLE_ARN: !Sub '${LambdaExecutionRole.Arn}' AWS_S3_BUCKET: !Ref BucketName AWS_IOT_ENDPOINT: !GetAtt IotEndpoint.address AWS_DYNAMODB_TABLE: !Ref DataTable ASSETLIBRARY_API_FUNCTION_NAME: !Ref AssetLibraryFunctionName PROVISIONING_API_FUNCTION_NAME: !Ref ProvisioningFunctionName Tracing: Active VpcConfig: Fn::If: - DeployInVPC - SubnetIds: !Ref PrivateSubNetIds SecurityGroupIds: - !Ref CDFSecurityGroupId - Ref: AWS::NoValue Events: TopicCommandResponseRule: Type: IoTRule Properties: AwsIotSqlVersion: '2016-03-23' Sql: !Sub "SELECT topic(4) as thingName, topic(5) as correlationId, topic(6) as action, * as payload FROM 'cmd/cdf/cac/+/+/+'" ShadowCommandResponseLambdaFunction: Type: AWS::Serverless::Function Properties: FunctionName: !Sub 'cdf-commandandcontrol-shadow-${Environment}' CodeUri: ../bundle.zip Handler: lambda_shadow_command_response_handler.handler MemorySize: 512 Role: !GetAtt LambdaExecutionRole.Arn Runtime: nodejs16.x Timeout: 30 AutoPublishAlias: live DeploymentPreference: Type: AllAtOnce ProvisionedConcurrencyConfig: Fn::If: - DeployWithProvisionedCapacity - ProvisionedConcurrentExecutions: !Ref ProvisionedConcurrentExecutions - Ref: AWS::NoValue Environment: Variables: APP_CONFIG_DIR: 'config' APP_CONFIG: !Ref ApplicationConfigurationOverride AWS_ACCOUNTID: !Ref AWS::AccountId AWS_S3_ROLE_ARN: !Sub '${LambdaExecutionRole.Arn}' AWS_S3_BUCKET: !Ref BucketName AWS_IOT_ENDPOINT: !GetAtt IotEndpoint.address AWS_DYNAMODB_TABLE: !Ref DataTable ASSETLIBRARY_API_FUNCTION_NAME: !Ref AssetLibraryFunctionName PROVISIONING_API_FUNCTION_NAME: !Ref ProvisioningFunctionName Tracing: Active VpcConfig: Fn::If: - DeployInVPC - SubnetIds: !Ref PrivateSubNetIds SecurityGroupIds: - !Ref CDFSecurityGroupId - Ref: AWS::NoValue Events: ShadowCommandResponseRule: Type: IoTRule Properties: AwsIotSqlVersion: '2016-03-23' Sql: "SELECT topic(3) as thingName, clientToken as correlationId, state.reported as payload FROM '$aws/things/+/shadow/name/cac/update/accepted' where isUndefined(clientToken)=False AND isUndefined(state.reported)=False" JobCommandResponseLambdaFunction: Type: AWS::Serverless::Function Properties: FunctionName: !Sub 'cdf-commandandcontrol-job-${Environment}' CodeUri: ../bundle.zip Handler: lambda_job_command_response_handler.handler MemorySize: 512 Role: !GetAtt LambdaExecutionRole.Arn Runtime: nodejs16.x Timeout: 30 AutoPublishAlias: live DeploymentPreference: Type: AllAtOnce ProvisionedConcurrencyConfig: Fn::If: - DeployWithProvisionedCapacity - ProvisionedConcurrentExecutions: !Ref ProvisionedConcurrentExecutions - Ref: AWS::NoValue Environment: Variables: APP_CONFIG_DIR: 'config' APP_CONFIG: !Ref ApplicationConfigurationOverride AWS_ACCOUNTID: !Ref AWS::AccountId ASSETLIBRARY_API_FUNCTION_NAME: !Ref AssetLibraryFunctionName AWS_DYNAMODB_TABLE: !Ref DataTable AWS_IOT_ENDPOINT: !GetAtt IotEndpoint.address AWS_S3_BUCKET: !Ref BucketName AWS_S3_ROLE_ARN: !Sub '${LambdaExecutionRole.Arn}' PROVISIONING_API_FUNCTION_NAME: !Ref ProvisioningFunctionName Tracing: Active VpcConfig: Fn::If: - DeployInVPC - SubnetIds: !Ref PrivateSubNetIds SecurityGroupIds: - !Ref CDFSecurityGroupId - Ref: AWS::NoValue Events: StandardJobCommmandResponseRule: Type: IoTRule Properties: AwsIotSqlVersion: '2016-03-23' #TODO: if using standard jobs, need to check that a correlation id has been provided so that we can limit the number of actions executed Sql: "SELECT * FROM '$aws/events/job/+/+' WHERE topic(5)='completed' OR topic(5)='canceled' OR topic(5)='deleted'" SQSLambdaFunction: Type: AWS::Serverless::Function Properties: FunctionName: !Sub 'cdf-commandandcontrol-sqs-${Environment}' CodeUri: ../bundle.zip Handler: lambda_sqs_handler.handler MemorySize: 512 Role: !GetAtt LambdaExecutionRole.Arn Runtime: nodejs16.x Timeout: 900 AutoPublishAlias: live DeploymentPreference: Type: AllAtOnce ProvisionedConcurrencyConfig: Fn::If: - DeployWithProvisionedCapacity - ProvisionedConcurrentExecutions: !Ref ProvisionedConcurrentExecutions - Ref: AWS::NoValue Environment: Variables: APP_CONFIG_DIR: 'config' APP_CONFIG: !Ref ApplicationConfigurationOverride AWS_ACCOUNTID: !Ref AWS::AccountId ASSETLIBRARY_API_FUNCTION_NAME: !Ref AssetLibraryFunctionName AWS_DYNAMODB_TABLE: !Ref DataTable AWS_IOT_ENDPOINT: !GetAtt IotEndpoint.address AWS_S3_BUCKET: !Ref BucketName AWS_S3_ROLE_ARN: !Sub '${LambdaExecutionRole.Arn}' AWS_SQS_QUEUES_MESSAGES_QUEUEURL: !Ref MessagesQueue AWS_SQS_QUEUES_COMMANDS_QUEUEURL: !Ref CommandsQueue PROVISIONING_API_FUNCTION_NAME: !Ref ProvisioningFunctionName Tracing: Active VpcConfig: Fn::If: - DeployInVPC - SubnetIds: !Ref PrivateSubNetIds SecurityGroupIds: - !Ref CDFSecurityGroupId - Ref: AWS::NoValue Events: MessagesQueue: Type: SQS Properties: Queue: !GetAtt MessagesQueue.Arn BatchSize: 1 CommandsQueue: Type: SQS Properties: Queue: !GetAtt CommandsQueue.Arn BatchSize: 1 DataTable: Type: AWS::DynamoDB::Table Metadata: cfn_nag: rules_to_suppress: - id: W28 reason: 'safely naming this resources to be unique by environment' - id: W73 reason: 'Can be decided by the customer, to specify the billing mode' Properties: PointInTimeRecoverySpecification: PointInTimeRecoveryEnabled: true SSESpecification: Fn::If: - KmsKeyIdProvided - KMSMasterKeyId: !Ref KmsKeyId SSEEnabled: true SSEType: KMS - Ref: AWS::NoValue TableName: !Sub 'cdf-commandandcontrol-${Environment}' KeySchema: - AttributeName: pk KeyType: HASH - AttributeName: sk KeyType: RANGE AttributeDefinitions: - AttributeName: pk AttributeType: S - AttributeName: sk AttributeType: S - AttributeName: siKey1 AttributeType: S - AttributeName: siKey2 AttributeType: S - AttributeName: siSort2 AttributeType: S ProvisionedThroughput: ReadCapacityUnits: '5' WriteCapacityUnits: '5' GlobalSecondaryIndexes: # GSI-1 - IndexName: 'siKey1-sk-index' KeySchema: - AttributeName: siKey1 KeyType: HASH - AttributeName: sk KeyType: RANGE Projection: ProjectionType: ALL ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 # GSI-2 - IndexName: 'siKey2-siSort2-index' KeySchema: - AttributeName: siKey2 KeyType: HASH - AttributeName: siSort2 KeyType: RANGE Projection: ProjectionType: ALL ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 IotEndpoint: Type: Custom::IotEndpoint Version: 1.0 Properties: ServiceToken: !Ref CustomResourceLambdaArn MessagesQueue: Type: AWS::SQS::Queue Properties: MessageRetentionPeriod: 604800 VisibilityTimeout: 960 RedrivePolicy: deadLetterTargetArn: !GetAtt DlqQueue.Arn maxReceiveCount: 10 KmsMasterKeyId: Fn::If: - KmsKeyIdProvided - Ref: KmsKeyId - Ref: AWS::NoValue CommandsQueue: Type: AWS::SQS::Queue Properties: MessageRetentionPeriod: 604800 VisibilityTimeout: 960 RedrivePolicy: deadLetterTargetArn: !GetAtt DlqQueue.Arn maxReceiveCount: 10 KmsMasterKeyId: Fn::If: - KmsKeyIdProvided - Ref: KmsKeyId - Ref: AWS::NoValue DlqQueue: Type: AWS::SQS::Queue Properties: MessageRetentionPeriod: 1209600 KmsMasterKeyId: Fn::If: - KmsKeyIdProvided - Ref: KmsKeyId - Ref: AWS::NoValue Outputs: RESTLambdaArn: Description: REST API Lambda Arn Value: !Sub '${RESTLambdaFunction.Arn}' Export: Name: !Sub 'cdf-commandandcontrol-${Environment}-RESTLambdaArn' RestApiFunctionName: Description: Command & Control REST API lambda function name Value: !Ref RESTLambdaFunction Export: Name: !Sub 'cdf-commandandcontrol-${Environment}-restApiFunctionName' ApiGatewayUrl: Description: Command & Control REST API URL Value: !Sub 'https://${ApiGatewayApi}.execute-api.${AWS::Region}.amazonaws.com/Prod' Export: Name: !Sub 'cdf-commandandcontrol-${Environment}-apigatewayurl' ApiGatewayHost: Description: Command & Control REST API host Value: !Sub '${ApiGatewayApi}.execute-api.${AWS::Region}.amazonaws.com' Export: Name: !Sub 'cdf-commandandcontrol-${Environment}-apigatewayhost'