├── .gitignore ├── src ├── lambda │ ├── function-generate │ │ ├── requirements.txt │ │ ├── app.py │ │ └── template.html │ ├── function-generate-selected │ │ ├── requirements.txt │ │ ├── app.py │ │ └── template.html │ ├── function-fetch-esms │ │ └── app.py │ └── function-selected │ │ └── app.py └── sfn │ ├── state-machine-selected.asl.json │ └── state-machine.asl.json ├── imgs └── architecture.png ├── CODE_OF_CONDUCT.md ├── LICENSE.txt ├── VERSIONS.md ├── CONTRIBUTING.md ├── README.md └── template.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | samconfig.toml 2 | **/.aws-sam/** 3 | -------------------------------------------------------------------------------- /src/lambda/function-generate/requirements.txt: -------------------------------------------------------------------------------- 1 | jinja2 2 | requests 3 | bs4 -------------------------------------------------------------------------------- /src/lambda/function-generate-selected/requirements.txt: -------------------------------------------------------------------------------- 1 | jinja2 2 | requests 3 | bs4 -------------------------------------------------------------------------------- /imgs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-ops-review/HEAD/imgs/architecture.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/lambda/function-fetch-esms/app.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import json, os, copy 5 | import boto3 6 | 7 | 8 | lbd = boto3.client('lambda') 9 | s3 = boto3.client('s3') 10 | 11 | #env var load 12 | S3_BUCKET = os.environ['S3_BUCKET'] 13 | 14 | 15 | 16 | def esms_fetch(): 17 | response = lbd.list_event_source_mappings() 18 | response = response['EventSourceMappings'] 19 | return response 20 | 21 | 22 | def handler(event, context): 23 | 24 | esms = esms_fetch() 25 | 26 | key = event['FolderKey'] 27 | esms_to_s3 = s3.put_object(Bucket=S3_BUCKET, Key=key+'/esms.json', Body=json.dumps(esms, default=str)) 28 | 29 | 30 | return { 31 | "statusCode": 200, 32 | "FolderKey": key 33 | } -------------------------------------------------------------------------------- /src/lambda/function-selected/app.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import boto3 5 | import os 6 | import json 7 | 8 | STATE_MACHINE = os.environ['SFN_SELECTED'] 9 | sfn = boto3.client('stepfunctions') 10 | cfn = boto3.client('cloudformation') 11 | 12 | def handler(event, context): 13 | # Fetch stack name from event: 14 | stack_name = event['resources'] 15 | 16 | #Fetch stack parameters: 17 | functions = cfn.describe_stacks( 18 | StackName=stack_name[0] 19 | ) 20 | functions = functions['Stacks'][0]['Parameters'][0]['ParameterValue'].split(',') 21 | 22 | #Start state machine execution: 23 | sfn_input = json.dumps({"Functions": functions}) 24 | 25 | sfn_selected = sfn.start_execution( 26 | stateMachineArn=STATE_MACHINE, 27 | input=sfn_input 28 | ) -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /VERSIONS.md: -------------------------------------------------------------------------------- 1 | # Serverless Operational Review Tool Versions 2 | 3 | - **Version 1.3.0** 4 | - Added evaluation of deprecated runtimes and runtimes that are scheduled to deprecate, including blocked update and creation dates. 5 | - Functions with deprecated runtime versions allocated to RED Danger Zone. 6 | - Functions with runtime versions scheduled to deprecate allocated to YELLOW Warning Zone. 7 | 8 | - **Version 1.2.1** 9 | - Added runtimes : Python 12, Node.js 20, Java 21, OS-only Runtime Amazon Linux 2023 10 | - Removed runtimes: Java 11, Node.js 16, .NET 6 11 | 12 | - **Version 1.2.0** 13 | - SAM CLI version 1.94 or higher is required to deploy this tool as all backend compute is using Python 3.11! 14 | - Reporting for all Lambda functions agregates all data into single report file. 15 | - Direct passing from JSON to HTML 16 | - Event Source Mappings have its own subsection inside Reviewed function configurations and provide almost all information depending on the type of the event source. 17 | 18 | - **Version 1.1.1** 19 | - Reporting on all or selected Lambda functions in the region the tool is deployed to. 20 | - Generating 1 report per up to 50 Lambda functions due to CLI limitations. 21 | - Backend Lambda functions using Python 3.10. 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Operational Review Tool 2 | 3 | This tool has been created to help evaluate **AWS Serverless services** in bulk and to automatically generate a full review report which aims to assist in customer resource analysis. 4 | 5 | **Tool versions with changelog are present [here](VERSIONS.md).** 6 | 7 | ## Currently supported features 8 | 9 | - **AWS Lambda** 10 | - RED Danger Zone - Risks: 11 | - Multi-AZ checks evaluate if a function is VPC enabled and if the linked subnets follow multi-AZ approach. 12 | - Runtime checks evaluate if a function runtime has been deprecated (ZIP type only). This check separates functions based on runtime and provides details on all affected function versions. 13 | - **AWS Trusted Advisor checks** (the following checks are included as part of the report if the AWS Account has [AWS Trusted Advisor](https://docs.aws.amazon.com/awssupport/latest/user/trusted-advisor.html) enabled): 14 | - Functions with high error rates 15 | - Functions with excessive timeouts 16 | 17 | - YELLOW Warnings Zone: 18 | - Runtime checks that evaluate if a function runtime versions is scheduled to be deprecated in the near future. 19 | 20 | - BLUE Optimization Zone - Recommendations: 21 | - Provides optimization recommendations focused on function memory and benefits of optimizing (duration and cost). 22 | - This information is pulled from AWS Compute Optimizer so there has to be historical usage available. 23 | - Lists functions using x86 architecture type and recommends converting to arm64 (Graviton2). 24 | 25 | - Reviewed Functions - Configurations: 26 | - Provides the list of reviewed functions with their configurations. 27 | - Provides a list of function event source mappings configurations. 28 | 29 | ## Architecture 30 | 31 | ![Architecture](imgs/architecture.png) 32 | 33 | This tool is using an event-driven approach to automatically trigger resource review right after successful deployment. Secondary review can be run using a manual trigger of the AWS Step Functions state machine. 34 | 35 | ## User guide 36 | 37 | ### Step 1 - Deployment 38 | 39 | - Install [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) 40 | 41 | - Clone this repository to your local machine, which can access your AWS account containing the resources you would like to review (you may need to configure [AWS CLI credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html)) 42 | 43 | - Execute `sam build` command from the root folder - this will build and package a deployable bundle. 44 | 45 | - Execute `sam deploy --guided` command and pass the following parameters in the terminal: 46 | 47 | - Stack Name [sam-app]: ops-review 48 | - AWS Region [region]: **Select the region where your resources are located**. 49 | - Parameter IsTrustedAdvisorEnabled [True]: If the target AWS Account has AWS Trusted Advisor **enabled**, set this parameter set to `true` for additional checks. In case the AWS Trusted Advisor is **not enabled** for the target AWS Account, please set this parameter to `false`. 50 | - Parameter Functions [All]: Leave this parameter as defaul `All` to run a review of all functions in the region **OR** pass a **commadelimited list of function names you would like to review** (up to 40 supported). 51 | - The rest of the configuration can be left with defaults. 52 | 53 | - This process will trigger AWS CloudFormation stack deployment within your selected account. ONce the deployment is complete, the review and the report generation process will begin automatically so there is no need to trigger it. 54 | 55 | ### Step 2 - Review reports 56 | 57 | - Open the [AWS Step Functions console](https://console.aws.amazon.com/states/home) and locate the deployed state machine - its name will be provided in the terminal outputs after SAM CLI completes deployment or it can be found within Outputs tab of the deployed CloudFormation stack in CloudFormation console. 58 | - Locate the latest execution withing the **Executions** section and click on it to open it. 59 | - Navigate to the list of events and locate the **last** execution event called **ExecutionSucceeded** or you can click on the last execution stap in the workflow diagram. The body of this event will contain an S3 presigned URL (valid for an hour), that can be used to access the generated reports. 60 | - Copy the presigned URL and paste it into a browser to download the report file. 61 | - ALternatively you can access the report.html file from every run in past 14 days from the deployed S3 bucket (can be located through the CloudFormation Stack resources or Outputs). Every run has its own timestamped folder. 62 | 63 | #### NOTE 64 | 65 | - **Viewing historical reports** 66 | - All reports and related information is saved on the ReportBucket S3 bucket within a time stamped folder (UTC time stamps). 67 | - These files can be accessed even if the presigned URL has expired. 68 | - The report is saved within report.html file within corresponding report folder. 69 | 70 | 71 | ### Triggering a report manually 72 | 73 | - **All functions** - execute the state machine from [Step Functions console](https://console.aws.amazon.com/states/home) with default or empty input. New set of presigned URLs will be created pointing to new set of reports. 74 | 75 | - **Selected functions**: 76 | - Check the Operational Review CloudFormation stack Parameters tab in the console, to see if a list of functions has been provided (if the value for Functions parameter is **All**, you will need to run the deployment again and pass a commadelimited list of function names into to the parameter) 77 | - To re-run the report for specific set of functions, the state machine input should follow the format of: 78 | 79 | ``` 80 | { 81 | "Functions": ["function1", "function2", "function3"] 82 | } 83 | ``` 84 | - **The list provided to the state machine doe not need to match the list provided to the deployment.** 85 | 86 | ## Resource Cleanup 87 | 88 | - Remove all files from the deployed S3 bucket (name can be located in the CloudFormation stack Outputs) 89 | - Delete the deployed CloudFormation stack using `sam delete` command from terminal from the deployment folder or delete the CloudFormation stack from console. 90 | 91 | -------------------------------------------------------------------------------- /src/sfn/state-machine-selected.asl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "This StateMachine fetches information related to Lambda functions for the purpose of AWS provided Operational Review.", 3 | "StartAt": "MapSelectedFunctions", 4 | "States": { 5 | "ErrorCatchSelected": { 6 | "End": true, 7 | "Parameters": { 8 | "Body.$": "$", 9 | "Bucket": "reports-ap-southeast-1-808632258563", 10 | "Key.$": "States.Format('error-selected-{}.json', $$.State.EnteredTime)" 11 | }, 12 | "Resource": "arn:aws:states:::aws-sdk:s3:putObject", 13 | "Type": "Task" 14 | }, 15 | "MapSelectedFunctions": { 16 | "Catch": [ 17 | { 18 | "Comment": "Function not found error.", 19 | "ErrorEquals": [ 20 | "States.TaskFailed" 21 | ], 22 | "Next": "ErrorCatchSelected" 23 | } 24 | ], 25 | "ItemProcessor": { 26 | "ProcessorConfig": { 27 | "Mode": "INLINE" 28 | }, 29 | "StartAt": "GetFunction", 30 | "States": { 31 | "GetFunction": { 32 | "InputPath": "$", 33 | "Next": "SelectedGetFunctionConcurrency", 34 | "Parameters": { 35 | "FunctionName.$": "$" 36 | }, 37 | "Resource": "arn:aws:states:::aws-sdk:lambda:getFunction", 38 | "Type": "Task" 39 | }, 40 | "IsVpcEnabledChoiceSelected": { 41 | "Choices": [ 42 | { 43 | "And": [ 44 | { 45 | "Variable": "$.Configuration.VpcConfig.VpcId", 46 | "IsPresent": true 47 | }, 48 | { 49 | "Not": { 50 | "Variable": "$.Configuration.VpcConfig.VpcId", 51 | "StringMatches": "" 52 | } 53 | } 54 | ], 55 | "Next": "IsRuntimePresentVpcEnabledSelected" 56 | } 57 | ], 58 | "Default": "IsRuntimePresentNoVpcEnabledSelected", 59 | "Type": "Choice" 60 | }, 61 | "IsRuntimePresentNoVpcEnabledSelected": { 62 | "Type": "Choice", 63 | "Choices": [ 64 | { 65 | "Not": { 66 | "Variable": "$.Configuration.Runtime", 67 | "IsPresent": true 68 | }, 69 | "Next": "NoVpcEnabledSelectedAddRuntime" 70 | } 71 | ], 72 | "Default": "NoVpcEnabledSelected" 73 | }, 74 | "NoVpcEnabledSelectedAddRuntime": { 75 | "Type": "Pass", 76 | "End": true, 77 | "Parameters": { 78 | "Architectures.$": "States.ArrayGetItem($.Configuration.Architectures, 0)", 79 | "CodeSize.$": "$.Configuration.CodeSize", 80 | "EphemeralStorage.$": "$.Configuration.EphemeralStorage.Size", 81 | "FunctionArn.$": "$.Configuration.FunctionArn", 82 | "FunctionName.$": "$.Configuration.FunctionName", 83 | "MemorySize.$": "$.Configuration.MemorySize", 84 | "PackageType.$": "$.Configuration.PackageType", 85 | "ProvisionedConcurrencyConfigs.$": "$.Configuration.FunctionProvisionedConcurrency.ProvisionedConcurrencyConfigs", 86 | "ReservedConcurrency.$": "$.Configuration.FunctionReservedConcurrency", 87 | "Role.$": "$.Configuration.Role", 88 | "Runtime": "Container", 89 | "SnapStartOn.$": "$.Configuration.SnapStart.ApplyOn", 90 | "SnapStartOptimizationStatus.$": "$.Configuration.SnapStart.OptimizationStatus", 91 | "Timeout.$": "$.Configuration.Timeout", 92 | "TracingConfig.$": "$.Configuration.TracingConfig.Mode" 93 | } 94 | }, 95 | "IsRuntimePresentVpcEnabledSelected": { 96 | "Type": "Choice", 97 | "Choices": [ 98 | { 99 | "Not": { 100 | "Variable": "$.Configuration.Runtime", 101 | "IsPresent": true 102 | }, 103 | "Next": "VpcEnabledSelectedAddRuntime" 104 | } 105 | ], 106 | "Default": "IsVpcEnabledSelected" 107 | }, 108 | "VpcEnabledSelectedAddRuntime": { 109 | "Type": "Pass", 110 | "End": true, 111 | "Parameters": { 112 | "Architectures.$": "States.ArrayGetItem($.Configuration.Architectures, 0)", 113 | "CodeSize.$": "$.Configuration.CodeSize", 114 | "EphemeralStorage.$": "$.Configuration.EphemeralStorage.Size", 115 | "FunctionArn.$": "$.Configuration.FunctionArn", 116 | "FunctionName.$": "$.Configuration.FunctionName", 117 | "MemorySize.$": "$.Configuration.MemorySize", 118 | "PackageType.$": "$.Configuration.PackageType", 119 | "ProvisionedConcurrencyConfigs.$": "$.Configuration.FunctionProvisionedConcurrency.ProvisionedConcurrencyConfigs", 120 | "ReservedConcurrency.$": "$.Configuration.FunctionReservedConcurrency", 121 | "Role.$": "$.Configuration.Role", 122 | "Runtime": "Container", 123 | "SecurityGroupIds.$": "$.Configuration.VpcConfig.SecurityGroupIds", 124 | "SnapStartOn.$": "$.Configuration.SnapStart.ApplyOn", 125 | "SnapStartOptimizationStatus.$": "$.Configuration.SnapStart.OptimizationStatus", 126 | "SubnetIds.$": "$.Configuration.VpcConfig.SubnetIds", 127 | "Timeout.$": "$.Configuration.Timeout", 128 | "TracingConfig.$": "$.Configuration.TracingConfig.Mode", 129 | "VpcId.$": "$.Configuration.VpcConfig.VpcId" 130 | } 131 | }, 132 | "IsVpcEnabledSelected": { 133 | "End": true, 134 | "Parameters": { 135 | "Architectures.$": "States.ArrayGetItem($.Configuration.Architectures, 0)", 136 | "CodeSize.$": "$.Configuration.CodeSize", 137 | "EphemeralStorage.$": "$.Configuration.EphemeralStorage.Size", 138 | "FunctionArn.$": "$.Configuration.FunctionArn", 139 | "FunctionName.$": "$.Configuration.FunctionName", 140 | "MemorySize.$": "$.Configuration.MemorySize", 141 | "PackageType.$": "$.Configuration.PackageType", 142 | "ProvisionedConcurrencyConfigs.$": "$.Configuration.FunctionProvisionedConcurrency.ProvisionedConcurrencyConfigs", 143 | "ReservedConcurrency.$": "$.Configuration.FunctionReservedConcurrency", 144 | "Role.$": "$.Configuration.Role", 145 | "Runtime.$": "$.Configuration.Runtime", 146 | "SecurityGroupIds.$": "$.Configuration.VpcConfig.SecurityGroupIds", 147 | "SnapStartOn.$": "$.Configuration.SnapStart.ApplyOn", 148 | "SnapStartOptimizationStatus.$": "$.Configuration.SnapStart.OptimizationStatus", 149 | "SubnetIds.$": "$.Configuration.VpcConfig.SubnetIds", 150 | "Timeout.$": "$.Configuration.Timeout", 151 | "TracingConfig.$": "$.Configuration.TracingConfig.Mode", 152 | "VpcId.$": "$.Configuration.VpcConfig.VpcId" 153 | }, 154 | "Type": "Pass" 155 | }, 156 | "ListProvisionedConcurrencyConfigs": { 157 | "Next": "IsVpcEnabledChoiceSelected", 158 | "Parameters": { 159 | "FunctionName.$": "$.Configuration.FunctionName" 160 | }, 161 | "Resource": "arn:aws:states:::aws-sdk:lambda:listProvisionedConcurrencyConfigs", 162 | "ResultPath": "$.Configuration.FunctionProvisionedConcurrency", 163 | "Type": "Task" 164 | }, 165 | "NoVpcEnabledSelected": { 166 | "End": true, 167 | "Parameters": { 168 | "Architectures.$": "States.ArrayGetItem($.Configuration.Architectures, 0)", 169 | "CodeSize.$": "$.Configuration.CodeSize", 170 | "EphemeralStorage.$": "$.Configuration.EphemeralStorage.Size", 171 | "FunctionArn.$": "$.Configuration.FunctionArn", 172 | "FunctionName.$": "$.Configuration.FunctionName", 173 | "MemorySize.$": "$.Configuration.MemorySize", 174 | "PackageType.$": "$.Configuration.PackageType", 175 | "ProvisionedConcurrencyConfigs.$": "$.Configuration.FunctionProvisionedConcurrency.ProvisionedConcurrencyConfigs", 176 | "ReservedConcurrency.$": "$.Configuration.FunctionReservedConcurrency", 177 | "Role.$": "$.Configuration.Role", 178 | "Runtime.$": "$.Configuration.Runtime", 179 | "SnapStartOn.$": "$.Configuration.SnapStart.ApplyOn", 180 | "SnapStartOptimizationStatus.$": "$.Configuration.SnapStart.OptimizationStatus", 181 | "Timeout.$": "$.Configuration.Timeout", 182 | "TracingConfig.$": "$.Configuration.TracingConfig.Mode" 183 | }, 184 | "Type": "Pass" 185 | }, 186 | "SelectedGetFunctionConcurrency": { 187 | "Next": "ListProvisionedConcurrencyConfigs", 188 | "Parameters": { 189 | "FunctionName.$": "$.Configuration.FunctionName" 190 | }, 191 | "Resource": "arn:aws:states:::aws-sdk:lambda:getFunctionConcurrency", 192 | "ResultPath": "$.Configuration.FunctionReservedConcurrency", 193 | "Type": "Task" 194 | } 195 | } 196 | }, 197 | "ItemsPath": "$.Functions", 198 | "Next": "S3PutSelected", 199 | "Type": "Map" 200 | }, 201 | "S3PutSelected": { 202 | "Next": "GetRecommendations", 203 | "Parameters": { 204 | "Body.$": "$", 205 | "Bucket": "${ReportBucketName}", 206 | "Key.$": "States.Format('review-{}/selected-functions.json', $$.State.EnteredTime)" 207 | }, 208 | "Resource": "arn:aws:states:::aws-sdk:s3:putObject", 209 | "Type": "Task", 210 | "ResultSelector": { 211 | "Bucket": "${ReportBucketName}", 212 | "Prefix.$": "States.Format('review-{}', $$.State.EnteredTime)", 213 | "FunctionsObject": "selected-functions.json" 214 | } 215 | }, 216 | "GetRecommendations": { 217 | "Type": "Task", 218 | "Parameters": {}, 219 | "Resource": "arn:aws:states:::aws-sdk:computeoptimizer:getLambdaFunctionRecommendations", 220 | "ResultPath": "$.Recommendations", 221 | "Next": "RecommendationsToS3" 222 | }, 223 | "RecommendationsToS3": { 224 | "Type": "Task", 225 | "Parameters": { 226 | "Body.$": "$", 227 | "Bucket.$": "$.Bucket", 228 | "Key.$": "States.Format('{}/recommendations.json', $.Prefix)" 229 | }, 230 | "Resource": "arn:aws:states:::aws-sdk:s3:putObject", 231 | "Next": "FilterRecommendationsOutput", 232 | "ResultPath": null 233 | }, 234 | "FilterRecommendationsOutput": { 235 | "Type": "Pass", 236 | "Next": "ListEventSourceMappings", 237 | "Parameters": { 238 | "Bucket.$": "$.Bucket", 239 | "FunctionsObject.$": "$.FunctionsObject", 240 | "Prefix.$": "$.Prefix" 241 | } 242 | }, 243 | "ListEventSourceMappings": { 244 | "Type": "Task", 245 | "Next": "EventSourceMappingsToS3", 246 | "Parameters": {}, 247 | "Resource": "arn:aws:states:::aws-sdk:lambda:listEventSourceMappings", 248 | "ResultPath": "$.EventSourceMappings", 249 | "Retry": [ 250 | { 251 | "ErrorEquals": [ 252 | "States.ALL" 253 | ], 254 | "IntervalSeconds": 1, 255 | "MaxAttempts": 5, 256 | "BackoffRate": 1.5 257 | } 258 | ] 259 | }, 260 | "EventSourceMappingsToS3": { 261 | "Type": "Task", 262 | "Next": "RemoveMappingsFromInput", 263 | "Parameters": { 264 | "Body.$": "$", 265 | "Bucket.$": "$.Bucket", 266 | "Key.$": "States.Format('{}/esms.json', $.Prefix)" 267 | }, 268 | "Resource": "arn:aws:states:::aws-sdk:s3:putObject", 269 | "ResultPath": null 270 | }, 271 | "RemoveMappingsFromInput": { 272 | "Type": "Pass", 273 | "Next": "GenerateReport", 274 | "Parameters": { 275 | "Bucket.$": "$.Bucket", 276 | "FunctionsObject.$": "$.FunctionsObject", 277 | "Prefix.$": "$.Prefix" 278 | } 279 | }, 280 | "GenerateReport": { 281 | "Type": "Task", 282 | "End": true, 283 | "Resource": "arn:aws:states:::lambda:invoke", 284 | "Parameters": { 285 | "Payload.$": "$", 286 | "FunctionName": "${GenerateLambda}" 287 | }, 288 | "Retry": [ 289 | { 290 | "ErrorEquals": [ 291 | "Lambda.ServiceException", 292 | "Lambda.AWSLambdaException", 293 | "Lambda.SdkClientException", 294 | "Lambda.TooManyRequestsException" 295 | ], 296 | "IntervalSeconds": 2, 297 | "MaxAttempts": 6, 298 | "BackoffRate": 2 299 | } 300 | ], 301 | "OutputPath": "$.Payload" 302 | } 303 | } 304 | 305 | } -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | # Serverless Ops Review 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | 5 | Transform: AWS::Serverless-2016-10-31 6 | 7 | Globals: 8 | Function: 9 | Runtime: python3.11 10 | 11 | Parameters: 12 | IsTrustedAdvisorEnabled: 13 | AllowedValues: 14 | - true 15 | - false 16 | Type: String 17 | Default: true 18 | 19 | Functions: 20 | Type: CommaDelimitedList 21 | Default: All 22 | Description: If you want to check only specific functions, please insert their names here in the format of function1,function2,function3, etc. If left with default value, the check will run on all functions in the region. 23 | 24 | Conditions: 25 | FunctionsNotSet: !Equals [!Select [0, !Ref Functions], "All"] 26 | FunctionsSet: !Not [!Equals [!Select [0, !Ref Functions], "All"]] 27 | 28 | Mappings: 29 | ActRuntimes: 30 | AllRuntimes: 31 | Runtimes: 'python3.10,python3.11,python3.12,nodejs20.x,nodejs18.x,java21,java17,dotnet7,ruby3.2,provided.al2,provided.al2023,Container' 32 | 33 | Resources: 34 | ############################################### Resources for All functions reporting ############################################### 35 | MainStateMachineAll: 36 | Condition: FunctionsNotSet 37 | Type: AWS::Serverless::StateMachine 38 | Properties: 39 | Type: EXPRESS 40 | DefinitionUri: src/sfn/state-machine.asl.json 41 | Role: !GetAtt MainStateMachineRole.Arn 42 | DefinitionSubstitutions: 43 | ReportBucketName: !Ref ReportBucket 44 | GenerateLambda: !GetAtt GenerateReportFunctionAll.Arn 45 | EsmLambda: !GetAtt FetchESMsFunctionAll.Arn 46 | Logging: 47 | Level: ALL 48 | IncludeExecutionData: true 49 | Destinations: 50 | - CloudWatchLogsLogGroup: 51 | LogGroupArn: !GetAtt SfnLogGroup.Arn 52 | Events: 53 | CfnCompleteTrigger: 54 | Type: EventBridgeRule 55 | Properties: 56 | Pattern: 57 | source: [aws.cloudformation] 58 | detail-type: [CloudFormation Stack Status Change] 59 | resources: [!Ref AWS::StackId] 60 | detail: 61 | stack-id: [!Ref AWS::StackId] 62 | status-details: 63 | status: [CREATE_COMPLETE, UPDATE_COMPLETE] 64 | 65 | GenerateReportFunctionAll: 66 | Condition: FunctionsNotSet 67 | Type: AWS::Serverless::Function 68 | Properties: 69 | MemorySize: 256 70 | Architectures: 71 | - arm64 72 | Handler: app.handler 73 | Timeout: 10 74 | CodeUri: src/lambda/function-generate 75 | Environment: 76 | Variables: 77 | S3_BUCKET: !Ref ReportBucket 78 | REGION: !Ref AWS::Region 79 | ACCOUNT: !Ref AWS::AccountId 80 | ACT_RUNTIMES: !FindInMap [ ActRuntimes, AllRuntimes, Runtimes ] 81 | STACK_NAME: !Ref AWS::StackName 82 | TA_ENABLED: !Ref IsTrustedAdvisorEnabled 83 | Policies: 84 | - Version: '2012-10-17' 85 | Statement: 86 | - Effect: Allow 87 | Action: 88 | - 's3:GetObject' 89 | - 's3:PutObject' 90 | Resource: !Sub ${ReportBucket.Arn}/* 91 | - Effect: Allow 92 | Action: 93 | - 's3:ListBucket' 94 | Resource: !Sub ${ReportBucket.Arn} 95 | - Effect: Allow 96 | Action: 97 | - 'ec2:DescribeSubnets' 98 | Resource: '*' 99 | - Effect: Allow 100 | Action: 101 | - 'support:DescribeTrustedAdvisorCheckResult' 102 | Resource: '*' 103 | FetchESMsFunctionAll: 104 | Condition: FunctionsNotSet 105 | Type: AWS::Serverless::Function 106 | Properties: 107 | MemorySize: 256 108 | Architectures: 109 | - arm64 110 | Handler: app.handler 111 | Timeout: 10 112 | CodeUri: src/lambda/function-fetch-esms 113 | Environment: 114 | Variables: 115 | S3_BUCKET: !Ref ReportBucket 116 | Policies: 117 | - Version: '2012-10-17' 118 | Statement: 119 | - Effect: Allow 120 | Action: 121 | - 's3:GetObject' 122 | - 's3:PutObject' 123 | Resource: !Sub ${ReportBucket.Arn}/* 124 | - Effect: Allow 125 | Action: 126 | - 'lambda:ListEventSourceMappings' 127 | Resource: '*' 128 | 129 | 130 | GenerateReportFunctionAllLogGroup: 131 | Condition: FunctionsNotSet 132 | Type: AWS::Logs::LogGroup 133 | Properties: 134 | LogGroupName: !Sub /aws/lambda/${GenerateReportFunctionAll} 135 | RetentionInDays: 14 136 | 137 | FetchESMsFunctionAllLogGroup: 138 | Condition: FunctionsNotSet 139 | Type: AWS::Logs::LogGroup 140 | Properties: 141 | LogGroupName: !Sub /aws/lambda/${FetchESMsFunctionAll} 142 | RetentionInDays: 14 143 | 144 | 145 | 146 | 147 | 148 | ############################################################################################################################################# 149 | 150 | MainStateMachineSelected: 151 | Condition: FunctionsSet 152 | Type: AWS::Serverless::StateMachine 153 | Properties: 154 | Type: EXPRESS 155 | DefinitionUri: src/sfn/state-machine-selected.asl.json 156 | Role: !GetAtt MainStateMachineRole.Arn 157 | DefinitionSubstitutions: 158 | ReportBucketName: !Ref ReportBucket 159 | GenerateLambda: !GetAtt GenerateReportFunctionSelected.Arn 160 | Logging: 161 | Level: ALL 162 | IncludeExecutionData: true 163 | Destinations: 164 | - CloudWatchLogsLogGroup: 165 | LogGroupArn: !GetAtt SfnLogGroup.Arn 166 | 167 | FunctionSelected: 168 | Condition: FunctionsSet 169 | Type: AWS::Serverless::Function 170 | Properties: 171 | Architectures: 172 | - arm64 173 | Handler: app.handler 174 | CodeUri: src/lambda/function-selected 175 | Environment: 176 | Variables: 177 | SFN_SELECTED: !Ref MainStateMachineSelected 178 | Policies: 179 | - Version: '2012-10-17' 180 | Statement: 181 | - Effect: Allow 182 | Action: 183 | - 'states:StartExecution' 184 | Resource: !Ref MainStateMachineSelected 185 | - Version: '2012-10-17' 186 | Statement: 187 | - Effect: Allow 188 | Action: 189 | - 'cloudformation:DescribeStacks' 190 | Resource: !Ref AWS::StackId 191 | Events: 192 | CfnCompleteTrigger: 193 | Type: EventBridgeRule 194 | Properties: 195 | Pattern: 196 | source: [aws.cloudformation] 197 | detail-type: [CloudFormation Stack Status Change] 198 | resources: [!Ref AWS::StackId] 199 | detail: 200 | stack-id: [!Ref AWS::StackId] 201 | status-details: 202 | status: [CREATE_COMPLETE, UPDATE_COMPLETE] 203 | 204 | FunctionSelectedLogGroup: 205 | Condition: FunctionsSet 206 | Type: AWS::Logs::LogGroup 207 | Properties: 208 | LogGroupName: !Sub /aws/lambda/${FunctionSelected} 209 | RetentionInDays: 14 210 | 211 | GenerateReportFunctionSelected: 212 | Condition: FunctionsSet 213 | Type: AWS::Serverless::Function 214 | Properties: 215 | MemorySize: 256 216 | Architectures: 217 | - arm64 218 | Handler: app.handler 219 | Timeout: 10 220 | CodeUri: src/lambda/function-generate-selected 221 | Environment: 222 | Variables: 223 | S3_BUCKET: !Ref ReportBucket 224 | REGION: !Ref AWS::Region 225 | ACCOUNT: !Ref AWS::AccountId 226 | ACT_RUNTIMES: !FindInMap [ ActRuntimes, AllRuntimes, Runtimes ] 227 | STACK_NAME: !Ref AWS::StackName 228 | TA_ENABLED: !Ref IsTrustedAdvisorEnabled 229 | Policies: 230 | - Version: '2012-10-17' 231 | Statement: 232 | - Effect: Allow 233 | Action: 234 | - 's3:GetObject' 235 | - 's3:PutObject' 236 | Resource: !Sub ${ReportBucket.Arn}/* 237 | - Effect: Allow 238 | Action: 239 | - 's3:ListBucket' 240 | Resource: !Sub ${ReportBucket.Arn} 241 | - Effect: Allow 242 | Action: 243 | - 'ec2:DescribeSubnets' 244 | Resource: '*' 245 | - Effect: Allow 246 | Action: 247 | - 'support:DescribeTrustedAdvisorCheckResult' 248 | Resource: '*' 249 | 250 | GenerateReportFunctionSelectedLogGroup: 251 | Condition: FunctionsSet 252 | Type: AWS::Logs::LogGroup 253 | Properties: 254 | LogGroupName: !Sub /aws/lambda/${GenerateReportFunctionSelected} 255 | RetentionInDays: 14 256 | ############################################################################################################################################# 257 | 258 | 259 | 260 | ############################################### Shared Resources ############################################### 261 | 262 | 263 | 264 | SfnLogGroup: 265 | Type: AWS::Logs::LogGroup 266 | Properties: 267 | LogGroupName: /aws/vendedlogs/states/Ops-Review-Sfn 268 | RetentionInDays: 14 269 | 270 | ReportBucket: 271 | Type: AWS::S3::Bucket 272 | Properties: 273 | BucketName: !Sub reports-${AWS::Region}-${AWS::AccountId} 274 | LifecycleConfiguration: 275 | Rules: 276 | - Id: DeleteOldObjects 277 | Status: Enabled 278 | ExpirationInDays: 30 279 | 280 | MainStateMachineRole: 281 | Type: 'AWS::IAM::Role' 282 | Properties: 283 | AssumeRolePolicyDocument: 284 | Version: "2012-10-17" 285 | Statement: 286 | - Effect: Allow 287 | Principal: 288 | Service: 289 | - states.amazonaws.com 290 | Action: 291 | - 'sts:AssumeRole' 292 | Path: / 293 | Policies: 294 | - PolicyName: CloudWatchLogsDeliveryFullAccessPolicy 295 | PolicyDocument: 296 | Version: "2012-10-17" 297 | Statement: 298 | - Effect: Allow 299 | Action: 300 | - 'logs:CreateLogDelivery' 301 | - 'logs:GetLogDelivery' 302 | - 'logs:UpdateLogDelivery' 303 | - 'logs:DeleteLogDelivery' 304 | - 'logs:ListLogDeliveries' 305 | - 'logs:PutResourcePolicy' 306 | - 'logs:DescribeResourcePolicies' 307 | - 'logs:DescribeLogGroups' 308 | - 'logs:CreateLogGroup' 309 | - 'logs:CreateLogStream' 310 | Resource: '*' 311 | - PolicyName: S3Put 312 | PolicyDocument: 313 | Version: "2012-10-17" 314 | Statement: 315 | - Effect: Allow 316 | Action: 317 | - 's3:PutObject' 318 | Resource: !Sub arn:aws:s3:::${ReportBucket}/* 319 | - PolicyName: LambdaRead 320 | PolicyDocument: 321 | Version: "2012-10-17" 322 | Statement: 323 | - 324 | Effect: Allow 325 | Action: 326 | - 'lambda:ListFunctions' 327 | - 'lambda:GetFunction' 328 | - 'lambda:GetFunctionConcurrency' 329 | - 'lambda:ListProvisionedConcurrencyConfigs' 330 | - 'lambda:ListEventSourceMappings' 331 | Resource: '*' 332 | - PolicyName: InvokeFunctions 333 | PolicyDocument: 334 | Version: "2012-10-17" 335 | Statement: 336 | - 337 | Effect: Allow 338 | Action: 339 | - 'lambda:InvokeFunction' 340 | Resource: '*' 341 | - PolicyName: ComputeOptimizerReadLambda 342 | PolicyDocument: 343 | Version: "2012-10-17" 344 | Statement: 345 | - 346 | Effect: Allow 347 | Action: 348 | - 'compute-optimizer:GetLambdaFunctionRecommendations' 349 | Resource: '*' 350 | - PolicyName: ServiceQuotas 351 | PolicyDocument: 352 | Version: "2012-10-17" 353 | Statement: 354 | - 355 | Effect: Allow 356 | Action: 357 | - 'servicequotas:ListServiceQuotas' 358 | - 'servicequotas:GetServiceQuotas' 359 | Resource: '*' 360 | 361 | 362 | ############################################################################################################################################# 363 | 364 | Outputs: 365 | S3Bucket: 366 | Value: !Ref ReportBucket 367 | Description: S3 bucket that contains output files from processing of the Operational Review Tool. Each execution of the tool stores information inside of a timestamped prefix. 368 | 369 | StateMachineAllFunctions: 370 | Value: !GetAtt MainStateMachineAll.Name 371 | Description: Step Functions State Machine executing the processing of the review either on selected functions or on all functions in the region. 372 | Condition: FunctionsNotSet 373 | 374 | StateMachineSelectedFunctions: 375 | Value: !GetAtt MainStateMachineSelected.Name 376 | Description: Step Functions State Machine executing the processing of the review either on selected functions or on all functions in the region. 377 | Condition: FunctionsSet -------------------------------------------------------------------------------- /src/sfn/state-machine.asl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "This StateMachine fetches information related to Lambda functions for the purpose of AWS provided Operational Review.", 3 | "StartAt": "GenerateFolder", 4 | "States": { 5 | "GenerateFolder": { 6 | "Type": "Pass", 7 | "Next": "ListFunctions", 8 | "Parameters": { 9 | "FolderKey.$": "States.Format('review-{}', $$.State.EnteredTime)" 10 | } 11 | }, 12 | "ListFunctions": { 13 | "Next": "IsNextMarkerPresent", 14 | "Parameters": { 15 | "FunctionVersion": "ALL" 16 | }, 17 | "Resource": "arn:aws:states:::aws-sdk:lambda:listFunctions", 18 | "Type": "Task", 19 | "ResultPath": "$.SetOfFunctions", 20 | "Retry": [ 21 | { 22 | "ErrorEquals": [ 23 | "States.ALL" 24 | ], 25 | "BackoffRate": 2, 26 | "IntervalSeconds": 1, 27 | "MaxAttempts": 3 28 | } 29 | ] 30 | }, 31 | "IsNextMarkerPresent": { 32 | "Type": "Choice", 33 | "Choices": [ 34 | { 35 | "Not": { 36 | "Variable": "$.SetOfFunctions.NextMarker", 37 | "IsPresent": true 38 | }, 39 | "Next": "MarkerNotPresent" 40 | } 41 | ], 42 | "Default": "MarkerPresent" 43 | }, 44 | "MarkerPresent": { 45 | "Type": "Pass", 46 | "Parameters": { 47 | "FolderKey.$": "$.FolderKey", 48 | "NextMarker.$": "$.SetOfFunctions.NextMarker", 49 | "Functions.$": "$.SetOfFunctions.Functions" 50 | }, 51 | "Next": "MapAllFunctions" 52 | }, 53 | "MarkerNotPresent": { 54 | "Type": "Pass", 55 | "Parameters": { 56 | "FolderKey.$": "$.FolderKey", 57 | "NextMarker": "None", 58 | "Functions.$": "$.SetOfFunctions.Functions" 59 | }, 60 | "Next": "MapAllFunctions" 61 | }, 62 | "MapAllFunctions": { 63 | "ItemProcessor": { 64 | "ProcessorConfig": { 65 | "Mode": "INLINE" 66 | }, 67 | "StartAt": "AllFunctionsListProvisionedConcurrency", 68 | "States": { 69 | "AllFunctionsGetFunctionConcurrency": { 70 | "Next": "IsVpcEnabledChoice", 71 | "Parameters": { 72 | "FunctionName.$": "$.FunctionName" 73 | }, 74 | "Resource": "arn:aws:states:::aws-sdk:lambda:getFunctionConcurrency", 75 | "ResultPath": "$.FunctionReservedConcurrency", 76 | "Type": "Task" 77 | }, 78 | "AllFunctionsListProvisionedConcurrency": { 79 | "Next": "AllFunctionsGetFunctionConcurrency", 80 | "Parameters": { 81 | "FunctionName.$": "$.FunctionName" 82 | }, 83 | "Resource": "arn:aws:states:::aws-sdk:lambda:listProvisionedConcurrencyConfigs", 84 | "ResultPath": "$.FunctionProvisionedConcurrency", 85 | "Type": "Task" 86 | }, 87 | "IsVpcEnabled": { 88 | "End": true, 89 | "Parameters": { 90 | "Architectures.$": "States.ArrayGetItem($.Architectures, 0)", 91 | "CodeSize.$": "$.CodeSize", 92 | "EphemeralStorage.$": "$.EphemeralStorage.Size", 93 | "FunctionArn.$": "$.FunctionArn", 94 | "FunctionName.$": "$.FunctionName", 95 | "MemorySize.$": "$.MemorySize", 96 | "PackageType.$": "$.PackageType", 97 | "ProvisionedConcurrencyConfigs.$": "$.FunctionProvisionedConcurrency.ProvisionedConcurrencyConfigs", 98 | "ReservedConcurrency.$": "$.FunctionReservedConcurrency", 99 | "Role.$": "$.Role", 100 | "Runtime.$": "$.Runtime", 101 | "SecurityGroupIds.$": "$.VpcConfig.SecurityGroupIds", 102 | "SnapStartOn.$": "$.SnapStart.ApplyOn", 103 | "SnapStartOptimizationStatus.$": "$.SnapStart.OptimizationStatus", 104 | "SubnetIds.$": "$.VpcConfig.SubnetIds", 105 | "Timeout.$": "$.Timeout", 106 | "TracingConfig.$": "$.TracingConfig.Mode", 107 | "VpcId.$": "$.VpcConfig.VpcId" 108 | }, 109 | "Type": "Pass" 110 | }, 111 | "IsVpcEnabledChoice": { 112 | "Choices": [ 113 | { 114 | "And": [ 115 | { 116 | "Variable": "$.VpcConfig.VpcId", 117 | "IsPresent": true 118 | }, 119 | { 120 | "Not": { 121 | "Variable": "$.VpcConfig.VpcId", 122 | "StringMatches": "" 123 | } 124 | } 125 | ], 126 | "Next": "IsRuntimePresentVpcEnabled" 127 | } 128 | ], 129 | "Default": "IsRuntimePresentNoVpcEnabled", 130 | "Type": "Choice" 131 | }, 132 | "IsRuntimePresentNoVpcEnabled": { 133 | "Type": "Choice", 134 | "Choices": [ 135 | { 136 | "Not": { 137 | "Variable": "$.Runtime", 138 | "IsPresent": true 139 | }, 140 | "Next": "NoVpcEnaledAddRuntime" 141 | } 142 | ], 143 | "Default": "NoVpcEnabled" 144 | }, 145 | "NoVpcEnaledAddRuntime": { 146 | "Type": "Pass", 147 | "End": true, 148 | "Parameters": { 149 | "Architectures.$": "States.ArrayGetItem($.Architectures, 0)", 150 | "CodeSize.$": "$.CodeSize", 151 | "EphemeralStorage.$": "$.EphemeralStorage.Size", 152 | "FunctionArn.$": "$.FunctionArn", 153 | "FunctionName.$": "$.FunctionName", 154 | "MemorySize.$": "$.MemorySize", 155 | "PackageType.$": "$.PackageType", 156 | "ProvisionedConcurrencyConfigs.$": "$.FunctionProvisionedConcurrency.ProvisionedConcurrencyConfigs", 157 | "ReservedConcurrency.$": "$.FunctionReservedConcurrency", 158 | "Role.$": "$.Role", 159 | "Runtime": "Container", 160 | "SnapStartOn.$": "$.SnapStart.ApplyOn", 161 | "SnapStartOptimizationStatus.$": "$.SnapStart.OptimizationStatus", 162 | "Timeout.$": "$.Timeout", 163 | "TracingConfig.$": "$.TracingConfig.Mode" 164 | } 165 | }, 166 | "IsRuntimePresentVpcEnabled": { 167 | "Type": "Choice", 168 | "Choices": [ 169 | { 170 | "Not": { 171 | "Variable": "$.Runtime", 172 | "IsPresent": true 173 | }, 174 | "Next": "IsVpcEnaledAddRuntime" 175 | } 176 | ], 177 | "Default": "IsVpcEnabled" 178 | }, 179 | "IsVpcEnaledAddRuntime": { 180 | "Type": "Pass", 181 | "End": true, 182 | "Parameters": { 183 | "Architectures.$": "States.ArrayGetItem($.Architectures, 0)", 184 | "CodeSize.$": "$.CodeSize", 185 | "EphemeralStorage.$": "$.EphemeralStorage.Size", 186 | "FunctionArn.$": "$.FunctionArn", 187 | "FunctionName.$": "$.FunctionName", 188 | "MemorySize.$": "$.MemorySize", 189 | "PackageType.$": "$.PackageType", 190 | "ProvisionedConcurrencyConfigs.$": "$.FunctionProvisionedConcurrency.ProvisionedConcurrencyConfigs", 191 | "ReservedConcurrency.$": "$.FunctionReservedConcurrency", 192 | "Role.$": "$.Role", 193 | "Runtime": "Container", 194 | "SecurityGroupIds.$": "$.VpcConfig.SecurityGroupIds", 195 | "SnapStartOn.$": "$.SnapStart.ApplyOn", 196 | "SnapStartOptimizationStatus.$": "$.SnapStart.OptimizationStatus", 197 | "SubnetIds.$": "$.VpcConfig.SubnetIds", 198 | "Timeout.$": "$.Timeout", 199 | "TracingConfig.$": "$.TracingConfig.Mode", 200 | "VpcId.$": "$.VpcConfig.VpcId" 201 | } 202 | }, 203 | "NoVpcEnabled": { 204 | "End": true, 205 | "Parameters": { 206 | "Architectures.$": "States.ArrayGetItem($.Architectures, 0)", 207 | "CodeSize.$": "$.CodeSize", 208 | "EphemeralStorage.$": "$.EphemeralStorage.Size", 209 | "FunctionArn.$": "$.FunctionArn", 210 | "FunctionName.$": "$.FunctionName", 211 | "MemorySize.$": "$.MemorySize", 212 | "PackageType.$": "$.PackageType", 213 | "ProvisionedConcurrencyConfigs.$": "$.FunctionProvisionedConcurrency.ProvisionedConcurrencyConfigs", 214 | "ReservedConcurrency.$": "$.FunctionReservedConcurrency", 215 | "Role.$": "$.Role", 216 | "Runtime.$": "$.Runtime", 217 | "SnapStartOn.$": "$.SnapStart.ApplyOn", 218 | "SnapStartOptimizationStatus.$": "$.SnapStart.OptimizationStatus", 219 | "Timeout.$": "$.Timeout", 220 | "TracingConfig.$": "$.TracingConfig.Mode" 221 | }, 222 | "Type": "Pass" 223 | } 224 | } 225 | }, 226 | "ItemsPath": "$.Functions", 227 | "Type": "Map", 228 | "Next": "RemoveDuplicates", 229 | "Retry": [ 230 | { 231 | "ErrorEquals": [ 232 | "States.ALL" 233 | ], 234 | "BackoffRate": 2, 235 | "IntervalSeconds": 0, 236 | "MaxAttempts": 3 237 | } 238 | ], 239 | "ResultPath": "$.ProcessedFunctions" 240 | }, 241 | "RemoveDuplicates": { 242 | "Type": "Pass", 243 | "Next": "S3PutAllFunctions", 244 | "Parameters": { 245 | "FolderKey.$": "$.FolderKey", 246 | "NextMarker.$": "$.NextMarker", 247 | "Functions.$": "$.ProcessedFunctions" 248 | } 249 | }, 250 | "ListFunctionsCntd": { 251 | "Next": "IsNextMarkerPresent", 252 | "Parameters": { 253 | "FunctionVersion": "ALL", 254 | "Marker.$": "$.NextMarker" 255 | }, 256 | "Resource": "arn:aws:states:::aws-sdk:lambda:listFunctions", 257 | "Type": "Task", 258 | "ResultPath": "$.SetOfFunctions", 259 | "Retry": [ 260 | { 261 | "ErrorEquals": [ 262 | "States.ALL" 263 | ], 264 | "BackoffRate": 2, 265 | "IntervalSeconds": 2, 266 | "MaxAttempts": 4 267 | } 268 | ] 269 | }, 270 | "S3PutAllFunctions": { 271 | "Next": "FilterParams", 272 | "Parameters": { 273 | "Body.$": "$.Functions", 274 | "Bucket": "${ReportBucketName}", 275 | "Key.$": "States.Format('{}/functions/{}.json', $.FolderKey, States.UUID())" 276 | }, 277 | "Resource": "arn:aws:states:::aws-sdk:s3:putObject", 278 | "Type": "Task", 279 | "ResultPath": "$.Output", 280 | "Retry": [ 281 | { 282 | "ErrorEquals": [ 283 | "States.ALL" 284 | ], 285 | "BackoffRate": 2, 286 | "IntervalSeconds": 1, 287 | "MaxAttempts": 2 288 | } 289 | ] 290 | }, 291 | "FilterParams": { 292 | "Type": "Pass", 293 | "Next": "RerunCheck", 294 | "Parameters": { 295 | "Bucket": "${ReportBucketName}", 296 | "FolderKey.$": "$.FolderKey", 297 | "NextMarker.$": "$.NextMarker" 298 | } 299 | }, 300 | "RerunCheck": { 301 | "Type": "Choice", 302 | "Choices": [ 303 | { 304 | "Not": { 305 | "Variable": "$.NextMarker", 306 | "StringEquals": "None" 307 | }, 308 | "Next": "Wait" 309 | } 310 | ], 311 | "Default": "GetRecommendations" 312 | }, 313 | "Wait": { 314 | "Type": "Wait", 315 | "Seconds": 3, 316 | "Next": "ListFunctionsCntd" 317 | }, 318 | "GetRecommendations": { 319 | "Type": "Task", 320 | "Parameters": {}, 321 | "Resource": "arn:aws:states:::aws-sdk:computeoptimizer:getLambdaFunctionRecommendations", 322 | "ResultPath": "$.Recommendations", 323 | "Next": "RecommendationsToS3", 324 | "Retry": [ 325 | { 326 | "ErrorEquals": [ 327 | "States.TaskFailed" 328 | ], 329 | "BackoffRate": 2, 330 | "IntervalSeconds": 1, 331 | "MaxAttempts": 2 332 | } 333 | ] 334 | }, 335 | "RecommendationsToS3": { 336 | "Type": "Task", 337 | "Parameters": { 338 | "Body.$": "$.Recommendations.LambdaFunctionRecommendations", 339 | "Bucket": "${ReportBucketName}", 340 | "Key.$": "States.Format('{}/recommendations.json', $.FolderKey)" 341 | }, 342 | "Resource": "arn:aws:states:::aws-sdk:s3:putObject", 343 | "Next": "FilterRecommendationsOutput", 344 | "ResultPath": null, 345 | "Retry": [ 346 | { 347 | "ErrorEquals": [ 348 | "States.ALL" 349 | ], 350 | "BackoffRate": 2, 351 | "IntervalSeconds": 1, 352 | "MaxAttempts": 2 353 | } 354 | ] 355 | }, 356 | "FilterRecommendationsOutput": { 357 | "Type": "Pass", 358 | "Next": "ListESMsAndSaveToS3", 359 | "Parameters": { 360 | "FolderKey.$": "$.FolderKey" 361 | } 362 | }, 363 | "ListESMsAndSaveToS3": { 364 | "Type": "Task", 365 | "Resource": "arn:aws:states:::lambda:invoke", 366 | "OutputPath": "$.Payload", 367 | "Parameters": { 368 | "Payload.$": "$", 369 | "FunctionName": "${EsmLambda}" 370 | }, 371 | "Retry": [ 372 | { 373 | "ErrorEquals": [ 374 | "Lambda.ServiceException", 375 | "Lambda.AWSLambdaException", 376 | "Lambda.SdkClientException", 377 | "Lambda.TooManyRequestsException" 378 | ], 379 | "IntervalSeconds": 2, 380 | "MaxAttempts": 6, 381 | "BackoffRate": 2 382 | } 383 | ], 384 | "Next": "GenerateReport" 385 | }, 386 | "GenerateReport": { 387 | "Type": "Task", 388 | "Resource": "arn:aws:states:::lambda:invoke", 389 | "Parameters": { 390 | "Payload.$": "$", 391 | "FunctionName": "${GenerateLambda}" 392 | }, 393 | "Retry": [ 394 | { 395 | "ErrorEquals": [ 396 | "Lambda.ServiceException", 397 | "Lambda.AWSLambdaException", 398 | "Lambda.SdkClientException", 399 | "Lambda.TooManyRequestsException" 400 | ], 401 | "IntervalSeconds": 2, 402 | "MaxAttempts": 6, 403 | "BackoffRate": 2 404 | } 405 | ], 406 | "OutputPath": "$.Payload", 407 | "End": true 408 | } 409 | } 410 | } -------------------------------------------------------------------------------- /src/lambda/function-generate/app.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import json, os, copy 5 | import boto3, requests 6 | from jinja2 import Template 7 | from bs4 import BeautifulSoup 8 | 9 | s3 = boto3.client('s3') 10 | s3_r = boto3.resource('s3') 11 | ec2 = boto3.client('ec2') 12 | ssm = boto3.client('ssm') 13 | supp = boto3.client('support') 14 | 15 | #env var load 16 | S3_BUCKET = os.environ['S3_BUCKET'] 17 | AWS_REGION = os.environ['REGION'] 18 | AWS_ACCOUNT = os.environ['ACCOUNT'] 19 | ACT_RUNTIMES = os.environ['ACT_RUNTIMES'] 20 | STACK_NAME = os.environ['STACK_NAME'] 21 | TA_ENABLED = os.environ['TA_ENABLED'] 22 | DOC_URL = "https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html" 23 | 24 | 25 | ################################################################################################################################### 26 | # Runtime evaluation code functionality 27 | ################################################################################################################################### 28 | def check_dep_runtime(fns,dep_runs): 29 | # Create a dictionary to map Identifiers to runtime details 30 | runtime_map = {item['Identifier']: { 31 | 'Deprecation date': item['Deprecation date'], 32 | 'Block function create': item['Block function create'], 33 | 'Block function update': item['Block function update'] 34 | } for item in dep_runs['Deprecated Runtimes']} 35 | 36 | # Iterate through the function data and append the runtime details 37 | for function in fns: 38 | runtime = function['Runtime'] 39 | for identifier, details in runtime_map.items(): 40 | if runtime == identifier: 41 | function['Deprecation date'] = details['Deprecation date'] 42 | function['Block function create'] = details['Block function create'] 43 | function['Block function update'] = details['Block function update'] 44 | break 45 | 46 | return fns 47 | 48 | def check_sup_runtime(fns,sup_runs): 49 | # Create a dictionary to map Identifiers to runtime details 50 | print(sup_runs) 51 | runtime_map = {item['Identifier']: { 52 | 'Deprecation date': item['Deprecation date'], 53 | 'Block function create': item['Block function create'], 54 | 'Block function update': item['Block function update'] 55 | } for item in sup_runs['Supported Runtimes']} 56 | 57 | # Iterate through the function data and append the runtime details 58 | for function in fns: 59 | runtime = function['Runtime'] 60 | for identifier, details in runtime_map.items(): 61 | if runtime == identifier: 62 | function['Deprecation date'] = details['Deprecation date'] 63 | function['Block function create'] = details['Block function create'] 64 | function['Block function update'] = details['Block function update'] 65 | break 66 | 67 | return fns 68 | 69 | 70 | ################################################################################################################################### 71 | 72 | ################################################################################################################################### 73 | #Deprecated runtime fetch: 74 | ################################################################################################################################### 75 | def fetch_deprecated_runtimes(dep_run_soup): 76 | """ 77 | Fetch the list of deprecated runtimes from the AWS Lambda docs 78 | """ 79 | # Find the Deprecated Runtime table 80 | element = dep_run_soup.find(id="runtimes-deprecated") 81 | table = element.find_next("table") 82 | 83 | # Extract header row 84 | headers = [header.text for header in table.find('thead').find_all('tr')] 85 | headers = headers[1] 86 | headers = headers.split('\n') 87 | headers = headers[1:-1] 88 | 89 | rows = [] 90 | 91 | for row in table.find_all('tr')[1:]: 92 | cells = row.find_all('td') 93 | data = [cell.text for cell in cells] 94 | data = [line.strip() for line in data] 95 | rows.append(dict(zip(headers, data))) 96 | rows = rows[1:] 97 | 98 | 99 | return rows 100 | ################################################################################################################################### 101 | 102 | ################################################################################################################################### 103 | #Supported runtime fetch: 104 | ################################################################################################################################### 105 | 106 | def fetch_supported_runtimes(sup_run_soup): 107 | """ 108 | Fetch the list of supported runtimes from the AWS Lambda docs 109 | """ 110 | # Find the Supported Runtime table 111 | element = sup_run_soup.find(id="runtimes-supported") 112 | table = element.find_next("table") 113 | 114 | # Extract header row 115 | headers = [header.text for header in table.find('thead').find_all('tr')] 116 | headers = headers[1] 117 | headers = headers.split('\n') 118 | headers = headers[1:-1] 119 | 120 | rows = [] 121 | 122 | for row in table.find_all('tr')[1:]: 123 | cells = row.find_all('td') 124 | data = [cell.text for cell in cells] 125 | data = [line.strip() for line in data] 126 | rows.append(dict(zip(headers, data))) 127 | rows = rows[1:] 128 | 129 | 130 | return rows 131 | 132 | ################################################################################################################################### 133 | 134 | ################################################################################################################################### 135 | #TA - High Error Rates 136 | ################################################################################################################################### 137 | def check_ta_high_errors(fns): 138 | ta_he = supp.describe_trusted_advisor_check_result(checkId='L4dfs2Q3C2') 139 | ta_he_flagged = ta_he['result']['flaggedResources'] 140 | ta_he_flagged_list = [] 141 | ta_he_flagged_dict = dict 142 | checked_fns = fns 143 | 144 | ### ADD cross check for functions 145 | 146 | #data cleanup 147 | for f in ta_he_flagged: 148 | f_arn = f['metadata'][2].removesuffix(f['metadata'][2].split(':')[-1]) 149 | f_arn = f_arn.rstrip(f_arn[-1]) 150 | 151 | for fn in checked_fns: 152 | if f_arn == fn['FunctionArn']: 153 | ta_he_flagged_dict = { 154 | 'Status': f['metadata'][0], 155 | 'Region': f['metadata'][1], 156 | 'FunctionArn': f['metadata'][2], 157 | 'MaxDailyErrorRatePerc': f['metadata'][3], 158 | 'DateOfMaxErrorRate': f['metadata'][4], 159 | 'AverageDailyErrorRatePerc': f['metadata'][5] 160 | } 161 | ta_he_flagged_list.append(ta_he_flagged_dict) 162 | break 163 | 164 | return ta_he_flagged_list 165 | 166 | ################################################################################################################################### 167 | 168 | ################################################################################################################################### 169 | #TA - Excessive Timeouts 170 | ################################################################################################################################### 171 | def check_ta_excessive_timeout(fns): 172 | ta_et = supp.describe_trusted_advisor_check_result(checkId='L4dfs2Q3C3') 173 | ta_et_flagged = ta_et['result']['flaggedResources'] 174 | ta_et_flagged_list = [] 175 | ta_et_flagged_dict = dict 176 | checked_fns = fns 177 | 178 | ### ADD cross check for functions 179 | 180 | #data cleanup 181 | for f in ta_et_flagged: 182 | f_arn = f['metadata'][2].removesuffix(f['metadata'][2].split(':')[-1]) 183 | f_arn = f_arn.rstrip(f_arn[-1]) 184 | 185 | for fn in checked_fns: 186 | if f_arn == fn['FunctionArn']: 187 | ta_et_flagged_dict = { 188 | 'Status': f['metadata'][0], 189 | 'Region': f['metadata'][1], 190 | 'FunctionArn': f['metadata'][2], 191 | 'MaxDailyTimeoutRatePerc': f['metadata'][3], 192 | 'DateOfMaxTimeoutRate': f['metadata'][4], 193 | 'AverageDailyTimeoutRatePerc': f['metadata'][5], 194 | 'FunctionTimeoutSettings': f['metadata'][6] 195 | } 196 | ta_et_flagged_list.append(ta_et_flagged_dict) 197 | break 198 | 199 | return ta_et_flagged_list 200 | ################################################################################################################################### 201 | 202 | ################################################################################################################################### 203 | #multi-az evaluation code functionality 204 | ################################################################################################################################### 205 | def check_multi_az(fns): 206 | 207 | vpc_fns = [] 208 | sublist = [] 209 | 210 | for fn in fns: 211 | if 'SubnetIds' in fn.keys() and fn['SubnetIds'] != []: 212 | vpc_fns.append(fn) 213 | sublist.append(fn['SubnetIds']) 214 | sublist = [item for row in sublist for item in row] 215 | sublist = list(dict.fromkeys(sublist)) 216 | subnets = ec2.describe_subnets(SubnetIds=sublist)['Subnets'] 217 | sub_az_list = [] 218 | for sub in sublist: 219 | for s in subnets: 220 | if sub == s['SubnetId']: 221 | subaz = { 222 | 'SubnetId':s['SubnetId'], 223 | 'AZ': s['AvailabilityZone'] 224 | } 225 | sub_az_list.append(subaz) 226 | vpc_functions = [] 227 | for fn in vpc_fns: 228 | s_list = [] 229 | for s in fn['SubnetIds']: 230 | for saz in sub_az_list: 231 | if s == saz['SubnetId']: 232 | s = { 233 | 'SubnetId':saz['SubnetId'], 234 | 'AZ': saz['AZ'] 235 | } 236 | s_list.append(s) 237 | vpc_function = { 238 | 'FunctionArn':fn['FunctionArn'], 239 | 'Subnets':s_list 240 | } 241 | vpc_functions.append(vpc_function) 242 | 243 | non_multiaz_functions = [] 244 | for f in vpc_functions: 245 | f_azs = [] 246 | for az in f['Subnets']: 247 | f_azs.append(az['AZ']) 248 | if len(f_azs) != len(set(f_azs)) or len(f_azs) == 1: 249 | non_multiaz_functions.append(f) 250 | return non_multiaz_functions 251 | 252 | ################################################################################################################################### 253 | 254 | def handler(event, context): 255 | #List function config objects from S3: 256 | obj_functions = [] 257 | s3_prefix = event['FolderKey'] 258 | s3_f_folder = event['FolderKey'] + '/functions/' 259 | bucket = s3_r.Bucket(S3_BUCKET) 260 | for objects in bucket.objects.filter(Prefix=s3_f_folder): 261 | obj_functions.append(objects.key) 262 | 263 | #Fetch function config objects from S3: 264 | functions = [] 265 | count = 0 266 | for obj in obj_functions: 267 | s3_function_set = s3.get_object(Bucket=S3_BUCKET, Key=obj) 268 | s3_function_set = json.loads(s3_function_set['Body'].read()) 269 | functions.extend(s3_function_set) 270 | count = count + len(s3_function_set) 271 | 272 | #Remove review functions from list: 273 | for function in functions: 274 | if function['FunctionName'].startswith(STACK_NAME): 275 | functions.remove(function) 276 | #Set versions: 277 | for function in functions: 278 | function['Version'] = function['FunctionArn'].split(':')[-1] 279 | 280 | #get recommendations.json and event source mappings list from S3 281 | s3_recommendations = s3.get_object(Bucket=S3_BUCKET, Key=s3_prefix+'/recommendations.json') 282 | s3_event_source_mappings = s3.get_object(Bucket=S3_BUCKET, Key=s3_prefix+'/esms.json') 283 | 284 | #convert json to python dict 285 | recommendations = json.loads(s3_recommendations['Body'].read()) 286 | #recommendations = recommendations['Recommendations']['LambdaFunctionRecommendations'] 287 | esms = json.loads(s3_event_source_mappings['Body'].read()) 288 | 289 | 290 | 291 | ################################################################################# 292 | #Runtime evaluation method pointer: 293 | ################################################################################# 294 | 295 | html = requests.get(DOC_URL) 296 | html = html.text 297 | 298 | soup = BeautifulSoup(html, 'html.parser') 299 | 300 | supported_runtimes = {'Supported Runtimes': fetch_supported_runtimes(soup)} 301 | 302 | deprecated_runtimes = {'Deprecated Runtimes': fetch_deprecated_runtimes(soup)} 303 | 304 | dep_fns = copy.deepcopy(functions) 305 | dep_runtime_fns = check_dep_runtime(dep_fns,deprecated_runtimes) 306 | sup_fns = copy.deepcopy(functions) 307 | sup_runtime_fns = check_sup_runtime(sup_fns,supported_runtimes) 308 | 309 | # Separate deprecated functions by runtime: 310 | py_dep_run = [] 311 | for function in dep_runtime_fns: 312 | runtime = function['Runtime'] 313 | if runtime.startswith('python') and 'Deprecation date' in function and function['Deprecation date'] != '': 314 | py_dep_run.append(function) 315 | 316 | no_dep_run = [] 317 | for function in dep_runtime_fns: 318 | runtime = function['Runtime'] 319 | if runtime.startswith('nodejs') and 'Deprecation date' in function and function['Deprecation date'] != '': 320 | no_dep_run.append(function) 321 | 322 | ja_dep_run = [] 323 | for function in dep_runtime_fns: 324 | runtime = function['Runtime'] 325 | if runtime.startswith('java') and 'Deprecation date' in function and function['Deprecation date'] != '': 326 | ja_dep_run.append(function) 327 | 328 | do_dep_run = [] 329 | for function in dep_runtime_fns: 330 | runtime = function['Runtime'] 331 | if runtime.startswith('dotnet') and 'Deprecation date' in function and function['Deprecation date'] != '': 332 | do_dep_run.append(function) 333 | 334 | ru_dep_run = [] 335 | for function in dep_runtime_fns: 336 | runtime = function['Runtime'] 337 | if runtime.startswith('ruby') and 'Deprecation date' in function and function['Deprecation date'] != '': 338 | ru_dep_run.append(function) 339 | 340 | go_dep_run = [] 341 | for function in dep_runtime_fns: 342 | runtime = function['Runtime'] 343 | if runtime.startswith('golang') and 'Deprecation date' in function and function['Deprecation date'] != '': 344 | go_dep_run.append(function) 345 | 346 | cu_dep_run = [] 347 | for function in dep_runtime_fns: 348 | runtime = function['Runtime'] 349 | if runtime.startswith('provided') and 'Deprecation date' in function and function['Deprecation date'] != '': 350 | cu_dep_run.append(function) 351 | 352 | 353 | # Separate supported functions by runtime: 354 | py_sup_run = [] 355 | for function in sup_runtime_fns: 356 | runtime = function['Runtime'] 357 | if runtime.startswith('python') and 'Deprecation date' in function and function['Deprecation date'] != '': 358 | py_sup_run.append(function) 359 | 360 | no_sup_run = [] 361 | for function in sup_runtime_fns: 362 | runtime = function['Runtime'] 363 | if runtime.startswith('nodejs') and 'Deprecation date' in function and function['Deprecation date'] != '': 364 | no_sup_run.append(function) 365 | 366 | ja_sup_run = [] 367 | for function in sup_runtime_fns: 368 | runtime = function['Runtime'] 369 | if runtime.startswith('java') and 'Deprecation date' in function and function['Deprecation date'] != '': 370 | ja_sup_run.append(function) 371 | 372 | do_sup_run = [] 373 | for function in sup_runtime_fns: 374 | runtime = function['Runtime'] 375 | if runtime.startswith('dotnet') and 'Deprecation date' in function and function['Deprecation date'] != '': 376 | do_sup_run.append(function) 377 | 378 | ru_sup_run = [] 379 | for function in sup_runtime_fns: 380 | runtime = function['Runtime'] 381 | if runtime.startswith('ruby') and 'Deprecation date' in function and function['Deprecation date'] != '': 382 | ru_sup_run.append(function) 383 | 384 | go_sup_run = [] 385 | for function in sup_runtime_fns: 386 | runtime = function['Runtime'] 387 | if runtime.startswith('golang') and 'Deprecation date' in function and function['Deprecation date'] != '': 388 | go_sup_run.append(function) 389 | 390 | cu_sup_run = [] 391 | for function in sup_runtime_fns: 392 | runtime = function['Runtime'] 393 | if runtime.startswith('provided') and 'Deprecation date' in function and function['Deprecation date'] != '': 394 | cu_sup_run.append(function) 395 | 396 | 397 | ################################################################################# 398 | #Trusted Advisor evaluations 399 | ################################################################################# 400 | if TA_ENABLED == 'true': 401 | ta_fns = copy.deepcopy(functions) 402 | # Get HighErrorRate Check result 403 | warnings_ta_high_errors = check_ta_high_errors(ta_fns) 404 | 405 | #Get ExcessiveTimeout Check result 406 | warnings_ta_excessive_timeout = check_ta_excessive_timeout(ta_fns) 407 | 408 | 409 | ################################################################################# 410 | #Mutli-az evaluation: 411 | ################################################################################# 412 | 413 | vpc_fns = copy.deepcopy(functions) 414 | 415 | warnings_vpc_functions = check_multi_az(vpc_fns) 416 | 417 | ################################################################################# 418 | 419 | ################################################################################# 420 | #Recommendations evaluations: 421 | ################################################################################# 422 | #Match Compute Optimizer recommendations to functions 423 | 424 | not_optimized_functions = [] 425 | unavailable_functions = [] 426 | savings_not_present = {"EstimatedMonthlySavings":{"Currency": "USD","Value": "N/A"},"SavingsOpportunityPercentage": "N/A"} 427 | for recommendation in recommendations: 428 | rec_function_arn = recommendation['FunctionArn'] 429 | rec_function_name = recommendation['FunctionArn'].split(':') 430 | rec_function_name = rec_function_name[6]+':'+rec_function_name[7] 431 | 432 | for function in functions: 433 | 434 | if rec_function_arn == function['FunctionArn']: 435 | recommendation['FunctionName'] = rec_function_name 436 | if recommendation['Finding'] == 'NotOptimized': 437 | if not 'SavingsOpportunity' in recommendation['MemorySizeRecommendationOptions'][0]: 438 | recommendation['MemorySizeRecommendationOptions'][0]['SavingsOpportunity'] = savings_not_present 439 | not_optimized_functions.append(recommendation) 440 | 441 | 442 | else: 443 | unavailable_functions.append(recommendation) 444 | 445 | #Check functions with x86 architecture 446 | 447 | old_arch_fns = [] 448 | for function in functions: 449 | if 'x86_64' in function['Architectures']: 450 | old_arch_fns.append(function['FunctionArn']) 451 | 452 | ################################################################################# 453 | 454 | 455 | 456 | ################################################################################# 457 | #Create a dict wilh all data that will populate the template 458 | ################################################################################# 459 | data = {} 460 | data['account'] = AWS_ACCOUNT 461 | data['region'] = AWS_REGION 462 | data['reviewed_functions'] = functions 463 | data['dep_python_functions'] = py_dep_run 464 | data['dep_nodejs_functions'] = no_dep_run 465 | data['dep_java_functions'] = ja_dep_run 466 | data['dep_dotnet_functions'] = do_dep_run 467 | data['dep_ruby_functions'] = ru_dep_run 468 | data['dep_golang_functions'] = go_dep_run 469 | data['dep_custom_functions'] = cu_dep_run 470 | data['sup_python_functions'] = py_sup_run 471 | data['sup_nodejs_functions'] = no_sup_run 472 | data['sup_java_functions'] = ja_sup_run 473 | data['sup_dotnet_functions'] = do_sup_run 474 | data['sup_ruby_functions'] = ru_sup_run 475 | data['sup_golang_functions'] = go_sup_run 476 | data['sup_custom_functions'] = cu_sup_run 477 | if TA_ENABLED == 'true': 478 | data['warnings_ta_high_errors'] = warnings_ta_high_errors 479 | data['warnings_ta_excessive_timeouts'] = warnings_ta_excessive_timeout 480 | data['warnings_vpc'] = warnings_vpc_functions 481 | data['recfunctions'] = not_optimized_functions 482 | data['recarch'] = old_arch_fns 483 | data['esms'] = esms 484 | 485 | 486 | #render the template 487 | with open('template.html', 'r') as file: 488 | template = Template(file.read(),trim_blocks=True) 489 | rendered_file = template.render(data=data) 490 | 491 | s3.put_object(Body=rendered_file, Bucket=S3_BUCKET, Key=s3_prefix+'/report.html') 492 | 493 | presign = s3.generate_presigned_url(ClientMethod='get_object',Params={'Bucket': S3_BUCKET,'Key': s3_prefix+'/report.html'}, ExpiresIn=3600) 494 | 495 | return { 496 | "statusCode": 200, 497 | "ReportUrl": presign 498 | } 499 | 500 | 501 | 502 | -------------------------------------------------------------------------------- /src/lambda/function-generate-selected/app.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import json, os, copy 5 | import boto3, requests 6 | from jinja2 import Template 7 | from bs4 import BeautifulSoup 8 | 9 | s3 = boto3.client('s3') 10 | s3_r = boto3.resource('s3') 11 | ec2 = boto3.client('ec2') 12 | ssm = boto3.client('ssm') 13 | supp = boto3.client('support') 14 | 15 | #env var load 16 | S3_BUCKET = os.environ['S3_BUCKET'] 17 | AWS_REGION = os.environ['REGION'] 18 | AWS_ACCOUNT = os.environ['ACCOUNT'] 19 | ACT_RUNTIMES = os.environ['ACT_RUNTIMES'] 20 | STACK_NAME = os.environ['STACK_NAME'] 21 | TA_ENABLED = os.environ['TA_ENABLED'] 22 | DOC_URL = "https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html" 23 | 24 | 25 | ################################################################################################################################### 26 | # Runtime evaluation code functionality 27 | ################################################################################################################################### 28 | def check_dep_runtime(fns,dep_runs): 29 | # Create a dictionary to map Identifiers to runtime details 30 | runtime_map = {item['Identifier']: { 31 | 'Deprecation date': item['Deprecation date'], 32 | 'Block function create': item['Block function create'], 33 | 'Block function update': item['Block function update'] 34 | } for item in dep_runs['Deprecated Runtimes']} 35 | 36 | # Iterate through the function data and append the runtime details 37 | for function in fns: 38 | runtime = function['Runtime'] 39 | for identifier, details in runtime_map.items(): 40 | if runtime == identifier: 41 | function['Deprecation date'] = details['Deprecation date'] 42 | function['Block function create'] = details['Block function create'] 43 | function['Block function update'] = details['Block function update'] 44 | break 45 | 46 | return fns 47 | 48 | def check_sup_runtime(fns,sup_runs): 49 | # Create a dictionary to map Identifiers to runtime details 50 | runtime_map = {item['Identifier']: { 51 | 'Deprecation date': item['Deprecation date'], 52 | 'Block function create': item['Block function create'], 53 | 'Block function update': item['Block function update'] 54 | } for item in sup_runs['Supported Runtimes']} 55 | 56 | # Iterate through the function data and append the runtime details 57 | for function in fns: 58 | runtime = function['Runtime'] 59 | for identifier, details in runtime_map.items(): 60 | if runtime == identifier: 61 | function['Deprecation date'] = details['Deprecation date'] 62 | function['Block function create'] = details['Block function create'] 63 | function['Block function update'] = details['Block function update'] 64 | break 65 | 66 | return fns 67 | 68 | 69 | ################################################################################################################################### 70 | 71 | ################################################################################################################################### 72 | #TA - High Error Rates 73 | ################################################################################################################################### 74 | def check_ta_high_errors(fns): 75 | ta_he = supp.describe_trusted_advisor_check_result(checkId='L4dfs2Q3C2') 76 | ta_he_flagged = ta_he['result']['flaggedResources'] 77 | ta_he_flagged_list = [] 78 | ta_he_flagged_dict = dict 79 | checked_fns = fns 80 | 81 | ### ADD cross check for functions 82 | 83 | #data cleanup 84 | for f in ta_he_flagged: 85 | f_arn = f['metadata'][2].removesuffix(f['metadata'][2].split(':')[-1]) 86 | f_arn = f_arn.rstrip(f_arn[-1]) 87 | 88 | for fn in checked_fns: 89 | if f_arn == fn['FunctionArn']: 90 | ta_he_flagged_dict = { 91 | 'Status': f['metadata'][0], 92 | 'Region': f['metadata'][1], 93 | 'FunctionArn': f['metadata'][2], 94 | 'MaxDailyErrorRatePerc': f['metadata'][3], 95 | 'DateOfMaxErrorRate': f['metadata'][4], 96 | 'AverageDailyErrorRatePerc': f['metadata'][5] 97 | } 98 | ta_he_flagged_list.append(ta_he_flagged_dict) 99 | break 100 | 101 | return ta_he_flagged_list 102 | 103 | ################################################################################################################################### 104 | 105 | ################################################################################################################################### 106 | #TA - Excessive Timeouts 107 | ################################################################################################################################### 108 | def check_ta_excessive_timeout(fns): 109 | ta_et = supp.describe_trusted_advisor_check_result(checkId='L4dfs2Q3C3') 110 | ta_et_flagged = ta_et['result']['flaggedResources'] 111 | ta_et_flagged_list = [] 112 | ta_et_flagged_dict = dict 113 | checked_fns = fns 114 | 115 | ### ADD cross check for functions 116 | 117 | #data cleanup 118 | for f in ta_et_flagged: 119 | f_arn = f['metadata'][2].removesuffix(f['metadata'][2].split(':')[-1]) 120 | f_arn = f_arn.rstrip(f_arn[-1]) 121 | 122 | for fn in checked_fns: 123 | if f_arn == fn['FunctionArn']: 124 | ta_et_flagged_dict = { 125 | 'Status': f['metadata'][0], 126 | 'Region': f['metadata'][1], 127 | 'FunctionArn': f['metadata'][2], 128 | 'MaxDailyTimeoutRatePerc': f['metadata'][3], 129 | 'DateOfMaxTimeoutRate': f['metadata'][4], 130 | 'AverageDailyTimeoutRatePerc': f['metadata'][5], 131 | 'FunctionTimeoutSettings': f['metadata'][6] 132 | } 133 | ta_et_flagged_list.append(ta_et_flagged_dict) 134 | break 135 | 136 | return ta_et_flagged_list 137 | ################################################################################################################################### 138 | 139 | ################################################################################################################################### 140 | #multi-az evaluation code functionality 141 | ################################################################################################################################### 142 | def check_multi_az(fns): 143 | 144 | vpc_fns = [] 145 | sublist = [] 146 | 147 | for fn in fns: 148 | if 'SubnetIds' in fn.keys() and fn['SubnetIds'] != []: 149 | vpc_fns.append(fn) 150 | sublist.append(fn['SubnetIds']) 151 | sublist = [item for row in sublist for item in row] 152 | sublist = list(dict.fromkeys(sublist)) 153 | subnets = ec2.describe_subnets(SubnetIds=sublist)['Subnets'] 154 | sub_az_list = [] 155 | for sub in sublist: 156 | for s in subnets: 157 | if sub == s['SubnetId']: 158 | subaz = { 159 | 'SubnetId':s['SubnetId'], 160 | 'AZ': s['AvailabilityZone'] 161 | } 162 | sub_az_list.append(subaz) 163 | vpc_functions = [] 164 | for fn in vpc_fns: 165 | s_list = [] 166 | for s in fn['SubnetIds']: 167 | for saz in sub_az_list: 168 | if s == saz['SubnetId']: 169 | s = { 170 | 'SubnetId':saz['SubnetId'], 171 | 'AZ': saz['AZ'] 172 | } 173 | s_list.append(s) 174 | vpc_function = { 175 | 'FunctionArn':fn['FunctionArn'], 176 | 'Subnets':s_list 177 | } 178 | vpc_functions.append(vpc_function) 179 | 180 | non_multiaz_functions = [] 181 | for f in vpc_functions: 182 | f_azs = [] 183 | for az in f['Subnets']: 184 | f_azs.append(az['AZ']) 185 | if len(f_azs) != len(set(f_azs)) or len(f_azs) == 1: 186 | non_multiaz_functions.append(f) 187 | return non_multiaz_functions 188 | 189 | ################################################################################################################################### 190 | 191 | ################################################################################################################################### 192 | #Deprecated runtime fetch: 193 | ################################################################################################################################### 194 | def fetch_deprecated_runtimes(dep_run_soup): 195 | """ 196 | Fetch the list of deprecated runtimes from the AWS Lambda docs 197 | """ 198 | # Find the Deprecated Runtime table 199 | element = dep_run_soup.find(id="runtimes-deprecated") 200 | table = element.find_next("table") 201 | 202 | # Extract header row 203 | headers = [header.text for header in table.find('thead').find_all('tr')] 204 | headers = headers[1] 205 | headers = headers.split('\n') 206 | headers = headers[1:-1] 207 | 208 | rows = [] 209 | 210 | for row in table.find_all('tr')[1:]: 211 | cells = row.find_all('td') 212 | data = [cell.text for cell in cells] 213 | data = [line.strip() for line in data] 214 | rows.append(dict(zip(headers, data))) 215 | rows = rows[1:] 216 | 217 | 218 | return rows 219 | ################################################################################################################################### 220 | 221 | ################################################################################################################################### 222 | #Supported runtime fetch: 223 | ################################################################################################################################### 224 | 225 | def fetch_supported_runtimes(sup_run_soup): 226 | """ 227 | Fetch the list of supported runtimes from the AWS Lambda docs 228 | """ 229 | # Find the Supported Runtime table 230 | element = sup_run_soup.find(id="runtimes-supported") 231 | table = element.find_next("table") 232 | 233 | # Extract header row 234 | headers = [header.text for header in table.find('thead').find_all('tr')] 235 | headers = headers[1] 236 | headers = headers.split('\n') 237 | headers = headers[1:-1] 238 | 239 | rows = [] 240 | 241 | for row in table.find_all('tr')[1:]: 242 | cells = row.find_all('td') 243 | data = [cell.text for cell in cells] 244 | data = [line.strip() for line in data] 245 | rows.append(dict(zip(headers, data))) 246 | rows = rows[1:] 247 | 248 | 249 | return rows 250 | 251 | ################################################################################################################################### 252 | 253 | def handler(event, context): 254 | #List function config objects from S3: 255 | s3_prefix = event['Prefix'] 256 | 257 | #Fetch function config objects from S3: 258 | 259 | 260 | functions = s3.get_object(Bucket=S3_BUCKET, Key=s3_prefix+'/'+event['FunctionsObject']) 261 | functions = json.loads(functions['Body'].read()) 262 | 263 | #Remove review functions from list: 264 | for function in functions: 265 | if function['FunctionName'].startswith(STACK_NAME): 266 | functions.remove(function) 267 | #Set versions: 268 | for function in functions: 269 | function['Version'] = function['FunctionArn'].split(':')[-1] 270 | 271 | #get recommendations.json and event source mappings list from S3 272 | s3_recommendations = s3.get_object(Bucket=S3_BUCKET, Key=s3_prefix+'/recommendations.json') 273 | s3_event_source_mappings = s3.get_object(Bucket=S3_BUCKET, Key=s3_prefix+'/esms.json') 274 | 275 | #convert json to python dict 276 | recommendations = json.loads(s3_recommendations['Body'].read()) 277 | recommendations = recommendations['Recommendations']['LambdaFunctionRecommendations'] 278 | esms = json.loads(s3_event_source_mappings['Body'].read()) 279 | esms = esms['EventSourceMappings']['EventSourceMappings'] 280 | 281 | 282 | 283 | ################################################################################# 284 | #Runtime evaluation method pointer: 285 | ################################################################################# 286 | 287 | html = requests.get(DOC_URL) 288 | html = html.text 289 | 290 | soup = BeautifulSoup(html, 'html.parser') 291 | 292 | supported_runtimes = {'Supported Runtimes': fetch_supported_runtimes(soup)} 293 | 294 | deprecated_runtimes = {'Deprecated Runtimes': fetch_deprecated_runtimes(soup)} 295 | 296 | dep_fns = copy.deepcopy(functions) 297 | dep_runtime_fns = check_dep_runtime(dep_fns,deprecated_runtimes) 298 | sup_fns = copy.deepcopy(functions) 299 | sup_runtime_fns = check_sup_runtime(sup_fns,supported_runtimes) 300 | 301 | # Separate deprecated functions by runtime: 302 | py_dep_run = [] 303 | for function in dep_runtime_fns: 304 | runtime = function['Runtime'] 305 | if runtime.startswith('python') and 'Deprecation date' in function and function['Deprecation date'] != '': 306 | py_dep_run.append(function) 307 | 308 | no_dep_run = [] 309 | for function in dep_runtime_fns: 310 | runtime = function['Runtime'] 311 | if runtime.startswith('nodejs') and 'Deprecation date' in function and function['Deprecation date'] != '': 312 | no_dep_run.append(function) 313 | 314 | ja_dep_run = [] 315 | for function in dep_runtime_fns: 316 | runtime = function['Runtime'] 317 | if runtime.startswith('java') and 'Deprecation date' in function and function['Deprecation date'] != '': 318 | ja_dep_run.append(function) 319 | 320 | do_dep_run = [] 321 | for function in dep_runtime_fns: 322 | runtime = function['Runtime'] 323 | if runtime.startswith('dotnet') and 'Deprecation date' in function and function['Deprecation date'] != '': 324 | do_dep_run.append(function) 325 | 326 | ru_dep_run = [] 327 | for function in dep_runtime_fns: 328 | runtime = function['Runtime'] 329 | if runtime.startswith('ruby') and 'Deprecation date' in function and function['Deprecation date'] != '': 330 | ru_dep_run.append(function) 331 | 332 | go_dep_run = [] 333 | for function in dep_runtime_fns: 334 | runtime = function['Runtime'] 335 | if runtime.startswith('golang') and 'Deprecation date' in function and function['Deprecation date'] != '': 336 | go_dep_run.append(function) 337 | 338 | cu_dep_run = [] 339 | for function in dep_runtime_fns: 340 | runtime = function['Runtime'] 341 | if runtime.startswith('provided') and 'Deprecation date' in function and function['Deprecation date'] != '': 342 | cu_dep_run.append(function) 343 | 344 | 345 | # Separate supported functions by runtime: 346 | py_sup_run = [] 347 | for function in sup_runtime_fns: 348 | runtime = function['Runtime'] 349 | if runtime.startswith('python') and 'Deprecation date' in function and function['Deprecation date'] != '': 350 | py_sup_run.append(function) 351 | 352 | no_sup_run = [] 353 | for function in sup_runtime_fns: 354 | runtime = function['Runtime'] 355 | if runtime.startswith('nodejs') and 'Deprecation date' in function and function['Deprecation date'] != '': 356 | no_sup_run.append(function) 357 | 358 | ja_sup_run = [] 359 | for function in sup_runtime_fns: 360 | runtime = function['Runtime'] 361 | if runtime.startswith('java') and 'Deprecation date' in function and function['Deprecation date'] != '': 362 | ja_sup_run.append(function) 363 | 364 | do_sup_run = [] 365 | for function in sup_runtime_fns: 366 | runtime = function['Runtime'] 367 | if runtime.startswith('dotnet') and 'Deprecation date' in function and function['Deprecation date'] != '': 368 | do_sup_run.append(function) 369 | 370 | ru_sup_run = [] 371 | for function in sup_runtime_fns: 372 | runtime = function['Runtime'] 373 | if runtime.startswith('ruby') and 'Deprecation date' in function and function['Deprecation date'] != '': 374 | ru_sup_run.append(function) 375 | 376 | go_sup_run = [] 377 | for function in sup_runtime_fns: 378 | runtime = function['Runtime'] 379 | if runtime.startswith('golang') and 'Deprecation date' in function and function['Deprecation date'] != '': 380 | go_sup_run.append(function) 381 | 382 | cu_sup_run = [] 383 | for function in sup_runtime_fns: 384 | runtime = function['Runtime'] 385 | if runtime.startswith('provided') and 'Deprecation date' in function and function['Deprecation date'] != '': 386 | cu_sup_run.append(function) 387 | 388 | 389 | ################################################################################# 390 | #Trusted Advisor evaluations 391 | ################################################################################# 392 | if TA_ENABLED == 'true': 393 | ta_fns = copy.deepcopy(functions) 394 | # Get HighErrorRate Check result 395 | warnings_ta_high_errors = check_ta_high_errors(ta_fns) 396 | 397 | #Get ExcessiveTimeout Check result 398 | warnings_ta_excessive_timeout = check_ta_excessive_timeout(ta_fns) 399 | 400 | 401 | ################################################################################# 402 | #Mutli-az evaluation: 403 | ################################################################################# 404 | 405 | vpc_fns = copy.deepcopy(functions) 406 | 407 | warnings_vpc_functions = check_multi_az(vpc_fns) 408 | 409 | ################################################################################# 410 | 411 | ################################################################################# 412 | #Recommendations evaluations: 413 | ################################################################################# 414 | #Match Compute Optimizer recommendations to functions 415 | 416 | not_optimized_functions = [] 417 | unavailable_functions = [] 418 | savings_not_present = {"EstimatedMonthlySavings":{"Currency": "USD","Value": "N/A"},"SavingsOpportunityPercentage": "N/A"} 419 | for recommendation in recommendations: 420 | rec_function_arn = recommendation['FunctionArn'] 421 | rec_function_name = recommendation['FunctionArn'].split(':') 422 | rec_function_name = rec_function_name[6]+':'+rec_function_name[7] 423 | 424 | for function in functions: 425 | 426 | if rec_function_arn == function['FunctionArn']: 427 | recommendation['FunctionName'] = rec_function_name 428 | if recommendation['Finding'] == 'NotOptimized': 429 | if not 'SavingsOpportunity' in recommendation['MemorySizeRecommendationOptions'][0]: 430 | recommendation['MemorySizeRecommendationOptions'][0]['SavingsOpportunity'] = savings_not_present 431 | not_optimized_functions.append(recommendation) 432 | 433 | 434 | else: 435 | unavailable_functions.append(recommendation) 436 | 437 | #Check functions with x86 architecture 438 | 439 | old_arch_fns = [] 440 | for function in functions: 441 | if 'x86_64' in function['Architectures']: 442 | old_arch_fns.append(function['FunctionArn']) 443 | 444 | ################################################################################# 445 | 446 | ################################################################################# 447 | #Functions configuration compilation: 448 | ################################################################################# 449 | 450 | #cleanup event source mappings 451 | 452 | filtered_esms = [] 453 | for fn in functions: 454 | for esm in esms: 455 | if fn['FunctionArn'] == esm['FunctionArn']: 456 | filtered_esms.append(esm) 457 | 458 | 459 | ################################################################################# 460 | 461 | ################################################################################# 462 | #Create a dict with all data that will populate the template 463 | ################################################################################# 464 | data = {} 465 | data['account'] = AWS_ACCOUNT 466 | data['region'] = AWS_REGION 467 | data['reviewed_functions'] = functions 468 | data['dep_python_functions'] = py_dep_run 469 | data['dep_nodejs_functions'] = no_dep_run 470 | data['dep_java_functions'] = ja_dep_run 471 | data['dep_dotnet_functions'] = do_dep_run 472 | data['dep_ruby_functions'] = ru_dep_run 473 | data['dep_golang_functions'] = go_dep_run 474 | data['dep_custom_functions'] = cu_dep_run 475 | data['sup_python_functions'] = py_sup_run 476 | data['sup_nodejs_functions'] = no_sup_run 477 | data['sup_java_functions'] = ja_sup_run 478 | data['sup_dotnet_functions'] = do_sup_run 479 | data['sup_ruby_functions'] = ru_sup_run 480 | data['sup_golang_functions'] = go_sup_run 481 | data['sup_custom_functions'] = cu_sup_run 482 | if TA_ENABLED == 'true': 483 | data['warnings_ta_high_errors'] = warnings_ta_high_errors 484 | data['warnings_ta_excessive_timeouts'] = warnings_ta_excessive_timeout 485 | data['warnings_vpc'] = warnings_vpc_functions 486 | data['recfunctions'] = not_optimized_functions 487 | data['recarch'] = old_arch_fns 488 | data['esms'] = filtered_esms 489 | 490 | 491 | #render the template 492 | with open('template.html', 'r') as file: 493 | template = Template(file.read(),trim_blocks=True) 494 | rendered_file = template.render(data=data) 495 | 496 | s3.put_object(Body=rendered_file, Bucket=S3_BUCKET, Key=s3_prefix+'/report.html') 497 | 498 | presign = s3.generate_presigned_url(ClientMethod='get_object',Params={'Bucket': S3_BUCKET,'Key': s3_prefix+'/report.html'}, ExpiresIn=3600) 499 | 500 | return { 501 | "statusCode": 200, 502 | "ReportUrl": presign 503 | } 504 | 505 | 506 | 507 | -------------------------------------------------------------------------------- /src/lambda/function-generate/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Serverless Operational Review 8 | 9 | 43 | 44 |
45 |
46 |

Serverless Operational Review

47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
AWS Account:
{{data.account}}
Region:
{{data.region}}
57 |
58 |
59 | 60 |
61 |

AWS Lambda Review

62 |
63 |
64 | 65 |
66 |
67 |

RED Danger Zone - Risks

68 | 72 |
73 |
74 | 75 |
76 |

Deprecated Function Runtime Versions!

77 | 80 |

81 |
82 |
Python functions
83 | {% if data.dep_python_functions != [] %} 84 |
85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | {% for fn in data.dep_python_functions %} 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | {% endfor %} 107 | 108 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
109 |
110 | {% else %} 111 |
112 |

No functions with a deprecated version of this runtime.

113 |
114 | {% endif %} 115 |
116 |
117 |
118 |
Node.js functions
119 | {% if data.dep_nodejs_functions != [] %} 120 |
121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | {% for fn in data.dep_nodejs_functions %} 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | {% endfor %} 143 | 144 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
145 |
146 | {% else %} 147 |
148 |

No functions with a deprecated version of this runtime.

149 |
150 | {% endif %} 151 |
152 |
153 |
154 |
Java functions
155 | {% if data.dep_java_functions != [] %} 156 |
157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | {% for fn in data.dep_java_functions %} 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | {% endfor %} 179 | 180 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
181 |
182 | {% else %} 183 |
184 |

No functions with a deprecated version of this runtime.

185 |
186 | {% endif %} 187 |
188 |
189 |
190 |
.Net functions
191 | {% if data.dep_dotnet_functions != [] %} 192 |
193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | {% for fn in data.dep_dotnet_functions %} 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | {% endfor %} 215 | 216 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
217 |
218 | {% else %} 219 |
220 |

No functions with a deprecated version of this runtime.

221 |
222 | {% endif %} 223 |
224 |
225 |
226 |
Ruby functions
227 | {% if data.dep_ruby_functions != [] %} 228 |
229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | {% for fn in data.dep_ruby_functions %} 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | {% endfor %} 251 | 252 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
253 |
254 | {% else %} 255 |
256 |

No functions with a deprecated version of this runtime.

257 |
258 | {% endif %} 259 |
260 |
261 |
262 |
Golang functions
263 | {% if data.dep_golang_functions != [] %} 264 |
265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | {% for fn in data.dep_golang_functions %} 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | {% endfor %} 287 | 288 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
289 |
290 | {% else %} 291 |
292 |

No functions with a deprecated version of this runtime.

293 |
294 | {% endif %} 295 |
296 |
297 |
298 |
Custom runtime functions
299 | {% if data.dep_custom_functions != [] %} 300 |
301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | {% for fn in data.dep_custom_functions %} 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | {% endfor %} 323 | 324 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
325 |
326 | {% else %} 327 |
328 |

No functions with a deprecated version of this runtime.

329 |
330 | {% endif %} 331 |
332 |

333 | 334 | {% if data.warnings_ta_high_errors != [] %} 335 |
336 |

Functions with High Error Rates

337 |
338 | 339 |
340 |
341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | {% for warnings_ta_high_errors in data.warnings_ta_high_errors %} 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | {% endfor %} 361 | 362 |
Function ARNStatusAverage Daily Errors %Maximum Daily Errors %Date of Max Error Rate
{{warnings_ta_high_errors['FunctionArn']}}{{warnings_ta_high_errors['Status']}}{{warnings_ta_high_errors['AverageDailyErrorRatePerc']}}{{warnings_ta_high_errors['MaxDailyErrorRatePerc']}}{{warnings_ta_high_errors['DateOfMaxErrorRate']}}
363 |
364 |
365 | {% endif %} 366 | 367 |

368 | 369 | {% if data.warnings_ta_excessive_timeouts != [] %} 370 |
371 |

Functions with Excessive Timeouts

372 |
373 | 374 |
375 |
376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | {% for warnings_ta_excessive_timeout in data.warnings_ta_excessive_timeouts %} 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | {% endfor %} 398 | 399 |
Function ARNStatusAverage Daily Timeouts %Maximum Daily Timeouts %Date of Max TimeoutsFunction Timeout Config
{{warnings_ta_excessive_timeout['FunctionArn']}}{{warnings_ta_excessive_timeout['Status']}}{{warnings_ta_excessive_timeout['AverageDailyTimeoutRatePerc']}}{{warnings_ta_excessive_timeout['MaxDailyTimeoutRatePerc']}}{{warnings_ta_excessive_timeout['DateOfMaxTimeoutRate']}}{{warnings_ta_excessive_timeout['FunctionTimeoutSettings']}}
400 |
401 |
402 | {% endif %} 403 | 404 |

405 | 406 | {% if data.warnings_vpc != [] %} 407 |
408 |

VPC-enabled Functions - Incorrect multi-AZ configuration

409 |
410 | 411 |
412 |
413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | {% for warnings_vpc in data.warnings_vpc %} 423 | 424 | 425 | {% for subaz in warnings_vpc['Subnets'] %} 426 | 427 | 428 | 429 | 430 | 431 | {% endfor %} 432 | 433 | {% endfor %} 434 | 435 |
Function ARNSubnetIDAvalabitlity Zone
{{warnings_vpc['FunctionArn']}}
{{subaz['SubnetId']}}{{subaz['AZ']}}
436 |
437 | {% endif %} 438 |
439 | 440 |
441 | 442 |
443 |
444 |

YELLOW Warnings Zone

445 | 449 |
450 |
451 |
452 |
Python functions
453 | {% if data.sup_python_functions != [] %} 454 |
455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | {% for fn in data.sup_python_functions %} 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | {% endfor %} 477 | 478 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
479 |
480 | {% else %} 481 |
482 |

No functions with this runtime are scheduled to deprecate at the moment.

483 |
484 | {% endif %} 485 |
486 |
487 |
488 |
Node.js functions
489 | {% if data.sup_nodejs_functions != [] %} 490 |
491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | {% for fn in data.sup_nodejs_functions %} 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | {% endfor %} 513 | 514 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
515 |
516 | {% else %} 517 |
518 |

No functions with this runtime are scheduled to deprecate at the moment.

519 |
520 | {% endif %} 521 |
522 |
523 |
524 |
Java functions
525 | {% if data.sup_java_functions != [] %} 526 |
527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | {% for fn in data.sup_java_functions %} 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | {% endfor %} 549 | 550 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
551 |
552 | {% else %} 553 |
554 |

No functions with this runtime are scheduled to deprecate at the moment.

555 |
556 | {% endif %} 557 |
558 |
559 |
560 |
.Net functions
561 | {% if data.sup_dotnet_functions != [] %} 562 |
563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | {% for fn in data.sup_dotnet_functions %} 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | {% endfor %} 585 | 586 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
587 |
588 | {% else %} 589 |
590 |

No functions with this runtime are scheduled to deprecate at the moment.

591 |
592 | {% endif %} 593 |
594 |
595 |
596 |
Ruby functions
597 | {% if data.sup_ruby_functions != [] %} 598 |
599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | {% for fn in data.sup_ruby_functions %} 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | {% endfor %} 621 | 622 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
623 |
624 | {% else %} 625 |
626 |

No functions with this runtime are scheduled to deprecate at the moment.

627 |
628 | {% endif %} 629 |
630 |
631 |
632 |
Golang functions
633 | {% if data.sup_golang_functions != [] %} 634 |
635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | {% for fn in data.sup_golang_functions %} 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | {% endfor %} 657 | 658 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
659 |
660 | {% else %} 661 |
662 |

No functions with this runtime are scheduled to deprecate at the moment.

663 |
664 | {% endif %} 665 |
666 |
667 |
668 |
Custom runtime functions
669 | {% if data.sup_custom_functions != [] %} 670 |
671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | {% for fn in data.sup_custom_functions %} 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | {% endfor %} 693 | 694 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
695 |
696 | {% else %} 697 |
698 |

No functions with this runtime are scheduled to deprecate at the moment.

699 |
700 | {% endif %} 701 |
702 |
703 |
704 | 705 |
706 |

BLUE Optimization Zone - Recommendations

707 | 710 |
711 |
712 | 713 | 714 | {% if data.recfunctions != [] %} 715 |
716 |

Memory Optimization Recommendations

717 | 721 |
722 |
723 | 724 |
725 |
726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | {% for rec in data.recfunctions %} 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | {% endfor %} 752 | 753 |
Function ARNFindingNumber of InvocationsCurrent Memory SizeRecommended Memory SizeEst. Montly Saving (USD)Est. Montly Saving %Duration Current - Projected (ms)
{{rec['FunctionArn']}}{{rec['FindingReasonCodes'][0]}}{{rec['NumberOfInvocations']}}{{rec['CurrentMemorySize']}}{{rec['MemorySizeRecommendationOptions'][0]['MemorySize']}}{{rec['MemorySizeRecommendationOptions'][0]['SavingsOpportunity']['EstimatedMonthlySavings']['Value']}}{{rec['MemorySizeRecommendationOptions'][0]['SavingsOpportunity']['SavingsOpportunityPercentage']}}{{rec['UtilizationMetrics'][0]['Value']}} - {{rec['MemorySizeRecommendationOptions'][0]['ProjectedUtilizationMetrics'][1]['Value']}}
754 |
755 | {% endif %} 756 |
757 |

758 | 759 | 760 | {% if data.recarch != [] %} 761 |
762 |

Functions running on x86 Architecture Type

763 | 764 |
765 | 766 |
767 |
768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | {% for f in data.recarch %} 776 | 777 | 778 | 779 | {% endfor %} 780 | 781 |
Functions running x86 architecture, consider upgrade to arm64
{{f}}
782 |
783 | {% endif %} 784 |
785 |

786 |
787 | 788 |
789 |
790 |

Reviewed Functions - Configurations

791 | 792 |
793 |
794 | 795 | {% if data.reviewed_functions != [] %} 796 |
797 |
798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | {% for f in data.reviewed_functions %} 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | {% if f.ReservedConcurrency != {} %} 833 | 834 | {% else %} 835 | 836 | {% endif %} 837 | 838 | 839 | 840 | {% if f.ProvisionedConcurrencyConfigs != [] %} 841 | {% for c in f.ProvisionedConcurrencyConfigs %} 842 | {% if f.FunctionArn == c.FunctionArn %} 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | {% endif %} 863 | {% endfor %} 864 | {% else %} 865 | 866 | {% endif %} 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | {% endfor %} 892 | 893 |
Reviewed functions configurations
Function ARN:{{f['FunctionArn']}}
Memory (MB):<{{f['MemorySize']}}
Timeout (S):{{f['Timeout']}}
Runtime:{{f['Runtime']}}
Architecture:{{f['Architectures']}}
Package Type:{{f['PackageType']}}
Reserved Concurrency:{{f.ReservedConcurrency.ReservedConcurrentExecutions}}0
Provisioned Concurrency Config:
Status: {{c.Status}}
Requested PC Executions: {{c.RequestedProvisionedConcurrentExecutions}}
Allocated PC Executions: {{c.AllocatedProvisionedConcurrentExecutions}}
Available PC Executions: {{c.AvailableProvisionedConcurrentExecutions}}
Last Modified: {{c.LastModified}} Not Set
Execution Role:{{f['Role']}}
Code Size (B):{{f['CodeSize']}}
Ephemeral Storage Size (MB):{{f['EphemeralStorage']}}
X-Ray tracing:{{f['TracingConfig']}}
SnapStart Status (only Java):{{f['SnapStartOptimizationStatus']}}

894 |
895 | {% endif %} 896 |
897 |

898 | 899 | {% if data.esms != [] %} 900 | 901 |
902 |
903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | {% for e in data.esms %} 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | {% if e.StartingPosition is defined %} 936 | 937 | 938 | 939 | 940 | {% endif %} 941 | {% if e.StartingPositionTimestamp is defined %} 942 | 943 | 944 | 945 | 946 | {% endif %} 947 | {% if e.StateTransitionReason is defined %} 948 | 949 | 950 | 951 | 952 | {% endif %} 953 | {% if e.AmazonManagedKafkaEventSourceConfig is defined %} 954 | 955 | 956 | 957 | 958 | {% endif %} 959 | {% if e.BisectBatchOnFunctionError is defined %} 960 | 961 | 962 | 963 | 964 | {% endif %} 965 | {% if e.TumblingWindowInSeconds is defined %} 966 | 967 | 968 | 969 | 970 | {% endif %} 971 | {% if e.DestinationConfig is defined %} 972 | {% if e.DestinationConfig.OnFailure is defined %} 973 | 974 | 975 | 976 | 977 | {% endif %} 978 | {% if e.DestinationConfig.OnSuccess is defined %} 979 | 980 | 981 | 982 | 983 | {% endif %} 984 | {% endif %} 985 | {% if e.FunctionResponseTypes is defined %} 986 | 987 | 988 | 989 | 990 | {% endif %} 991 | {% if e.DocumentDBEventSourceConfig is defined %} 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 | 1003 | 1004 | {% endif %} 1005 | {% if e.FilterCriteria is defined %} 1006 | 1007 | 1008 | 1009 | {% for f in e.FilterCriteria.Filters %} 1010 | 1011 | 1012 | 1013 | 1014 | {% endfor %} 1015 | {% endif %} 1016 | 1017 | 1018 | 1019 | 1020 | {% if e.MaximumRecordAgeInSeconds is defined %} 1021 | 1022 | 1023 | 1024 | 1025 | {% endif %} 1026 | {% if e.MaximumRetryAttempts is defined %} 1027 | 1028 | 1029 | 1030 | 1031 | {% endif %} 1032 | {% if e.ParallelizationFactor is defined %} 1033 | 1034 | 1035 | 1036 | 1037 | {% endif %} 1038 | {% if e.ScalingConfig is defined %} 1039 | 1040 | 1041 | 1042 | 1043 | {% endif %} 1044 | {% if e.SelfManagedEventSource is defined %} 1045 | 1046 | 1047 | 1048 | 1049 | {% endif %} 1050 | {% if e.SelfManagedKafkaEventSourceConfig is defined %} 1051 | 1052 | 1053 | 1054 | 1055 | {% endif %} 1056 | 1057 | 1058 | 1059 | {% endfor %} 1060 | 1061 |
Event Source Mapping Configuration Details
Event Source ARN:{{e.EventSourceArn}}
Linked Function ARN:{{e.FunctionArn}}
State of Event Source Mapping:{{e.State}}
Batch Size:{{e.BatchSize}}
Last Modified:{{e.LastModified}}
Last Processing Result:{{e.LastProcessingResult}}
Starting Position:{{e.StartingPosition}}
Starting Position Timestamp:{{e.StartingPositionTimestamp}}
State Transition Reason:{{e.StateTransitionReason}}
Kafka Consumer Group Id:{{e.AmazonManagedKafkaEventSourceConfig.ConsumerGroupId}}
Bisect Batch on Function Error Config:{{e.BisectBatchOnFunctionError}}
Tumbling Window (s):{{e.TumblingWindowInSeconds}}
Destination On Failure:{{e.DestinationConfig.OnFailure.Destination}}
Destination On Success:{{e.DestinationConfig.OnSuccess.Destination}}
Function Response Types:{{e.FunctionResponseTypes}}
Collection Name:{{e.DocumentDBEventSourceConfig.CollectionName}}
Database Name:{{e.DocumentDBEventSourceConfig.DatabaseName}}
Full Document:{{e.DocumentDBEventSourceConfig.FullDocument}}
Filter Configurations:
{{f.Pattern}}
Max Batching Window (s):{{e.MaximumBatchingWindowInSeconds}}
Max Record Age (s):{{e.MaximumRecordAgeInSeconds}}
Max Retry Attempts:{{e.MaximumRetryAttempts}}
Parallelization Factor:{{e.ParallelizationFactor}}
Scaling Configuration (Concurrency):{{e.ScalingConfig.MaximumConcurrency}}
Self Managed Event Source Endpoints:{{e.SelfManagedEventSource.Endpoints}}
Self Managed Kafka Event Source Config:Consumer Group ID: {{e.SelfManagedKafkaEventSourceConfig.ConsumerGroupId}}

1062 |
1063 | {% endif %} 1064 |
1065 |
1066 |
1067 |

END OF REPORT

1068 |
1069 | 1070 | 1071 | 1072 | 1073 | 1074 | -------------------------------------------------------------------------------- /src/lambda/function-generate-selected/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Serverless Operational Review 8 | 9 | 43 | 44 |
45 |
46 |

Serverless Operational Review

47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
AWS Account:
{{data.account}}
Region:
{{data.region}}
57 |
58 |
59 | 60 |
61 |

AWS Lambda Review

62 |
63 |
64 | 65 |
66 |
67 |

RED Danger Zone - Risks

68 | 72 |
73 |
74 | 75 |
76 |

Deprecated Function Runtime Versions!

77 | 80 |

81 |
82 |
Python functions
83 | {% if data.dep_python_functions != [] %} 84 |
85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | {% for fn in data.dep_python_functions %} 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | {% endfor %} 107 | 108 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
109 |
110 | {% else %} 111 |
112 |

No functions with a deprecated version of this runtime.

113 |
114 | {% endif %} 115 |
116 |
117 |
118 |
Node.js functions
119 | {% if data.dep_nodejs_functions != [] %} 120 |
121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | {% for fn in data.dep_nodejs_functions %} 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | {% endfor %} 143 | 144 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
145 |
146 | {% else %} 147 |
148 |

No functions with a deprecated version of this runtime.

149 |
150 | {% endif %} 151 |
152 |
153 |
154 |
Java functions
155 | {% if data.dep_java_functions != [] %} 156 |
157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | {% for fn in data.dep_java_functions %} 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | {% endfor %} 179 | 180 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
181 |
182 | {% else %} 183 |
184 |

No functions with a deprecated version of this runtime.

185 |
186 | {% endif %} 187 |
188 |
189 |
190 |
.Net functions
191 | {% if data.dep_dotnet_functions != [] %} 192 |
193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | {% for fn in data.dep_dotnet_functions %} 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | {% endfor %} 215 | 216 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
217 |
218 | {% else %} 219 |
220 |

No functions with a deprecated version of this runtime.

221 |
222 | {% endif %} 223 |
224 |
225 |
226 |
Ruby functions
227 | {% if data.dep_ruby_functions != [] %} 228 |
229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | {% for fn in data.dep_ruby_functions %} 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | {% endfor %} 251 | 252 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
253 |
254 | {% else %} 255 |
256 |

No functions with a deprecated version of this runtime.

257 |
258 | {% endif %} 259 |
260 |
261 |
262 |
Golang functions
263 | {% if data.dep_golang_functions != [] %} 264 |
265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | {% for fn in data.dep_golang_functions %} 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | {% endfor %} 287 | 288 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
289 |
290 | {% else %} 291 |
292 |

No functions with a deprecated version of this runtime.

293 |
294 | {% endif %} 295 |
296 |
297 |
298 |
Custom runtime functions
299 | {% if data.dep_custom_functions != [] %} 300 |
301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | {% for fn in data.dep_custom_functions %} 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | {% endfor %} 323 | 324 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
325 |
326 | {% else %} 327 |
328 |

No functions with a deprecated version of this runtime.

329 |
330 | {% endif %} 331 |
332 |

333 | 334 | {% if data.warnings_ta_high_errors != [] %} 335 |
336 |

Functions with High Error Rates

337 |
338 | 339 |
340 |
341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | {% for warnings_ta_high_errors in data.warnings_ta_high_errors %} 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | {% endfor %} 361 | 362 |
Function ARNStatusAverage Daily Errors %Maximum Daily Errors %Date of Max Error Rate
{{warnings_ta_high_errors['FunctionArn']}}{{warnings_ta_high_errors['Status']}}{{warnings_ta_high_errors['AverageDailyErrorRatePerc']}}{{warnings_ta_high_errors['MaxDailyErrorRatePerc']}}{{warnings_ta_high_errors['DateOfMaxErrorRate']}}
363 |
364 |
365 | {% endif %} 366 | 367 |

368 | 369 | {% if data.warnings_ta_excessive_timeouts != [] %} 370 |
371 |

Functions with Excessive Timeouts

372 |
373 | 374 |
375 |
376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | {% for warnings_ta_excessive_timeout in data.warnings_ta_excessive_timeouts %} 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | {% endfor %} 398 | 399 |
Function ARNStatusAverage Daily Timeouts %Maximum Daily Timeouts %Date of Max TimeoutsFunction Timeout Config
{{warnings_ta_excessive_timeout['FunctionArn']}}{{warnings_ta_excessive_timeout['Status']}}{{warnings_ta_excessive_timeout['AverageDailyTimeoutRatePerc']}}{{warnings_ta_excessive_timeout['MaxDailyTimeoutRatePerc']}}{{warnings_ta_excessive_timeout['DateOfMaxTimeoutRate']}}{{warnings_ta_excessive_timeout['FunctionTimeoutSettings']}}
400 |
401 |
402 | {% endif %} 403 | 404 |

405 | 406 | {% if data.warnings_vpc != [] %} 407 |
408 |

VPC-enabled Functions - Incorrect multi-AZ configuration

409 |
410 | 411 |
412 |
413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | {% for warnings_vpc in data.warnings_vpc %} 423 | 424 | 425 | {% for subaz in warnings_vpc['Subnets'] %} 426 | 427 | 428 | 429 | 430 | 431 | {% endfor %} 432 | 433 | {% endfor %} 434 | 435 |
Function ARNSubnetIDAvalabitlity Zone
{{warnings_vpc['FunctionArn']}}
{{subaz['SubnetId']}}{{subaz['AZ']}}
436 |
437 | {% endif %} 438 |
439 | 440 |
441 | 442 |
443 |
444 |

YELLOW Warnings Zone

445 | 449 |
450 |
451 |
452 |
Python functions
453 | {% if data.sup_python_functions != [] %} 454 |
455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | {% for fn in data.sup_python_functions %} 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | {% endfor %} 477 | 478 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
479 |
480 | {% else %} 481 |
482 |

No functions with this runtime are scheduled to deprecate at the moment.

483 |
484 | {% endif %} 485 |
486 |
487 |
488 |
Node.js functions
489 | {% if data.sup_nodejs_functions != [] %} 490 |
491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | {% for fn in data.sup_nodejs_functions %} 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | {% endfor %} 513 | 514 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
515 |
516 | {% else %} 517 |
518 |

No functions with this runtime are scheduled to deprecate at the moment.

519 |
520 | {% endif %} 521 |
522 |
523 |
524 |
Java functions
525 | {% if data.sup_java_functions != [] %} 526 |
527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | {% for fn in data.sup_java_functions %} 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | {% endfor %} 549 | 550 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
551 |
552 | {% else %} 553 |
554 |

No functions with this runtime are scheduled to deprecate at the moment.

555 |
556 | {% endif %} 557 |
558 |
559 |
560 |
.Net functions
561 | {% if data.sup_dotnet_functions != [] %} 562 |
563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | {% for fn in data.sup_dotnet_functions %} 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | {% endfor %} 585 | 586 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
587 |
588 | {% else %} 589 |
590 |

No functions with this runtime are scheduled to deprecate at the moment.

591 |
592 | {% endif %} 593 |
594 |
595 |
596 |
Ruby functions
597 | {% if data.sup_ruby_functions != [] %} 598 |
599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | {% for fn in data.sup_ruby_functions %} 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | {% endfor %} 621 | 622 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
623 |
624 | {% else %} 625 |
626 |

No functions with this runtime are scheduled to deprecate at the moment.

627 |
628 | {% endif %} 629 |
630 |
631 |
632 |
Golang functions
633 | {% if data.sup_golang_functions != [] %} 634 |
635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | {% for fn in data.sup_golang_functions %} 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | {% endfor %} 657 | 658 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
659 |
660 | {% else %} 661 |
662 |

No functions with this runtime are scheduled to deprecate at the moment.

663 |
664 | {% endif %} 665 |
666 |
667 |
668 |
Custom runtime functions
669 | {% if data.sup_custom_functions != [] %} 670 |
671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | {% for fn in data.sup_custom_functions %} 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | {% endfor %} 693 | 694 |
Function NameFunction VersionRuntime VersionDeprecation DateUpdate Blocked DateCreation Block Date
{{fn['FunctionName']}}{{fn['Version']}}{{fn['Runtime']}}{{fn['Deprecation date']}}{{fn['Block function update']}}{{fn['Block function create']}}
695 |
696 | {% else %} 697 |
698 |

No functions with this runtime are scheduled to deprecate at the moment.

699 |
700 | {% endif %} 701 |
702 |
703 |
704 | 705 |
706 |

BLUE Optimization Zone - Recommendations

707 | 710 |
711 |
712 | 713 | 714 | {% if data.recfunctions != [] %} 715 |
716 |

Memory Optimization Recommendations

717 | 721 |
722 |
723 | 724 |
725 |
726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | {% for rec in data.recfunctions %} 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | {% endfor %} 752 | 753 |
Function ARNFindingNumber of InvocationsCurrent Memory SizeRecommended Memory SizeEst. Montly Saving (USD)Est. Montly Saving %Duration Current - Projected (ms)
{{rec['FunctionArn']}}{{rec['FindingReasonCodes'][0]}}{{rec['NumberOfInvocations']}}{{rec['CurrentMemorySize']}}{{rec['MemorySizeRecommendationOptions'][0]['MemorySize']}}{{rec['MemorySizeRecommendationOptions'][0]['SavingsOpportunity']['EstimatedMonthlySavings']['Value']}}{{rec['MemorySizeRecommendationOptions'][0]['SavingsOpportunity']['SavingsOpportunityPercentage']}}{{rec['UtilizationMetrics'][0]['Value']}} - {{rec['MemorySizeRecommendationOptions'][0]['ProjectedUtilizationMetrics'][1]['Value']}}
754 |
755 | {% endif %} 756 |
757 |

758 | 759 | 760 | {% if data.recarch != [] %} 761 |
762 |

Functions running on x86 Architecture Type

763 | 764 |
765 | 766 |
767 |
768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | {% for f in data.recarch %} 776 | 777 | 778 | 779 | {% endfor %} 780 | 781 |
Functions running x86 architecture, consider upgrade to arm64
{{f}}
782 |
783 | {% endif %} 784 |
785 |

786 |
787 | 788 |
789 |
790 |

Reviewed Functions - Configurations

791 | 792 |
793 |
794 | 795 | {% if data.reviewed_functions != [] %} 796 |
797 |
798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | {% for f in data.reviewed_functions %} 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | {% if f.ReservedConcurrency != {} %} 833 | 834 | {% else %} 835 | 836 | {% endif %} 837 | 838 | 839 | 840 | {% if f.ProvisionedConcurrencyConfigs != [] %} 841 | {% for c in f.ProvisionedConcurrencyConfigs %} 842 | {% if f.FunctionArn == c.FunctionArn %} 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | {% endif %} 863 | {% endfor %} 864 | {% else %} 865 | 866 | {% endif %} 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | {% endfor %} 892 | 893 |
Reviewed functions configurations
Function ARN:{{f['FunctionArn']}}
Memory (MB):<{{f['MemorySize']}}
Timeout (S):{{f['Timeout']}}
Runtime:{{f['Runtime']}}
Architecture:{{f['Architectures']}}
Package Type:{{f['PackageType']}}
Reserved Concurrency:{{f.ReservedConcurrency.ReservedConcurrentExecutions}}0
Provisioned Concurrency Config:
Status: {{c.Status}}
Requested PC Executions: {{c.RequestedProvisionedConcurrentExecutions}}
Allocated PC Executions: {{c.AllocatedProvisionedConcurrentExecutions}}
Available PC Executions: {{c.AvailableProvisionedConcurrentExecutions}}
Last Modified: {{c.LastModified}} Not Set
Execution Role:{{f['Role']}}
Code Size (B):{{f['CodeSize']}}
Ephemeral Storage Size (MB):{{f['EphemeralStorage']}}
X-Ray tracing:{{f['TracingConfig']}}
SnapStart Status (only Java):{{f['SnapStartOptimizationStatus']}}

894 |
895 | {% endif %} 896 |
897 |

898 | 899 | {% if data.esms != [] %} 900 | 901 |
902 |
903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | {% for e in data.esms %} 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | {% if e.StartingPosition is defined %} 936 | 937 | 938 | 939 | 940 | {% endif %} 941 | {% if e.StartingPositionTimestamp is defined %} 942 | 943 | 944 | 945 | 946 | {% endif %} 947 | {% if e.StateTransitionReason is defined %} 948 | 949 | 950 | 951 | 952 | {% endif %} 953 | {% if e.AmazonManagedKafkaEventSourceConfig is defined %} 954 | 955 | 956 | 957 | 958 | {% endif %} 959 | {% if e.BisectBatchOnFunctionError is defined %} 960 | 961 | 962 | 963 | 964 | {% endif %} 965 | {% if e.TumblingWindowInSeconds is defined %} 966 | 967 | 968 | 969 | 970 | {% endif %} 971 | {% if e.DestinationConfig is defined %} 972 | {% if e.DestinationConfig.OnFailure is defined %} 973 | 974 | 975 | 976 | 977 | {% endif %} 978 | {% if e.DestinationConfig.OnSuccess is defined %} 979 | 980 | 981 | 982 | 983 | {% endif %} 984 | {% endif %} 985 | {% if e.FunctionResponseTypes is defined %} 986 | 987 | 988 | 989 | 990 | {% endif %} 991 | {% if e.DocumentDBEventSourceConfig is defined %} 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 | 1003 | 1004 | {% endif %} 1005 | {% if e.FilterCriteria is defined %} 1006 | 1007 | 1008 | 1009 | {% for f in e.FilterCriteria.Filters %} 1010 | 1011 | 1012 | 1013 | 1014 | {% endfor %} 1015 | {% endif %} 1016 | 1017 | 1018 | 1019 | 1020 | {% if e.MaximumRecordAgeInSeconds is defined %} 1021 | 1022 | 1023 | 1024 | 1025 | {% endif %} 1026 | {% if e.MaximumRetryAttempts is defined %} 1027 | 1028 | 1029 | 1030 | 1031 | {% endif %} 1032 | {% if e.ParallelizationFactor is defined %} 1033 | 1034 | 1035 | 1036 | 1037 | {% endif %} 1038 | {% if e.ScalingConfig is defined %} 1039 | 1040 | 1041 | 1042 | 1043 | {% endif %} 1044 | {% if e.SelfManagedEventSource is defined %} 1045 | 1046 | 1047 | 1048 | 1049 | {% endif %} 1050 | {% if e.SelfManagedKafkaEventSourceConfig is defined %} 1051 | 1052 | 1053 | 1054 | 1055 | {% endif %} 1056 | 1057 | 1058 | 1059 | {% endfor %} 1060 | 1061 |
Event Source Mapping Configuration Details
Event Source ARN:{{e.EventSourceArn}}
Linked Function ARN:{{e.FunctionArn}}
State of Event Source Mapping:{{e.State}}
Batch Size:{{e.BatchSize}}
Last Modified:{{e.LastModified}}
Last Processing Result:{{e.LastProcessingResult}}
Starting Position:{{e.StartingPosition}}
Starting Position Timestamp:{{e.StartingPositionTimestamp}}
State Transition Reason:{{e.StateTransitionReason}}
Kafka Consumer Group Id:{{e.AmazonManagedKafkaEventSourceConfig.ConsumerGroupId}}
Bisect Batch on Function Error Config:{{e.BisectBatchOnFunctionError}}
Tumbling Window (s):{{e.TumblingWindowInSeconds}}
Destination On Failure:{{e.DestinationConfig.OnFailure.Destination}}
Destination On Success:{{e.DestinationConfig.OnSuccess.Destination}}
Function Response Types:{{e.FunctionResponseTypes}}
Collection Name:{{e.DocumentDBEventSourceConfig.CollectionName}}
Database Name:{{e.DocumentDBEventSourceConfig.DatabaseName}}
Full Document:{{e.DocumentDBEventSourceConfig.FullDocument}}
Filter Configurations:
{{f.Pattern}}
Max Batching Window (s):{{e.MaximumBatchingWindowInSeconds}}
Max Record Age (s):{{e.MaximumRecordAgeInSeconds}}
Max Retry Attempts:{{e.MaximumRetryAttempts}}
Parallelization Factor:{{e.ParallelizationFactor}}
Scaling Configuration (Concurrency):{{e.ScalingConfig.MaximumConcurrency}}
Self Managed Event Source Endpoints:{{e.SelfManagedEventSource.Endpoints}}
Self Managed Kafka Event Source Config:Consumer Group ID: {{e.SelfManagedKafkaEventSourceConfig.ConsumerGroupId}}

1062 |
1063 | {% endif %} 1064 |
1065 |
1066 |
1067 |

END OF REPORT

1068 |
1069 | 1070 | 1071 | 1072 | 1073 | 1074 | --------------------------------------------------------------------------------