# # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy of this # software and associated documentation files (the "Software"), to deal in the Software # without restriction, including without limitation the rights to use, copy, modify, # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # AWSTemplateFormatVersion: 2010-09-09 Description: > **WARNING** This template will create and use the following AWS resources: Amazon API Gateway, CloudTrail, CloudWatch, Lambda, Simple Storage Service (S3), and Simple Notification Service (SNS). You will be billed for the resources used if you create a stack from this template. Resources: CustomResourceEndpoint: Type: 'AWS::ApiGateway::RestApi' Properties: Name: Custom Resource Scaling API Description: A new REST API in API Gateway to use with Application Auto Scaling and your own custom resources. Body: swagger: "2.0" info: description: "Swagger REST API Specification" version: "1.0.0" title: "Scaling API" license: name: MIT-0 url: https://spdx.org/licenses/MIT-0.html basePath: "/v1" schemes: - "http" produces: - "application/json" paths: /scalableTargetDimensions/{scalableTargetDimensionId}: get: tags: - "ScalableTargets" summary: "Describe target" description: "Returns information about about a registered scalable target dimension, \ including the desired and actual capacity." operationId: "controllers.default_controller.scalable_target_id_get" parameters: - name: "scalableTargetDimensionId" in: "path" description: "The identifier of a scalable target dimension to retrieve." required: true type: "string" format: "" responses: '200': description: "A JSON object that contains information about the resource." schema: '$ref': "#/definitions/ScalableTargetDimension" '400': description: "Client Error" schema: '$ref': "#/definitions/Error" '403': description: "Client Error: Not authorized" schema: '$ref': "#/definitions/Error" '404': description: "Client Error: ScalableTargetDimension not found" schema: '$ref': "#/definitions/Error" '429': description: "Client Error: Too Many Requests" schema: '$ref': "#/definitions/Error" '500': description: "Server Error" schema: '$ref': "#/definitions/Error" x-tags: - tag: "ScalableTargets" security: - sigv4: [] x-amazon-apigateway-integration: uri: !Ref IntegrationHttpEndpoint passthroughBehavior: "when_no_match" httpMethod: "GET" responses: default: statusCode: "500" '200': statusCode: "200" '403': statusCode: "403" '404': statusCode: "404" '429': statusCode: "429" '4\d{2}': statusCode: "400" requestParameters: integration.request.path.scalableTargetDimensionId: "method.request.path.scalableTargetDimensionId" type: "http" patch: tags: - "ScalableTargets" summary: "Update ScalableTargetDimension" operationId: "controllers.default_controller.scalable_target_id_patch" consumes: - "application/json" parameters: - name: "scalableTargetDimensionId" in: "path" description: "The identifier of the scalable target dimension to update." required: true type: "string" format: "" - in: "body" name: "updateRequest" description: "A request sent in JSON to update the scalable target dimension." required: true schema: '$ref': "#/definitions/ScalableTargetDimensionUpdate" responses: '200': description: "A JSON object that contains information about the resource." schema: '$ref': "#/definitions/ScalableTargetDimension" '400': description: "Client Error" schema: '$ref': "#/definitions/Error" '403': description: "Client Error: Not authorized" schema: '$ref': "#/definitions/Error" '404': description: "Client Error: ScalableTargetDimension not found" schema: '$ref': "#/definitions/Error" '429': description: "Client Error: Too Many Requests" schema: '$ref': "#/definitions/Error" '500': description: "Server Error" schema: '$ref': "#/definitions/Error" x-tags: - tag: "ScalableTargets" security: - sigv4: [] x-amazon-apigateway-integration: uri: !Ref IntegrationHttpEndpoint passthroughBehavior: "when_no_match" httpMethod: "PATCH" responses: default: statusCode: "500" '200': statusCode: "200" '403': statusCode: "403" '404': statusCode: "404" '429': statusCode: "429" '4\d{2}': statusCode: "400" requestParameters: integration.request.path.scalableTargetDimensionId: "method.request.path.scalableTargetDimensionId" type: "http" securityDefinitions: sigv4: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "awsSigv4" definitions: Error: type: "object" properties: message: type: "string" ScalableTargetDimension: description: "A resource that represents the scaling state for a single dimension \ of a scalable target." type: "object" required: - "actualCapacity" - "desiredCapacity" - "scalableTargetDimensionId" - "scalingStatus" - "version" properties: scalableTargetDimensionId: type: "string" format: "" description: "A unique identifier representing a specific scalable target dimension." version: type: "string" format: "" description: "The version associated with the scalable target dimension." actualCapacity: type: "number" format: "double" description: "The actual capacity of the scalable target dimension." desiredCapacity: type: "number" format: "double" description: "The desired capacity of the scalable target dimension." scalingStatus: type: "string" format: "" description: "The current status of scaling activity." enum: - "Pending" # scaling action has not yet begun - "InProgress" # scaling action is in progress - "Successful" # last scaling action was successful - "Failed" # last scaling action has failed resourceName: type: "string" format: "" description: "Optional user-friendly name for a specific resource." dimensionName: type: "string" format: "" description: "Optional user-friendly name for the scalable dimension associated with the resource." failureReason: type: "string" format: "" description: "Optional failure reason that is provided if a scaling action fails." ScalableTargetDimensionUpdate: description: "An update to be applied to the scalable target dimension." type: "object" required: - "desiredCapacity" properties: desiredCapacity: type: "number" format: "double" description: "The new desired capacity of the target." PreProdStageDescription: DependsOn: - PreProdClientCertificate - Account Type: "AWS::ApiGateway::Stage" Properties: DeploymentId: !Ref PreProdDeployment Description: "Pre-prod stage" RestApiId: !Ref CustomResourceEndpoint StageName: "preprod" ClientCertificateId: !Ref PreProdClientCertificate MethodSettings: - HttpMethod: "*" LoggingLevel: "INFO" ResourcePath: "/*" MetricsEnabled: true DataTraceEnabled: true ProdStageDescription: DependsOn: - ProdClientCertificate - Account Type: "AWS::ApiGateway::Stage" Properties: DeploymentId: !Ref ProdDeployment Description: "Prod stage" RestApiId: !Ref CustomResourceEndpoint StageName: "prod" ClientCertificateId: !Ref ProdClientCertificate MethodSettings: - HttpMethod: "*" LoggingLevel: "ERROR" ResourcePath: "/*" MetricsEnabled: true DataTraceEnabled: true PreProdClientCertificate: Type: "AWS::ApiGateway::ClientCertificate" Properties: Description: "Pre-prod client certificate" ProdClientCertificate: Type: "AWS::ApiGateway::ClientCertificate" Properties: Description: "Prod client certificate" PreProdDeployment: Type: "AWS::ApiGateway::Deployment" Properties: Description: "The pre-prod path through which the Scaling API deployment is accessible." RestApiId: !Ref CustomResourceEndpoint ProdDeployment: Type: "AWS::ApiGateway::Deployment" Properties: Description: "The prod path through which the Scaling API deployment is accessible." RestApiId: !Ref CustomResourceEndpoint LambdaServiceRole: # This service role allows Lambda function to call API Gateway and SNS on your behalf to check the client certificate # and send you emails when it expires in a short time. Type: "AWS::IAM::Role" Properties: RoleName: "CustomResourceNotification-LambdaServiceRole" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "lambda.amazonaws.com" Action: - "sts:AssumeRole" Policies: - PolicyName: "APIGatewaySNSFullAccess" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - "apigateway:GET" - "sns:Publish" - "sns:ListTopics" Resource: "*" CloudWatchRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - "apigateway.amazonaws.com" Action: "sts:AssumeRole" Path: "/" ManagedPolicyArns: - "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" Account: Type: "AWS::ApiGateway::Account" Properties: CloudWatchRoleArn: "Fn::GetAtt": - CloudWatchRole - Arn SNSTopic: Type: 'AWS::SNS::Topic' Properties: TopicName: "CustomResourceCertificateExpiryNotification" Subscription: - Endpoint: !Ref SNSSubscriptionEmail Protocol: "email" LambdaFunction: Type: "AWS::Lambda::Function" Properties: Description: "A Lambda function to send certificate expiry notifications for API Gateway client certificates." Handler: "index.handler" Role: Fn::GetAtt: - "LambdaServiceRole" - "Arn" Code: ZipFile: | import boto3 import sys import datetime def list_topic_arns(client): response = client.list_topics() topic_arns = [topic['TopicArn'] for topic in response['Topics']] while 'NextToken' in response: response = client.list_topics(NextToken=response['NextToken']) topic_arns += [topic_arn for _, topic_arn in response['Topics']] return topic_arns def get_topic_arn_by_name(client, topic_name): topic_arn_list = list_topic_arns(client) for topic_arn in topic_arn_list: if topic_arn.endswith(':'+topic_name): return topic_arn return None def send_email(client, message, topic_arn): if topic_arn is not None: client.publish(TopicArn=topic_arn, Message=message) def handler(event, context): sns_client = boto3.client('sns') apigateway_client = boto3.client('apigateway') rest_apis = apigateway_client.get_rest_apis()['items'] sns_topic_name = "CustomResourceCertificateExpiryNotification" # This topic name should be the same as what is used when creating the sns topic topic_arn = get_topic_arn_by_name(sns_client, sns_topic_name) for rest_api in rest_apis: stages = apigateway_client.get_stages(restApiId=rest_api['id'])['item'] for stage in stages: if 'clientCertificateId' not in stage: print("There is no client certificate created in stage {}".format(stage['stageName'])) else: client_certificate_id = stage['clientCertificateId'] expiration_date = apigateway_client.get_client_certificate(clientCertificateId=client_certificate_id)['expirationDate'] current_date = datetime.datetime.now(datetime.timezone.utc) diff_days = (expiration_date - current_date).days if diff_days in [1, 3, 7]: # Send customer notifications 1, 3, or 7 days before the expiration date message = "Your client certificate {} is going to expire on {}. Please generate a new certificate before the expiration date".format(client_certificate_id, expiration_date.astimezone(datetime.timezone.utc)) send_email(sns_client, message, topic_arn) Runtime: "python3.6" Timeout: "25" DependsOn: - PreProdStageDescription - ProdStageDescription - SNSTopic - LambdaServiceRole ScheduledRule: Type: "AWS::Events::Rule" Properties: Description: "ScheduledRule" ScheduleExpression: "rate(1 day)" State: "ENABLED" Targets: - Arn: Fn::GetAtt: - "LambdaFunction" - "Arn" Id: "TargetFunctionV1" DependsOn: - LambdaFunction PermissionForEventsToInvokeLambda: Type: "AWS::Lambda::Permission" Properties: FunctionName: Ref: "LambdaFunction" Action: "lambda:InvokeFunction" Principal: "events.amazonaws.com" SourceArn: Fn::GetAtt: - "ScheduledRule" - "Arn" S3Bucket: DeletionPolicy: Retain Type: "AWS::S3::Bucket" Properties: {} BucketPolicy: Type: "AWS::S3::BucketPolicy" Properties: Bucket: Ref: S3Bucket PolicyDocument: Version: "2012-10-17" Statement: - Sid: "AWSCloudTrailAclCheck" Effect: "Allow" Principal: Service: "cloudtrail.amazonaws.com" Action: "s3:GetBucketAcl" Resource: !Sub |- arn:aws:s3:::${S3Bucket} - Sid: "AWSCloudTrailWrite" Effect: "Allow" Principal: Service: "cloudtrail.amazonaws.com" Action: "s3:PutObject" Resource: !Sub |- arn:aws:s3:::${S3Bucket}/AWSLogs/${AWS::AccountId}/* Condition: StringEquals: s3:x-amz-acl: "bucket-owner-full-control" CustomResourceScalingTrail: DependsOn: - BucketPolicy Type: "AWS::CloudTrail::Trail" Properties: S3BucketName: Ref: S3Bucket IsLogging: true Parameters: IntegrationHttpEndpoint: Type: String Description: The integration endpoint URL of the target service. MinLength: 62 # http://a/scalableTargetDimensions/{scalableTargetDimensionId} MaxLength: 1600 # limit on length of Resource ID for Register API AllowedPattern: ^(https?:\/\/.*\/)scalableTargetDimensions\/\{scalableTargetDimensionId\}$ SNSSubscriptionEmail: Type: String Description: The email address to receive client certificate expiry notification. Outputs: PreProdResourceIdPrefix: Description: Application Auto Scaling Resource ID prefix for Preprod. Value: !Sub |- https://${CustomResourceEndpoint}.execute-api.${AWS::Region}.amazonaws.com/${PreProdStageDescription}/scalableTargetDimensions/ ProdResourceIdPrefix: Description: Application Auto Scaling Resource ID prefix for Prod. Value: !Sub |- https://${CustomResourceEndpoint}.execute-api.${AWS::Region}.amazonaws.com/${ProdStageDescription}/scalableTargetDimensions/ S3BucketName: Value: !Ref S3Bucket Description: The location of the CloudTrail logs for API-related event history. PreProdClientCertificate: Value: !Ref PreProdClientCertificate Description: API Gateway Client Cert ProdClientCertificate: Value: !Ref ProdClientCertificate Description: API Gateway Client Cert