diff --git a/CHANGELOG.md b/CHANGELOG.md index 3033038..0699752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,64 +5,99 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [6.3.0] - 2024-11-22 + +### Added + +- Simulation capability for quota utilization events to test notification workflows +- Configurable monitoring for SageMaker and Connect services +- Optional Spoke notification stack for localized alerts +- Support for AWS GCR Regions +- Link to quota limit increase request in email/Slack notifications +- Custom quota threshold value input option + +### Changed + +- In ORG/HYBRID mode, Resetting SSM parameters to NOP now triggers: + - Deletion of stack instances + - Clearing of event bus permissions +- In HYBRID mode, deployment now proceeds with valid entries from either the OU ID list or the Account ID list, rather than requiring both to be valid +- Made SNS notifications human readable + +### Fixed + +- GitHub Issues [#155](https://github.com/aws-solutions/quota-monitor-for-aws/issues/155), [#157](https://github.com/aws-solutions/quota-monitor-for-aws/issues/157), [#177](https://github.com/aws-solutions/quota-monitor-for-aws/issues/177), [#187](https://github.com/aws-solutions/quota-monitor-for-aws/issues/187), and [#202](https://github.com/aws-solutions/quota-monitor-for-aws/issues/202) + ## [6.2.11] - 2024-10-10 ### Changed + - Add batching to getQuotasWithUtilizationMetrics function - Refactor _putMonitoredQuotas function to use batch writes - Changed the memory allocation for the QMListManager Lambda function to 256 MB - Added better error handling for CloudWatch ValidationErrors, with attempt to identify problematic quotas ### Fixed + - GitHub Issues [#200](https://github.com/aws-solutions/quota-monitor-for-aws/issues/200) and [#201](https://github.com/aws-solutions/quota-monitor-for-aws/issues/201) ## [6.2.10] - 2024-09-18 ### Fixed + - Update path-to-regexp to address [CVE-2024-45296](https://nvd.nist.gov/vuln/detail/CVE-2024-45296) - Update micromatch to address [CVE-2024-4067](https://nvd.nist.gov/vuln/detail/CVE-2024-4067) ## [6.2.9] - 2024-07-31 ### Fixed + - Update fast-xml-parser to address [CVE-2024-41818](https://nvd.nist.gov/vuln/detail/CVE-2024-41818) ## [6.2.8] - 2024-06-26 ### Fixed + - Update dependency to address [CVE-2024-4068](https://avd.aquasec.com/nvd/cve-2024-4068) ## [6.2.7] - 2024-06-10 ### Fixed + - Added batching to get getMetricData calls to avoid limits - Added quotaCode to metric Ids to avoid duplicate Ids. ## [6.2.6] - 2024-03-18 ### Changed + - First of month schedule for quotaListManager Lambda function changed to every 30 days - Add rate limiting delay between listServiceQuota API calls - Add page size to Service Quotas API calls ### Fixed + - GitHub Issue [#183](https://github.com/aws-solutions/quota-monitor-for-aws/issues/183), PR [#147](https://github.com/aws-solutions/quota-monitor-for-aws/pull/47) - fix expiration of DynamoDB records ## [6.2.5] - 2024-01-08 ### Changed + - Made reporting of OK Messages optional - Added percentage marker on Service Quota notifications ### Fixed + - Added manual resource cleanup after sqs message consumption ## [6.2.4] - 2023-11-09 ### Changed + - Scoped permissions down for Stackset operations ### Fixed + - [Error](https://github.com/aws-solutions/quota-monitor-for-aws/issues/172) in saving notifications to summary table ## [6.2.3] - 2023-10-24 @@ -74,31 +109,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [6.2.2] - 2023-08-16 ### Added + - Service Quotas spoke template parameters exposed in the hub template too ### Changed + - Lambda run times upgraded to Node.js18 - Dependency updates - ## [6.2.1] - 2023-06-28 + ### Changed + - Dependency updates addressing [CVE-2023-26920](https://cwe.mitre.org/data/definitions/1321.html) ## [6.2.0] - 2023-06-01 ### Added + - Support for monitoring resources with Service Catalog AppRegistry ### Changed + - Customer Managed Keys for the resources in hub stacks ### Fixed + - Bugs resulting in dynamoDb tables not being populated ## [6.1.0] - 2023-04-05 ### Added + - Support for monitoring all usage reporting quotas from all services supported by Service Quotas - Ability to mute selected notifications - Support for GovCloud regions @@ -107,6 +149,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow customization to Stack Set deployments configuration ### Changed + - Use AWS Managed keys for the resources in the templates to help reduce the cost of deployment. ## [6.0.0] - 2022-10-14 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b6a8e35..779379f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ information to effectively respond to your bug report or contribution. We welcome you to use the GitHub issue tracker to report bugs or suggest features. -When filing an issue, please check [existing open](https://github.com/aws-solutions/quota-monitor-for-aws/issues), or [recently closed](https://github.com/aws-solutions/quota-monitor-for-aws/issues?q=is%3Aissue+is%3Aclosed), issues to make sure somebody else hasn't already +When filing an issue, please check [existing open](https://github.com/aws-solutions/quota-monitor-for-aws/issues), or [recently closed](https://github.com/aws-solutions/quota-monitor-for-aws/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: - A reproducible test case or series of steps @@ -38,6 +38,10 @@ To send us a pull request, please: GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). +## Finding contributions to work on + +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'](https://github.com/aws-solutions/quota-monitor-for-aws/labels/help%20wanted) issues is a great place to start. + ## Code of Conduct This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). @@ -52,4 +56,4 @@ If you discover a potential security issue in this project we ask that you notif See the [LICENSE](https://github.com/aws-solutions/quota-monitor-for-aws/blob/main/LICENSE.txt) file for our project's licensing. We will ask you to confirm the licensing of your contribution. -We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. +We may ask you to sign a [Contributor License Agreement (CLA)](https://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/LICENSE.txt b/LICENSE.txt index c925d29..298f0e2 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,174 +1,174 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. diff --git a/NOTICE.txt b/NOTICE.txt index 18868fb..79b6872 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -12,45 +12,565 @@ THIRD PARTY COMPONENTS ********************** This software includes third party software subject to the following copyrights: -AWS SDK under the Apache License Version 2.0 -async under the Massachusetts Institute of Technology (MIT) license -https under the Massachusetts Institute of Technology (MIT) license -moment under the Massachusetts Institute of Technology (MIT) license -underscore under the Massachusetts Institute of Technology (MIT) license -uuid under the Massachusetts Institute of Technology (MIT) license -url under the Massachusetts Institute of Technology (MIT) license -@aws-sdk/client-cloudformation under Apache License 2.0 -@aws-sdk/client-cloudwatch under Apache License 2.0 -@aws-sdk/client-cloudwatch-events under Apache License 2.0 -@aws-sdk/client-dynamodb under Apache License 2.0 -@aws-sdk/client-ec2 under Apache License 2.0 -@aws-sdk/client-organizations under Apache License 2.0 -@aws-sdk/client-service-quotas under Apache License 2.0 -@aws-sdk/client-ssm under Apache License 2.0 -@aws-sdk/client-sqs under Apache License 2.0 -@aws-sdk/client-sns under Apache License 2.0 -@aws-sdk/client-support under Apache License 2.0 -@aws-sdk/lib-dynamodb under Apache License 2.0 -@aws-sdk/client-dynamodb-streams under Apache License 2.0 -got under the Massachusetts Institute of Technology (MIT) license -winston under the Massachusetts Institute of Technology (MIT) license -@types/jest under the Massachusetts Institute of Technology (MIT) license -@types/node under the Massachusetts Institute of Technology (MIT) license -aws-sdk-client-mock under the Massachusetts Institute of Technology (MIT) license -aws-sdk-client-mock-jest under the Massachusetts Institute of Technology (MIT) license -aws-cdk under Apache License 2.0 -aws-cdk-lib under Apache License 2.0 -cdk-nag under Apache License 2.0 -constructs under Apache License 2.0 -jest under the Massachusetts Institute of Technology (MIT) license -ts-jest under the Massachusetts Institute of Technology (MIT) license -ts-node under the Massachusetts Institute of Technology (MIT) license -typescript under Apache License 2.0 -uuid under the Massachusetts Institute of Technology (MIT) license -@types/uuid under the Massachusetts Institute of Technology (MIT) license -@typescript-eslint/eslint-plugin under the Massachusetts Institute of Technology (MIT) license -@typescript-eslint/parser under BSD-2-Clause license -eslint under the Massachusetts Institute of Technology (MIT) license -eslint-config-prettier under the Massachusetts Institute of Technology (MIT) license -eslint-plugin-prettier under the Massachusetts Institute of Technology (MIT) license -prettier under the Massachusetts Institute of Technology (MIT) license +@aws-cdk/aws-servicecatalogappregistry-alpha under the Apache-2.0 license +@aws-sdk/client-cloudformation under the Apache-2.0 license +@aws-sdk/client-cloudwatch under the Apache-2.0 license +@aws-sdk/client-cloudwatch-events under the Apache-2.0 license +@aws-sdk/client-dynamodb under the Apache-2.0 license +@aws-sdk/client-dynamodb-streams under the Apache-2.0 license +@aws-sdk/client-ec2 under the Apache-2.0 license +@aws-sdk/client-organizations under the Apache-2.0 license +@aws-sdk/client-service-quotas under the Apache-2.0 license +@aws-sdk/client-sns under the Apache-2.0 license +@aws-sdk/client-sqs under the Apache-2.0 license +@aws-sdk/client-ssm under the Apache-2.0 license +@aws-sdk/client-support under the Apache-2.0 license +@aws-sdk/lib-dynamodb under the Apache-2.0 license +@types/aws-lambda under the MIT license +@types/jest under the MIT license +@types/node under the MIT license +@types/uuid under the MIT license +@typescript-eslint/eslint-plugin under the MIT license +@typescript-eslint/parser under the BSD-2-Clause license +aws-cdk under the Apache-2.0 license +aws-cdk-lib under the Apache-2.0 license +aws-sdk-client-mock under the MIT license +aws-sdk-client-mock-jest under the MIT license +cdk-nag under the Apache-2.0 license +constructs under the Apache-2.0 license +eslint under the MIT license +eslint-config-prettier under the MIT license +eslint-plugin-prettier under the MIT license +got under the MIT license +https under the MIT license +jest under the MIT license +prettier under the MIT license +ts-jest under the MIT license +ts-node under the MIT license +typescript under the Apache-2.0 license +url under the MIT license +uuid under the MIT license +winston under the MIT license +@aashutoshrathi/word-wrap under the MIT license +@ampproject/remapping under the Apache-2.0 license +@aws-cdk/asset-awscli-v1 under the Apache-2.0 license +@aws-cdk/asset-kubectl-v20 under the Apache-2.0 license +@aws-cdk/asset-node-proxy-agent-v6 under the Apache-2.0 license +@aws-crypto/sha256-browser under the Apache-2.0 license +@aws-crypto/sha256-js under the Apache-2.0 license +@aws-crypto/supports-web-crypto under the Apache-2.0 license +@aws-crypto/util under the Apache-2.0 license +@aws-sdk/client-sso-oidc under the Apache-2.0 license +@aws-sdk/client-sso under the Apache-2.0 license +@aws-sdk/client-sts under the Apache-2.0 license +@aws-sdk/core under the Apache-2.0 license +@aws-sdk/credential-provider-env under the Apache-2.0 license +@aws-sdk/credential-provider-http under the Apache-2.0 license +@aws-sdk/credential-provider-ini under the Apache-2.0 license +@aws-sdk/credential-provider-node under the Apache-2.0 license +@aws-sdk/credential-provider-process under the Apache-2.0 license +@aws-sdk/credential-provider-sso under the Apache-2.0 license +@aws-sdk/credential-provider-web-identity under the Apache-2.0 license +@aws-sdk/endpoint-cache under the Apache-2.0 license +@aws-sdk/middleware-endpoint-discovery under the Apache-2.0 license +@aws-sdk/middleware-host-header under the Apache-2.0 license +@aws-sdk/middleware-logger under the Apache-2.0 license +@aws-sdk/middleware-recursion-detection under the Apache-2.0 license +@aws-sdk/middleware-sdk-ec2 under the Apache-2.0 license +@aws-sdk/middleware-sdk-sqs under the Apache-2.0 license +@aws-sdk/middleware-user-agent under the Apache-2.0 license +@aws-sdk/region-config-resolver under the Apache-2.0 license +@aws-sdk/token-providers under the Apache-2.0 license +@aws-sdk/types under the Apache-2.0 license +@aws-sdk/util-dynamodb under the Apache-2.0 license +@aws-sdk/util-endpoints under the Apache-2.0 license +@aws-sdk/util-format-url under the Apache-2.0 license +@aws-sdk/util-locate-window under the Apache-2.0 license +@aws-sdk/util-user-agent-browser under the Apache-2.0 license +@aws-sdk/util-user-agent-node under the Apache-2.0 license +@babel/code-frame under the MIT license +@babel/compat-data under the MIT license +@babel/generator under the MIT license +@babel/helper-compilation-targets under the MIT license +@babel/helper-environment-visitor under the MIT license +@babel/helper-function-name under the MIT license +@babel/helper-hoist-variables under the MIT license +@babel/helper-module-imports under the MIT license +@babel/helper-module-transforms under the MIT license +@babel/helper-plugin-utils under the MIT license +@babel/helper-simple-access under the MIT license +@babel/helper-split-export-declaration under the MIT license +@babel/helper-string-parser under the MIT license +@babel/helper-validator-identifier under the MIT license +@babel/helper-validator-option under the MIT license +@babel/helpers under the MIT license +@babel/highlight under the MIT license +@babel/parser under the MIT license +@babel/plugin-syntax-async-generators under the MIT license +@babel/plugin-syntax-bigint under the MIT license +@babel/plugin-syntax-class-properties under the MIT license +@babel/plugin-syntax-import-meta under the MIT license +@babel/plugin-syntax-json-strings under the MIT license +@babel/plugin-syntax-jsx under the MIT license +@babel/plugin-syntax-logical-assignment-operators under the MIT license +@babel/plugin-syntax-nullish-coalescing-operator under the MIT license +@babel/plugin-syntax-numeric-separator under the MIT license +@babel/plugin-syntax-object-rest-spread under the MIT license +@babel/plugin-syntax-optional-catch-binding under the MIT license +@babel/plugin-syntax-optional-chaining under the MIT license +@babel/plugin-syntax-top-level-await under the MIT license +@babel/plugin-syntax-typescript under the MIT license +@babel/template under the MIT license +@babel/traverse under the MIT license +@balena/dockerignore under the Apache-2.0 license +@bcoe/v8-coverage under the MIT license +@colors/colors under the MIT license +@cspotcode/source-map-support under the MIT license +@dabh/diagnostics under the MIT license +@eslint-community/eslint-utils under the MIT license +@eslint-community/regexpp under the MIT license +@eslint/eslintrc under the MIT license +@eslint/js under the MIT license +@humanwhocodes/config-array under the Apache-2.0 license +@humanwhocodes/module-importer under the Apache-2.0 license +@humanwhocodes/object-schema under the BSD-3-Clause license +@istanbuljs/load-nyc-config under the ISC license +@istanbuljs/schema under the MIT license +@jest/console under the MIT license +@jest/environment under the MIT license +@jest/expect-utils under the MIT license +@jest/expect under the MIT license +@jest/fake-timers under the MIT license +@jest/globals under the MIT license +@jest/reporters under the MIT license +@jest/schemas under the MIT license +@jest/source-map under the MIT license +@jest/test-result under the MIT license +@jest/test-sequencer under the MIT license +@jest/transform under the MIT license +@jridgewell/gen-mapping under the MIT license +@jridgewell/resolve-uri under the MIT license +@jridgewell/set-array under the MIT license +@jridgewell/sourcemap-codec under the MIT license +@jridgewell/trace-mapping under the MIT license +@nodelib/fs.scandir under the MIT license +@nodelib/fs.stat under the MIT license +@nodelib/fs.walk under the MIT license +@sinclair/typebox under the MIT license +@sindresorhus/is under the MIT license +@sinonjs/commons under the BSD-3-Clause license +@sinonjs/samsam under the BSD-3-Clause license +@sinonjs/text-encoding under the (Unlicense OR Apache-2.0) license +@smithy/abort-controller under the Apache-2.0 license +@smithy/config-resolver under the Apache-2.0 license +@smithy/credential-provider-imds under the Apache-2.0 license +@smithy/fetch-http-handler under the Apache-2.0 license +@smithy/hash-node under the Apache-2.0 license +@smithy/invalid-dependency under the Apache-2.0 license +@smithy/is-array-buffer under the Apache-2.0 license +@smithy/md5-js under the Apache-2.0 license +@smithy/middleware-compression under the Apache-2.0 license +@smithy/middleware-content-length under the Apache-2.0 license +@smithy/middleware-endpoint under the Apache-2.0 license +@smithy/middleware-retry under the Apache-2.0 license +@smithy/middleware-serde under the Apache-2.0 license +@smithy/middleware-stack under the Apache-2.0 license +@smithy/node-config-provider under the Apache-2.0 license +@smithy/node-http-handler under the Apache-2.0 license +@smithy/property-provider under the Apache-2.0 license +@smithy/protocol-http under the Apache-2.0 license +@smithy/querystring-builder under the Apache-2.0 license +@smithy/querystring-parser under the Apache-2.0 license +@smithy/service-error-classification under the Apache-2.0 license +@smithy/shared-ini-file-loader under the Apache-2.0 license +@smithy/signature-v4 under the Apache-2.0 license +@smithy/smithy-client under the Apache-2.0 license +@smithy/url-parser under the Apache-2.0 license +@smithy/util-base64 under the Apache-2.0 license +@smithy/util-body-length-browser under the Apache-2.0 license +@smithy/util-body-length-node under the Apache-2.0 license +@smithy/util-buffer-from under the Apache-2.0 license +@smithy/util-config-provider under the Apache-2.0 license +@smithy/util-defaults-mode-browser under the Apache-2.0 license +@smithy/util-defaults-mode-node under the Apache-2.0 license +@smithy/util-hex-encoding under the Apache-2.0 license +@smithy/util-middleware under the Apache-2.0 license +@smithy/util-retry under the Apache-2.0 license +@smithy/util-stream under the Apache-2.0 license +@smithy/util-uri-escape under the Apache-2.0 license +@smithy/util-utf8 under the Apache-2.0 license +@smithy/util-waiter under the Apache-2.0 license +@szmarczak/http-timer under the MIT license +@tsconfig/node10 under the MIT license +@tsconfig/node12 under the MIT license +@tsconfig/node14 under the MIT license +@tsconfig/node16 under the MIT license +@types/babel__core under the MIT license +@types/babel__generator under the MIT license +@types/babel__template under the MIT license +@types/babel__traverse under the MIT license +@types/cacheable-request under the MIT license +@types/graceful-fs under the MIT license +@types/http-cache-semantics under the MIT license +@types/istanbul-lib-coverage under the MIT license +@types/istanbul-lib-report under the MIT license +@types/istanbul-reports under the MIT license +@types/json-schema under the MIT license +@types/keyv under the MIT license +@types/responselike under the MIT license +@types/semver under the MIT license +@types/sinon under the MIT license +@types/sinonjs__fake-timers under the MIT license +@types/stack-utils under the MIT license +@types/triple-beam under the MIT license +@types/yargs-parser under the MIT license +@types/yargs under the MIT license +@typescript-eslint/scope-manager under the MIT license +@typescript-eslint/type-utils under the MIT license +@typescript-eslint/typescript-estree under the BSD-2-Clause license +@typescript-eslint/utils under the MIT license +@typescript-eslint/visitor-keys under the MIT license +@ungap/structured-clone under the ISC license +acorn-jsx under the MIT license +acorn-walk under the MIT license +acorn under the MIT license +ajv under the MIT license +ansi-escapes under the MIT license +ansi-regex under the MIT license +ansi-styles under the MIT license +anymatch under the ISC license +arg under the MIT license +argparse under the MIT license +array-union under the MIT license +astral-regex under the MIT license +async under the MIT license +babel-jest under the MIT license +babel-plugin-istanbul under the BSD-3-Clause license +babel-plugin-jest-hoist under the MIT license +babel-preset-current-node-syntax under the MIT license +babel-preset-jest under the MIT license +balanced-match under the MIT license +bowser under the MIT license +brace-expansion under the MIT license +braces under the MIT license +browserslist under the MIT license +bs-logger under the MIT license +bser under the Apache-2.0 license +buffer-from under the MIT license +cacheable-lookup under the MIT license +call-bind under the MIT license +callsites under the MIT license +camelcase under the MIT license +caniuse-lite under the CC-BY-4.0 license +case under the MIT license +chalk under the MIT license +char-regex under the MIT license +ci-info under the MIT license +cjs-module-lexer under the MIT license +cliui under the ISC license +clone-response under the MIT license +co under the MIT license +collect-v8-coverage under the MIT license +color-convert under the MIT license +color-name under the MIT license +color-string under the MIT license +color under the MIT license +colorspace under the MIT license +concat-map under the MIT license +convert-source-map under the MIT license +create-jest under the MIT license +create-require under the MIT license +cross-spawn under the MIT license +debug under the MIT license +decompress-response under the MIT license +dedent under the MIT license +deep-is under the MIT license +deepmerge under the MIT license +defer-to-connect under the MIT license +define-data-property under the MIT license +detect-newline under the MIT license +diff-sequences under the MIT license +diff under the BSD-3-Clause license +dir-glob under the MIT license +doctrine under the Apache-2.0 license +electron-to-chromium under the ISC license +emittery under the MIT license +emoji-regex under the MIT license +enabled under the MIT license +end-of-stream under the MIT license +error-ex under the MIT license +escalade under the MIT license +escape-string-regexp under the MIT license +eslint-scope under the BSD-2-Clause license +eslint-visitor-keys under the Apache-2.0 license +espree under the BSD-2-Clause license +esprima under the BSD-2-Clause license +esquery under the BSD-3-Clause license +esrecurse under the BSD-2-Clause license +estraverse under the BSD-2-Clause license +esutils under the BSD-2-Clause license +execa under the MIT license +exit under the MIT license +fast-deep-equal under the MIT license +fast-diff under the Apache-2.0 license +fast-glob under the MIT license +fast-json-stable-stringify under the MIT license +fast-levenshtein under the MIT license +fast-xml-parser under the MIT license +fastq under the ISC license +fb-watchman under the Apache-2.0 license +fecha under the MIT license +fflate under the MIT license +file-entry-cache under the MIT license +fill-range under the MIT license +find-up under the MIT license +flat-cache under the MIT license +flatted under the ISC license +fn.name under the MIT license +fs-extra under the MIT license +fs.realpath under the ISC license +fsevents under the MIT license +function-bind under the MIT license +gensync under the MIT license +get-caller-file under the ISC license +get-intrinsic under the MIT license +get-package-type under the MIT license +get-stream under the MIT license +glob-parent under the ISC license +glob under the ISC license +globby under the MIT license +gopd under the MIT license +graphemer under the MIT license +has-flag under the MIT license +has-property-descriptors under the MIT license +has-proto under the MIT license +has-symbols under the MIT license +hasown under the MIT license +html-escaper under the MIT license +http2-wrapper under the MIT license +human-signals under the Apache-2.0 license +ignore under the MIT license +import-fresh under the MIT license +import-local under the MIT license +imurmurhash under the MIT license +inflight under the ISC license +inherits under the ISC license +is-arrayish under the MIT license +is-core-module under the MIT license +is-extglob under the MIT license +is-fullwidth-code-point under the MIT license +is-generator-fn under the MIT license +is-glob under the MIT license +is-number under the MIT license +is-path-inside under the MIT license +is-stream under the MIT license +isarray under the MIT license +isexe under the ISC license +istanbul-lib-instrument under the BSD-3-Clause license +istanbul-lib-source-maps under the BSD-3-Clause license +jest-changed-files under the MIT license +jest-circus under the MIT license +jest-cli under the MIT license +jest-config under the MIT license +jest-diff under the MIT license +jest-docblock under the MIT license +jest-each under the MIT license +jest-environment-node under the MIT license +jest-get-type under the MIT license +jest-haste-map under the MIT license +jest-leak-detector under the MIT license +jest-matcher-utils under the MIT license +jest-message-util under the MIT license +jest-mock under the MIT license +jest-pnp-resolver under the MIT license +jest-regex-util under the MIT license +jest-resolve-dependencies under the MIT license +jest-resolve under the MIT license +jest-runner under the MIT license +jest-runtime under the MIT license +jest-snapshot under the MIT license +jest-util under the MIT license +jest-validate under the MIT license +jest-watcher under the MIT license +jest-worker under the MIT license +js-tokens under the MIT license +js-yaml under the MIT license +jsesc under the MIT license +json-buffer under the MIT license +json-parse-even-better-errors under the MIT license +json-schema-traverse under the MIT license +json-stable-stringify-without-jsonify under the MIT license +json5 under the MIT license +jsonfile under the MIT license +jsonschema under the MIT license +just-extend under the MIT license +kleur under the MIT license +kuler under the MIT license +leven under the MIT license +levn under the MIT license +lines-and-columns under the MIT license +locate-path under the MIT license +lodash.get under the MIT license +lodash.memoize under the MIT license +lodash.merge under the MIT license +lodash.truncate under the MIT license +logform under the MIT license +lowercase-keys under the MIT license +lru-cache under the ISC license +make-dir under the MIT license +make-error under the ISC license +makeerror under the BSD-3-Clause license +merge-stream under the MIT license +merge2 under the MIT license +micromatch under the MIT license +mimic-fn under the MIT license +mimic-response under the MIT license +minimatch under the ISC license +mnemonist under the MIT license +ms under the MIT license +natural-compare under the MIT license +nise under the BSD-3-Clause license +node-int64 under the MIT license +node-releases under the MIT license +normalize-path under the MIT license +normalize-url under the MIT license +npm-run-path under the MIT license +object-inspect under the MIT license +obliterator under the MIT license +once under the ISC license +one-time under the MIT license +onetime under the MIT license +optionator under the MIT license +p-cancelable under the MIT license +p-limit under the MIT license +p-locate under the MIT license +p-try under the MIT license +parent-module under the MIT license +parse-json under the MIT license +path-exists under the MIT license +path-is-absolute under the MIT license +path-key under the MIT license +path-parse under the MIT license +path-to-regexp under the MIT license +path-type under the MIT license +picocolors under the ISC license +picomatch under the MIT license +pirates under the MIT license +pkg-dir under the MIT license +prelude-ls under the MIT license +prettier-linter-helpers under the MIT license +pretty-format under the MIT license +prompts under the MIT license +pump under the MIT license +punycode under the MIT license +pure-rand under the MIT license +qs under the BSD-3-Clause license +queue-microtask under the MIT license +quick-lru under the MIT license +react-is under the MIT license +readable-stream under the MIT license +require-directory under the MIT license +require-from-string under the MIT license +resolve-alpn under the MIT license +resolve-cwd under the MIT license +resolve-from under the MIT license +resolve.exports under the MIT license +resolve under the MIT license +reusify under the MIT license +rimraf under the ISC license +run-parallel under the MIT license +safe-buffer under the MIT license +safe-stable-stringify under the MIT license +set-function-length under the MIT license +shebang-command under the MIT license +shebang-regex under the MIT license +side-channel under the MIT license +signal-exit under the ISC license +simple-swizzle under the MIT license +sisteransi under the MIT license +slash under the MIT license +slice-ansi under the MIT license +sprintf-js under the BSD-3-Clause license +stack-trace under the MIT license +string-length under the MIT license +string-width under the MIT license +string_decoder under the MIT license +strip-ansi under the MIT license +strip-bom under the MIT license +strip-final-newline under the MIT license +strip-json-comments under the MIT license +strnum under the MIT license +supports-color under the MIT license +supports-preserve-symlinks-flag under the MIT license +synckit under the MIT license +table under the BSD-3-Clause license +test-exclude under the ISC license +text-hex under the MIT license +text-table under the MIT license +tmpl under the BSD-3-Clause license +to-fast-properties under the MIT license +to-regex-range under the MIT license +ts-api-utils under the MIT license +tslib under the 0BSD license +type-check under the MIT license +type-detect under the MIT license +type-fest under the (MIT OR CC0-1.0) license +undici-types under the MIT license +universalify under the MIT license +update-browserslist-db under the MIT license +uri-js under the BSD-2-Clause license +util-deprecate under the MIT license +v8-compile-cache-lib under the MIT license +v8-to-istanbul under the ISC license +walker under the Apache-2.0 license +which under the ISC license +winston-transport under the MIT license +wrap-ansi under the MIT license +wrappy under the ISC license +write-file-atomic under the ISC license +y18n under the ISC license +yallist under the ISC license +yaml under the ISC license +yn under the MIT license +yocto-queue under the MIT license +globals under the MIT license. +keyv under the MIT license. +@typescript-eslint/types under the MIT license. +semver under the ISC license. +@pkgr/core under the MIT license. +expect under the MIT license. +@jest/types under the MIT license. +graceful-fs under the ISC license. +stack-utils under the MIT license. +@jest/core under the MIT license. +@babel/core under the MIT license. +@babel/types under the MIT license. +istanbul-lib-coverage under the BSD-3-Clause license. +istanbul-lib-report under the BSD-3-Clause license. +source-map under the BSD-3-Clause license. +istanbul-reports under the BSD-3-Clause license. +@sinonjs/fake-timers under the BSD-3-Clause license. +source-map-support under the MIT license. +yargs under the MIT license. +yargs-parser under the ISC license. +@smithy/types under the Apache-2.0 license. +@smithy/core under the Apache-2.0 license. +@smithy/util-endpoints under the Apache-2.0 license. +cacheable-request under the MIT license. +http-cache-semantics under the BSD-2-Clause license. +responselike under the MIT license. +triple-beam under the MIT license. +sinon under the BSD-3-Clause license. + +******************** +OPEN SOURCE LICENSES +******************** + +0BSD - https://opensource.org/licenses/0BSD +Apache-2.0 - https://opensource.org/licenses/Apache-2.0 +BSD-2-Clause - https://opensource.org/licenses/BSD-2-Clause +BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause +CC-BY-4.0 - https://opensource.org/licenses/CC-BY-4.0 +ISC - https://opensource.org/licenses/ISC +MIT - https://opensource.org/licenses/MIT +Python-2.0 - https://spdx.org/licenses/Python-2.0.html diff --git a/README.md b/README.md index 78749f2..46fed1e 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ _Note: ta-spoke.template should be deployed in us-east-1 ONLY. sq-spoke.template - [quota-monitor-hub-no-ou.template](https://solutions-reference.s3.amazonaws.com/quota-monitor-for-aws/latest/quota-monitor-hub-no-ou.template) - [quota-monitor-ta-spoke.template](https://solutions-reference.s3.amazonaws.com/quota-monitor-for-aws/latest/quota-monitor-ta-spoke.template) - [quota-monitor-sq-spoke.template](https://solutions-reference.s3.amazonaws.com/quota-monitor-for-aws/latest/quota-monitor-sq-spoke.template) +- [quota-monitor-sns-spoke.template](https://solutions-reference.s3.amazonaws.com/quota-monitor-for-aws/latest/quota-monitor-sns-spoke.template) - [quota-monitor-prerequisite.template](https://solutions-reference.s3.amazonaws.com/quota-monitor-for-aws/latest/quota-monitor-prerequisite.template) _Note: hub, hub-no-ou and sq-spoke templates can be deployed in ANY region; prerequisite and ta-spoke template can be deployed in us-east-1 ONLY._ @@ -125,6 +126,7 @@ npm ci ``` Bootstrap your CDK environment +- This solution requires that you have bootstrapped your AWS account with CDK using the default stack name. If you haven't done so already, run: ``` npm run cdk -- bootstrap --profile @@ -139,17 +141,23 @@ _Note:_ - STACK_NAME, substitute the name of the stack that you want to deploy, check cdk [app](./source/resources/bin/app.ts) - PROFILE_NAME, substitute the name of an AWS CLI profile that contains appropriate credentials for deploying in your preferred region +- The deployment scripts assume the CDK bootstrap stack is named CDKToolkit. If you've used a custom name for your bootstrap stack, you'll need to modify the get-cdk-bucket script in package.json. +- For the ORG/HYBRID mode, Ensure that the account or organization you're deploying stacks to has the necessary permissions to access the CDK assets bucket. +- CDK creates an S3 bucket for assets in only one region. As a result, spoke stacks will deploy successfully only in the region where this bucket is created. _✅ Solution stack is deployed with your customized code._ ## Independent spoke templates -There are two spoke templates packaged with the solution +There are three spoke templates packaged with the solution: - ta-spoke: provisions resources to support Trusted Advisor quota checks - sq-spoke: provisions resources to support Service Quotas checks +- sns-spoke: provisions resources to support spoke account-specific notifications -Both spoke templates are independent standalone stacks that can be individually deployed. You can deploy the spoke stack and route usage events and notifications to your preferred destinations. Additionally, in sq-spoke stack you can control which services to monitor, by toggling _monitored_ status of the services in the DynamoDB table _ServiceTable_. For deploying sq-spoke stack: +All three spoke templates (TA, SQ, and SNS) are independent standalone stacks that can be individually deployed. You can deploy these spoke stacks and route usage events and notifications to your preferred destinations. + +For the SQ spoke stack, you can control which services to monitor by toggling the _monitored_ status of the services in the DynamoDB table _ServiceTable_. The SNS spoke stack provides an additional option for routing notifications within spoke accounts. For deploying sq-spoke stack: ``` npm run cdk -- deploy quota-monitor-sq-spoke --parameters EventBusArn= --profile diff --git a/SECURITY.md b/SECURITY.md index 9c82ab1..4f8dec4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,9 @@ -Reporting Security Issues ----------------------------------------------------------------------------------------------------------- -We take all security reports seriously. When we receive such reports, we will investigate and -subsequently address any potential vulnerabilities as quickly as possible. If you discover a potential -security issue in this project, please notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) or -directly via email to [AWS Security](mailto:aws-security@amazon.com). Please do not create a public GitHub issue in this project. \ No newline at end of file +## Reporting Security Issues + +We take all security reports seriously. When we receive such reports, +we will investigate and subsequently address any potential vulnerabilities as +quickly as possible. If you discover a potential security issue in this project, +please notify AWS/Amazon Security via our [vulnerability reporting page] +(http://aws.amazon.com/security/vulnerability-reporting/) or directly via email +to [AWS Security](mailto:aws-security@amazon.com). +Please do *not* create a public GitHub issue in this project. diff --git a/architecture.png b/architecture.png index 7b0aa95..331f95b 100644 Binary files a/architecture.png and b/architecture.png differ diff --git a/deployment/build-s3-dist.sh b/deployment/build-s3-dist.sh index 5d8ef91..a2326f6 100755 --- a/deployment/build-s3-dist.sh +++ b/deployment/build-s3-dist.sh @@ -2,10 +2,10 @@ # # This script packages your project into a solution distributable that can be # used as an input to the solution builder validation pipeline. -# +# # This script will perform the following tasks: # 1. Remove any old dist files from previous runs. -# 2. Install dependencies for the cdk-solution-helper; responsible for +# 2. Install dependencies for the cdk-solution-helper; responsible for # converting standard 'cdk synth' output into solution assets. # 3. Build and synthesize your CDK project. # 4. Run the cdk-solution-helper on template outputs and organize @@ -42,9 +42,6 @@ staging_dist_dir="$template_dir/staging" template_dist_dir="$template_dir/global-s3-assets" build_dist_dir="$template_dir/regional-s3-assets" source_dir="$template_dir/../source" -lambda_services_dir="$source_dir/lambda/services" -lambda_utils_dir="$source_dir/lambda/utilsLayer" -cdk_dir="$source_dir/resources" headline "[Init] Clean old folders" @@ -72,7 +69,7 @@ cp $staging_dist_dir/*.template.json $template_dist_dir/ rm *.template.json # Rename all *.template.json files to *.template -for f in $template_dist_dir/*.template.json; do +for f in $template_dist_dir/*.template.json; do mv -- "$f" "${f%.template.json}.template" done @@ -118,13 +115,14 @@ find $staging_dist_dir -iname "node_modules" -type d -exec rm -rf "{}" \; 2> /de # ... For each asset.* source code artifact in the temporary /staging folder... cd $staging_dist_dir +# shellcheck disable=SC2044 for i in `find . -mindepth 1 -maxdepth 1 -type f \( -iname "*.zip" \) -or -type d`; do # Rename the artifact, removing the period for handler compatibility - pfname="$(basename -- $i)" + pfname="$(basename -- $i)" fname="$(echo $pfname | sed -e 's/\.//')" mv $i $fname - + if [[ $fname != *".zip" ]] then # Zip the artifact @@ -132,15 +130,15 @@ for i in `find . -mindepth 1 -maxdepth 1 -type f \( -iname "*.zip" \) -or -type zip -rj $fname.zip $fname fi -# ... repeat until all source code artifacts are zipped +# ... repeat until all source code artifacts are zipped done cp -R *.zip $build_dist_dir -# the spoke templates need to be in a regional S3 bucket for GovCloud regions +# the spoke templates need to be in a regional S3 bucket for GovCloud and China regions # copy all templates to regional assets folder (less brittle against refactoring ...) cp $template_dist_dir/* $build_dist_dir headline "[Cleanup] Remove temporary files" rm -rf *.zip -rm -rf $staging_dist_dir \ No newline at end of file +rm -rf $staging_dist_dir diff --git a/deployment/cdk-solution-helper/README.md b/deployment/cdk-solution-helper/README.md old mode 100755 new mode 100644 diff --git a/deployment/cdk-solution-helper/index.js b/deployment/cdk-solution-helper/index.js old mode 100755 new mode 100644 index 2b4784b..cb5ea34 --- a/deployment/cdk-solution-helper/index.js +++ b/deployment/cdk-solution-helper/index.js @@ -18,18 +18,12 @@ fs.readdirSync(global_s3_assets).forEach((file) => { // Clean-up Lambda function code dependencies const resources = template.Resources ? template.Resources : {}; const lambdaFunctions = Object.keys(resources).filter(function (key) { - return ( - resources[key].Type === "AWS::Lambda::Function" || - resources[key].Type === "AWS::Lambda::LayerVersion" - ); + return resources[key].Type === "AWS::Lambda::Function" || resources[key].Type === "AWS::Lambda::LayerVersion"; }); lambdaFunctions.forEach(function (f) { const fn = template.Resources[f]; - const assetProperty = - fn.Type === "AWS::Lambda::Function" - ? fn.Properties.Code - : fn.Properties.Content; + const assetProperty = fn.Type === "AWS::Lambda::Function" ? fn.Properties.Code : fn.Properties.Content; if (assetProperty.hasOwnProperty("S3Bucket")) { // Set the S3 key reference diff --git a/deployment/cdk-solution-helper/package.json b/deployment/cdk-solution-helper/package.json old mode 100755 new mode 100644 diff --git a/deployment/quota-monitor-hub-cn.template b/deployment/quota-monitor-hub-cn.template new file mode 100644 index 0000000..67b0271 --- /dev/null +++ b/deployment/quota-monitor-hub-cn.template @@ -0,0 +1,3409 @@ +{ + "Description": "(SO0005) - quota-monitor-for-aws - Hub Template. Version v6.3.0", + "AWSTemplateFormatVersion": "2010-09-09", + "Metadata": { + "AWS::CloudFormation::Interface": { + "ParameterGroups": [ + { + "Label": { + "default": "Deployment Configuration" + }, + "Parameters": [ + "DeploymentModel", + "RegionsList", + "SnsSpokeRegion", + "ManagementAccountId" + ] + }, + { + "Label": { + "default": "Stackset Deployment Options" + }, + "Parameters": [ + "RegionConcurrency", + "MaxConcurrentPercentage", + "FailureTolerancePercentage" + ] + }, + { + "Label": { + "default": "Notification Configuration" + }, + "Parameters": [ + "SNSEmail", + "SlackNotification" + ] + }, + { + "Label": { + "default": "Stackset Stack Configuration Parameters" + }, + "Parameters": [ + "SQNotificationThreshold", + "SQMonitoringFrequency", + "SQReportOKNotifications", + "SageMakerMonitoring", + "ConnectMonitoring" + ] + } + ], + "ParameterLabels": { + "DeploymentModel": { + "default": "Do you want to monitor quotas across Organizational Units, Accounts or both?" + }, + "SNSEmail": { + "default": "Email address for notifications" + }, + "SlackNotification": { + "default": "Do you want slack notifications?" + }, + "ManagementAccountId": { + "default": "Organization's management Id to scope permissions down for Stackset creation" + }, + "RegionsList": { + "default": "List of regions to deploy resources to monitor service quotas" + }, + "SnsSpokeRegion": { + "default": "Region in which to launch the SNS stack in the spoke accounts." + }, + "RegionConcurrencyType": { + "default": "Region Concurrency" + }, + "MaxConcurrentPercentage": { + "default": "Percentage Maximum concurrent accounts" + }, + "FailureTolerancePercentage": { + "default": "Percentage Failure tolerance" + }, + "SQNotificationThreshold": { + "default": "At what quota utilization do you want notifications?" + }, + "SQMonitoringFrequency": { + "default": "Frequency to monitor quota utilization" + }, + "SQReportOKNotifications": { + "default": "Report OK Notifications" + }, + "SageMakerMonitoring": { + "default": "Enable monitoring for SageMaker quotas" + }, + "ConnectMonitoring": { + "default": "Enable monitoring for Connect quotas" + } + } + } + }, + "Parameters": { + "SNSEmail": { + "Type": "String", + "Default": "", + "Description": "To disable email notifications, leave this blank." + }, + "SlackNotification": { + "Type": "String", + "Default": "No", + "AllowedValues": [ + "Yes", + "No" + ] + }, + "DeploymentModel": { + "Type": "String", + "Default": "Organizations", + "AllowedValues": [ + "Organizations", + "Hybrid" + ] + }, + "ManagementAccountId": { + "Type": "String", + "Default": "*", + "AllowedPattern": "^([0-9]{1}\\d{11})|\\*$", + "Description": "AWS Account Id for the organization's management account or *" + }, + "RegionsList": { + "Type": "String", + "Default": "ALL", + "Description": "Comma separated list of regions like us-east-1,us-east-2 or ALL or leave it blank for ALL" + }, + "SnsSpokeRegion": { + "Type": "String", + "Default": "", + "Description": "The region in which to launch the SNS stack in each spoke account. Leave blank if the spoke SNS is not needed" + }, + "RegionConcurrency": { + "Type": "String", + "Default": "PARALLEL", + "AllowedValues": [ + "PARALLEL", + "SEQUENTIAL" + ], + "Description": "Choose to deploy StackSets into regions sequentially or in parallel" + }, + "MaxConcurrentPercentage": { + "Type": "Number", + "Default": 100, + "Description": "Percentage of accounts per region to which you can deploy stacks at one time. The higher the number, the faster the operation", + "MaxValue": 100, + "MinValue": 1 + }, + "FailureTolerancePercentage": { + "Type": "Number", + "Default": 0, + "Description": "Percentage of account, per region, for which stacks can fail before CloudFormation stops the operation in that region. If the operation is stopped in one region, it does not continue in other regions. The lower the number the safer the operation", + "MaxValue": 100, + "MinValue": 0 + }, + "SQNotificationThreshold": { + "Type": "String", + "Default": "80", + "AllowedPattern": "^([1-9]|[1-9][0-9])$", + "ConstraintDescription": "Threshold must be a whole number between 0 and 100", + "Description": "Threshold percentage for quota utilization alerts (0-100)" + }, + "SQMonitoringFrequency": { + "Type": "String", + "Default": "rate(12 hours)", + "AllowedValues": [ + "rate(6 hours)", + "rate(12 hours)", + "rate(1 day)" + ] + }, + "SQReportOKNotifications": { + "Type": "String", + "Default": "No", + "AllowedValues": [ + "Yes", + "No" + ] + }, + "SageMakerMonitoring": { + "Type": "String", + "Default": "Yes", + "AllowedValues": [ + "Yes", + "No" + ], + "Description": "Enable monitoring for SageMaker quotas. NOTE: (1) SageMaker monitoring consumes a high number of quotas, potentially resulting in higher usage cost. (2) Changing this value during a stack update will affect all spoke accounts but if left unchanged, it preserves existing spoke accounts customizations." + }, + "ConnectMonitoring": { + "Type": "String", + "Default": "Yes", + "AllowedValues": [ + "Yes", + "No" + ], + "Description": "Enable monitoring for Connect quotas. NOTE: (1) Connect monitoring consumes a high number of quotas, potentially resulting in higher usage cost. (2) Changing this value during a stack update will affect all spoke accounts but if left unchanged, it preserves existing spoke accounts customizations." + } + }, + "Mappings": { + "QuotaMonitorMap": { + "Metrics": { + "SendAnonymizedData": "Yes", + "MetricsEndpoint": "https://metrics.awssolutionsbuilder.com/generic" + }, + "SSMParameters": { + "SlackHook": "/QuotaMonitor/SlackHook", + "Accounts": "/QuotaMonitor/Accounts", + "OrganizationalUnits": "/QuotaMonitor/OUs", + "NotificationMutingConfig": "/QuotaMonitor/NotificationConfiguration", + "RegionsList": "/QuotaMonitor/RegionsToDeploy" + } + } + }, + "Conditions": { + "EmailTrueCondition": { + "Fn::Not": [ + { + "Fn::Equals": [ + { + "Ref": "SNSEmail" + }, + "" + ] + } + ] + }, + "SlackTrueCondition": { + "Fn::Equals": [ + { + "Ref": "SlackNotification" + }, + "Yes" + ] + }, + "AccountDeployCondition": { + "Fn::Equals": [ + { + "Ref": "DeploymentModel" + }, + "Hybrid" + ] + }, + "IsChinaPartition": { + "Fn::Equals": [ + { + "Ref": "AWS::Partition" + }, + "aws-cn" + ] + }, + "CDKMetadataAvailable": { + "Fn::Or": [ + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "af-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ca-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-northwest-1" + ] + } + ] + }, + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-3" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "il-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "me-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "me-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "sa-east-1" + ] + } + ] + }, + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-east-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-west-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-west-2" + ] + } + ] + } + ] + } + }, + "Resources": { + "QMBusFF5C6C0C": { + "Type": "AWS::Events::EventBus", + "Properties": { + "Name": "QuotaMonitorBus" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Bus/Resource" + } + }, + "KMSHubQMEncryptionKeyA80F8C05": { + "Type": "AWS::KMS::Key", + "Properties": { + "Description": "CMK for AWS resources provisioned by Quota Monitor in this account", + "EnableKeyRotation": true, + "Enabled": true, + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/KMS-Hub/QM-EncryptionKey/Resource" + } + }, + "KMSHubQMEncryptionKeyAlias6C248240": { + "Type": "AWS::KMS::Alias", + "Properties": { + "AliasName": "alias/CMK-KMS-Hub", + "TargetKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/KMS-Hub/QM-EncryptionKey/Alias/Resource" + } + }, + "QMSlackHook4F1AD495": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Slack Hook URL to send Quota Monitor events", + "Name": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "SSMParameters", + "SlackHook" + ] + }, + "Type": "String", + "Value": "NOP" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SlackHook/Resource" + }, + "Condition": "SlackTrueCondition" + }, + "QMOUs122D8EB4": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "List of target Organizational Units", + "Name": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "SSMParameters", + "OrganizationalUnits" + ] + }, + "Type": "StringList", + "Value": "NOP" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-OUs/Resource" + } + }, + "QMAccounts3D743F6B": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "List of target Accounts", + "Name": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "SSMParameters", + "Accounts" + ] + }, + "Type": "StringList", + "Value": "NOP" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Accounts/Resource" + }, + "Condition": "AccountDeployCondition" + }, + "QMNotificationMutingConfig3B7948BA": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Muting configuration for services, limits e.g. ec2:L-1216C47A,ec2:Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances,dynamodb,logs:*,geo:L-05EFD12D", + "Name": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "SSMParameters", + "NotificationMutingConfig" + ] + }, + "Type": "StringList", + "Value": "NOP" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-NotificationMutingConfig/Resource" + } + }, + "QMRegionsList17794003": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "list of regions to deploy spoke resources (eg. us-east-1,us-west-2)", + "Name": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "SSMParameters", + "RegionsList" + ] + }, + "Type": "StringList", + "Value": { + "Ref": "RegionsList" + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-RegionsList/Resource" + } + }, + "QMUtilsLayerQMUtilsLayerLayer80D5D993": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "CompatibleRuntimes": [ + "nodejs18.x" + ], + "Content": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assete8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip" + }, + "LayerName": "QM-UtilsLayer" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-UtilsLayer/QM-UtilsLayer-Layer/Resource", + "aws:asset:path": "asset.e8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Content" + } + }, + "QMHelperQMHelperFunctionServiceRole0506622D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Helper/QM-Helper-Function/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMHelperQMHelperFunction91954E97": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assetf4ee0c3d949f011b3f0f60d231fdacecab71c5f3ccf9674352231cedf831f6cd.zip" + }, + "Description": "SO0005 quota-monitor-for-aws - QM-Helper-Function", + "Environment": { + "Variables": { + "METRICS_ENDPOINT": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "MetricsEndpoint" + ] + }, + "SEND_METRIC": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "SendAnonymizedData" + ] + }, + "QM_STACK_ID": "quota-monitor-hub-cn", + "QM_SLACK_NOTIFICATION": { + "Ref": "SlackNotification" + }, + "QM_EMAIL_NOTIFICATION": { + "Fn::If": [ + "EmailTrueCondition", + "Yes", + "No" + ] + }, + "SAGEMAKER_MONITORING": { + "Ref": "SageMakerMonitoring" + }, + "CONNECT_MONITORING": { + "Ref": "ConnectMonitoring" + }, + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "QMHelperQMHelperFunctionServiceRole0506622D", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 5 + }, + "DependsOn": [ + "QMHelperQMHelperFunctionServiceRole0506622D" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Helper/QM-Helper-Function/Resource", + "aws:asset:path": "asset.f4ee0c3d949f011b3f0f60d231fdacecab71c5f3ccf9674352231cedf831f6cd.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK", + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMHelperQMHelperFunctionEventInvokeConfig580F9F5F": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "QMHelperQMHelperFunction91954E97" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Helper/QM-Helper-Function/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Helper/QM-Helper-Provider/framework-onEvent/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMHelperQMHelperProviderframeworkonEventServiceRoleDefaultPolicy86C1FCC1": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "QMHelperQMHelperFunction91954E97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "QMHelperQMHelperFunction91954E97", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMHelperQMHelperProviderframeworkonEventServiceRoleDefaultPolicy86C1FCC1", + "Roles": [ + { + "Ref": "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Helper/QM-Helper-Provider/framework-onEvent/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMHelperQMHelperProviderframeworkonEventB1DF6D3F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/asset7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94.zip" + }, + "Description": "AWS CDK resource provider framework - onEvent (quota-monitor-hub-cn/QM-Helper/QM-Helper-Provider)", + "Environment": { + "Variables": { + "USER_ON_EVENT_FUNCTION_ARN": { + "Fn::GetAtt": [ + "QMHelperQMHelperFunction91954E97", + "Arn" + ] + } + } + }, + "Handler": "framework.onEvent", + "Role": { + "Fn::GetAtt": [ + "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 900 + }, + "DependsOn": [ + "QMHelperQMHelperProviderframeworkonEventServiceRoleDefaultPolicy86C1FCC1", + "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Helper/QM-Helper-Provider/framework-onEvent/Resource", + "aws:asset:path": "asset.7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMHelperCreateUUIDE0D423E6": { + "Type": "Custom::CreateUUID", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "QMHelperQMHelperProviderframeworkonEventB1DF6D3F", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Helper/CreateUUID/Default" + } + }, + "QMHelperLaunchData6F23B2C3": { + "Type": "Custom::LaunchData", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "QMHelperQMHelperProviderframeworkonEventB1DF6D3F", + "Arn" + ] + }, + "SOLUTION_UUID": { + "Fn::GetAtt": [ + "QMHelperCreateUUIDE0D423E6", + "UUID" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Helper/LaunchData/Default" + } + }, + "QMSlackNotifierQMSlackNotifierEventsRuleC3528E53": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - QM-SlackNotifier-EventsRule", + "EventBusName": { + "Ref": "QMBusFF5C6C0C" + }, + "EventPattern": { + "detail": { + "status": [ + "WARN", + "ERROR" + ] + }, + "detail-type": [ + "Trusted Advisor Check Item Refresh Notification", + "Service Quotas Utilization Notification" + ], + "source": [ + "aws.trustedadvisor", + "aws-solutions.quota-monitor" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "QMSlackNotifierQMSlackNotifierLambda95713661", + "Arn" + ] + }, + "Id": "Target0" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SlackNotifier/QM-SlackNotifier-EventsRule/Resource" + }, + "Condition": "SlackTrueCondition" + }, + "QMSlackNotifierQMSlackNotifierEventsRuleAllowEventRulequotamonitorhubcnQMSlackNotifierQMSlackNotifierLambdaA45E70CD2991BDD8": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "QMSlackNotifierQMSlackNotifierLambda95713661", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "QMSlackNotifierQMSlackNotifierEventsRuleC3528E53", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SlackNotifier/QM-SlackNotifier-EventsRule/AllowEventRulequotamonitorhubcnQMSlackNotifierQMSlackNotifierLambdaA45E70CD" + }, + "Condition": "SlackTrueCondition" + }, + "QMSlackNotifierQMSlackNotifierLambdaDeadLetterQueue74B865F7": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SlackNotifier/QM-SlackNotifier-Lambda-Dead-Letter-Queue/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "Queue itself is dead-letter queue", + "id": "AwsSolutions-SQS3" + } + ] + } + }, + "Condition": "SlackTrueCondition" + }, + "QMSlackNotifierQMSlackNotifierLambdaDeadLetterQueuePolicy719E4C6A": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "QMSlackNotifierQMSlackNotifierLambdaDeadLetterQueue74B865F7", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "QMSlackNotifierQMSlackNotifierLambdaDeadLetterQueue74B865F7" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SlackNotifier/QM-SlackNotifier-Lambda-Dead-Letter-Queue/Policy/Resource" + }, + "Condition": "SlackTrueCondition" + }, + "QMSlackNotifierQMSlackNotifierLambdaServiceRole6342FD1D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SlackNotifier/QM-SlackNotifier-Lambda/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + }, + "Condition": "SlackTrueCondition" + }, + "QMSlackNotifierQMSlackNotifierLambdaServiceRoleDefaultPolicy4C4D219B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMSlackNotifierQMSlackNotifierLambdaDeadLetterQueue74B865F7", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Encrypt", + "kms:Decrypt", + "kms:CreateGrant" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + { + "Action": "kms:ListAliases", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "QMSlackHook4F1AD495" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "QMNotificationMutingConfig3B7948BA" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMSlackNotifierQMSlackNotifierLambdaServiceRoleDefaultPolicy4C4D219B", + "Roles": [ + { + "Ref": "QMSlackNotifierQMSlackNotifierLambdaServiceRole6342FD1D" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SlackNotifier/QM-SlackNotifier-Lambda/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + }, + "Condition": "SlackTrueCondition" + }, + "QMSlackNotifierQMSlackNotifierLambda95713661": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/asset11434a0b3246f0b4445dd28fdbc9e4e7dc808ccf355077acd9b000c5d88e6713.zip" + }, + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "QMSlackNotifierQMSlackNotifierLambdaDeadLetterQueue74B865F7", + "Arn" + ] + } + }, + "Description": "SO0005 quota-monitor-for-aws - QM-SlackNotifier-Lambda", + "Environment": { + "Variables": { + "SLACK_HOOK": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "SSMParameters", + "SlackHook" + ] + }, + "QM_NOTIFICATION_MUTING_CONFIG_PARAMETER": { + "Ref": "QMNotificationMutingConfig3B7948BA" + }, + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "KmsKeyArn": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + }, + "Layers": [ + { + "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "QMSlackNotifierQMSlackNotifierLambdaServiceRole6342FD1D", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 60 + }, + "DependsOn": [ + "QMSlackNotifierQMSlackNotifierLambdaServiceRoleDefaultPolicy4C4D219B", + "QMSlackNotifierQMSlackNotifierLambdaServiceRole6342FD1D" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SlackNotifier/QM-SlackNotifier-Lambda/Resource", + "aws:asset:path": "asset.11434a0b3246f0b4445dd28fdbc9e4e7dc808ccf355077acd9b000c5d88e6713.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + }, + "Condition": "SlackTrueCondition" + }, + "QMSlackNotifierQMSlackNotifierLambdaEventInvokeConfig5340A982": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "QMSlackNotifierQMSlackNotifierLambda95713661" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SlackNotifier/QM-SlackNotifier-Lambda/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + }, + "Condition": "SlackTrueCondition" + }, + "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4": { + "Type": "AWS::SNS::Topic", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SNSPublisher/QM-SNSPublisher-SNSTopic/Resource" + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionEventsRule5BDCD4FD": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - QM-SNSPublisherFunction-EventsRule", + "EventBusName": { + "Ref": "QMBusFF5C6C0C" + }, + "EventPattern": { + "detail": { + "status": [ + "WARN", + "ERROR" + ] + }, + "detail-type": [ + "Trusted Advisor Check Item Refresh Notification", + "Service Quotas Utilization Notification" + ], + "source": [ + "aws.trustedadvisor", + "aws-solutions.quota-monitor" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambda8BD2DBC1", + "Arn" + ] + }, + "Id": "Target0" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SNSPublisherFunction/QM-SNSPublisherFunction-EventsRule/Resource" + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionEventsRuleAllowEventRulequotamonitorhubcnQMSNSPublisherFunctionQMSNSPublisherFunctionLambdaD528E83C01F26CCC": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambda8BD2DBC1", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionEventsRule5BDCD4FD", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SNSPublisherFunction/QM-SNSPublisherFunction-EventsRule/AllowEventRulequotamonitorhubcnQMSNSPublisherFunctionQMSNSPublisherFunctionLambdaD528E83C" + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda-Dead-Letter-Queue/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "Queue itself is dead-letter queue", + "id": "AwsSolutions-SQS3" + } + ] + } + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueuePolicyBA6A8707": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda-Dead-Letter-Queue/Policy/Resource" + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleA2F00B10": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleDefaultPolicy1E6E152C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Encrypt", + "kms:Decrypt", + "kms:CreateGrant" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + { + "Action": "kms:ListAliases", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "SNS:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4" + } + }, + { + "Action": "kms:GenerateDataKey", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "QMNotificationMutingConfig3B7948BA" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleDefaultPolicy1E6E152C", + "Roles": [ + { + "Ref": "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleA2F00B10" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambda8BD2DBC1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assete7a324e67e467d0c22e13b0693ca4efdceb0d53025c7fb45fe524870a5c18046.zip" + }, + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A", + "Arn" + ] + } + }, + "Description": "SO0005 quota-monitor-for-aws - QM-SNSPublisherFunction-Lambda", + "Environment": { + "Variables": { + "QM_NOTIFICATION_MUTING_CONFIG_PARAMETER": { + "Ref": "QMNotificationMutingConfig3B7948BA" + }, + "SOLUTION_UUID": { + "Fn::GetAtt": [ + "QMHelperCreateUUIDE0D423E6", + "UUID" + ] + }, + "METRICS_ENDPOINT": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "MetricsEndpoint" + ] + }, + "SEND_METRIC": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "SendAnonymizedData" + ] + }, + "TOPIC_ARN": { + "Ref": "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4" + }, + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "KmsKeyArn": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + }, + "Layers": [ + { + "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleA2F00B10", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 60 + }, + "DependsOn": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleDefaultPolicy1E6E152C", + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleA2F00B10" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda/Resource", + "aws:asset:path": "asset.e7a324e67e467d0c22e13b0693ca4efdceb0d53025c7fb45fe524870a5c18046.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaEventInvokeConfig7A963AA0": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "QMSNSPublisherFunctionQMSNSPublisherFunctionLambda8BD2DBC1" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMEmailSubscription32E71F90": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { + "Ref": "SNSEmail" + }, + "Protocol": "email", + "TopicArn": { + "Ref": "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4" + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-EmailSubscription/Resource" + }, + "Condition": "EmailTrueCondition" + }, + "QMSummarizerEventQueueQMSummarizerEventQueueEventsRuleE50B8D7C": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - QM-Summarizer-EventQueue-EventsRule", + "EventBusName": { + "Ref": "QMBusFF5C6C0C" + }, + "EventPattern": { + "detail": { + "status": [ + "OK", + "WARN", + "ERROR" + ] + }, + "detail-type": [ + "Trusted Advisor Check Item Refresh Notification", + "Service Quotas Utilization Notification" + ], + "source": [ + "aws.trustedadvisor", + "aws-solutions.quota-monitor" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A", + "Arn" + ] + }, + "Id": "Target0" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Summarizer-EventQueue/QM-Summarizer-EventQueue-EventsRule/Resource" + } + }, + "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + }, + "VisibilityTimeout": 60 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Summarizer-EventQueue/QM-Summarizer-EventQueue-Queue/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "dlq not implemented on sqs, will evaluate in future if there is need", + "id": "AwsSolutions-SQS3" + } + ] + } + } + }, + "QMSummarizerEventQueueQMSummarizerEventQueueQueuePolicyE7E1F6D8": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A", + "Arn" + ] + } + }, + { + "Action": [ + "sqs:SendMessage", + "sqs:GetQueueAttributes", + "sqs:GetQueueUrl" + ], + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Summarizer-EventQueue/QM-Summarizer-EventQueue-Queue/Policy/Resource" + } + }, + "QMTable336670B0": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "MessageId", + "AttributeType": "S" + }, + { + "AttributeName": "TimeStamp", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": [ + { + "AttributeName": "MessageId", + "KeyType": "HASH" + }, + { + "AttributeName": "TimeStamp", + "KeyType": "RANGE" + } + ], + "PointInTimeRecoverySpecification": { + "PointInTimeRecoveryEnabled": true + }, + "SSESpecification": { + "KMSMasterKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + }, + "SSEEnabled": true, + "SSEType": "KMS" + }, + "TimeToLiveSpecification": { + "AttributeName": "ExpiryTime", + "Enabled": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Table/Resource" + } + }, + "QMReporterQMReporterEventsRule0BF77282": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - QM-Reporter-EventsRule", + "ScheduleExpression": "rate(5 minutes)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "QMReporterQMReporterLambda7D98A6E4", + "Arn" + ] + }, + "Id": "Target0" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Reporter/QM-Reporter-EventsRule/Resource" + } + }, + "QMReporterQMReporterEventsRuleAllowEventRulequotamonitorhubcnQMReporterQMReporterLambda446E624CF1BB259D": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "QMReporterQMReporterLambda7D98A6E4", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "QMReporterQMReporterEventsRule0BF77282", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Reporter/QM-Reporter-EventsRule/AllowEventRulequotamonitorhubcnQMReporterQMReporterLambda446E624C" + } + }, + "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Reporter/QM-Reporter-Lambda-Dead-Letter-Queue/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "Queue itself is dead-letter queue", + "id": "AwsSolutions-SQS3" + } + ] + } + } + }, + "QMReporterQMReporterLambdaDeadLetterQueuePolicyE714847D": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Reporter/QM-Reporter-Lambda-Dead-Letter-Queue/Policy/Resource" + } + }, + "QMReporterQMReporterLambdaServiceRoleBA4CED84": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Reporter/QM-Reporter-Lambda/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMReporterQMReporterLambdaServiceRoleDefaultPolicyC6B87A76": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Encrypt", + "kms:Decrypt", + "kms:CreateGrant" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + { + "Action": "kms:ListAliases", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "sqs:DeleteMessage", + "sqs:ReceiveMessage" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A", + "Arn" + ] + } + }, + { + "Action": [ + "dynamodb:GetItem", + "dynamodb:PutItem" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMTable336670B0", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMReporterQMReporterLambdaServiceRoleDefaultPolicyC6B87A76", + "Roles": [ + { + "Ref": "QMReporterQMReporterLambdaServiceRoleBA4CED84" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Reporter/QM-Reporter-Lambda/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMReporterQMReporterLambda7D98A6E4": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/asseta6fda81c73d731886f04e1734d036f12ceb7b94c2efec30bb511f477ac58aa9c.zip" + }, + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC", + "Arn" + ] + } + }, + "Description": "SO0005 quota-monitor-for-aws - QM-Reporter-Lambda", + "Environment": { + "Variables": { + "QUOTA_TABLE": { + "Ref": "QMTable336670B0" + }, + "SQS_URL": { + "Ref": "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A" + }, + "MAX_MESSAGES": "10", + "MAX_LOOPS": "10", + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "KmsKeyArn": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + }, + "Layers": [ + { + "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "QMReporterQMReporterLambdaServiceRoleBA4CED84", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 10 + }, + "DependsOn": [ + "QMReporterQMReporterLambdaServiceRoleDefaultPolicyC6B87A76", + "QMReporterQMReporterLambdaServiceRoleBA4CED84" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Reporter/QM-Reporter-Lambda/Resource", + "aws:asset:path": "asset.a6fda81c73d731886f04e1734d036f12ceb7b94c2efec30bb511f477ac58aa9c.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMReporterQMReporterLambdaEventInvokeConfig07548BFA": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "QMReporterQMReporterLambda7D98A6E4" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Reporter/QM-Reporter-Lambda/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMTAStackSet": { + "Type": "AWS::CloudFormation::StackSet", + "Properties": { + "AutoDeployment": { + "Enabled": true, + "RetainStacksOnAccountRemoval": false + }, + "CallAs": "DELEGATED_ADMIN", + "Capabilities": [ + "CAPABILITY_IAM" + ], + "Description": "StackSet for deploying Quota Monitor Trusted Advisor spokes in Organization", + "ManagedExecution": { + "Active": true + }, + "Parameters": [ + { + "ParameterKey": "EventBusArn", + "ParameterValue": { + "Fn::GetAtt": [ + "QMBusFF5C6C0C", + "Arn" + ] + } + } + ], + "PermissionModel": "SERVICE_MANAGED", + "StackSetName": "QM-TA-Spoke-StackSet", + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://solutions-", + { + "Ref": "AWS::Region" + }, + ".s3.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com", + { + "Fn::If": [ + "IsChinaPartition", + ".cn", + "" + ] + }, + "/quota-monitor-for-aws/v6.3.0/", + { + "Fn::If": [ + "IsChinaPartition", + "quota-monitor-ta-spoke-cn.template", + "quota-monitor-ta-spoke.template" + ] + } + ] + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-TA-StackSet" + } + }, + "QMSQStackSet": { + "Type": "AWS::CloudFormation::StackSet", + "Properties": { + "AutoDeployment": { + "Enabled": true, + "RetainStacksOnAccountRemoval": false + }, + "CallAs": "DELEGATED_ADMIN", + "Capabilities": [ + "CAPABILITY_IAM" + ], + "Description": "StackSet for deploying Quota Monitor Service Quota spokes in Organization", + "ManagedExecution": { + "Active": true + }, + "Parameters": [ + { + "ParameterKey": "EventBusArn", + "ParameterValue": { + "Fn::GetAtt": [ + "QMBusFF5C6C0C", + "Arn" + ] + } + }, + { + "ParameterKey": "SpokeSnsRegion", + "ParameterValue": { + "Ref": "SnsSpokeRegion" + } + }, + { + "ParameterKey": "SageMakerMonitoring", + "ParameterValue": { + "Ref": "SageMakerMonitoring" + } + }, + { + "ParameterKey": "ConnectMonitoring", + "ParameterValue": { + "Ref": "ConnectMonitoring" + } + } + ], + "PermissionModel": "SERVICE_MANAGED", + "StackSetName": "QM-SQ-Spoke-StackSet", + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://solutions-", + { + "Ref": "AWS::Region" + }, + ".s3.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com", + { + "Fn::If": [ + "IsChinaPartition", + ".cn", + "" + ] + }, + "/quota-monitor-for-aws/v6.3.0/", + { + "Fn::If": [ + "IsChinaPartition", + "quota-monitor-sq-spoke-cn.template", + "quota-monitor-sq-spoke.template" + ] + } + ] + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SQ-StackSet" + } + }, + "QMSNSStackSet": { + "Type": "AWS::CloudFormation::StackSet", + "Properties": { + "AutoDeployment": { + "Enabled": true, + "RetainStacksOnAccountRemoval": false + }, + "CallAs": "DELEGATED_ADMIN", + "Capabilities": [ + "CAPABILITY_IAM" + ], + "Description": "StackSet for deploying Quota Monitor notification spokes in Organization", + "ManagedExecution": { + "Active": true + }, + "Parameters": [], + "PermissionModel": "SERVICE_MANAGED", + "StackSetName": "QM-SNS-Spoke-StackSet", + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://solutions-", + { + "Ref": "AWS::Region" + }, + ".s3.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com", + { + "Fn::If": [ + "IsChinaPartition", + ".cn", + "" + ] + }, + "/quota-monitor-for-aws/v6.3.0/", + { + "Fn::If": [ + "IsChinaPartition", + "quota-monitor-sns-spoke-cn.template", + "quota-monitor-sns-spoke.template" + ] + } + ] + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-SNS-StackSet" + } + }, + "QMDeploymentManagerQMDeploymentManagerEventsRule53DB2DA9": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - QM-Deployment-Manager-EventsRule", + "EventPattern": { + "detail-type": [ + "Parameter Store Change" + ], + "source": [ + "aws.ssm" + ], + "resources": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "QMOUs122D8EB4" + } + ] + ] + }, + { + "Fn::If": [ + "AccountDeployCondition", + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "QMAccounts3D743F6B" + } + ] + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "QMRegionsList17794003" + } + ] + ] + } + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerLambdaB36F1B21", + "Arn" + ] + }, + "Id": "Target0" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Deployment-Manager/QM-Deployment-Manager-EventsRule/Resource" + } + }, + "QMDeploymentManagerQMDeploymentManagerEventsRuleAllowEventRulequotamonitorhubcnQMDeploymentManagerQMDeploymentManagerLambdaB3A37CA5C9D41F7E": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerLambdaB36F1B21", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerEventsRule53DB2DA9", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Deployment-Manager/QM-Deployment-Manager-EventsRule/AllowEventRulequotamonitorhubcnQMDeploymentManagerQMDeploymentManagerLambdaB3A37CA5" + } + }, + "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Deployment-Manager/QM-Deployment-Manager-Lambda-Dead-Letter-Queue/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "Queue itself is dead-letter queue", + "id": "AwsSolutions-SQS3" + } + ] + } + } + }, + "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueuePolicy6B59E185": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Deployment-Manager/QM-Deployment-Manager-Lambda-Dead-Letter-Queue/Policy/Resource" + } + }, + "QMDeploymentManagerQMDeploymentManagerLambdaServiceRole84304F72": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Deployment-Manager/QM-Deployment-Manager-Lambda/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMDeploymentManagerQMDeploymentManagerLambdaServiceRoleDefaultPolicy7E3D0777": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Encrypt", + "kms:Decrypt", + "kms:CreateGrant" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + { + "Action": "kms:ListAliases", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "events:PutPermission", + "events:RemovePermission" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "events:DescribeEventBus", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMBusFF5C6C0C", + "Arn" + ] + } + }, + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "QMOUs122D8EB4" + } + ] + ] + }, + { + "Fn::If": [ + "AccountDeployCondition", + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "QMAccounts3D743F6B" + } + ] + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "QMRegionsList17794003" + } + ] + ] + } + ] + }, + { + "Action": [ + "organizations:DescribeOrganization", + "organizations:ListRoots", + "organizations:ListAccounts" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "organizations:ListDelegatedAdministrators", + "organizations:ListAccountsForParent" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "cloudformation:DescribeStackSet", + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:*:", + { + "Ref": "ManagementAccountId" + }, + ":stackset/QM-TA-Spoke-StackSet:*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:*:", + { + "Ref": "ManagementAccountId" + }, + ":stackset/QM-SQ-Spoke-StackSet:*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:*:", + { + "Ref": "ManagementAccountId" + }, + ":stackset/QM-SNS-Spoke-StackSet:*" + ] + ] + } + ] + }, + { + "Action": [ + "cloudformation:CreateStackInstances", + "cloudformation:DeleteStackInstances", + "cloudformation:ListStackInstances" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:*:", + { + "Ref": "ManagementAccountId" + }, + ":stackset/QM-TA-Spoke-StackSet:*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:*:", + { + "Ref": "ManagementAccountId" + }, + ":stackset/QM-SQ-Spoke-StackSet:*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:*:", + { + "Ref": "ManagementAccountId" + }, + ":stackset/QM-SNS-Spoke-StackSet:*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:*:", + { + "Ref": "ManagementAccountId" + }, + ":stackset-target/QM-SNS-Spoke-StackSet:*/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:*:", + { + "Ref": "ManagementAccountId" + }, + ":stackset-target/QM-TA-Spoke-StackSet:*/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:*:", + { + "Ref": "ManagementAccountId" + }, + ":stackset-target/QM-SQ-Spoke-StackSet:*/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:*::type/resource/*" + ] + ] + } + ] + }, + { + "Action": "ec2:DescribeRegions", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "support:DescribeTrustedAdvisorChecks", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMDeploymentManagerQMDeploymentManagerLambdaServiceRoleDefaultPolicy7E3D0777", + "Roles": [ + { + "Ref": "QMDeploymentManagerQMDeploymentManagerLambdaServiceRole84304F72" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Deployment-Manager/QM-Deployment-Manager-Lambda/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMDeploymentManagerQMDeploymentManagerLambdaB36F1B21": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/asset6a1cf55956fc481a1f22a54b0fa78a3d78b7e61cd41e12bf80ac8c9404ff9eb2.zip" + }, + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2", + "Arn" + ] + } + }, + "Description": "SO0005 quota-monitor-for-aws - QM-Deployment-Manager-Lambda", + "Environment": { + "Variables": { + "EVENT_BUS_NAME": { + "Ref": "QMBusFF5C6C0C" + }, + "EVENT_BUS_ARN": { + "Fn::GetAtt": [ + "QMBusFF5C6C0C", + "Arn" + ] + }, + "TA_STACKSET_ID": { + "Fn::GetAtt": [ + "QMTAStackSet", + "StackSetId" + ] + }, + "SQ_STACKSET_ID": { + "Fn::GetAtt": [ + "QMSQStackSet", + "StackSetId" + ] + }, + "SNS_STACKSET_ID": { + "Fn::GetAtt": [ + "QMSNSStackSet", + "StackSetId" + ] + }, + "QM_OU_PARAMETER": { + "Ref": "QMOUs122D8EB4" + }, + "QM_ACCOUNT_PARAMETER": { + "Fn::If": [ + "AccountDeployCondition", + { + "Ref": "QMAccounts3D743F6B" + }, + { + "Ref": "AWS::NoValue" + } + ] + }, + "DEPLOYMENT_MODEL": { + "Ref": "DeploymentModel" + }, + "REGIONS_LIST": { + "Ref": "RegionsList" + }, + "QM_REGIONS_LIST_PARAMETER": { + "Ref": "QMRegionsList17794003" + }, + "SNS_SPOKE_REGION": { + "Ref": "SnsSpokeRegion" + }, + "REGIONS_CONCURRENCY_TYPE": { + "Ref": "RegionConcurrency" + }, + "MAX_CONCURRENT_PERCENTAGE": { + "Ref": "MaxConcurrentPercentage" + }, + "FAILURE_TOLERANCE_PERCENTAGE": { + "Ref": "FailureTolerancePercentage" + }, + "SQ_NOTIFICATION_THRESHOLD": { + "Ref": "SQNotificationThreshold" + }, + "SQ_MONITORING_FREQUENCY": { + "Ref": "SQMonitoringFrequency" + }, + "SQ_REPORT_OK_NOTIFICATIONS": { + "Ref": "SQReportOKNotifications" + }, + "SOLUTION_UUID": { + "Fn::GetAtt": [ + "QMHelperCreateUUIDE0D423E6", + "UUID" + ] + }, + "METRICS_ENDPOINT": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "MetricsEndpoint" + ] + }, + "SEND_METRIC": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "SendAnonymizedData" + ] + }, + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "KmsKeyArn": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + }, + "Layers": [ + { + "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerLambdaServiceRole84304F72", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 60 + }, + "DependsOn": [ + "QMDeploymentManagerQMDeploymentManagerLambdaServiceRoleDefaultPolicy7E3D0777", + "QMDeploymentManagerQMDeploymentManagerLambdaServiceRole84304F72" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Deployment-Manager/QM-Deployment-Manager-Lambda/Resource", + "aws:asset:path": "asset.6a1cf55956fc481a1f22a54b0fa78a3d78b7e61cd41e12bf80ac8c9404ff9eb2.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMDeploymentManagerQMDeploymentManagerLambdaEventInvokeConfig4C3821AB": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "QMDeploymentManagerQMDeploymentManagerLambdaB36F1B21" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/QM-Deployment-Manager/QM-Deployment-Manager-Lambda/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "CDKMetadata": { + "Type": "AWS::CDK::Metadata", + "Properties": { + "Analytics": "v2:deflate64:H4sIAAAAAAAA/11RwW7CMAz9lt1DBkzazoA2aRrTuoJ2RWlqUGiTdHHSCVX998UpK7BL/Z797PjVcz6bPfHpnfjBiSyrSa0K3m28kBVb7U0mnNDgwRF5F02jzIHgyppSeWUNi327DlowHnn3THEZkCQjzkMNlKDYs0pH3RucKENhUSuR9An0DFHT+y4+dPP4hQzFtUI/5npWC12UgndrcQL3BQ5pt9h2w1+Ckf5cGHHa89W0toLoaq8O4/L/kxk4rZAmxTUfdgIRoukFhcj5MsgK/FIgMCWiidyefVPMbK1kMj2gOOE7/ojPACGJBpC+F+kVjXoT9VvbKEmlAWxCgdKp5s/UNe9ZeTJC2zLecyuKYZcEeiZrG8q9dVqQklMnXXwDvmfJT6QHOnUOaIOTwGRAb/XOnTnyzNlWlXSbVBmFZPgKfwTfhDjV2BL4Ee/b2SOfT/n87ohKTVwwXmng+RB/AVPhZ8CMAgAA" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-cn/CDKMetadata/Default" + }, + "Condition": "CDKMetadataAvailable" + } + }, + "Outputs": { + "SlackHookKey": { + "Description": "SSM parameter for Slack Web Hook, change the value for your slack workspace", + "Value": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "SSMParameters", + "SlackHook" + ] + }, + "Condition": "SlackTrueCondition" + }, + "UUID": { + "Description": "UUID for the deployment", + "Value": { + "Fn::GetAtt": [ + "QMHelperCreateUUIDE0D423E6", + "UUID" + ] + } + }, + "EventBus": { + "Description": "Event Bus Arn in hub", + "Value": { + "Fn::GetAtt": [ + "QMBusFF5C6C0C", + "Arn" + ] + } + }, + "SNSTopic": { + "Description": "The SNS Topic where notifications are published to", + "Value": { + "Ref": "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4" + } + } + } +} \ No newline at end of file diff --git a/deployment/quota-monitor-hub-no-ou-cn.template b/deployment/quota-monitor-hub-no-ou-cn.template new file mode 100644 index 0000000..43ca683 --- /dev/null +++ b/deployment/quota-monitor-hub-no-ou-cn.template @@ -0,0 +1,2598 @@ +{ + "Description": "(SO0005-NoOU) - quota-monitor-for-aws - Hub Template, use it when you are not using AWS Organizations. Version v6.3.0", + "AWSTemplateFormatVersion": "2010-09-09", + "Metadata": { + "AWS::CloudFormation::Interface": { + "ParameterGroups": [ + { + "Label": { + "default": "Notification Configuration" + }, + "Parameters": [ + "SNSEmail", + "SlackNotification" + ] + } + ], + "ParameterLabels": { + "SNSEmail": { + "default": "Email address for notifications" + }, + "SlackNotification": { + "default": "Do you want slack notifications?" + } + } + } + }, + "Parameters": { + "SNSEmail": { + "Type": "String", + "Default": "", + "Description": "To disable email notifications, leave this blank." + }, + "SlackNotification": { + "Type": "String", + "Default": "No", + "AllowedValues": [ + "Yes", + "No" + ] + } + }, + "Mappings": { + "QuotaMonitorMap": { + "Metrics": { + "SendAnonymizedData": "Yes", + "MetricsEndpoint": "https://metrics.awssolutionsbuilder.com/generic" + }, + "SSMParameters": { + "SlackHook": "/QuotaMonitor/SlackHook", + "Accounts": "/QuotaMonitor/Accounts", + "NotificationMutingConfig": "/QuotaMonitor/NotificationConfiguration" + } + } + }, + "Conditions": { + "EmailTrueCondition": { + "Fn::Not": [ + { + "Fn::Equals": [ + { + "Ref": "SNSEmail" + }, + "" + ] + } + ] + }, + "SlackTrueCondition": { + "Fn::Equals": [ + { + "Ref": "SlackNotification" + }, + "Yes" + ] + }, + "CDKMetadataAvailable": { + "Fn::Or": [ + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "af-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ca-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-northwest-1" + ] + } + ] + }, + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-3" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "il-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "me-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "me-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "sa-east-1" + ] + } + ] + }, + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-east-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-west-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-west-2" + ] + } + ] + } + ] + } + }, + "Resources": { + "QMBusFF5C6C0C": { + "Type": "AWS::Events::EventBus", + "Properties": { + "Name": "QuotaMonitorBus" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Bus/Resource" + } + }, + "KMSHubQMEncryptionKeyA80F8C05": { + "Type": "AWS::KMS::Key", + "Properties": { + "Description": "CMK for AWS resources provisioned by Quota Monitor in this account", + "EnableKeyRotation": true, + "Enabled": true, + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/KMS-Hub/QM-EncryptionKey/Resource" + } + }, + "KMSHubQMEncryptionKeyAlias6C248240": { + "Type": "AWS::KMS::Alias", + "Properties": { + "AliasName": "alias/CMK-KMS-Hub", + "TargetKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/KMS-Hub/QM-EncryptionKey/Alias/Resource" + } + }, + "QMSlackHook4F1AD495": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Slack Hook URL to send Quota Monitor events", + "Name": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "SSMParameters", + "SlackHook" + ] + }, + "Type": "String", + "Value": "NOP" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SlackHook/Resource" + }, + "Condition": "SlackTrueCondition" + }, + "QMAccounts3D743F6B": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "List of target Accounts", + "Name": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "SSMParameters", + "Accounts" + ] + }, + "Type": "StringList", + "Value": "NOP" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Accounts/Resource" + } + }, + "QMNotificationMutingConfig3B7948BA": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Muting configuration for services, limits e.g. ec2:L-1216C47A,ec2:Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances,dynamodb,logs:*,geo:L-05EFD12D", + "Name": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "SSMParameters", + "NotificationMutingConfig" + ] + }, + "Type": "StringList", + "Value": "NOP" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-NotificationMutingConfig/Resource" + } + }, + "QMUtilsLayerQMUtilsLayerLayer80D5D993": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "CompatibleRuntimes": [ + "nodejs18.x" + ], + "Content": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assete8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip" + }, + "LayerName": "QM-UtilsLayer" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-UtilsLayer/QM-UtilsLayer-Layer/Resource", + "aws:asset:path": "asset.e8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Content" + } + }, + "QMSlackNotifierQMSlackNotifierEventsRuleC3528E53": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - QM-SlackNotifier-EventsRule", + "EventBusName": { + "Ref": "QMBusFF5C6C0C" + }, + "EventPattern": { + "detail": { + "status": [ + "WARN", + "ERROR" + ] + }, + "detail-type": [ + "Trusted Advisor Check Item Refresh Notification", + "Service Quotas Utilization Notification" + ], + "source": [ + "aws.trustedadvisor", + "aws-solutions.quota-monitor" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "QMSlackNotifierQMSlackNotifierLambda95713661", + "Arn" + ] + }, + "Id": "Target0" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SlackNotifier/QM-SlackNotifier-EventsRule/Resource" + }, + "Condition": "SlackTrueCondition" + }, + "QMSlackNotifierQMSlackNotifierEventsRuleAllowEventRulequotamonitorhubnooucnQMSlackNotifierQMSlackNotifierLambda096A2BDB3C6D4EFE": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "QMSlackNotifierQMSlackNotifierLambda95713661", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "QMSlackNotifierQMSlackNotifierEventsRuleC3528E53", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SlackNotifier/QM-SlackNotifier-EventsRule/AllowEventRulequotamonitorhubnooucnQMSlackNotifierQMSlackNotifierLambda096A2BDB" + }, + "Condition": "SlackTrueCondition" + }, + "QMSlackNotifierQMSlackNotifierLambdaDeadLetterQueue74B865F7": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SlackNotifier/QM-SlackNotifier-Lambda-Dead-Letter-Queue/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "Queue itself is dead-letter queue", + "id": "AwsSolutions-SQS3" + } + ] + } + }, + "Condition": "SlackTrueCondition" + }, + "QMSlackNotifierQMSlackNotifierLambdaDeadLetterQueuePolicy719E4C6A": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "QMSlackNotifierQMSlackNotifierLambdaDeadLetterQueue74B865F7", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "QMSlackNotifierQMSlackNotifierLambdaDeadLetterQueue74B865F7" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SlackNotifier/QM-SlackNotifier-Lambda-Dead-Letter-Queue/Policy/Resource" + }, + "Condition": "SlackTrueCondition" + }, + "QMSlackNotifierQMSlackNotifierLambdaServiceRole6342FD1D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SlackNotifier/QM-SlackNotifier-Lambda/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + }, + "Condition": "SlackTrueCondition" + }, + "QMSlackNotifierQMSlackNotifierLambdaServiceRoleDefaultPolicy4C4D219B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMSlackNotifierQMSlackNotifierLambdaDeadLetterQueue74B865F7", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Encrypt", + "kms:Decrypt", + "kms:CreateGrant" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + { + "Action": "kms:ListAliases", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "QMSlackHook4F1AD495" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "QMNotificationMutingConfig3B7948BA" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMSlackNotifierQMSlackNotifierLambdaServiceRoleDefaultPolicy4C4D219B", + "Roles": [ + { + "Ref": "QMSlackNotifierQMSlackNotifierLambdaServiceRole6342FD1D" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SlackNotifier/QM-SlackNotifier-Lambda/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + }, + "Condition": "SlackTrueCondition" + }, + "QMSlackNotifierQMSlackNotifierLambda95713661": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/asset11434a0b3246f0b4445dd28fdbc9e4e7dc808ccf355077acd9b000c5d88e6713.zip" + }, + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "QMSlackNotifierQMSlackNotifierLambdaDeadLetterQueue74B865F7", + "Arn" + ] + } + }, + "Description": "SO0005 quota-monitor-for-aws - QM-SlackNotifier-Lambda", + "Environment": { + "Variables": { + "SLACK_HOOK": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "SSMParameters", + "SlackHook" + ] + }, + "QM_NOTIFICATION_MUTING_CONFIG_PARAMETER": { + "Ref": "QMNotificationMutingConfig3B7948BA" + }, + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "KmsKeyArn": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + }, + "Layers": [ + { + "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "QMSlackNotifierQMSlackNotifierLambdaServiceRole6342FD1D", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 60 + }, + "DependsOn": [ + "QMSlackNotifierQMSlackNotifierLambdaServiceRoleDefaultPolicy4C4D219B", + "QMSlackNotifierQMSlackNotifierLambdaServiceRole6342FD1D" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SlackNotifier/QM-SlackNotifier-Lambda/Resource", + "aws:asset:path": "asset.11434a0b3246f0b4445dd28fdbc9e4e7dc808ccf355077acd9b000c5d88e6713.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK", + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + }, + "Condition": "SlackTrueCondition" + }, + "QMSlackNotifierQMSlackNotifierLambdaEventInvokeConfig5340A982": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "QMSlackNotifierQMSlackNotifierLambda95713661" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SlackNotifier/QM-SlackNotifier-Lambda/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + }, + "Condition": "SlackTrueCondition" + }, + "QMHelperQMHelperFunctionServiceRole0506622D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Helper/QM-Helper-Function/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMHelperQMHelperFunction91954E97": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assetf4ee0c3d949f011b3f0f60d231fdacecab71c5f3ccf9674352231cedf831f6cd.zip" + }, + "Description": "SO0005 quota-monitor-for-aws - QM-Helper-Function", + "Environment": { + "Variables": { + "METRICS_ENDPOINT": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "MetricsEndpoint" + ] + }, + "SEND_METRIC": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "SendAnonymizedData" + ] + }, + "QM_STACK_ID": "quota-monitor-hub-no-ou-cn", + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "QMHelperQMHelperFunctionServiceRole0506622D", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 5 + }, + "DependsOn": [ + "QMHelperQMHelperFunctionServiceRole0506622D" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Helper/QM-Helper-Function/Resource", + "aws:asset:path": "asset.f4ee0c3d949f011b3f0f60d231fdacecab71c5f3ccf9674352231cedf831f6cd.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK", + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMHelperQMHelperFunctionEventInvokeConfig580F9F5F": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "QMHelperQMHelperFunction91954E97" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Helper/QM-Helper-Function/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Helper/QM-Helper-Provider/framework-onEvent/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMHelperQMHelperProviderframeworkonEventServiceRoleDefaultPolicy86C1FCC1": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "QMHelperQMHelperFunction91954E97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "QMHelperQMHelperFunction91954E97", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMHelperQMHelperProviderframeworkonEventServiceRoleDefaultPolicy86C1FCC1", + "Roles": [ + { + "Ref": "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Helper/QM-Helper-Provider/framework-onEvent/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMHelperQMHelperProviderframeworkonEventB1DF6D3F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/asset7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94.zip" + }, + "Description": "AWS CDK resource provider framework - onEvent (quota-monitor-hub-no-ou-cn/QM-Helper/QM-Helper-Provider)", + "Environment": { + "Variables": { + "USER_ON_EVENT_FUNCTION_ARN": { + "Fn::GetAtt": [ + "QMHelperQMHelperFunction91954E97", + "Arn" + ] + } + } + }, + "Handler": "framework.onEvent", + "Role": { + "Fn::GetAtt": [ + "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 900 + }, + "DependsOn": [ + "QMHelperQMHelperProviderframeworkonEventServiceRoleDefaultPolicy86C1FCC1", + "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Helper/QM-Helper-Provider/framework-onEvent/Resource", + "aws:asset:path": "asset.7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMHelperCreateUUIDE0D423E6": { + "Type": "Custom::CreateUUID", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "QMHelperQMHelperProviderframeworkonEventB1DF6D3F", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Helper/CreateUUID/Default" + } + }, + "QMHelperLaunchData6F23B2C3": { + "Type": "Custom::LaunchData", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "QMHelperQMHelperProviderframeworkonEventB1DF6D3F", + "Arn" + ] + }, + "SOLUTION_UUID": { + "Fn::GetAtt": [ + "QMHelperCreateUUIDE0D423E6", + "UUID" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Helper/LaunchData/Default" + } + }, + "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4": { + "Type": "AWS::SNS::Topic", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SNSPublisher/QM-SNSPublisher-SNSTopic/Resource" + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionEventsRule5BDCD4FD": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - QM-SNSPublisherFunction-EventsRule", + "EventBusName": { + "Ref": "QMBusFF5C6C0C" + }, + "EventPattern": { + "detail": { + "status": [ + "WARN", + "ERROR" + ] + }, + "detail-type": [ + "Trusted Advisor Check Item Refresh Notification", + "Service Quotas Utilization Notification" + ], + "source": [ + "aws.trustedadvisor", + "aws-solutions.quota-monitor" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambda8BD2DBC1", + "Arn" + ] + }, + "Id": "Target0" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SNSPublisherFunction/QM-SNSPublisherFunction-EventsRule/Resource" + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionEventsRuleAllowEventRulequotamonitorhubnooucnQMSNSPublisherFunctionQMSNSPublisherFunctionLambda24847F3F220BD47F": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambda8BD2DBC1", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionEventsRule5BDCD4FD", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SNSPublisherFunction/QM-SNSPublisherFunction-EventsRule/AllowEventRulequotamonitorhubnooucnQMSNSPublisherFunctionQMSNSPublisherFunctionLambda24847F3F" + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda-Dead-Letter-Queue/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "Queue itself is dead-letter queue", + "id": "AwsSolutions-SQS3" + } + ] + } + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueuePolicyBA6A8707": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda-Dead-Letter-Queue/Policy/Resource" + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleA2F00B10": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleDefaultPolicy1E6E152C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Encrypt", + "kms:Decrypt", + "kms:CreateGrant" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + { + "Action": "kms:ListAliases", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "SNS:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4" + } + }, + { + "Action": "kms:GenerateDataKey", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "QMNotificationMutingConfig3B7948BA" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleDefaultPolicy1E6E152C", + "Roles": [ + { + "Ref": "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleA2F00B10" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambda8BD2DBC1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assete7a324e67e467d0c22e13b0693ca4efdceb0d53025c7fb45fe524870a5c18046.zip" + }, + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A", + "Arn" + ] + } + }, + "Description": "SO0005 quota-monitor-for-aws - QM-SNSPublisherFunction-Lambda", + "Environment": { + "Variables": { + "QM_NOTIFICATION_MUTING_CONFIG_PARAMETER": { + "Ref": "QMNotificationMutingConfig3B7948BA" + }, + "SOLUTION_UUID": { + "Fn::GetAtt": [ + "QMHelperCreateUUIDE0D423E6", + "UUID" + ] + }, + "METRICS_ENDPOINT": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "MetricsEndpoint" + ] + }, + "SEND_METRIC": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "SendAnonymizedData" + ] + }, + "TOPIC_ARN": { + "Ref": "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4" + }, + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "KmsKeyArn": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + }, + "Layers": [ + { + "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleA2F00B10", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 60 + }, + "DependsOn": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleDefaultPolicy1E6E152C", + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleA2F00B10" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda/Resource", + "aws:asset:path": "asset.e7a324e67e467d0c22e13b0693ca4efdceb0d53025c7fb45fe524870a5c18046.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaEventInvokeConfig7A963AA0": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "QMSNSPublisherFunctionQMSNSPublisherFunctionLambda8BD2DBC1" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMEmailSubscription32E71F90": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { + "Ref": "SNSEmail" + }, + "Protocol": "email", + "TopicArn": { + "Ref": "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4" + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-EmailSubscription/Resource" + }, + "Condition": "EmailTrueCondition" + }, + "QMSummarizerEventQueueQMSummarizerEventQueueEventsRuleE50B8D7C": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - QM-Summarizer-EventQueue-EventsRule", + "EventBusName": { + "Ref": "QMBusFF5C6C0C" + }, + "EventPattern": { + "detail": { + "status": [ + "OK", + "WARN", + "ERROR" + ] + }, + "detail-type": [ + "Trusted Advisor Check Item Refresh Notification", + "Service Quotas Utilization Notification" + ], + "source": [ + "aws.trustedadvisor", + "aws-solutions.quota-monitor" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A", + "Arn" + ] + }, + "Id": "Target0" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Summarizer-EventQueue/QM-Summarizer-EventQueue-EventsRule/Resource" + } + }, + "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + }, + "VisibilityTimeout": 60 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Summarizer-EventQueue/QM-Summarizer-EventQueue-Queue/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "dlq not implemented on sqs, will evaluate in future if there is need", + "id": "AwsSolutions-SQS3" + } + ] + } + } + }, + "QMSummarizerEventQueueQMSummarizerEventQueueQueuePolicyE7E1F6D8": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A", + "Arn" + ] + } + }, + { + "Action": [ + "sqs:SendMessage", + "sqs:GetQueueAttributes", + "sqs:GetQueueUrl" + ], + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Summarizer-EventQueue/QM-Summarizer-EventQueue-Queue/Policy/Resource" + } + }, + "QMTable336670B0": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "MessageId", + "AttributeType": "S" + }, + { + "AttributeName": "TimeStamp", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": [ + { + "AttributeName": "MessageId", + "KeyType": "HASH" + }, + { + "AttributeName": "TimeStamp", + "KeyType": "RANGE" + } + ], + "PointInTimeRecoverySpecification": { + "PointInTimeRecoveryEnabled": true + }, + "SSESpecification": { + "KMSMasterKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + }, + "SSEEnabled": true, + "SSEType": "KMS" + }, + "TimeToLiveSpecification": { + "AttributeName": "ExpiryTime", + "Enabled": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Table/Resource" + } + }, + "QMReporterQMReporterEventsRule0BF77282": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - QM-Reporter-EventsRule", + "ScheduleExpression": "rate(5 minutes)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "QMReporterQMReporterLambda7D98A6E4", + "Arn" + ] + }, + "Id": "Target0" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Reporter/QM-Reporter-EventsRule/Resource" + } + }, + "QMReporterQMReporterEventsRuleAllowEventRulequotamonitorhubnooucnQMReporterQMReporterLambda7910947094DD4E7C": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "QMReporterQMReporterLambda7D98A6E4", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "QMReporterQMReporterEventsRule0BF77282", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Reporter/QM-Reporter-EventsRule/AllowEventRulequotamonitorhubnooucnQMReporterQMReporterLambda79109470" + } + }, + "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Reporter/QM-Reporter-Lambda-Dead-Letter-Queue/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "Queue itself is dead-letter queue", + "id": "AwsSolutions-SQS3" + } + ] + } + } + }, + "QMReporterQMReporterLambdaDeadLetterQueuePolicyE714847D": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Reporter/QM-Reporter-Lambda-Dead-Letter-Queue/Policy/Resource" + } + }, + "QMReporterQMReporterLambdaServiceRoleBA4CED84": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Reporter/QM-Reporter-Lambda/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMReporterQMReporterLambdaServiceRoleDefaultPolicyC6B87A76": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Encrypt", + "kms:Decrypt", + "kms:CreateGrant" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + { + "Action": "kms:ListAliases", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "sqs:DeleteMessage", + "sqs:ReceiveMessage" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A", + "Arn" + ] + } + }, + { + "Action": [ + "dynamodb:GetItem", + "dynamodb:PutItem" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMTable336670B0", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMReporterQMReporterLambdaServiceRoleDefaultPolicyC6B87A76", + "Roles": [ + { + "Ref": "QMReporterQMReporterLambdaServiceRoleBA4CED84" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Reporter/QM-Reporter-Lambda/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMReporterQMReporterLambda7D98A6E4": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/asseta6fda81c73d731886f04e1734d036f12ceb7b94c2efec30bb511f477ac58aa9c.zip" + }, + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC", + "Arn" + ] + } + }, + "Description": "SO0005 quota-monitor-for-aws - QM-Reporter-Lambda", + "Environment": { + "Variables": { + "QUOTA_TABLE": { + "Ref": "QMTable336670B0" + }, + "SQS_URL": { + "Ref": "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A" + }, + "MAX_MESSAGES": "10", + "MAX_LOOPS": "10", + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "KmsKeyArn": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + }, + "Layers": [ + { + "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "QMReporterQMReporterLambdaServiceRoleBA4CED84", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 10 + }, + "DependsOn": [ + "QMReporterQMReporterLambdaServiceRoleDefaultPolicyC6B87A76", + "QMReporterQMReporterLambdaServiceRoleBA4CED84" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Reporter/QM-Reporter-Lambda/Resource", + "aws:asset:path": "asset.a6fda81c73d731886f04e1734d036f12ceb7b94c2efec30bb511f477ac58aa9c.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMReporterQMReporterLambdaEventInvokeConfig07548BFA": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "QMReporterQMReporterLambda7D98A6E4" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Reporter/QM-Reporter-Lambda/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMDeploymentManagerQMDeploymentManagerEventsRule53DB2DA9": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - QM-Deployment-Manager-EventsRule", + "EventPattern": { + "detail-type": [ + "Parameter Store Change" + ], + "source": [ + "aws.ssm" + ], + "resources": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "QMAccounts3D743F6B" + } + ] + ] + } + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerLambdaB36F1B21", + "Arn" + ] + }, + "Id": "Target0" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Deployment-Manager/QM-Deployment-Manager-EventsRule/Resource" + } + }, + "QMDeploymentManagerQMDeploymentManagerEventsRuleAllowEventRulequotamonitorhubnooucnQMDeploymentManagerQMDeploymentManagerLambda63626F447B6D89F5": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerLambdaB36F1B21", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerEventsRule53DB2DA9", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Deployment-Manager/QM-Deployment-Manager-EventsRule/AllowEventRulequotamonitorhubnooucnQMDeploymentManagerQMDeploymentManagerLambda63626F44" + } + }, + "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Deployment-Manager/QM-Deployment-Manager-Lambda-Dead-Letter-Queue/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "Queue itself is dead-letter queue", + "id": "AwsSolutions-SQS3" + } + ] + } + } + }, + "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueuePolicy6B59E185": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Deployment-Manager/QM-Deployment-Manager-Lambda-Dead-Letter-Queue/Policy/Resource" + } + }, + "QMDeploymentManagerQMDeploymentManagerLambdaServiceRole84304F72": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Deployment-Manager/QM-Deployment-Manager-Lambda/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMDeploymentManagerQMDeploymentManagerLambdaServiceRoleDefaultPolicy7E3D0777": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Encrypt", + "kms:Decrypt", + "kms:CreateGrant" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + { + "Action": "kms:ListAliases", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "events:PutPermission", + "events:RemovePermission" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "events:DescribeEventBus", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMBusFF5C6C0C", + "Arn" + ] + } + }, + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "QMAccounts3D743F6B" + } + ] + ] + } + }, + { + "Action": "support:DescribeTrustedAdvisorChecks", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMDeploymentManagerQMDeploymentManagerLambdaServiceRoleDefaultPolicy7E3D0777", + "Roles": [ + { + "Ref": "QMDeploymentManagerQMDeploymentManagerLambdaServiceRole84304F72" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Deployment-Manager/QM-Deployment-Manager-Lambda/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMDeploymentManagerQMDeploymentManagerLambdaB36F1B21": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/asset6a1cf55956fc481a1f22a54b0fa78a3d78b7e61cd41e12bf80ac8c9404ff9eb2.zip" + }, + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2", + "Arn" + ] + } + }, + "Description": "SO0005 quota-monitor-for-aws - QM-Deployment-Manager-Lambda", + "Environment": { + "Variables": { + "EVENT_BUS_NAME": { + "Ref": "QMBusFF5C6C0C" + }, + "EVENT_BUS_ARN": { + "Fn::GetAtt": [ + "QMBusFF5C6C0C", + "Arn" + ] + }, + "QM_ACCOUNT_PARAMETER": { + "Ref": "QMAccounts3D743F6B" + }, + "DEPLOYMENT_MODEL": "Accounts", + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "KmsKeyArn": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + }, + "Layers": [ + { + "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerLambdaServiceRole84304F72", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 60 + }, + "DependsOn": [ + "QMDeploymentManagerQMDeploymentManagerLambdaServiceRoleDefaultPolicy7E3D0777", + "QMDeploymentManagerQMDeploymentManagerLambdaServiceRole84304F72" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Deployment-Manager/QM-Deployment-Manager-Lambda/Resource", + "aws:asset:path": "asset.6a1cf55956fc481a1f22a54b0fa78a3d78b7e61cd41e12bf80ac8c9404ff9eb2.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMDeploymentManagerQMDeploymentManagerLambdaEventInvokeConfig4C3821AB": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "QMDeploymentManagerQMDeploymentManagerLambdaB36F1B21" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/QM-Deployment-Manager/QM-Deployment-Manager-Lambda/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "CDKMetadata": { + "Type": "AWS::CDK::Metadata", + "Properties": { + "Analytics": "v2:deflate64:H4sIAAAAAAAA/11R30/CMBD+W3gvFTDRZyCaGDHOQXwlXXeQY1s7e+3MQvq/23Yw0Zd9P3rt3bdb8Pn8kc8m4pumsqymNRb8vLVCVmx9UJkwogELJoo30baojpGutSrRolYs3NufoQNliZ+fIq4cxZKR566GaET0rGpC3Sv00YmwrFGk+kQ8I2pifxMa/Wn+K4bDDZIdPc9q0RSl4OeN6MF8gqE4W7j2X2dgGqSknp2S9mKPPE39ojpdQch4wOMY5dYMY97vBRGE0MsIQfOVkxXYlSBg9BVCfjhwKfhA0jfTNcp+NAfpGYoQOteX/xTxt/BaQyq8udMtyugOZOsKkgbba4xb7VnZK9HoMuxzJ4rh7US8Z2nosOVj3GcOpJ2RwKQjq5u9uWjimdEdlnEB6WQsjFPe8HdnW2c9U7oEfqK7bv7AFzO+mJwIcWqcstgAzwf8ATSVsh5xAgAA" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou-cn/CDKMetadata/Default" + }, + "Condition": "CDKMetadataAvailable" + } + }, + "Outputs": { + "SlackHookKey": { + "Description": "SSM parameter for Slack Web Hook, change the value for your slack workspace", + "Value": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "SSMParameters", + "SlackHook" + ] + }, + "Condition": "SlackTrueCondition" + }, + "UUID": { + "Description": "UUID for the deployment", + "Value": { + "Fn::GetAtt": [ + "QMHelperCreateUUIDE0D423E6", + "UUID" + ] + } + }, + "EventBus": { + "Description": "Event Bus Arn in hub", + "Value": { + "Fn::GetAtt": [ + "QMBusFF5C6C0C", + "Arn" + ] + } + }, + "SNSTopic": { + "Description": "The SNS Topic where notifications are published to", + "Value": { + "Ref": "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4" + } + } + } +} \ No newline at end of file diff --git a/deployment/quota-monitor-hub-no-ou.template b/deployment/quota-monitor-hub-no-ou.template index 615dec0..5db0e9d 100644 --- a/deployment/quota-monitor-hub-no-ou.template +++ b/deployment/quota-monitor-hub-no-ou.template @@ -1,5 +1,5 @@ { - "Description": "(SO0005-NoOU) - quota-monitor-for-aws - Hub Template, use it when you are not using AWS Organizations. Version v6.2.11", + "Description": "(SO0005-NoOU) - quota-monitor-for-aws - Hub Template, use it when you are not using AWS Organizations. Version v6.3.0", "AWSTemplateFormatVersion": "2010-09-09", "Metadata": { "AWS::CloudFormation::Interface": { @@ -426,13 +426,13 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset11e8019ce0550d340c842c1a14bc8797872e01186f9f5b7ba02745ff143d145a.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/assete8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip" }, "LayerName": "QM-UtilsLayer" }, "Metadata": { "aws:cdk:path": "quota-monitor-hub-no-ou/QM-UtilsLayer/QM-UtilsLayer-Layer/Resource", - "aws:asset:path": "asset.11e8019ce0550d340c842c1a14bc8797872e01186f9f5b7ba02745ff143d145a.zip", + "aws:asset:path": "asset.e8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Content" } @@ -738,7 +738,7 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset7ac08df917158d6dd06a4ad585fb1297c04d8abaaebdbfd2406468890bbd116a.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/asset11434a0b3246f0b4445dd28fdbc9e4e7dc808ccf355077acd9b000c5d88e6713.zip" }, "DeadLetterConfig": { "TargetArn": { @@ -762,8 +762,8 @@ "Ref": "QMNotificationMutingConfig3B7948BA" }, "LOG_LEVEL": "info", - "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.2.11", - "VERSION": "v6.2.11", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", "SOLUTION_ID": "SO0005" } }, @@ -795,7 +795,7 @@ ], "Metadata": { "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SlackNotifier/QM-SlackNotifier-Lambda/Resource", - "aws:asset:path": "asset.7ac08df917158d6dd06a4ad585fb1297c04d8abaaebdbfd2406468890bbd116a.zip", + "aws:asset:path": "asset.11434a0b3246f0b4445dd28fdbc9e4e7dc808ccf355077acd9b000c5d88e6713.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Code", "cdk_nag": { @@ -805,6 +805,14 @@ "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK", + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } }, "Condition": "SlackTrueCondition" @@ -831,143 +839,153 @@ }, "Condition": "SlackTrueCondition" }, - "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4": { - "Type": "AWS::SNS::Topic", - "Properties": { - "KmsMasterKeyId": { - "Fn::GetAtt": [ - "KMSHubQMEncryptionKeyA80F8C05", - "Arn" - ] - } - }, - "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisher/QM-SNSPublisher-SNSTopic/Resource" - } - }, - "QMSNSPublisherFunctionQMSNSPublisherFunctionEventsRule5BDCD4FD": { - "Type": "AWS::Events::Rule", + "QMHelperQMHelperFunctionServiceRole0506622D": { + "Type": "AWS::IAM::Role", "Properties": { - "Description": "SO0005 quota-monitor-for-aws - QM-SNSPublisherFunction-EventsRule", - "EventBusName": { - "Ref": "QMBusFF5C6C0C" - }, - "EventPattern": { - "detail": { - "status": [ - "WARN", - "ERROR" - ] - }, - "detail-type": [ - "Trusted Advisor Check Item Refresh Notification", - "Service Quotas Utilization Notification" + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } ], - "source": [ - "aws.trustedadvisor", - "aws-solutions.quota-monitor" - ] + "Version": "2012-10-17" }, - "State": "ENABLED", - "Targets": [ + "ManagedPolicyArns": [ { - "Arn": { - "Fn::GetAtt": [ - "QMSNSPublisherFunctionQMSNSPublisherFunctionLambda8BD2DBC1", - "Arn" + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" ] - }, - "Id": "Target0" + ] } ] }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisherFunction/QM-SNSPublisherFunction-EventsRule/Resource" - } - }, - "QMSNSPublisherFunctionQMSNSPublisherFunctionEventsRuleAllowEventRulequotamonitorhubnoouQMSNSPublisherFunctionQMSNSPublisherFunctionLambda76203A7F3F46BC24": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "FunctionName": { - "Fn::GetAtt": [ - "QMSNSPublisherFunctionQMSNSPublisherFunctionLambda8BD2DBC1", - "Arn" - ] - }, - "Principal": "events.amazonaws.com", - "SourceArn": { - "Fn::GetAtt": [ - "QMSNSPublisherFunctionQMSNSPublisherFunctionEventsRule5BDCD4FD", - "Arn" + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Helper/QM-Helper-Function/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } ] } - }, - "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisherFunction/QM-SNSPublisherFunction-EventsRule/AllowEventRulequotamonitorhubnoouQMSNSPublisherFunctionQMSNSPublisherFunctionLambda76203A7F" } }, - "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A": { - "Type": "AWS::SQS::Queue", + "QMHelperQMHelperFunction91954E97": { + "Type": "AWS::Lambda::Function", "Properties": { - "KmsMasterKeyId": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assetf4ee0c3d949f011b3f0f60d231fdacecab71c5f3ccf9674352231cedf831f6cd.zip" + }, + "Description": "SO0005 quota-monitor-for-aws - QM-Helper-Function", + "Environment": { + "Variables": { + "METRICS_ENDPOINT": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "MetricsEndpoint" + ] + }, + "SEND_METRIC": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "SendAnonymizedData" + ] + }, + "QM_STACK_ID": "quota-monitor-hub-no-ou", + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" + } + ], + "MemorySize": 128, + "Role": { "Fn::GetAtt": [ - "KMSHubQMEncryptionKeyA80F8C05", + "QMHelperQMHelperFunctionServiceRole0506622D", "Arn" ] - } + }, + "Runtime": "nodejs18.x", + "Timeout": 5 }, - "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete", + "DependsOn": [ + "QMHelperQMHelperFunctionServiceRole0506622D" + ], "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda-Dead-Letter-Queue/Resource", + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Helper/QM-Helper-Function/Resource", + "aws:asset:path": "asset.f4ee0c3d949f011b3f0f60d231fdacecab71c5f3ccf9674352231cedf831f6cd.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", "cdk_nag": { "rules_to_suppress": [ { - "reason": "Queue itself is dead-letter queue", - "id": "AwsSolutions-SQS3" + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK", + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } } }, - "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueuePolicyBA6A8707": { - "Type": "AWS::SQS::QueuePolicy", + "QMHelperQMHelperFunctionEventInvokeConfig580F9F5F": { + "Type": "AWS::Lambda::EventInvokeConfig", "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": "sqs:*", - "Condition": { - "Bool": { - "aws:SecureTransport": "false" - } - }, - "Effect": "Deny", - "Principal": { - "AWS": "*" - }, - "Resource": { - "Fn::GetAtt": [ - "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A", - "Arn" - ] - } - } - ], - "Version": "2012-10-17" + "FunctionName": { + "Ref": "QMHelperQMHelperFunction91954E97" }, - "Queues": [ - { - "Ref": "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A" - } - ] + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda-Dead-Letter-Queue/Policy/Resource" + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Helper/QM-Helper-Function/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } } }, - "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleA2F00B10": { + "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -998,7 +1016,7 @@ ] }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda/ServiceRole/Resource", + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Helper/QM-Helper-Provider/framework-onEvent/ServiceRole/Resource", "cdk_nag": { "rules_to_suppress": [ { @@ -1006,107 +1024,60 @@ "id": "AwsSolutions-IAM4" }, { - "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", "id": "AwsSolutions-IAM5" }, { - "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", "id": "AwsSolutions-L1" } ] } } }, - "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleDefaultPolicy1E6E152C": { + "QMHelperQMHelperProviderframeworkonEventServiceRoleDefaultPolicy86C1FCC1": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { "Statement": [ { - "Action": "sqs:SendMessage", - "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A", - "Arn" - ] - } - }, - { - "Action": [ - "kms:Encrypt", - "kms:Decrypt", - "kms:CreateGrant" - ], - "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "KMSHubQMEncryptionKeyA80F8C05", - "Arn" - ] - } - }, - { - "Action": "kms:ListAliases", - "Effect": "Allow", - "Resource": "*" - }, - { - "Action": "SNS:Publish", - "Effect": "Allow", - "Resource": { - "Ref": "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4" - } - }, - { - "Action": "kms:GenerateDataKey", + "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "KMSHubQMEncryptionKeyA80F8C05", - "Arn" - ] - } - }, - { - "Action": "ssm:GetParameter", - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":ssm:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":parameter", - { - "Ref": "QMNotificationMutingConfig3B7948BA" - } + "Resource": [ + { + "Fn::GetAtt": [ + "QMHelperQMHelperFunction91954E97", + "Arn" ] - ] - } + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "QMHelperQMHelperFunction91954E97", + "Arn" + ] + }, + ":*" + ] + ] + } + ] } ], "Version": "2012-10-17" }, - "PolicyName": "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleDefaultPolicy1E6E152C", + "PolicyName": "QMHelperQMHelperProviderframeworkonEventServiceRoleDefaultPolicy86C1FCC1", "Roles": [ { - "Ref": "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleA2F00B10" + "Ref": "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB" } ] }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda/ServiceRole/DefaultPolicy/Resource", + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Helper/QM-Helper-Provider/framework-onEvent/ServiceRole/DefaultPolicy/Resource", "cdk_nag": { "rules_to_suppress": [ { @@ -1114,138 +1085,142 @@ "id": "AwsSolutions-IAM4" }, { - "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", "id": "AwsSolutions-IAM5" }, { - "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", "id": "AwsSolutions-L1" } ] } } }, - "QMSNSPublisherFunctionQMSNSPublisherFunctionLambda8BD2DBC1": { + "QMHelperQMHelperProviderframeworkonEventB1DF6D3F": { "Type": "AWS::Lambda::Function", "Properties": { "Code": { "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/assetc7d0a18c007f94e3ee2cc35273b6a455312835f2ec08fa32e1395e13b0ae78e8.zip" - }, - "DeadLetterConfig": { - "TargetArn": { - "Fn::GetAtt": [ - "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A", - "Arn" - ] - } + "S3Key": "quota-monitor-for-aws/v6.3.0/asset7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94.zip" }, - "Description": "SO0005 quota-monitor-for-aws - QM-SNSPublisherFunction-Lambda", + "Description": "AWS CDK resource provider framework - onEvent (quota-monitor-hub-no-ou/QM-Helper/QM-Helper-Provider)", "Environment": { "Variables": { - "QM_NOTIFICATION_MUTING_CONFIG_PARAMETER": { - "Ref": "QMNotificationMutingConfig3B7948BA" - }, - "TOPIC_ARN": { - "Ref": "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4" - }, - "LOG_LEVEL": "info", - "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.2.11", - "VERSION": "v6.2.11", - "SOLUTION_ID": "SO0005" + "USER_ON_EVENT_FUNCTION_ARN": { + "Fn::GetAtt": [ + "QMHelperQMHelperFunction91954E97", + "Arn" + ] + } } }, - "Handler": "index.handler", - "KmsKeyArn": { - "Fn::GetAtt": [ - "KMSHubQMEncryptionKeyA80F8C05", - "Arn" - ] - }, - "Layers": [ - { - "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" - } - ], - "MemorySize": 128, + "Handler": "framework.onEvent", "Role": { "Fn::GetAtt": [ - "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleA2F00B10", + "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB", "Arn" ] }, "Runtime": "nodejs18.x", - "Timeout": 60 + "Timeout": 900 }, "DependsOn": [ - "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleDefaultPolicy1E6E152C", - "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleA2F00B10" + "QMHelperQMHelperProviderframeworkonEventServiceRoleDefaultPolicy86C1FCC1", + "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB" ], "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda/Resource", - "aws:asset:path": "asset.c7d0a18c007f94e3ee2cc35273b6a455312835f2ec08fa32e1395e13b0ae78e8.zip", + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Helper/QM-Helper-Provider/framework-onEvent/Resource", + "aws:asset:path": "asset.7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94", "aws:asset:is-bundled": false, "aws:asset:property": "Code", "cdk_nag": { "rules_to_suppress": [ { - "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } } }, - "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaEventInvokeConfig7A963AA0": { - "Type": "AWS::Lambda::EventInvokeConfig", + "QMHelperCreateUUIDE0D423E6": { + "Type": "Custom::CreateUUID", "Properties": { - "FunctionName": { - "Ref": "QMSNSPublisherFunctionQMSNSPublisherFunctionLambda8BD2DBC1" - }, - "MaximumEventAgeInSeconds": 14400, - "Qualifier": "$LATEST" - }, - "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda/EventInvokeConfig/Resource", - "cdk_nag": { - "rules_to_suppress": [ - { - "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", - "id": "AwsSolutions-L1" - } + "ServiceToken": { + "Fn::GetAtt": [ + "QMHelperQMHelperProviderframeworkonEventB1DF6D3F", + "Arn" ] } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Helper/CreateUUID/Default" } }, - "QMEmailSubscription32E71F90": { - "Type": "AWS::SNS::Subscription", + "QMHelperLaunchData6F23B2C3": { + "Type": "Custom::LaunchData", "Properties": { - "Endpoint": { - "Ref": "SNSEmail" + "ServiceToken": { + "Fn::GetAtt": [ + "QMHelperQMHelperProviderframeworkonEventB1DF6D3F", + "Arn" + ] }, - "Protocol": "email", - "TopicArn": { - "Ref": "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4" + "SOLUTION_UUID": { + "Fn::GetAtt": [ + "QMHelperCreateUUIDE0D423E6", + "UUID" + ] } }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-EmailSubscription/Resource" + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Helper/LaunchData/Default" + } + }, + "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4": { + "Type": "AWS::SNS::Topic", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } }, - "Condition": "EmailTrueCondition" + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisher/QM-SNSPublisher-SNSTopic/Resource" + } }, - "QMSummarizerEventQueueQMSummarizerEventQueueEventsRuleE50B8D7C": { + "QMSNSPublisherFunctionQMSNSPublisherFunctionEventsRule5BDCD4FD": { "Type": "AWS::Events::Rule", "Properties": { - "Description": "SO0005 quota-monitor-for-aws - QM-Summarizer-EventQueue-EventsRule", + "Description": "SO0005 quota-monitor-for-aws - QM-SNSPublisherFunction-EventsRule", "EventBusName": { "Ref": "QMBusFF5C6C0C" }, "EventPattern": { "detail": { "status": [ - "OK", "WARN", "ERROR" ] @@ -1264,7 +1239,7 @@ { "Arn": { "Fn::GetAtt": [ - "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A", + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambda8BD2DBC1", "Arn" ] }, @@ -1273,10 +1248,32 @@ ] }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Summarizer-EventQueue/QM-Summarizer-EventQueue-EventsRule/Resource" + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisherFunction/QM-SNSPublisherFunction-EventsRule/Resource" } }, - "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A": { + "QMSNSPublisherFunctionQMSNSPublisherFunctionEventsRuleAllowEventRulequotamonitorhubnoouQMSNSPublisherFunctionQMSNSPublisherFunctionLambda76203A7F3F46BC24": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambda8BD2DBC1", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionEventsRule5BDCD4FD", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisherFunction/QM-SNSPublisherFunction-EventsRule/AllowEventRulequotamonitorhubnoouQMSNSPublisherFunctionQMSNSPublisherFunctionLambda76203A7F" + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A": { "Type": "AWS::SQS::Queue", "Properties": { "KmsMasterKeyId": { @@ -1284,24 +1281,23 @@ "KMSHubQMEncryptionKeyA80F8C05", "Arn" ] - }, - "VisibilityTimeout": 60 + } }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete", "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Summarizer-EventQueue/QM-Summarizer-EventQueue-Queue/Resource", + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda-Dead-Letter-Queue/Resource", "cdk_nag": { "rules_to_suppress": [ { - "reason": "dlq not implemented on sqs, will evaluate in future if there is need", + "reason": "Queue itself is dead-letter queue", "id": "AwsSolutions-SQS3" } ] } } }, - "QMSummarizerEventQueueQMSummarizerEventQueueQueuePolicyE7E1F6D8": { + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueuePolicyBA6A8707": { "Type": "AWS::SQS::QueuePolicy", "Properties": { "PolicyDocument": { @@ -1319,24 +1315,7 @@ }, "Resource": { "Fn::GetAtt": [ - "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A", - "Arn" - ] - } - }, - { - "Action": [ - "sqs:SendMessage", - "sqs:GetQueueAttributes", - "sqs:GetQueueUrl" - ], - "Effect": "Allow", - "Principal": { - "Service": "events.amazonaws.com" - }, - "Resource": { - "Fn::GetAtt": [ - "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A", + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A", "Arn" ] } @@ -1346,167 +1325,15 @@ }, "Queues": [ { - "Ref": "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A" + "Ref": "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A" } ] }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Summarizer-EventQueue/QM-Summarizer-EventQueue-Queue/Policy/Resource" + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda-Dead-Letter-Queue/Policy/Resource" } }, - "QMTable336670B0": { - "Type": "AWS::DynamoDB::Table", - "Properties": { - "AttributeDefinitions": [ - { - "AttributeName": "MessageId", - "AttributeType": "S" - }, - { - "AttributeName": "TimeStamp", - "AttributeType": "S" - } - ], - "BillingMode": "PAY_PER_REQUEST", - "KeySchema": [ - { - "AttributeName": "MessageId", - "KeyType": "HASH" - }, - { - "AttributeName": "TimeStamp", - "KeyType": "RANGE" - } - ], - "PointInTimeRecoverySpecification": { - "PointInTimeRecoveryEnabled": true - }, - "SSESpecification": { - "KMSMasterKeyId": { - "Fn::GetAtt": [ - "KMSHubQMEncryptionKeyA80F8C05", - "Arn" - ] - }, - "SSEEnabled": true, - "SSEType": "KMS" - }, - "TimeToLiveSpecification": { - "AttributeName": "ExpiryTime", - "Enabled": true - } - }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain", - "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Table/Resource" - } - }, - "QMReporterQMReporterEventsRule0BF77282": { - "Type": "AWS::Events::Rule", - "Properties": { - "Description": "SO0005 quota-monitor-for-aws - QM-Reporter-EventsRule", - "ScheduleExpression": "rate(5 minutes)", - "State": "ENABLED", - "Targets": [ - { - "Arn": { - "Fn::GetAtt": [ - "QMReporterQMReporterLambda7D98A6E4", - "Arn" - ] - }, - "Id": "Target0" - } - ] - }, - "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Reporter/QM-Reporter-EventsRule/Resource" - } - }, - "QMReporterQMReporterEventsRuleAllowEventRulequotamonitorhubnoouQMReporterQMReporterLambda0CE086E3DDFD1F2A": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "FunctionName": { - "Fn::GetAtt": [ - "QMReporterQMReporterLambda7D98A6E4", - "Arn" - ] - }, - "Principal": "events.amazonaws.com", - "SourceArn": { - "Fn::GetAtt": [ - "QMReporterQMReporterEventsRule0BF77282", - "Arn" - ] - } - }, - "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Reporter/QM-Reporter-EventsRule/AllowEventRulequotamonitorhubnoouQMReporterQMReporterLambda0CE086E3" - } - }, - "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC": { - "Type": "AWS::SQS::Queue", - "Properties": { - "KmsMasterKeyId": { - "Fn::GetAtt": [ - "KMSHubQMEncryptionKeyA80F8C05", - "Arn" - ] - } - }, - "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete", - "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Reporter/QM-Reporter-Lambda-Dead-Letter-Queue/Resource", - "cdk_nag": { - "rules_to_suppress": [ - { - "reason": "Queue itself is dead-letter queue", - "id": "AwsSolutions-SQS3" - } - ] - } - } - }, - "QMReporterQMReporterLambdaDeadLetterQueuePolicyE714847D": { - "Type": "AWS::SQS::QueuePolicy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": "sqs:*", - "Condition": { - "Bool": { - "aws:SecureTransport": "false" - } - }, - "Effect": "Deny", - "Principal": { - "AWS": "*" - }, - "Resource": { - "Fn::GetAtt": [ - "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC", - "Arn" - ] - } - } - ], - "Version": "2012-10-17" - }, - "Queues": [ - { - "Ref": "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC" - } - ] - }, - "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Reporter/QM-Reporter-Lambda-Dead-Letter-Queue/Policy/Resource" - } - }, - "QMReporterQMReporterLambdaServiceRoleBA4CED84": { + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleA2F00B10": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -1537,7 +1364,7 @@ ] }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Reporter/QM-Reporter-Lambda/ServiceRole/Resource", + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda/ServiceRole/Resource", "cdk_nag": { "rules_to_suppress": [ { @@ -1556,7 +1383,7 @@ } } }, - "QMReporterQMReporterLambdaServiceRoleDefaultPolicyC6B87A76": { + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleDefaultPolicy1E6E152C": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -1566,7 +1393,7 @@ "Effect": "Allow", "Resource": { "Fn::GetAtt": [ - "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC", + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A", "Arn" ] } @@ -1591,11 +1418,297 @@ "Resource": "*" }, { - "Action": [ - "sqs:DeleteMessage", - "sqs:ReceiveMessage" - ], + "Action": "SNS:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4" + } + }, + { + "Action": "kms:GenerateDataKey", "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "QMNotificationMutingConfig3B7948BA" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleDefaultPolicy1E6E152C", + "Roles": [ + { + "Ref": "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleA2F00B10" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambda8BD2DBC1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assete7a324e67e467d0c22e13b0693ca4efdceb0d53025c7fb45fe524870a5c18046.zip" + }, + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaDeadLetterQueue72FF519A", + "Arn" + ] + } + }, + "Description": "SO0005 quota-monitor-for-aws - QM-SNSPublisherFunction-Lambda", + "Environment": { + "Variables": { + "QM_NOTIFICATION_MUTING_CONFIG_PARAMETER": { + "Ref": "QMNotificationMutingConfig3B7948BA" + }, + "SOLUTION_UUID": { + "Fn::GetAtt": [ + "QMHelperCreateUUIDE0D423E6", + "UUID" + ] + }, + "METRICS_ENDPOINT": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "MetricsEndpoint" + ] + }, + "SEND_METRIC": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "SendAnonymizedData" + ] + }, + "TOPIC_ARN": { + "Ref": "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4" + }, + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "KmsKeyArn": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + }, + "Layers": [ + { + "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleA2F00B10", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 60 + }, + "DependsOn": [ + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleDefaultPolicy1E6E152C", + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaServiceRoleA2F00B10" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda/Resource", + "aws:asset:path": "asset.e7a324e67e467d0c22e13b0693ca4efdceb0d53025c7fb45fe524870a5c18046.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMSNSPublisherFunctionQMSNSPublisherFunctionLambdaEventInvokeConfig7A963AA0": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "QMSNSPublisherFunctionQMSNSPublisherFunctionLambda8BD2DBC1" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMEmailSubscription32E71F90": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { + "Ref": "SNSEmail" + }, + "Protocol": "email", + "TopicArn": { + "Ref": "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4" + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-EmailSubscription/Resource" + }, + "Condition": "EmailTrueCondition" + }, + "QMSummarizerEventQueueQMSummarizerEventQueueEventsRuleE50B8D7C": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - QM-Summarizer-EventQueue-EventsRule", + "EventBusName": { + "Ref": "QMBusFF5C6C0C" + }, + "EventPattern": { + "detail": { + "status": [ + "OK", + "WARN", + "ERROR" + ] + }, + "detail-type": [ + "Trusted Advisor Check Item Refresh Notification", + "Service Quotas Utilization Notification" + ], + "source": [ + "aws.trustedadvisor", + "aws-solutions.quota-monitor" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A", + "Arn" + ] + }, + "Id": "Target0" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Summarizer-EventQueue/QM-Summarizer-EventQueue-EventsRule/Resource" + } + }, + "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + }, + "VisibilityTimeout": 60 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Summarizer-EventQueue/QM-Summarizer-EventQueue-Queue/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "dlq not implemented on sqs, will evaluate in future if there is need", + "id": "AwsSolutions-SQS3" + } + ] + } + } + }, + "QMSummarizerEventQueueQMSummarizerEventQueueQueuePolicyE7E1F6D8": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, "Resource": { "Fn::GetAtt": [ "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A", @@ -1605,13 +1718,17 @@ }, { "Action": [ - "dynamodb:GetItem", - "dynamodb:PutItem" + "sqs:SendMessage", + "sqs:GetQueueAttributes", + "sqs:GetQueueUrl" ], "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + }, "Resource": { "Fn::GetAtt": [ - "QMTable336670B0", + "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A", "Arn" ] } @@ -1619,172 +1736,75 @@ ], "Version": "2012-10-17" }, - "PolicyName": "QMReporterQMReporterLambdaServiceRoleDefaultPolicyC6B87A76", - "Roles": [ + "Queues": [ { - "Ref": "QMReporterQMReporterLambdaServiceRoleBA4CED84" + "Ref": "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A" } ] }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Reporter/QM-Reporter-Lambda/ServiceRole/DefaultPolicy/Resource", - "cdk_nag": { - "rules_to_suppress": [ - { - "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", - "id": "AwsSolutions-IAM4" - }, - { - "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", - "id": "AwsSolutions-IAM5" - }, - { - "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", - "id": "AwsSolutions-L1" - } - ] - } + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Summarizer-EventQueue/QM-Summarizer-EventQueue-Queue/Policy/Resource" } }, - "QMReporterQMReporterLambda7D98A6E4": { - "Type": "AWS::Lambda::Function", + "QMTable336670B0": { + "Type": "AWS::DynamoDB::Table", "Properties": { - "Code": { - "S3Bucket": { - "Fn::Sub": "solutions-${AWS::Region}" + "AttributeDefinitions": [ + { + "AttributeName": "MessageId", + "AttributeType": "S" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset3214e803bdf3b3311d409c896a5cc335cb056428c232c86da8c38175b6b0b79e.zip" - }, - "DeadLetterConfig": { - "TargetArn": { - "Fn::GetAtt": [ - "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC", - "Arn" - ] - } - }, - "Description": "SO0005 quota-monitor-for-aws - QM-Reporter-Lambda", - "Environment": { - "Variables": { - "QUOTA_TABLE": { - "Ref": "QMTable336670B0" - }, - "SQS_URL": { - "Ref": "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A" - }, - "MAX_MESSAGES": "10", - "MAX_LOOPS": "10", - "LOG_LEVEL": "info", - "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.2.11", - "VERSION": "v6.2.11", - "SOLUTION_ID": "SO0005" + { + "AttributeName": "TimeStamp", + "AttributeType": "S" } - }, - "Handler": "index.handler", - "KmsKeyArn": { - "Fn::GetAtt": [ - "KMSHubQMEncryptionKeyA80F8C05", - "Arn" - ] - }, - "Layers": [ + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": [ { - "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" + "AttributeName": "MessageId", + "KeyType": "HASH" + }, + { + "AttributeName": "TimeStamp", + "KeyType": "RANGE" } ], - "MemorySize": 512, - "Role": { - "Fn::GetAtt": [ - "QMReporterQMReporterLambdaServiceRoleBA4CED84", - "Arn" - ] + "PointInTimeRecoverySpecification": { + "PointInTimeRecoveryEnabled": true }, - "Runtime": "nodejs18.x", - "Timeout": 10 - }, - "DependsOn": [ - "QMReporterQMReporterLambdaServiceRoleDefaultPolicyC6B87A76", - "QMReporterQMReporterLambdaServiceRoleBA4CED84" - ], - "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Reporter/QM-Reporter-Lambda/Resource", - "aws:asset:path": "asset.3214e803bdf3b3311d409c896a5cc335cb056428c232c86da8c38175b6b0b79e.zip", - "aws:asset:is-bundled": false, - "aws:asset:property": "Code", - "cdk_nag": { - "rules_to_suppress": [ - { - "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", - "id": "AwsSolutions-L1" - } - ] - } - } - }, - "QMReporterQMReporterLambdaEventInvokeConfig07548BFA": { - "Type": "AWS::Lambda::EventInvokeConfig", - "Properties": { - "FunctionName": { - "Ref": "QMReporterQMReporterLambda7D98A6E4" + "SSESpecification": { + "KMSMasterKeyId": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + }, + "SSEEnabled": true, + "SSEType": "KMS" }, - "MaximumEventAgeInSeconds": 14400, - "Qualifier": "$LATEST" + "TimeToLiveSpecification": { + "AttributeName": "ExpiryTime", + "Enabled": true + } }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Reporter/QM-Reporter-Lambda/EventInvokeConfig/Resource", - "cdk_nag": { - "rules_to_suppress": [ - { - "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", - "id": "AwsSolutions-L1" - } - ] - } + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Table/Resource" } }, - "QMDeploymentManagerQMDeploymentManagerEventsRule53DB2DA9": { + "QMReporterQMReporterEventsRule0BF77282": { "Type": "AWS::Events::Rule", "Properties": { - "Description": "SO0005 quota-monitor-for-aws - QM-Deployment-Manager-EventsRule", - "EventPattern": { - "detail-type": [ - "Parameter Store Change" - ], - "source": [ - "aws.ssm" - ], - "resources": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":ssm:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":parameter", - { - "Ref": "QMAccounts3D743F6B" - } - ] - ] - } - ] - }, + "Description": "SO0005 quota-monitor-for-aws - QM-Reporter-EventsRule", + "ScheduleExpression": "rate(5 minutes)", "State": "ENABLED", "Targets": [ { "Arn": { "Fn::GetAtt": [ - "QMDeploymentManagerQMDeploymentManagerLambdaB36F1B21", + "QMReporterQMReporterLambda7D98A6E4", "Arn" ] }, @@ -1793,32 +1813,32 @@ ] }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Deployment-Manager/QM-Deployment-Manager-EventsRule/Resource" + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Reporter/QM-Reporter-EventsRule/Resource" } }, - "QMDeploymentManagerQMDeploymentManagerEventsRuleAllowEventRulequotamonitorhubnoouQMDeploymentManagerQMDeploymentManagerLambda69BB20E9F676A8A9": { + "QMReporterQMReporterEventsRuleAllowEventRulequotamonitorhubnoouQMReporterQMReporterLambda0CE086E3DDFD1F2A": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", "FunctionName": { "Fn::GetAtt": [ - "QMDeploymentManagerQMDeploymentManagerLambdaB36F1B21", + "QMReporterQMReporterLambda7D98A6E4", "Arn" ] }, "Principal": "events.amazonaws.com", "SourceArn": { "Fn::GetAtt": [ - "QMDeploymentManagerQMDeploymentManagerEventsRule53DB2DA9", + "QMReporterQMReporterEventsRule0BF77282", "Arn" ] } }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Deployment-Manager/QM-Deployment-Manager-EventsRule/AllowEventRulequotamonitorhubnoouQMDeploymentManagerQMDeploymentManagerLambda69BB20E9" + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Reporter/QM-Reporter-EventsRule/AllowEventRulequotamonitorhubnoouQMReporterQMReporterLambda0CE086E3" } }, - "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2": { + "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC": { "Type": "AWS::SQS::Queue", "Properties": { "KmsMasterKeyId": { @@ -1831,7 +1851,7 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete", "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Deployment-Manager/QM-Deployment-Manager-Lambda-Dead-Letter-Queue/Resource", + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Reporter/QM-Reporter-Lambda-Dead-Letter-Queue/Resource", "cdk_nag": { "rules_to_suppress": [ { @@ -1842,7 +1862,7 @@ } } }, - "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueuePolicy6B59E185": { + "QMReporterQMReporterLambdaDeadLetterQueuePolicyE714847D": { "Type": "AWS::SQS::QueuePolicy", "Properties": { "PolicyDocument": { @@ -1860,7 +1880,7 @@ }, "Resource": { "Fn::GetAtt": [ - "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2", + "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC", "Arn" ] } @@ -1870,15 +1890,15 @@ }, "Queues": [ { - "Ref": "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2" + "Ref": "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC" } ] }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Deployment-Manager/QM-Deployment-Manager-Lambda-Dead-Letter-Queue/Policy/Resource" + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Reporter/QM-Reporter-Lambda-Dead-Letter-Queue/Policy/Resource" } }, - "QMDeploymentManagerQMDeploymentManagerLambdaServiceRole84304F72": { + "QMReporterQMReporterLambdaServiceRoleBA4CED84": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -1909,7 +1929,7 @@ ] }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Deployment-Manager/QM-Deployment-Manager-Lambda/ServiceRole/Resource", + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Reporter/QM-Reporter-Lambda/ServiceRole/Resource", "cdk_nag": { "rules_to_suppress": [ { @@ -1928,7 +1948,7 @@ } } }, - "QMDeploymentManagerQMDeploymentManagerLambdaServiceRoleDefaultPolicy7E3D0777": { + "QMReporterQMReporterLambdaServiceRoleDefaultPolicyC6B87A76": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -1938,7 +1958,7 @@ "Effect": "Allow", "Resource": { "Fn::GetAtt": [ - "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2", + "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC", "Arn" ] } @@ -1957,73 +1977,49 @@ ] } }, - { - "Action": "kms:ListAliases", - "Effect": "Allow", - "Resource": "*" - }, - { - "Action": [ - "events:PutPermission", - "events:RemovePermission" - ], - "Effect": "Allow", - "Resource": "*" - }, - { - "Action": "events:DescribeEventBus", - "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "QMBusFF5C6C0C", - "Arn" - ] - } - }, - { - "Action": "ssm:GetParameter", - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":ssm:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":parameter", - { - "Ref": "QMAccounts3D743F6B" - } - ] + { + "Action": "kms:ListAliases", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "sqs:DeleteMessage", + "sqs:ReceiveMessage" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A", + "Arn" ] } }, { - "Action": "support:DescribeTrustedAdvisorChecks", + "Action": [ + "dynamodb:GetItem", + "dynamodb:PutItem" + ], "Effect": "Allow", - "Resource": "*" + "Resource": { + "Fn::GetAtt": [ + "QMTable336670B0", + "Arn" + ] + } } ], "Version": "2012-10-17" }, - "PolicyName": "QMDeploymentManagerQMDeploymentManagerLambdaServiceRoleDefaultPolicy7E3D0777", + "PolicyName": "QMReporterQMReporterLambdaServiceRoleDefaultPolicyC6B87A76", "Roles": [ { - "Ref": "QMDeploymentManagerQMDeploymentManagerLambdaServiceRole84304F72" + "Ref": "QMReporterQMReporterLambdaServiceRoleBA4CED84" } ] }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Deployment-Manager/QM-Deployment-Manager-Lambda/ServiceRole/DefaultPolicy/Resource", + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Reporter/QM-Reporter-Lambda/ServiceRole/DefaultPolicy/Resource", "cdk_nag": { "rules_to_suppress": [ { @@ -2042,42 +2038,37 @@ } } }, - "QMDeploymentManagerQMDeploymentManagerLambdaB36F1B21": { + "QMReporterQMReporterLambda7D98A6E4": { "Type": "AWS::Lambda::Function", "Properties": { "Code": { "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset5a81c4fb5c63f552f9d49c541884c214130c866132af0414ab4c7b3394fb57da.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/asseta6fda81c73d731886f04e1734d036f12ceb7b94c2efec30bb511f477ac58aa9c.zip" }, "DeadLetterConfig": { "TargetArn": { "Fn::GetAtt": [ - "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2", + "QMReporterQMReporterLambdaDeadLetterQueueA0C464BC", "Arn" ] } }, - "Description": "SO0005 quota-monitor-for-aws - QM-Deployment-Manager-Lambda", + "Description": "SO0005 quota-monitor-for-aws - QM-Reporter-Lambda", "Environment": { "Variables": { - "EVENT_BUS_NAME": { - "Ref": "QMBusFF5C6C0C" - }, - "EVENT_BUS_ARN": { - "Fn::GetAtt": [ - "QMBusFF5C6C0C", - "Arn" - ] + "QUOTA_TABLE": { + "Ref": "QMTable336670B0" }, - "QM_ACCOUNT_PARAMETER": { - "Ref": "QMAccounts3D743F6B" + "SQS_URL": { + "Ref": "QMSummarizerEventQueueQMSummarizerEventQueueQueue95FCCD2A" }, - "DEPLOYMENT_MODEL": "Accounts", + "MAX_MESSAGES": "10", + "MAX_LOOPS": "10", "LOG_LEVEL": "info", - "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.2.11", - "VERSION": "v6.2.11", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", "SOLUTION_ID": "SO0005" } }, @@ -2096,20 +2087,20 @@ "MemorySize": 512, "Role": { "Fn::GetAtt": [ - "QMDeploymentManagerQMDeploymentManagerLambdaServiceRole84304F72", + "QMReporterQMReporterLambdaServiceRoleBA4CED84", "Arn" ] }, "Runtime": "nodejs18.x", - "Timeout": 60 + "Timeout": 10 }, "DependsOn": [ - "QMDeploymentManagerQMDeploymentManagerLambdaServiceRoleDefaultPolicy7E3D0777", - "QMDeploymentManagerQMDeploymentManagerLambdaServiceRole84304F72" + "QMReporterQMReporterLambdaServiceRoleDefaultPolicyC6B87A76", + "QMReporterQMReporterLambdaServiceRoleBA4CED84" ], "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Deployment-Manager/QM-Deployment-Manager-Lambda/Resource", - "aws:asset:path": "asset.5a81c4fb5c63f552f9d49c541884c214130c866132af0414ab4c7b3394fb57da.zip", + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Reporter/QM-Reporter-Lambda/Resource", + "aws:asset:path": "asset.a6fda81c73d731886f04e1734d036f12ceb7b94c2efec30bb511f477ac58aa9c.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Code", "cdk_nag": { @@ -2119,20 +2110,26 @@ "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } } }, - "QMDeploymentManagerQMDeploymentManagerLambdaEventInvokeConfig4C3821AB": { + "QMReporterQMReporterLambdaEventInvokeConfig07548BFA": { "Type": "AWS::Lambda::EventInvokeConfig", "Properties": { "FunctionName": { - "Ref": "QMDeploymentManagerQMDeploymentManagerLambdaB36F1B21" + "Ref": "QMReporterQMReporterLambda7D98A6E4" }, "MaximumEventAgeInSeconds": 14400, "Qualifier": "$LATEST" }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Deployment-Manager/QM-Deployment-Manager-Lambda/EventInvokeConfig/Resource", + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Reporter/QM-Reporter-Lambda/EventInvokeConfig/Resource", "cdk_nag": { "rules_to_suppress": [ { @@ -2143,145 +2140,143 @@ } } }, - "QMHelperQMHelperFunctionServiceRole0506622D": { - "Type": "AWS::IAM::Role", + "QMDeploymentManagerQMDeploymentManagerEventsRule53DB2DA9": { + "Type": "AWS::Events::Rule", "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ + "Description": "SO0005 quota-monitor-for-aws - QM-Deployment-Manager-EventsRule", + "EventPattern": { + "detail-type": [ + "Parameter Store Change" + ], + "source": [ + "aws.ssm" + ], + "resources": [ { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com" - } + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "QMAccounts3D743F6B" + } + ] + ] } - ], - "Version": "2012-10-17" + ] }, - "ManagedPolicyArns": [ + "State": "ENABLED", + "Targets": [ { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + "Arn": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerLambdaB36F1B21", + "Arn" ] - ] + }, + "Id": "Target0" } ] }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Helper/QM-Helper-Function/ServiceRole/Resource", - "cdk_nag": { - "rules_to_suppress": [ - { - "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", - "id": "AwsSolutions-IAM4" - }, - { - "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", - "id": "AwsSolutions-IAM5" - }, - { - "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", - "id": "AwsSolutions-L1" - } + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Deployment-Manager/QM-Deployment-Manager-EventsRule/Resource" + } + }, + "QMDeploymentManagerQMDeploymentManagerEventsRuleAllowEventRulequotamonitorhubnoouQMDeploymentManagerQMDeploymentManagerLambda69BB20E9F676A8A9": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerLambdaB36F1B21", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerEventsRule53DB2DA9", + "Arn" ] } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Deployment-Manager/QM-Deployment-Manager-EventsRule/AllowEventRulequotamonitorhubnoouQMDeploymentManagerQMDeploymentManagerLambda69BB20E9" } }, - "QMHelperQMHelperFunction91954E97": { - "Type": "AWS::Lambda::Function", + "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2": { + "Type": "AWS::SQS::Queue", "Properties": { - "Code": { - "S3Bucket": { - "Fn::Sub": "solutions-${AWS::Region}" - }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset310b0a91abddfe8f7d3bfae3a5e7abd8562241a7c7bb419dfb838ef991cd57f2.zip" - }, - "Description": "SO0005 quota-monitor-for-aws - QM-Helper-Function", - "Environment": { - "Variables": { - "METRICS_ENDPOINT": { - "Fn::FindInMap": [ - "QuotaMonitorMap", - "Metrics", - "MetricsEndpoint" - ] - }, - "SEND_METRIC": { - "Fn::FindInMap": [ - "QuotaMonitorMap", - "Metrics", - "SendAnonymizedData" - ] - }, - "QM_STACK_ID": "quota-monitor-hub-no-ou", - "LOG_LEVEL": "info", - "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.2.11", - "VERSION": "v6.2.11", - "SOLUTION_ID": "SO0005" - } - }, - "Handler": "index.handler", - "Layers": [ - { - "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" - } - ], - "MemorySize": 128, - "Role": { + "KmsMasterKeyId": { "Fn::GetAtt": [ - "QMHelperQMHelperFunctionServiceRole0506622D", + "KMSHubQMEncryptionKeyA80F8C05", "Arn" ] - }, - "Runtime": "nodejs18.x", - "Timeout": 5 + } }, - "DependsOn": [ - "QMHelperQMHelperFunctionServiceRole0506622D" - ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Helper/QM-Helper-Function/Resource", - "aws:asset:path": "asset.310b0a91abddfe8f7d3bfae3a5e7abd8562241a7c7bb419dfb838ef991cd57f2.zip", - "aws:asset:is-bundled": false, - "aws:asset:property": "Code", + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Deployment-Manager/QM-Deployment-Manager-Lambda-Dead-Letter-Queue/Resource", "cdk_nag": { "rules_to_suppress": [ { - "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", - "id": "AwsSolutions-L1" + "reason": "Queue itself is dead-letter queue", + "id": "AwsSolutions-SQS3" + } + ] + } + } + }, + "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueuePolicy6B59E185": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2", + "Arn" + ] + } } - ] - } - } - }, - "QMHelperQMHelperFunctionEventInvokeConfig580F9F5F": { - "Type": "AWS::Lambda::EventInvokeConfig", - "Properties": { - "FunctionName": { - "Ref": "QMHelperQMHelperFunction91954E97" + ], + "Version": "2012-10-17" }, - "MaximumEventAgeInSeconds": 14400, - "Qualifier": "$LATEST" + "Queues": [ + { + "Ref": "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2" + } + ] }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Helper/QM-Helper-Function/EventInvokeConfig/Resource", - "cdk_nag": { - "rules_to_suppress": [ - { - "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", - "id": "AwsSolutions-L1" - } - ] - } + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Deployment-Manager/QM-Deployment-Manager-Lambda-Dead-Letter-Queue/Policy/Resource" } }, - "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB": { + "QMDeploymentManagerQMDeploymentManagerLambdaServiceRole84304F72": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -2312,7 +2307,7 @@ ] }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Helper/QM-Helper-Provider/framework-onEvent/ServiceRole/Resource", + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Deployment-Manager/QM-Deployment-Manager-Lambda/ServiceRole/Resource", "cdk_nag": { "rules_to_suppress": [ { @@ -2320,60 +2315,113 @@ "id": "AwsSolutions-IAM4" }, { - "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", "id": "AwsSolutions-IAM5" }, { - "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", "id": "AwsSolutions-L1" } ] } } }, - "QMHelperQMHelperProviderframeworkonEventServiceRoleDefaultPolicy86C1FCC1": { + "QMDeploymentManagerQMDeploymentManagerLambdaServiceRoleDefaultPolicy7E3D0777": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { "Statement": [ { - "Action": "lambda:InvokeFunction", + "Action": "sqs:SendMessage", "Effect": "Allow", - "Resource": [ - { - "Fn::GetAtt": [ - "QMHelperQMHelperFunction91954E97", - "Arn" - ] - }, - { - "Fn::Join": [ - "", - [ - { - "Fn::GetAtt": [ - "QMHelperQMHelperFunction91954E97", - "Arn" - ] - }, - ":*" - ] + "Resource": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Encrypt", + "kms:Decrypt", + "kms:CreateGrant" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + } + }, + { + "Action": "kms:ListAliases", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "events:PutPermission", + "events:RemovePermission" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "events:DescribeEventBus", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMBusFF5C6C0C", + "Arn" + ] + } + }, + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "QMAccounts3D743F6B" + } ] - } - ] + ] + } + }, + { + "Action": "support:DescribeTrustedAdvisorChecks", + "Effect": "Allow", + "Resource": "*" } ], "Version": "2012-10-17" }, - "PolicyName": "QMHelperQMHelperProviderframeworkonEventServiceRoleDefaultPolicy86C1FCC1", + "PolicyName": "QMDeploymentManagerQMDeploymentManagerLambdaServiceRoleDefaultPolicy7E3D0777", "Roles": [ { - "Ref": "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB" + "Ref": "QMDeploymentManagerQMDeploymentManagerLambdaServiceRole84304F72" } ] }, "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Helper/QM-Helper-Provider/framework-onEvent/ServiceRole/DefaultPolicy/Resource", + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Deployment-Manager/QM-Deployment-Manager-Lambda/ServiceRole/DefaultPolicy/Resource", "cdk_nag": { "rules_to_suppress": [ { @@ -2381,110 +2429,122 @@ "id": "AwsSolutions-IAM4" }, { - "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", "id": "AwsSolutions-IAM5" }, { - "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", "id": "AwsSolutions-L1" } ] } } }, - "QMHelperQMHelperProviderframeworkonEventB1DF6D3F": { + "QMDeploymentManagerQMDeploymentManagerLambdaB36F1B21": { "Type": "AWS::Lambda::Function", "Properties": { "Code": { "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/asset6a1cf55956fc481a1f22a54b0fa78a3d78b7e61cd41e12bf80ac8c9404ff9eb2.zip" }, - "Description": "AWS CDK resource provider framework - onEvent (quota-monitor-hub-no-ou/QM-Helper/QM-Helper-Provider)", + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "QMDeploymentManagerQMDeploymentManagerLambdaDeadLetterQueue9B4636C2", + "Arn" + ] + } + }, + "Description": "SO0005 quota-monitor-for-aws - QM-Deployment-Manager-Lambda", "Environment": { "Variables": { - "USER_ON_EVENT_FUNCTION_ARN": { + "EVENT_BUS_NAME": { + "Ref": "QMBusFF5C6C0C" + }, + "EVENT_BUS_ARN": { "Fn::GetAtt": [ - "QMHelperQMHelperFunction91954E97", + "QMBusFF5C6C0C", "Arn" ] - } + }, + "QM_ACCOUNT_PARAMETER": { + "Ref": "QMAccounts3D743F6B" + }, + "DEPLOYMENT_MODEL": "Accounts", + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" } }, - "Handler": "framework.onEvent", + "Handler": "index.handler", + "KmsKeyArn": { + "Fn::GetAtt": [ + "KMSHubQMEncryptionKeyA80F8C05", + "Arn" + ] + }, + "Layers": [ + { + "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" + } + ], + "MemorySize": 512, "Role": { "Fn::GetAtt": [ - "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB", + "QMDeploymentManagerQMDeploymentManagerLambdaServiceRole84304F72", "Arn" ] }, "Runtime": "nodejs18.x", - "Timeout": 900 + "Timeout": 60 }, "DependsOn": [ - "QMHelperQMHelperProviderframeworkonEventServiceRoleDefaultPolicy86C1FCC1", - "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB" + "QMDeploymentManagerQMDeploymentManagerLambdaServiceRoleDefaultPolicy7E3D0777", + "QMDeploymentManagerQMDeploymentManagerLambdaServiceRole84304F72" ], "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Helper/QM-Helper-Provider/framework-onEvent/Resource", - "aws:asset:path": "asset.7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94", + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Deployment-Manager/QM-Deployment-Manager-Lambda/Resource", + "aws:asset:path": "asset.6a1cf55956fc481a1f22a54b0fa78a3d78b7e61cd41e12bf80ac8c9404ff9eb2.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Code", "cdk_nag": { "rules_to_suppress": [ { - "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", - "id": "AwsSolutions-IAM4" - }, - { - "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", - "id": "AwsSolutions-IAM5" - }, - { - "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", "id": "AwsSolutions-L1" } ] - } - } - }, - "QMHelperCreateUUIDE0D423E6": { - "Type": "Custom::CreateUUID", - "Properties": { - "ServiceToken": { - "Fn::GetAtt": [ - "QMHelperQMHelperProviderframeworkonEventB1DF6D3F", - "Arn" + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" ] } - }, - "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete", - "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Helper/CreateUUID/Default" } }, - "QMHelperLaunchData6F23B2C3": { - "Type": "Custom::LaunchData", + "QMDeploymentManagerQMDeploymentManagerLambdaEventInvokeConfig4C3821AB": { + "Type": "AWS::Lambda::EventInvokeConfig", "Properties": { - "ServiceToken": { - "Fn::GetAtt": [ - "QMHelperQMHelperProviderframeworkonEventB1DF6D3F", - "Arn" - ] + "FunctionName": { + "Ref": "QMDeploymentManagerQMDeploymentManagerLambdaB36F1B21" }, - "SOLUTION_UUID": { - "Fn::GetAtt": [ - "QMHelperCreateUUIDE0D423E6", - "UUID" - ] - } + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" }, - "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete", "Metadata": { - "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Helper/LaunchData/Default" + "aws:cdk:path": "quota-monitor-hub-no-ou/QM-Deployment-Manager/QM-Deployment-Manager-Lambda/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } } }, "HubNoOUAppRegistryApplication11687F81": { @@ -2509,7 +2569,7 @@ "ApplicationType": "AWS-Solutions", "SolutionID": "SO0005-NoOU", "SolutionName": "quota-monitor-for-aws", - "SolutionVersion": "v6.2.11" + "SolutionVersion": "v6.3.0" } }, "Metadata": { @@ -2522,7 +2582,7 @@ "Attributes": { "solutionID": "SO0005-NoOU", "solutionName": "quota-monitor-for-aws", - "version": "v6.2.11", + "version": "v6.3.0", "applicationType": "AWS-Solutions" }, "Description": "Attribute group for application information", @@ -2544,7 +2604,7 @@ "ApplicationType": "AWS-Solutions", "SolutionID": "SO0005-NoOU", "SolutionName": "quota-monitor-for-aws", - "SolutionVersion": "v6.2.11" + "SolutionVersion": "v6.3.0" } }, "Metadata": { diff --git a/deployment/quota-monitor-hub.template b/deployment/quota-monitor-hub.template index 208f012..96b94e0 100644 --- a/deployment/quota-monitor-hub.template +++ b/deployment/quota-monitor-hub.template @@ -1,5 +1,5 @@ { - "Description": "(SO0005) - quota-monitor-for-aws - Hub Template. Version v6.2.11", + "Description": "(SO0005) - quota-monitor-for-aws - Hub Template. Version v6.3.0", "AWSTemplateFormatVersion": "2010-09-09", "Metadata": { "AWS::CloudFormation::Interface": { @@ -11,6 +11,7 @@ "Parameters": [ "DeploymentModel", "RegionsList", + "SnsSpokeRegion", "ManagementAccountId" ] }, @@ -40,7 +41,9 @@ "Parameters": [ "SQNotificationThreshold", "SQMonitoringFrequency", - "SQReportOKNotifications" + "SQReportOKNotifications", + "SageMakerMonitoring", + "ConnectMonitoring" ] } ], @@ -60,6 +63,9 @@ "RegionsList": { "default": "List of regions to deploy resources to monitor service quotas" }, + "SnsSpokeRegion": { + "default": "Region in which to launch the SNS stack in the spoke accounts." + }, "RegionConcurrencyType": { "default": "Region Concurrency" }, @@ -77,6 +83,12 @@ }, "SQReportOKNotifications": { "default": "Report OK Notifications" + }, + "SageMakerMonitoring": { + "default": "Enable monitoring for SageMaker quotas" + }, + "ConnectMonitoring": { + "default": "Enable monitoring for Connect quotas" } } } @@ -114,6 +126,11 @@ "Default": "ALL", "Description": "Comma separated list of regions like us-east-1,us-east-2 or ALL or leave it blank for ALL" }, + "SnsSpokeRegion": { + "Type": "String", + "Default": "", + "Description": "The region in which to launch the SNS stack in each spoke account. Leave blank if the spoke SNS is not needed" + }, "RegionConcurrency": { "Type": "String", "Default": "PARALLEL", @@ -140,18 +157,17 @@ "SQNotificationThreshold": { "Type": "String", "Default": "80", - "AllowedValues": [ - "60", - "70", - "80" - ] + "AllowedPattern": "^([1-9]|[1-9][0-9])$", + "ConstraintDescription": "Threshold must be a whole number between 0 and 100", + "Description": "Threshold percentage for quota utilization alerts (0-100)" }, "SQMonitoringFrequency": { "Type": "String", "Default": "rate(12 hours)", "AllowedValues": [ "rate(6 hours)", - "rate(12 hours)" + "rate(12 hours)", + "rate(1 day)" ] }, "SQReportOKNotifications": { @@ -161,6 +177,24 @@ "Yes", "No" ] + }, + "SageMakerMonitoring": { + "Type": "String", + "Default": "Yes", + "AllowedValues": [ + "Yes", + "No" + ], + "Description": "Enable monitoring for SageMaker quotas. NOTE: (1) SageMaker monitoring consumes a high number of quotas, potentially resulting in higher usage cost. (2) Changing this value during a stack update will affect all spoke accounts but if left unchanged, it preserves existing spoke accounts customizations." + }, + "ConnectMonitoring": { + "Type": "String", + "Default": "Yes", + "AllowedValues": [ + "Yes", + "No" + ], + "Description": "Enable monitoring for Connect quotas. NOTE: (1) Connect monitoring consumes a high number of quotas, potentially resulting in higher usage cost. (2) Changing this value during a stack update will affect all spoke accounts but if left unchanged, it preserves existing spoke accounts customizations." } }, "Mappings": { @@ -207,6 +241,14 @@ "Hybrid" ] }, + "IsChinaPartition": { + "Fn::Equals": [ + { + "Ref": "AWS::Partition" + }, + "aws-cn" + ] + }, "CDKMetadataAvailable": { "Fn::Or": [ { @@ -599,13 +641,13 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset11e8019ce0550d340c842c1a14bc8797872e01186f9f5b7ba02745ff143d145a.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/assete8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip" }, "LayerName": "QM-UtilsLayer" }, "Metadata": { "aws:cdk:path": "quota-monitor-hub/QM-UtilsLayer/QM-UtilsLayer-Layer/Resource", - "aws:asset:path": "asset.11e8019ce0550d340c842c1a14bc8797872e01186f9f5b7ba02745ff143d145a.zip", + "aws:asset:path": "asset.e8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Content" } @@ -667,7 +709,7 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset310b0a91abddfe8f7d3bfae3a5e7abd8562241a7c7bb419dfb838ef991cd57f2.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/assetf4ee0c3d949f011b3f0f60d231fdacecab71c5f3ccf9674352231cedf831f6cd.zip" }, "Description": "SO0005 quota-monitor-for-aws - QM-Helper-Function", "Environment": { @@ -697,9 +739,15 @@ "No" ] }, + "SAGEMAKER_MONITORING": { + "Ref": "SageMakerMonitoring" + }, + "CONNECT_MONITORING": { + "Ref": "ConnectMonitoring" + }, "LOG_LEVEL": "info", - "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.2.11", - "VERSION": "v6.2.11", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", "SOLUTION_ID": "SO0005" } }, @@ -724,7 +772,7 @@ ], "Metadata": { "aws:cdk:path": "quota-monitor-hub/QM-Helper/QM-Helper-Function/Resource", - "aws:asset:path": "asset.310b0a91abddfe8f7d3bfae3a5e7abd8562241a7c7bb419dfb838ef991cd57f2.zip", + "aws:asset:path": "asset.f4ee0c3d949f011b3f0f60d231fdacecab71c5f3ccf9674352231cedf831f6cd.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Code", "cdk_nag": { @@ -734,6 +782,14 @@ "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK", + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } } }, @@ -876,7 +932,7 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/asset7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94.zip" }, "Description": "AWS CDK resource provider framework - onEvent (quota-monitor-hub/QM-Helper/QM-Helper-Provider)", "Environment": { @@ -923,6 +979,12 @@ "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } } }, @@ -1265,7 +1327,7 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset7ac08df917158d6dd06a4ad585fb1297c04d8abaaebdbfd2406468890bbd116a.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/asset11434a0b3246f0b4445dd28fdbc9e4e7dc808ccf355077acd9b000c5d88e6713.zip" }, "DeadLetterConfig": { "TargetArn": { @@ -1289,8 +1351,8 @@ "Ref": "QMNotificationMutingConfig3B7948BA" }, "LOG_LEVEL": "info", - "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.2.11", - "VERSION": "v6.2.11", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", "SOLUTION_ID": "SO0005" } }, @@ -1322,7 +1384,7 @@ ], "Metadata": { "aws:cdk:path": "quota-monitor-hub/QM-SlackNotifier/QM-SlackNotifier-Lambda/Resource", - "aws:asset:path": "asset.7ac08df917158d6dd06a4ad585fb1297c04d8abaaebdbfd2406468890bbd116a.zip", + "aws:asset:path": "asset.11434a0b3246f0b4445dd28fdbc9e4e7dc808ccf355077acd9b000c5d88e6713.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Code", "cdk_nag": { @@ -1332,6 +1394,12 @@ "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } }, "Condition": "SlackTrueCondition" @@ -1659,7 +1727,7 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/assetc7d0a18c007f94e3ee2cc35273b6a455312835f2ec08fa32e1395e13b0ae78e8.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/assete7a324e67e467d0c22e13b0693ca4efdceb0d53025c7fb45fe524870a5c18046.zip" }, "DeadLetterConfig": { "TargetArn": { @@ -1699,8 +1767,8 @@ "Ref": "QMSNSPublisherQMSNSPublisherSNSTopic7EE2EBF4" }, "LOG_LEVEL": "info", - "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.2.11", - "VERSION": "v6.2.11", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", "SOLUTION_ID": "SO0005" } }, @@ -1732,7 +1800,7 @@ ], "Metadata": { "aws:cdk:path": "quota-monitor-hub/QM-SNSPublisherFunction/QM-SNSPublisherFunction-Lambda/Resource", - "aws:asset:path": "asset.c7d0a18c007f94e3ee2cc35273b6a455312835f2ec08fa32e1395e13b0ae78e8.zip", + "aws:asset:path": "asset.e7a324e67e467d0c22e13b0693ca4efdceb0d53025c7fb45fe524870a5c18046.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Code", "cdk_nag": { @@ -1742,6 +1810,12 @@ "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } } }, @@ -2200,7 +2274,7 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset3214e803bdf3b3311d409c896a5cc335cb056428c232c86da8c38175b6b0b79e.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/asseta6fda81c73d731886f04e1734d036f12ceb7b94c2efec30bb511f477ac58aa9c.zip" }, "DeadLetterConfig": { "TargetArn": { @@ -2222,8 +2296,8 @@ "MAX_MESSAGES": "10", "MAX_LOOPS": "10", "LOG_LEVEL": "info", - "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.2.11", - "VERSION": "v6.2.11", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", "SOLUTION_ID": "SO0005" } }, @@ -2255,7 +2329,7 @@ ], "Metadata": { "aws:cdk:path": "quota-monitor-hub/QM-Reporter/QM-Reporter-Lambda/Resource", - "aws:asset:path": "asset.3214e803bdf3b3311d409c896a5cc335cb056428c232c86da8c38175b6b0b79e.zip", + "aws:asset:path": "asset.a6fda81c73d731886f04e1734d036f12ceb7b94c2efec30bb511f477ac58aa9c.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Code", "cdk_nag": { @@ -2265,6 +2339,12 @@ "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } } }, @@ -2318,7 +2398,35 @@ "PermissionModel": "SERVICE_MANAGED", "StackSetName": "QM-TA-Spoke-StackSet", "TemplateURL": { - "Fn::Sub": "https://solutions-${AWS::Region}.s3.${AWS::Region}.amazonaws.com/quota-monitor-for-aws/v6.2.11/quota-monitor-ta-spoke.template" + "Fn::Join": [ + "", + [ + "https://solutions-", + { + "Ref": "AWS::Region" + }, + ".s3.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com", + { + "Fn::If": [ + "IsChinaPartition", + ".cn", + "" + ] + }, + "/quota-monitor-for-aws/v6.3.0/", + { + "Fn::If": [ + "IsChinaPartition", + "quota-monitor-ta-spoke-cn.template", + "quota-monitor-ta-spoke.template" + ] + } + ] + ] } }, "Metadata": { @@ -2349,18 +2457,118 @@ "Arn" ] } + }, + { + "ParameterKey": "SpokeSnsRegion", + "ParameterValue": { + "Ref": "SnsSpokeRegion" + } + }, + { + "ParameterKey": "SageMakerMonitoring", + "ParameterValue": { + "Ref": "SageMakerMonitoring" + } + }, + { + "ParameterKey": "ConnectMonitoring", + "ParameterValue": { + "Ref": "ConnectMonitoring" + } } ], "PermissionModel": "SERVICE_MANAGED", "StackSetName": "QM-SQ-Spoke-StackSet", "TemplateURL": { - "Fn::Sub": "https://solutions-${AWS::Region}.s3.${AWS::Region}.amazonaws.com/quota-monitor-for-aws/v6.2.11/quota-monitor-sq-spoke.template" + "Fn::Join": [ + "", + [ + "https://solutions-", + { + "Ref": "AWS::Region" + }, + ".s3.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com", + { + "Fn::If": [ + "IsChinaPartition", + ".cn", + "" + ] + }, + "/quota-monitor-for-aws/v6.3.0/", + { + "Fn::If": [ + "IsChinaPartition", + "quota-monitor-sq-spoke-cn.template", + "quota-monitor-sq-spoke.template" + ] + } + ] + ] } }, "Metadata": { "aws:cdk:path": "quota-monitor-hub/QM-SQ-StackSet" } }, + "QMSNSStackSet": { + "Type": "AWS::CloudFormation::StackSet", + "Properties": { + "AutoDeployment": { + "Enabled": true, + "RetainStacksOnAccountRemoval": false + }, + "CallAs": "DELEGATED_ADMIN", + "Capabilities": [ + "CAPABILITY_IAM" + ], + "Description": "StackSet for deploying Quota Monitor notification spokes in Organization", + "ManagedExecution": { + "Active": true + }, + "Parameters": [], + "PermissionModel": "SERVICE_MANAGED", + "StackSetName": "QM-SNS-Spoke-StackSet", + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://solutions-", + { + "Ref": "AWS::Region" + }, + ".s3.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com", + { + "Fn::If": [ + "IsChinaPartition", + ".cn", + "" + ] + }, + "/quota-monitor-for-aws/v6.3.0/", + { + "Fn::If": [ + "IsChinaPartition", + "quota-monitor-sns-spoke-cn.template", + "quota-monitor-sns-spoke.template" + ] + } + ] + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-hub/QM-SNS-StackSet" + } + }, "QMDeploymentManagerQMDeploymentManagerEventsRule53DB2DA9": { "Type": "AWS::Events::Rule", "Properties": { @@ -2788,6 +2996,22 @@ ":stackset/QM-SQ-Spoke-StackSet:*" ] ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:*:", + { + "Ref": "ManagementAccountId" + }, + ":stackset/QM-SNS-Spoke-StackSet:*" + ] + ] } ] }, @@ -2831,6 +3055,38 @@ ] ] }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:*:", + { + "Ref": "ManagementAccountId" + }, + ":stackset/QM-SNS-Spoke-StackSet:*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:*:", + { + "Ref": "ManagementAccountId" + }, + ":stackset-target/QM-SNS-Spoke-StackSet:*/*" + ] + ] + }, { "Fn::Join": [ "", @@ -2924,7 +3180,7 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset5a81c4fb5c63f552f9d49c541884c214130c866132af0414ab4c7b3394fb57da.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/asset6a1cf55956fc481a1f22a54b0fa78a3d78b7e61cd41e12bf80ac8c9404ff9eb2.zip" }, "DeadLetterConfig": { "TargetArn": { @@ -2958,6 +3214,12 @@ "StackSetId" ] }, + "SNS_STACKSET_ID": { + "Fn::GetAtt": [ + "QMSNSStackSet", + "StackSetId" + ] + }, "QM_OU_PARAMETER": { "Ref": "QMOUs122D8EB4" }, @@ -2981,6 +3243,9 @@ "QM_REGIONS_LIST_PARAMETER": { "Ref": "QMRegionsList17794003" }, + "SNS_SPOKE_REGION": { + "Ref": "SnsSpokeRegion" + }, "REGIONS_CONCURRENCY_TYPE": { "Ref": "RegionConcurrency" }, @@ -3020,8 +3285,8 @@ ] }, "LOG_LEVEL": "info", - "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.2.11", - "VERSION": "v6.2.11", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", "SOLUTION_ID": "SO0005" } }, @@ -3053,7 +3318,7 @@ ], "Metadata": { "aws:cdk:path": "quota-monitor-hub/QM-Deployment-Manager/QM-Deployment-Manager-Lambda/Resource", - "aws:asset:path": "asset.5a81c4fb5c63f552f9d49c541884c214130c866132af0414ab4c7b3394fb57da.zip", + "aws:asset:path": "asset.6a1cf55956fc481a1f22a54b0fa78a3d78b7e61cd41e12bf80ac8c9404ff9eb2.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Code", "cdk_nag": { @@ -3063,6 +3328,12 @@ "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } } }, @@ -3109,7 +3380,7 @@ "ApplicationType": "AWS-Solutions", "SolutionID": "SO0005", "SolutionName": "quota-monitor-for-aws", - "SolutionVersion": "v6.2.11" + "SolutionVersion": "v6.3.0" } }, "Metadata": { @@ -3122,7 +3393,7 @@ "Attributes": { "solutionID": "SO0005", "solutionName": "quota-monitor-for-aws", - "version": "v6.2.11", + "version": "v6.3.0", "applicationType": "AWS-Solutions" }, "Description": "Attribute group for application information", @@ -3144,7 +3415,7 @@ "ApplicationType": "AWS-Solutions", "SolutionID": "SO0005", "SolutionName": "quota-monitor-for-aws", - "SolutionVersion": "v6.2.11" + "SolutionVersion": "v6.3.0" } }, "Metadata": { diff --git a/deployment/quota-monitor-prerequisite-cn.template b/deployment/quota-monitor-prerequisite-cn.template new file mode 100644 index 0000000..73f5f2e --- /dev/null +++ b/deployment/quota-monitor-prerequisite-cn.template @@ -0,0 +1,1050 @@ +{ + "Description": "(SO0005-PreReq) - quota-monitor-for-aws - Prerequisite Template. Version v6.3.0", + "AWSTemplateFormatVersion": "2010-09-09", + "Metadata": { + "AWS::CloudFormation::Interface": { + "ParameterGroups": [ + { + "Label": { + "default": "Pre-Requisite Configuration" + }, + "Parameters": [ + "MonitoringAccountId" + ] + } + ], + "ParameterLabels": { + "MonitoringAccountId": { + "default": "Quota Monitor Monitoring Account" + } + } + } + }, + "Parameters": { + "MonitoringAccountId": { + "Type": "String", + "AllowedPattern": "^[0-9]{1}\\d{11}$", + "Description": "AWS Account Id for the monitoring account" + } + }, + "Mappings": { + "QuotaMonitorMap": { + "Metrics": { + "SendAnonymizedData": "Yes", + "MetricsEndpoint": "https://metrics.awssolutionsbuilder.com/generic" + } + } + }, + "Resources": { + "QMUtilsLayerQMUtilsLayerLayer80D5D993": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "CompatibleRuntimes": [ + "nodejs18.x" + ], + "Content": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assete8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip" + }, + "LayerName": "QM-UtilsLayer" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/QM-UtilsLayer/QM-UtilsLayer-Layer/Resource", + "aws:asset:path": "asset.e8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Content" + } + }, + "QMHelperQMHelperFunctionServiceRole0506622D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/QM-Helper/QM-Helper-Function/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMHelperQMHelperFunction91954E97": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assetf4ee0c3d949f011b3f0f60d231fdacecab71c5f3ccf9674352231cedf831f6cd.zip" + }, + "Description": "SO0005 quota-monitor-for-aws - QM-Helper-Function", + "Environment": { + "Variables": { + "METRICS_ENDPOINT": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "MetricsEndpoint" + ] + }, + "SEND_METRIC": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "SendAnonymizedData" + ] + }, + "QM_STACK_ID": "quota-monitor-prerequisite-cn", + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "QMHelperQMHelperFunctionServiceRole0506622D", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 5 + }, + "DependsOn": [ + "QMHelperQMHelperFunctionServiceRole0506622D" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/QM-Helper/QM-Helper-Function/Resource", + "aws:asset:path": "asset.f4ee0c3d949f011b3f0f60d231fdacecab71c5f3ccf9674352231cedf831f6cd.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK", + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK", + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMHelperQMHelperFunctionEventInvokeConfig580F9F5F": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "QMHelperQMHelperFunction91954E97" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/QM-Helper/QM-Helper-Function/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/QM-Helper/QM-Helper-Provider/framework-onEvent/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMHelperQMHelperProviderframeworkonEventServiceRoleDefaultPolicy86C1FCC1": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "QMHelperQMHelperFunction91954E97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "QMHelperQMHelperFunction91954E97", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMHelperQMHelperProviderframeworkonEventServiceRoleDefaultPolicy86C1FCC1", + "Roles": [ + { + "Ref": "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/QM-Helper/QM-Helper-Provider/framework-onEvent/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMHelperQMHelperProviderframeworkonEventB1DF6D3F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/asset7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94.zip" + }, + "Description": "AWS CDK resource provider framework - onEvent (quota-monitor-prerequisite-cn/QM-Helper/QM-Helper-Provider)", + "Environment": { + "Variables": { + "USER_ON_EVENT_FUNCTION_ARN": { + "Fn::GetAtt": [ + "QMHelperQMHelperFunction91954E97", + "Arn" + ] + } + } + }, + "Handler": "framework.onEvent", + "Role": { + "Fn::GetAtt": [ + "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 900 + }, + "DependsOn": [ + "QMHelperQMHelperProviderframeworkonEventServiceRoleDefaultPolicy86C1FCC1", + "QMHelperQMHelperProviderframeworkonEventServiceRole4A1EBBAB" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/QM-Helper/QM-Helper-Provider/framework-onEvent/Resource", + "aws:asset:path": "asset.7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK", + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMHelperCreateUUIDE0D423E6": { + "Type": "Custom::CreateUUID", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "QMHelperQMHelperProviderframeworkonEventB1DF6D3F", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/QM-Helper/CreateUUID/Default" + } + }, + "QMHelperLaunchData6F23B2C3": { + "Type": "Custom::LaunchData", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "QMHelperQMHelperProviderframeworkonEventB1DF6D3F", + "Arn" + ] + }, + "SOLUTION_UUID": { + "Fn::GetAtt": [ + "QMHelperCreateUUIDE0D423E6", + "UUID" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/QM-Helper/LaunchData/Default" + } + }, + "QMPreReqManagerQMPreReqManagerFunctionServiceRole8AAB636E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/QM-PreReqManager/QM-PreReqManager-Function/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + }, + { + "reason": "Actions do not support resource-level permissions", + "id": "AwsSolutions-IAM5" + } + ] + } + } + }, + "QMPreReqManagerQMPreReqManagerFunctionServiceRoleDefaultPolicy2A680D95": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "organizations:EnableAWSServiceAccess", + "organizations:DescribeOrganization", + "organizations:RegisterDelegatedAdministrator" + ], + "Effect": "Allow", + "Resource": "*", + "Sid": "QMPreReqWrite" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMPreReqManagerQMPreReqManagerFunctionServiceRoleDefaultPolicy2A680D95", + "Roles": [ + { + "Ref": "QMPreReqManagerQMPreReqManagerFunctionServiceRole8AAB636E" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/QM-PreReqManager/QM-PreReqManager-Function/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "Actions do not support resource-level permissions", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMPreReqManagerQMPreReqManagerFunction1DC63BE9": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assete3fd9e117d4b88891e0a84a7e27b4d4a52637e08ba8620125cbae56a0fa5b5b5.zip" + }, + "Description": "SO0005 quota-monitor-for-aws - QM-PreReqManager-Function", + "Environment": { + "Variables": { + "METRICS_ENDPOINT": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "MetricsEndpoint" + ] + }, + "SEND_METRIC": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "Metrics", + "SendAnonymizedData" + ] + }, + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "QMPreReqManagerQMPreReqManagerFunctionServiceRole8AAB636E", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 5 + }, + "DependsOn": [ + "QMPreReqManagerQMPreReqManagerFunctionServiceRoleDefaultPolicy2A680D95", + "QMPreReqManagerQMPreReqManagerFunctionServiceRole8AAB636E" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/QM-PreReqManager/QM-PreReqManager-Function/Resource", + "aws:asset:path": "asset.e3fd9e117d4b88891e0a84a7e27b4d4a52637e08ba8620125cbae56a0fa5b5b5.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK", + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMPreReqManagerQMPreReqManagerFunctionEventInvokeConfig83FEE4E4": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "QMPreReqManagerQMPreReqManagerFunction1DC63BE9" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/QM-PreReqManager/QM-PreReqManager-Function/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMPreReqManagerQMPreReqManagerProviderframeworkonEventServiceRole15413DEC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/QM-PreReqManager/QM-PreReqManager-Provider/framework-onEvent/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMPreReqManagerQMPreReqManagerProviderframeworkonEventServiceRoleDefaultPolicy58FD5499": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "QMPreReqManagerQMPreReqManagerFunction1DC63BE9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "QMPreReqManagerQMPreReqManagerFunction1DC63BE9", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMPreReqManagerQMPreReqManagerProviderframeworkonEventServiceRoleDefaultPolicy58FD5499", + "Roles": [ + { + "Ref": "QMPreReqManagerQMPreReqManagerProviderframeworkonEventServiceRole15413DEC" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/QM-PreReqManager/QM-PreReqManager-Provider/framework-onEvent/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMPreReqManagerQMPreReqManagerProviderframeworkonEvent898B02B6": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/asset7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94.zip" + }, + "Description": "AWS CDK resource provider framework - onEvent (quota-monitor-prerequisite-cn/QM-PreReqManager/QM-PreReqManager-Provider)", + "Environment": { + "Variables": { + "USER_ON_EVENT_FUNCTION_ARN": { + "Fn::GetAtt": [ + "QMPreReqManagerQMPreReqManagerFunction1DC63BE9", + "Arn" + ] + } + } + }, + "Handler": "framework.onEvent", + "Role": { + "Fn::GetAtt": [ + "QMPreReqManagerQMPreReqManagerProviderframeworkonEventServiceRole15413DEC", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 900 + }, + "DependsOn": [ + "QMPreReqManagerQMPreReqManagerProviderframeworkonEventServiceRoleDefaultPolicy58FD5499", + "QMPreReqManagerQMPreReqManagerProviderframeworkonEventServiceRole15413DEC" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/QM-PreReqManager/QM-PreReqManager-Provider/framework-onEvent/Resource", + "aws:asset:path": "asset.7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMPreReqManagerPreReqManagerCRB1E370C2": { + "Type": "Custom::PreReqManagerCR", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "QMPreReqManagerQMPreReqManagerProviderframeworkonEvent898B02B6", + "Arn" + ] + }, + "QMMonitoringAccountId": { + "Ref": "MonitoringAccountId" + }, + "AccountId": { + "Ref": "AWS::AccountId" + }, + "Region": { + "Ref": "AWS::Region" + }, + "SolutionUuid": { + "Fn::GetAtt": [ + "QMHelperCreateUUIDE0D423E6", + "UUID" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/QM-PreReqManager/PreReqManagerCR/Default" + } + }, + "CDKMetadata": { + "Type": "AWS::CDK::Metadata", + "Properties": { + "Analytics": "v2:deflate64:H4sIAAAAAAAA/2VP0WrDMBD7lr47tyaD7XktKww2FjLYa7g613BNbAefnVFC/n1xVsrGniRZOiMVkOePsN3gl2S66bKejzB9BNSd2p9siR4NBfJJvOEwsG3VEq2nHs2xQZhe8UL+k7ywsyn0Rx+i1eFq3PjzSDa82NF1tHf2xG1y/z3OSu5rFKEg8JRg0bCLuqOwQyHFaGCqXE/pesXS9awva+uVzbNaD5cxbapdkbjoNSkdJThT+6sWKL0buUkjV+cWTD//4u8xDDEktlRsOI2ZlXUNwVnuxvwBii0Um7MwZz7awIag+sFvZeuSSWUBAAA=" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-prerequisite-cn/CDKMetadata/Default" + }, + "Condition": "CDKMetadataAvailable" + } + }, + "Outputs": { + "UUID": { + "Description": "UUID for deployment", + "Value": { + "Fn::GetAtt": [ + "QMHelperCreateUUIDE0D423E6", + "UUID" + ] + } + } + }, + "Conditions": { + "CDKMetadataAvailable": { + "Fn::Or": [ + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "af-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ca-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-northwest-1" + ] + } + ] + }, + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-3" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "il-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "me-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "me-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "sa-east-1" + ] + } + ] + }, + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-east-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-west-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-west-2" + ] + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/deployment/quota-monitor-prerequisite.template b/deployment/quota-monitor-prerequisite.template index 35f9b45..e6600c1 100644 --- a/deployment/quota-monitor-prerequisite.template +++ b/deployment/quota-monitor-prerequisite.template @@ -1,5 +1,5 @@ { - "Description": "(SO0005-PreReq) - quota-monitor-for-aws - Prerequisite Template. Version v6.2.11", + "Description": "(SO0005-PreReq) - quota-monitor-for-aws - Prerequisite Template. Version v6.3.0", "AWSTemplateFormatVersion": "2010-09-09", "Metadata": { "AWS::CloudFormation::Interface": { @@ -46,13 +46,13 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset11e8019ce0550d340c842c1a14bc8797872e01186f9f5b7ba02745ff143d145a.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/assete8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip" }, "LayerName": "QM-UtilsLayer" }, "Metadata": { "aws:cdk:path": "quota-monitor-prerequisite/QM-UtilsLayer/QM-UtilsLayer-Layer/Resource", - "aws:asset:path": "asset.11e8019ce0550d340c842c1a14bc8797872e01186f9f5b7ba02745ff143d145a.zip", + "aws:asset:path": "asset.e8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Content" } @@ -114,7 +114,7 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset310b0a91abddfe8f7d3bfae3a5e7abd8562241a7c7bb419dfb838ef991cd57f2.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/assetf4ee0c3d949f011b3f0f60d231fdacecab71c5f3ccf9674352231cedf831f6cd.zip" }, "Description": "SO0005 quota-monitor-for-aws - QM-Helper-Function", "Environment": { @@ -135,8 +135,8 @@ }, "QM_STACK_ID": "quota-monitor-prerequisite", "LOG_LEVEL": "info", - "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.2.11", - "VERSION": "v6.2.11", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", "SOLUTION_ID": "SO0005" } }, @@ -161,7 +161,7 @@ ], "Metadata": { "aws:cdk:path": "quota-monitor-prerequisite/QM-Helper/QM-Helper-Function/Resource", - "aws:asset:path": "asset.310b0a91abddfe8f7d3bfae3a5e7abd8562241a7c7bb419dfb838ef991cd57f2.zip", + "aws:asset:path": "asset.f4ee0c3d949f011b3f0f60d231fdacecab71c5f3ccf9674352231cedf831f6cd.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Code", "cdk_nag": { @@ -171,6 +171,16 @@ "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK", + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK", + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } } }, @@ -313,7 +323,7 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/asset7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94.zip" }, "Description": "AWS CDK resource provider framework - onEvent (quota-monitor-prerequisite/QM-Helper/QM-Helper-Provider)", "Environment": { @@ -360,6 +370,14 @@ "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK", + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } } }, @@ -503,7 +521,7 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset12e53a9edbf51e99f12536499d39803686478d47d7df17c8a158f3632fa153f4.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/assete3fd9e117d4b88891e0a84a7e27b4d4a52637e08ba8620125cbae56a0fa5b5b5.zip" }, "Description": "SO0005 quota-monitor-for-aws - QM-PreReqManager-Function", "Environment": { @@ -523,8 +541,8 @@ ] }, "LOG_LEVEL": "info", - "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.2.11", - "VERSION": "v6.2.11", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", "SOLUTION_ID": "SO0005" } }, @@ -550,7 +568,7 @@ ], "Metadata": { "aws:cdk:path": "quota-monitor-prerequisite/QM-PreReqManager/QM-PreReqManager-Function/Resource", - "aws:asset:path": "asset.12e53a9edbf51e99f12536499d39803686478d47d7df17c8a158f3632fa153f4.zip", + "aws:asset:path": "asset.e3fd9e117d4b88891e0a84a7e27b4d4a52637e08ba8620125cbae56a0fa5b5b5.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Code", "cdk_nag": { @@ -560,6 +578,14 @@ "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK", + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } } }, @@ -702,7 +728,7 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/asset7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94.zip" }, "Description": "AWS CDK resource provider framework - onEvent (quota-monitor-prerequisite/QM-PreReqManager/QM-PreReqManager-Provider)", "Environment": { @@ -749,6 +775,12 @@ "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } } }, diff --git a/deployment/quota-monitor-sns-spoke-cn.template b/deployment/quota-monitor-sns-spoke-cn.template new file mode 100644 index 0000000..8fa1537 --- /dev/null +++ b/deployment/quota-monitor-sns-spoke-cn.template @@ -0,0 +1,506 @@ +{ + "Description": "(SO0005-SPOKE-SNS) - quota-monitor-for-aws - Service Quotas Template. Version v6.3.0", + "AWSTemplateFormatVersion": "2010-09-09", + "Mappings": { + "QuotaMonitorMap": { + "SSMParameters": { + "NotificationMutingConfig": "/QuotaMonitor/spoke/NotificationConfiguration" + } + } + }, + "Resources": { + "QMSNSSpokeBus29B8E7E8": { + "Type": "AWS::Events::EventBus", + "Properties": { + "Name": "QuotaMonitorSnsSpokeBus" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke-cn/QM-SNS-Spoke-Bus/Resource" + } + }, + "QMSNSSpokeBusallowedaccounts10D05AD6": { + "Type": "AWS::Events::EventBusPolicy", + "Properties": { + "EventBusName": { + "Ref": "QMSNSSpokeBus29B8E7E8" + }, + "Statement": { + "Action": "events:PutEvents", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": { + "Fn::GetAtt": [ + "QMSNSSpokeBus29B8E7E8", + "Arn" + ] + }, + "Sid": "allowed_accounts" + }, + "StatementId": "allowed_accounts" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke-cn/QM-SNS-Spoke-Bus/allowed_accounts/Resource" + } + }, + "QMUtilsLayerquotamonitorsnsspokecnQMUtilsLayerquotamonitorsnsspokecnLayerBDBD8EE2": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "CompatibleRuntimes": [ + "nodejs18.x" + ], + "Content": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assete8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip" + }, + "LayerName": "QM-UtilsLayer-quota-monitor-sns-spoke-cn" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke-cn/QM-UtilsLayer-quota-monitor-sns-spoke-cn/QM-UtilsLayer-quota-monitor-sns-spoke-cn-Layer/Resource", + "aws:asset:path": "asset.e8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Content" + } + }, + "sqspokeNotificationMutingConfigE6DD8BD9": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Muting configuration for services, limits e.g. ec2:L-1216C47A,ec2:Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances,dynamodb,logs:*,geo:L-05EFD12D", + "Name": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "SSMParameters", + "NotificationMutingConfig" + ] + }, + "Type": "StringList", + "Value": "NOP" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke-cn/sq-spoke-NotificationMutingConfig/Resource" + } + }, + "sqspokeSNSPublishersqspokeSNSPublisherSNSTopic5C405BCF": { + "Type": "AWS::SNS::Topic", + "Properties": { + "KmsMasterKeyId": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":kms:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":alias/aws/sns" + ] + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke-cn/sq-spoke-SNSPublisher/sq-spoke-SNSPublisher-SNSTopic/Resource" + } + }, + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionEventsRule7B09C7F3": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - sq-spoke-SNSPublisherFunction-EventsRule", + "EventBusName": { + "Ref": "QMSNSSpokeBus29B8E7E8" + }, + "EventPattern": { + "detail": { + "status": [ + "WARN", + "ERROR" + ] + }, + "detail-type": [ + "Trusted Advisor Check Item Refresh Notification", + "Service Quotas Utilization Notification" + ], + "source": [ + "aws.trustedadvisor", + "aws-solutions.quota-monitor" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaC0F8A9BF", + "Arn" + ] + }, + "Id": "Target0" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke-cn/sq-spoke-SNSPublisherFunction/sq-spoke-SNSPublisherFunction-EventsRule/Resource" + } + }, + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionEventsRuleAllowEventRulequotamonitorsnsspokecnsqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambda37DDF867C0B14A54": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaC0F8A9BF", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionEventsRule7B09C7F3", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke-cn/sq-spoke-SNSPublisherFunction/sq-spoke-SNSPublisherFunction-EventsRule/AllowEventRulequotamonitorsnsspokecnsqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambda37DDF867" + } + }, + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaDeadLetterQueue83F0C565": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": "alias/aws/sqs" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke-cn/sq-spoke-SNSPublisherFunction/sq-spoke-SNSPublisherFunction-Lambda-Dead-Letter-Queue/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "Queue itself is dead-letter queue", + "id": "AwsSolutions-SQS3" + } + ] + } + } + }, + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaDeadLetterQueuePolicyB89E703A": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaDeadLetterQueue83F0C565", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaDeadLetterQueue83F0C565" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke-cn/sq-spoke-SNSPublisherFunction/sq-spoke-SNSPublisherFunction-Lambda-Dead-Letter-Queue/Policy/Resource" + } + }, + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaServiceRoleE4D78096": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke-cn/sq-spoke-SNSPublisherFunction/sq-spoke-SNSPublisherFunction-Lambda/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaServiceRoleDefaultPolicyEAE21A8E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaDeadLetterQueue83F0C565", + "Arn" + ] + } + }, + { + "Action": "SNS:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "sqspokeSNSPublishersqspokeSNSPublisherSNSTopic5C405BCF" + } + }, + { + "Action": "kms:GenerateDataKey", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":kms:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":alias/aws/sns" + ] + ] + } + }, + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "sqspokeNotificationMutingConfigE6DD8BD9" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaServiceRoleDefaultPolicyEAE21A8E", + "Roles": [ + { + "Ref": "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaServiceRoleE4D78096" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke-cn/sq-spoke-SNSPublisherFunction/sq-spoke-SNSPublisherFunction-Lambda/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaC0F8A9BF": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assete7a324e67e467d0c22e13b0693ca4efdceb0d53025c7fb45fe524870a5c18046.zip" + }, + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaDeadLetterQueue83F0C565", + "Arn" + ] + } + }, + "Description": "SO0005 quota-monitor-for-aws - sq-spoke-SNSPublisherFunction-Lambda", + "Environment": { + "Variables": { + "QM_NOTIFICATION_MUTING_CONFIG_PARAMETER": { + "Ref": "sqspokeNotificationMutingConfigE6DD8BD9" + }, + "SEND_METRIC": "No", + "TOPIC_ARN": { + "Ref": "sqspokeSNSPublishersqspokeSNSPublisherSNSTopic5C405BCF" + }, + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "QMUtilsLayerquotamonitorsnsspokecnQMUtilsLayerquotamonitorsnsspokecnLayerBDBD8EE2" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaServiceRoleE4D78096", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 60 + }, + "DependsOn": [ + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaServiceRoleDefaultPolicyEAE21A8E", + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaServiceRoleE4D78096" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke-cn/sq-spoke-SNSPublisherFunction/sq-spoke-SNSPublisherFunction-Lambda/Resource", + "aws:asset:path": "asset.e7a324e67e467d0c22e13b0693ca4efdceb0d53025c7fb45fe524870a5c18046.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaEventInvokeConfigBD6349C2": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaC0F8A9BF" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke-cn/sq-spoke-SNSPublisherFunction/sq-spoke-SNSPublisherFunction-Lambda/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + } + }, + "Outputs": { + "SpokeSnsEventBus": { + "Description": "SNS Event Bus Arn in spoke account", + "Value": { + "Fn::GetAtt": [ + "QMSNSSpokeBus29B8E7E8", + "Arn" + ] + } + } + } +} \ No newline at end of file diff --git a/deployment/quota-monitor-sns-spoke.template b/deployment/quota-monitor-sns-spoke.template new file mode 100644 index 0000000..ccd92b6 --- /dev/null +++ b/deployment/quota-monitor-sns-spoke.template @@ -0,0 +1,606 @@ +{ + "Description": "(SO0005-SPOKE-SNS) - quota-monitor-for-aws - Service Quotas Template. Version v6.3.0", + "AWSTemplateFormatVersion": "2010-09-09", + "Mappings": { + "QuotaMonitorMap": { + "SSMParameters": { + "NotificationMutingConfig": "/QuotaMonitor/spoke/NotificationConfiguration" + } + } + }, + "Resources": { + "QMSNSSpokeBus29B8E7E8": { + "Type": "AWS::Events::EventBus", + "Properties": { + "Name": "QuotaMonitorSnsSpokeBus" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke/QM-SNS-Spoke-Bus/Resource" + } + }, + "QMSNSSpokeBusallowedaccounts10D05AD6": { + "Type": "AWS::Events::EventBusPolicy", + "Properties": { + "EventBusName": { + "Ref": "QMSNSSpokeBus29B8E7E8" + }, + "Statement": { + "Action": "events:PutEvents", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": { + "Fn::GetAtt": [ + "QMSNSSpokeBus29B8E7E8", + "Arn" + ] + }, + "Sid": "allowed_accounts" + }, + "StatementId": "allowed_accounts" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke/QM-SNS-Spoke-Bus/allowed_accounts/Resource" + } + }, + "QMUtilsLayerquotamonitorsnsspokeQMUtilsLayerquotamonitorsnsspokeLayer4E29C1C4": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "CompatibleRuntimes": [ + "nodejs18.x" + ], + "Content": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assete8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip" + }, + "LayerName": "QM-UtilsLayer-quota-monitor-sns-spoke" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke/QM-UtilsLayer-quota-monitor-sns-spoke/QM-UtilsLayer-quota-monitor-sns-spoke-Layer/Resource", + "aws:asset:path": "asset.e8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Content" + } + }, + "sqspokeNotificationMutingConfigE6DD8BD9": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Description": "Muting configuration for services, limits e.g. ec2:L-1216C47A,ec2:Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances,dynamodb,logs:*,geo:L-05EFD12D", + "Name": { + "Fn::FindInMap": [ + "QuotaMonitorMap", + "SSMParameters", + "NotificationMutingConfig" + ] + }, + "Type": "StringList", + "Value": "NOP" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke/sq-spoke-NotificationMutingConfig/Resource" + } + }, + "sqspokeSNSPublishersqspokeSNSPublisherSNSTopic5C405BCF": { + "Type": "AWS::SNS::Topic", + "Properties": { + "KmsMasterKeyId": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":kms:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":alias/aws/sns" + ] + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke/sq-spoke-SNSPublisher/sq-spoke-SNSPublisher-SNSTopic/Resource" + } + }, + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionEventsRule7B09C7F3": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - sq-spoke-SNSPublisherFunction-EventsRule", + "EventBusName": { + "Ref": "QMSNSSpokeBus29B8E7E8" + }, + "EventPattern": { + "detail": { + "status": [ + "WARN", + "ERROR" + ] + }, + "detail-type": [ + "Trusted Advisor Check Item Refresh Notification", + "Service Quotas Utilization Notification" + ], + "source": [ + "aws.trustedadvisor", + "aws-solutions.quota-monitor" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaC0F8A9BF", + "Arn" + ] + }, + "Id": "Target0" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke/sq-spoke-SNSPublisherFunction/sq-spoke-SNSPublisherFunction-EventsRule/Resource" + } + }, + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionEventsRuleAllowEventRulequotamonitorsnsspokesqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambda39FBB9D6A259BAD9": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaC0F8A9BF", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionEventsRule7B09C7F3", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke/sq-spoke-SNSPublisherFunction/sq-spoke-SNSPublisherFunction-EventsRule/AllowEventRulequotamonitorsnsspokesqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambda39FBB9D6" + } + }, + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaDeadLetterQueue83F0C565": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": "alias/aws/sqs" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke/sq-spoke-SNSPublisherFunction/sq-spoke-SNSPublisherFunction-Lambda-Dead-Letter-Queue/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "Queue itself is dead-letter queue", + "id": "AwsSolutions-SQS3" + } + ] + } + } + }, + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaDeadLetterQueuePolicyB89E703A": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaDeadLetterQueue83F0C565", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaDeadLetterQueue83F0C565" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke/sq-spoke-SNSPublisherFunction/sq-spoke-SNSPublisherFunction-Lambda-Dead-Letter-Queue/Policy/Resource" + } + }, + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaServiceRoleE4D78096": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke/sq-spoke-SNSPublisherFunction/sq-spoke-SNSPublisherFunction-Lambda/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaServiceRoleDefaultPolicyEAE21A8E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaDeadLetterQueue83F0C565", + "Arn" + ] + } + }, + { + "Action": "SNS:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "sqspokeSNSPublishersqspokeSNSPublisherSNSTopic5C405BCF" + } + }, + { + "Action": "kms:GenerateDataKey", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":kms:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":alias/aws/sns" + ] + ] + } + }, + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter", + { + "Ref": "sqspokeNotificationMutingConfigE6DD8BD9" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaServiceRoleDefaultPolicyEAE21A8E", + "Roles": [ + { + "Ref": "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaServiceRoleE4D78096" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke/sq-spoke-SNSPublisherFunction/sq-spoke-SNSPublisherFunction-Lambda/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaC0F8A9BF": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assete7a324e67e467d0c22e13b0693ca4efdceb0d53025c7fb45fe524870a5c18046.zip" + }, + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaDeadLetterQueue83F0C565", + "Arn" + ] + } + }, + "Description": "SO0005 quota-monitor-for-aws - sq-spoke-SNSPublisherFunction-Lambda", + "Environment": { + "Variables": { + "QM_NOTIFICATION_MUTING_CONFIG_PARAMETER": { + "Ref": "sqspokeNotificationMutingConfigE6DD8BD9" + }, + "SEND_METRIC": "No", + "TOPIC_ARN": { + "Ref": "sqspokeSNSPublishersqspokeSNSPublisherSNSTopic5C405BCF" + }, + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "QMUtilsLayerquotamonitorsnsspokeQMUtilsLayerquotamonitorsnsspokeLayer4E29C1C4" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaServiceRoleE4D78096", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 60 + }, + "DependsOn": [ + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaServiceRoleDefaultPolicyEAE21A8E", + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaServiceRoleE4D78096" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke/sq-spoke-SNSPublisherFunction/sq-spoke-SNSPublisherFunction-Lambda/Resource", + "aws:asset:path": "asset.e7a324e67e467d0c22e13b0693ca4efdceb0d53025c7fb45fe524870a5c18046.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaEventInvokeConfigBD6349C2": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "sqspokeSNSPublisherFunctionsqspokeSNSPublisherFunctionLambdaC0F8A9BF" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke/sq-spoke-SNSPublisherFunction/sq-spoke-SNSPublisherFunction-Lambda/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "SpokeSnsAppRegistryApplication6CB6C1C7": { + "Type": "AWS::ServiceCatalogAppRegistry::Application", + "Properties": { + "Description": "Service Catalog application to track and manage all your resources for the solution quota-monitor-for-aws", + "Name": { + "Fn::Join": [ + "-", + [ + { + "Ref": "AWS::Region" + }, + { + "Ref": "AWS::AccountId" + } + ] + ] + }, + "Tags": { + "ApplicationType": "AWS-Solutions", + "SolutionID": "SO0005-SPOKE-SNS", + "SolutionName": "quota-monitor-for-aws", + "SolutionVersion": "v6.3.0" + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke/SpokeSnsAppRegistryApplication/AppRegistryApplication/Resource" + } + }, + "SpokeSnsAppRegistryApplicationApplicationAttributeGroup3B112987": { + "Type": "AWS::ServiceCatalogAppRegistry::AttributeGroup", + "Properties": { + "Attributes": { + "solutionID": "SO0005-SPOKE-SNS", + "solutionName": "quota-monitor-for-aws", + "version": "v6.3.0", + "applicationType": "AWS-Solutions" + }, + "Description": "Attribute group for application information", + "Name": { + "Fn::Join": [ + "-", + [ + { + "Ref": "AWS::Region" + }, + { + "Ref": "AWS::AccountId" + } + ] + ] + }, + "Tags": { + "ApplicationType": "AWS-Solutions", + "SolutionID": "SO0005-SPOKE-SNS", + "SolutionName": "quota-monitor-for-aws", + "SolutionVersion": "v6.3.0" + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke/SpokeSnsAppRegistryApplication/AppRegistryApplication/ApplicationAttributeGroup/Resource" + } + }, + "SpokeSnsAppRegistryApplicationAttributeGroupAssociation0038a7d9d9f09BC2AD98": { + "Type": "AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation", + "Properties": { + "Application": { + "Fn::GetAtt": [ + "SpokeSnsAppRegistryApplication6CB6C1C7", + "Id" + ] + }, + "AttributeGroup": { + "Fn::GetAtt": [ + "SpokeSnsAppRegistryApplicationApplicationAttributeGroup3B112987", + "Id" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke/SpokeSnsAppRegistryApplication/AppRegistryApplication/AttributeGroupAssociation0038a7d9d9f0" + } + }, + "AppRegistryAssociation": { + "Type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation", + "Properties": { + "Application": { + "Fn::GetAtt": [ + "SpokeSnsAppRegistryApplication6CB6C1C7", + "Id" + ] + }, + "Resource": { + "Ref": "AWS::StackId" + }, + "ResourceType": "CFN_STACK" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sns-spoke/AppRegistryAssociation" + } + } + }, + "Outputs": { + "SpokeSnsEventBus": { + "Description": "SNS Event Bus Arn in spoke account", + "Value": { + "Fn::GetAtt": [ + "QMSNSSpokeBus29B8E7E8", + "Arn" + ] + } + } + } +} \ No newline at end of file diff --git a/deployment/quota-monitor-sq-spoke-cn.template b/deployment/quota-monitor-sq-spoke-cn.template new file mode 100644 index 0000000..b6fec8b --- /dev/null +++ b/deployment/quota-monitor-sq-spoke-cn.template @@ -0,0 +1,1479 @@ +{ + "Description": "(SO0005-SQ) - quota-monitor-for-aws - Service Quotas Template. Version v6.3.0", + "AWSTemplateFormatVersion": "2010-09-09", + "Metadata": { + "AWS::CloudFormation::Interface": { + "ParameterGroups": [ + { + "Label": { + "default": "Monitoring Account Configuration" + }, + "Parameters": [ + "EventBusArn", + "SpokeSnsRegion" + ] + }, + { + "Label": { + "default": "Service Quotas Configuration" + }, + "Parameters": [ + "NotificationThreshold", + "MonitoringFrequency", + "ReportOKNotifications", + "SageMakerMonitoring", + "ConnectMonitoring" + ] + } + ], + "ParameterLabels": { + "EventBusArn": { + "default": "Arn for the EventBridge bus in the monitoring account" + }, + "SpokeSnsRegion": { + "default": "Region in which the spoke SNS stack exists in this account" + }, + "NotificationThreshold": { + "default": "At what quota utilization do you want notifications?" + }, + "MonitoringFrequency": { + "default": "Frequency to monitor quota utilization" + }, + "ReportOKNotifications": { + "default": "Report OK Notifications" + }, + "SageMakerMonitoring": { + "default": "Enable monitoring for SageMaker quotas" + }, + "ConnectMonitoring": { + "default": "Enable monitoring for Connect quotas" + } + } + } + }, + "Parameters": { + "EventBusArn": { + "Type": "String" + }, + "SpokeSnsRegion": { + "Type": "String", + "Default": "", + "Description": "The region in which the spoke SNS stack exists in this account. Leave blank if the spoke SNS is not needed." + }, + "NotificationThreshold": { + "Type": "String", + "Default": "80", + "AllowedPattern": "^([1-9]|[1-9][0-9])$", + "ConstraintDescription": "Threshold must be a whole number between 0 and 100", + "Description": "Threshold percentage for quota utilization alerts (0-100)" + }, + "MonitoringFrequency": { + "Type": "String", + "Default": "rate(12 hours)", + "AllowedValues": [ + "rate(6 hours)", + "rate(12 hours)", + "rate(1 day)" + ] + }, + "ReportOKNotifications": { + "Type": "String", + "Default": "No", + "AllowedValues": [ + "Yes", + "No" + ] + }, + "SageMakerMonitoring": { + "Type": "String", + "Default": "Yes", + "AllowedValues": [ + "Yes", + "No" + ] + }, + "ConnectMonitoring": { + "Type": "String", + "Default": "Yes", + "AllowedValues": [ + "Yes", + "No" + ] + } + }, + "Mappings": { + "QuotaMonitorMap": { + "SSMParameters": { + "NotificationMutingConfig": "/QuotaMonitor/spoke/NotificationConfiguration" + } + } + }, + "Conditions": { + "SpokeSnsRegionExists": { + "Fn::Not": [ + { + "Fn::Equals": [ + { + "Ref": "SpokeSnsRegion" + }, + "" + ] + } + ] + } + }, + "Resources": { + "QMSpokeBus1D13B121": { + "Type": "AWS::Events::EventBus", + "Properties": { + "Name": "QuotaMonitorSpokeBus" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-Spoke-Bus/Resource" + } + }, + "QMUtilsLayerquotamonitorsqspokecnQMUtilsLayerquotamonitorsqspokecnLayerDD5A2E4A": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "CompatibleRuntimes": [ + "nodejs18.x" + ], + "Content": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assete8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip" + }, + "LayerName": "QM-UtilsLayer-quota-monitor-sq-spoke-cn" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-UtilsLayer-quota-monitor-sq-spoke-cn/QM-UtilsLayer-quota-monitor-sq-spoke-cn-Layer/Resource", + "aws:asset:path": "asset.e8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Content" + } + }, + "SQServiceTable0182B2D0": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "ServiceCode", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": [ + { + "AttributeName": "ServiceCode", + "KeyType": "HASH" + } + ], + "PointInTimeRecoverySpecification": { + "PointInTimeRecoveryEnabled": true + }, + "SSESpecification": { + "SSEEnabled": true + }, + "StreamSpecification": { + "StreamViewType": "NEW_AND_OLD_IMAGES" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/SQ-ServiceTable/Resource", + "guard": { + "SuppressedRules": [ + "DYNAMODB_TABLE_ENCRYPTED_KMS" + ] + } + } + }, + "SQQuotaTableD0BC5741": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "ServiceCode", + "AttributeType": "S" + }, + { + "AttributeName": "QuotaCode", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": [ + { + "AttributeName": "ServiceCode", + "KeyType": "HASH" + }, + { + "AttributeName": "QuotaCode", + "KeyType": "RANGE" + } + ], + "PointInTimeRecoverySpecification": { + "PointInTimeRecoveryEnabled": true + }, + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/SQ-QuotaTable/Resource", + "guard": { + "SuppressedRules": [ + "DYNAMODB_TABLE_ENCRYPTED_KMS" + ] + } + } + }, + "QMListManagerQMListManagerFunctionServiceRole12D19CB7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-ListManager/QM-ListManager-Function/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + }, + { + "reason": "Actions do not support resource-level permissions", + "id": "AwsSolutions-IAM5" + } + ] + } + } + }, + "QMListManagerQMListManagerFunctionServiceRoleDefaultPolicy314665D0": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:BatchWriteItem", + "dynamodb:DeleteItem", + "dynamodb:Query", + "dynamodb:Scan" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "SQServiceTable0182B2D0", + "Arn" + ] + } + }, + { + "Action": [ + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:BatchWriteItem", + "dynamodb:DeleteItem", + "dynamodb:Query", + "dynamodb:Scan" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "SQQuotaTableD0BC5741", + "Arn" + ] + } + }, + { + "Action": [ + "cloudwatch:GetMetricData", + "servicequotas:ListServiceQuotas", + "servicequotas:ListServices", + "dynamodb:DescribeLimits", + "autoscaling:DescribeAccountLimits", + "route53:GetAccountLimit", + "rds:DescribeAccountAttributes" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "dynamodb:ListStreams", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "dynamodb:DescribeStream", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "SQServiceTable0182B2D0", + "StreamArn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMListManagerQMListManagerFunctionServiceRoleDefaultPolicy314665D0", + "Roles": [ + { + "Ref": "QMListManagerQMListManagerFunctionServiceRole12D19CB7" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-ListManager/QM-ListManager-Function/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "Actions do not support resource-level permissions", + "id": "AwsSolutions-IAM5" + } + ] + } + } + }, + "QMListManagerQMListManagerFunction1F09A88F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/asset3701f2abae7e46f2ca278d27abfbafbf17499950bb5782fed31eb776c07ad072.zip" + }, + "Description": "SO0005 quota-monitor-for-aws - QM-ListManager-Function", + "Environment": { + "Variables": { + "SQ_SERVICE_TABLE": { + "Ref": "SQServiceTable0182B2D0" + }, + "SQ_QUOTA_TABLE": { + "Ref": "SQQuotaTableD0BC5741" + }, + "PARTITION_KEY": "ServiceCode", + "SORT": "QuotaCode", + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "QMUtilsLayerquotamonitorsqspokecnQMUtilsLayerquotamonitorsqspokecnLayerDD5A2E4A" + } + ], + "MemorySize": 256, + "Role": { + "Fn::GetAtt": [ + "QMListManagerQMListManagerFunctionServiceRole12D19CB7", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 900 + }, + "DependsOn": [ + "QMListManagerQMListManagerFunctionServiceRoleDefaultPolicy314665D0", + "QMListManagerQMListManagerFunctionServiceRole12D19CB7" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-ListManager/QM-ListManager-Function/Resource", + "aws:asset:path": "asset.3701f2abae7e46f2ca278d27abfbafbf17499950bb5782fed31eb776c07ad072.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK", + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMListManagerQMListManagerFunctionEventInvokeConfigDDD15BD1": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "QMListManagerQMListManagerFunction1F09A88F" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-ListManager/QM-ListManager-Function/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMListManagerQMListManagerFunctionDynamoDBEventSourcequotamonitorsqspokecnSQServiceTableDC4AF36E6E2F1383": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "BatchSize": 1, + "EventSourceArn": { + "Fn::GetAtt": [ + "SQServiceTable0182B2D0", + "StreamArn" + ] + }, + "FunctionName": { + "Ref": "QMListManagerQMListManagerFunction1F09A88F" + }, + "StartingPosition": "LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-ListManager/QM-ListManager-Function/DynamoDBEventSource:quotamonitorsqspokecnSQServiceTableDC4AF36E/Resource" + } + }, + "QMListManagerQMListManagerProviderframeworkonEventServiceRoleB85FCC1C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-ListManager/QM-ListManager-Provider/framework-onEvent/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMListManagerQMListManagerProviderframeworkonEventServiceRoleDefaultPolicy60F3D9D1": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "QMListManagerQMListManagerFunction1F09A88F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "QMListManagerQMListManagerFunction1F09A88F", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMListManagerQMListManagerProviderframeworkonEventServiceRoleDefaultPolicy60F3D9D1", + "Roles": [ + { + "Ref": "QMListManagerQMListManagerProviderframeworkonEventServiceRoleB85FCC1C" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-ListManager/QM-ListManager-Provider/framework-onEvent/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMListManagerQMListManagerProviderframeworkonEvent1F57B2C8": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/asset7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94.zip" + }, + "Description": "AWS CDK resource provider framework - onEvent (quota-monitor-sq-spoke-cn/QM-ListManager/QM-ListManager-Provider)", + "Environment": { + "Variables": { + "USER_ON_EVENT_FUNCTION_ARN": { + "Fn::GetAtt": [ + "QMListManagerQMListManagerFunction1F09A88F", + "Arn" + ] + } + } + }, + "Handler": "framework.onEvent", + "Role": { + "Fn::GetAtt": [ + "QMListManagerQMListManagerProviderframeworkonEventServiceRoleB85FCC1C", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 900 + }, + "DependsOn": [ + "QMListManagerQMListManagerProviderframeworkonEventServiceRoleDefaultPolicy60F3D9D1", + "QMListManagerQMListManagerProviderframeworkonEventServiceRoleB85FCC1C" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-ListManager/QM-ListManager-Provider/framework-onEvent/Resource", + "aws:asset:path": "asset.7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMListManagerSQServiceList2C145D4D": { + "Type": "Custom::SQServiceList", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "QMListManagerQMListManagerProviderframeworkonEvent1F57B2C8", + "Arn" + ] + }, + "VERSION": "v6.3.0", + "SageMakerMonitoring": { + "Ref": "SageMakerMonitoring" + }, + "ConnectMonitoring": { + "Ref": "ConnectMonitoring" + } + }, + "DependsOn": [ + "QMUtilizationErr3AEC9915", + "SQQuotaTableD0BC5741", + "SQServiceTable0182B2D0" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-ListManager/SQServiceList/Default" + } + }, + "QMListManagerSchedule2CDA6819": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - quota-monitor-sq-spoke-cn-EventsRule", + "ScheduleExpression": "rate(30 days)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "QMListManagerQMListManagerFunction1F09A88F", + "Arn" + ] + }, + "Id": "Target0" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-ListManagerSchedule/Resource" + } + }, + "QMListManagerScheduleAllowEventRulequotamonitorsqspokecnQMListManagerQMListManagerFunction4E7FBCD93F527F83": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "QMListManagerQMListManagerFunction1F09A88F", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "QMListManagerSchedule2CDA6819", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-ListManagerSchedule/AllowEventRulequotamonitorsqspokecnQMListManagerQMListManagerFunction4E7FBCD9" + } + }, + "QMCWPollerQMCWPollerEventsRuleE8CD588E": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - QM-CWPoller-EventsRule", + "ScheduleExpression": { + "Ref": "MonitoringFrequency" + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "QMCWPollerQMCWPollerLambda824ABE36", + "Arn" + ] + }, + "Id": "Target0" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-CWPoller/QM-CWPoller-EventsRule/Resource" + } + }, + "QMCWPollerQMCWPollerEventsRuleAllowEventRulequotamonitorsqspokecnQMCWPollerQMCWPollerLambdaD696DA33AC36AA70": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "QMCWPollerQMCWPollerLambda824ABE36", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "QMCWPollerQMCWPollerEventsRuleE8CD588E", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-CWPoller/QM-CWPoller-EventsRule/AllowEventRulequotamonitorsqspokecnQMCWPollerQMCWPollerLambdaD696DA33" + } + }, + "QMCWPollerQMCWPollerLambdaDeadLetterQueueE535D49E": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": "alias/aws/sqs" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-CWPoller/QM-CWPoller-Lambda-Dead-Letter-Queue/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "Queue itself is dead-letter queue", + "id": "AwsSolutions-SQS3" + } + ] + } + } + }, + "QMCWPollerQMCWPollerLambdaDeadLetterQueuePolicyC81A8B00": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "QMCWPollerQMCWPollerLambdaDeadLetterQueueE535D49E", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "QMCWPollerQMCWPollerLambdaDeadLetterQueueE535D49E" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-CWPoller/QM-CWPoller-Lambda-Dead-Letter-Queue/Policy/Resource" + } + }, + "QMCWPollerQMCWPollerLambdaServiceRole8985092D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-CWPoller/QM-CWPoller-Lambda/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMCWPollerQMCWPollerLambdaServiceRoleDefaultPolicy626BCE22": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMCWPollerQMCWPollerLambdaDeadLetterQueueE535D49E", + "Arn" + ] + } + }, + { + "Action": "dynamodb:Query", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "SQQuotaTableD0BC5741", + "Arn" + ] + } + }, + { + "Action": "dynamodb:Scan", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "SQServiceTable0182B2D0", + "Arn" + ] + } + }, + { + "Action": "cloudwatch:GetMetricData", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "events:PutEvents", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMSpokeBus1D13B121", + "Arn" + ] + } + }, + { + "Action": "servicequotas:ListServices", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMCWPollerQMCWPollerLambdaServiceRoleDefaultPolicy626BCE22", + "Roles": [ + { + "Ref": "QMCWPollerQMCWPollerLambdaServiceRole8985092D" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-CWPoller/QM-CWPoller-Lambda/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMCWPollerQMCWPollerLambda824ABE36": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/asset4ae69af36e954d598ae25d7f2f8f5ea5ecb93bf4ba61963aa7d8d571cf71ecce.zip" + }, + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "QMCWPollerQMCWPollerLambdaDeadLetterQueueE535D49E", + "Arn" + ] + } + }, + "Description": "SO0005 quota-monitor-for-aws - QM-CWPoller-Lambda", + "Environment": { + "Variables": { + "SQ_SERVICE_TABLE": { + "Ref": "SQServiceTable0182B2D0" + }, + "SQ_QUOTA_TABLE": { + "Ref": "SQQuotaTableD0BC5741" + }, + "SPOKE_EVENT_BUS": { + "Ref": "QMSpokeBus1D13B121" + }, + "POLLER_FREQUENCY": { + "Ref": "MonitoringFrequency" + }, + "THRESHOLD": { + "Ref": "NotificationThreshold" + }, + "SQ_REPORT_OK_NOTIFICATIONS": { + "Ref": "ReportOKNotifications" + }, + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "QMUtilsLayerquotamonitorsqspokecnQMUtilsLayerquotamonitorsqspokecnLayerDD5A2E4A" + } + ], + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "QMCWPollerQMCWPollerLambdaServiceRole8985092D", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 900 + }, + "DependsOn": [ + "QMCWPollerQMCWPollerLambdaServiceRoleDefaultPolicy626BCE22", + "QMCWPollerQMCWPollerLambdaServiceRole8985092D" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-CWPoller/QM-CWPoller-Lambda/Resource", + "aws:asset:path": "asset.4ae69af36e954d598ae25d7f2f8f5ea5ecb93bf4ba61963aa7d8d571cf71ecce.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMCWPollerQMCWPollerLambdaEventInvokeConfigB943EE46": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "QMCWPollerQMCWPollerLambda824ABE36" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-CWPoller/QM-CWPoller-Lambda/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMUtilizationOK588DBAE8": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - quota-monitor-sq-spoke-cn-EventsRule", + "EventBusName": { + "Ref": "QMSpokeBus1D13B121" + }, + "EventPattern": { + "account": [ + { + "Ref": "AWS::AccountId" + } + ], + "detail": { + "status": [ + "OK" + ] + }, + "detail-type": [ + "Service Quotas Utilization Notification" + ], + "source": [ + "aws-solutions.quota-monitor" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Ref": "EventBusArn" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "QMUtilizationOKEventsRoleC12899D6", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-Utilization-OK/Resource" + } + }, + "QMUtilizationOKEventsRoleC12899D6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-Utilization-OK/EventsRole/Resource" + } + }, + "QMUtilizationOKEventsRoleDefaultPolicyD9D7AF54": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "events:PutEvents", + "Effect": "Allow", + "Resource": { + "Ref": "EventBusArn" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMUtilizationOKEventsRoleDefaultPolicyD9D7AF54", + "Roles": [ + { + "Ref": "QMUtilizationOKEventsRoleC12899D6" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-Utilization-OK/EventsRole/DefaultPolicy/Resource" + } + }, + "QMUtilizationWarn1BF84C25": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - quota-monitor-sq-spoke-cn-EventsRule", + "EventBusName": { + "Ref": "QMSpokeBus1D13B121" + }, + "EventPattern": { + "account": [ + { + "Ref": "AWS::AccountId" + } + ], + "detail": { + "status": [ + "WARN" + ] + }, + "detail-type": [ + "Service Quotas Utilization Notification" + ], + "source": [ + "aws-solutions.quota-monitor" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Ref": "EventBusArn" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "QMUtilizationWarnEventsRole4BC4EAB1", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-Utilization-Warn/Resource" + } + }, + "QMUtilizationWarnEventsRole4BC4EAB1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-Utilization-Warn/EventsRole/Resource" + } + }, + "QMUtilizationWarnEventsRoleDefaultPolicyAE78A2DA": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "events:PutEvents", + "Effect": "Allow", + "Resource": { + "Ref": "EventBusArn" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMUtilizationWarnEventsRoleDefaultPolicyAE78A2DA", + "Roles": [ + { + "Ref": "QMUtilizationWarnEventsRole4BC4EAB1" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-Utilization-Warn/EventsRole/DefaultPolicy/Resource" + } + }, + "QMUtilizationErr3AEC9915": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - quota-monitor-sq-spoke-cn-EventsRule", + "EventBusName": { + "Ref": "QMSpokeBus1D13B121" + }, + "EventPattern": { + "account": [ + { + "Ref": "AWS::AccountId" + } + ], + "detail": { + "status": [ + "ERROR" + ] + }, + "detail-type": [ + "Service Quotas Utilization Notification" + ], + "source": [ + "aws-solutions.quota-monitor" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Ref": "EventBusArn" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "QMUtilizationErrEventsRoleAAC90710", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-Utilization-Err/Resource" + } + }, + "QMUtilizationErrEventsRoleAAC90710": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-Utilization-Err/EventsRole/Resource" + } + }, + "QMUtilizationErrEventsRoleDefaultPolicy4BE442C4": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "events:PutEvents", + "Effect": "Allow", + "Resource": { + "Ref": "EventBusArn" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMUtilizationErrEventsRoleDefaultPolicy4BE442C4", + "Roles": [ + { + "Ref": "QMUtilizationErrEventsRoleAAC90710" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/QM-Utilization-Err/EventsRole/DefaultPolicy/Resource" + } + }, + "SpokeSnsRule5A40CA85": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - quota-monitor-sq-spoke-cn-SpokeSnsEventsRule", + "EventBusName": { + "Ref": "QMSpokeBus1D13B121" + }, + "EventPattern": { + "detail": { + "status": [ + "WARN", + "ERROR" + ] + }, + "detail-type": [ + "Trusted Advisor Check Item Refresh Notification", + "Service Quotas Utilization Notification" + ], + "source": [ + "aws.trustedadvisor", + "aws-solutions.quota-monitor" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "SpokeSnsRegion" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":event-bus/QuotaMonitorSnsSpokeBus" + ] + ] + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "SpokeSnsRuleEventsRole851D8C25", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/SpokeSnsRule/Resource" + }, + "Condition": "SpokeSnsRegionExists" + }, + "SpokeSnsRuleEventsRole851D8C25": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/SpokeSnsRule/EventsRole/Resource" + }, + "Condition": "SpokeSnsRegionExists" + }, + "SpokeSnsRuleEventsRoleDefaultPolicyC16FF840": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "events:PutEvents", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "SpokeSnsRegion" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":event-bus/QuotaMonitorSnsSpokeBus" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "SpokeSnsRuleEventsRoleDefaultPolicyC16FF840", + "Roles": [ + { + "Ref": "SpokeSnsRuleEventsRole851D8C25" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke-cn/SpokeSnsRule/EventsRole/DefaultPolicy/Resource" + }, + "Condition": "SpokeSnsRegionExists" + } + } +} \ No newline at end of file diff --git a/deployment/quota-monitor-sq-spoke.template b/deployment/quota-monitor-sq-spoke.template index 600ae3b..cc2ff4c 100644 --- a/deployment/quota-monitor-sq-spoke.template +++ b/deployment/quota-monitor-sq-spoke.template @@ -1,5 +1,5 @@ { - "Description": "(SO0005-SQ) - quota-monitor-for-aws - Service Quotas Template. Version v6.2.11", + "Description": "(SO0005-SQ) - quota-monitor-for-aws - Service Quotas Template. Version v6.3.0", "AWSTemplateFormatVersion": "2010-09-09", "Metadata": { "AWS::CloudFormation::Interface": { @@ -9,7 +9,8 @@ "default": "Monitoring Account Configuration" }, "Parameters": [ - "EventBusArn" + "EventBusArn", + "SpokeSnsRegion" ] }, { @@ -19,7 +20,9 @@ "Parameters": [ "NotificationThreshold", "MonitoringFrequency", - "ReportOKNotifications" + "ReportOKNotifications", + "SageMakerMonitoring", + "ConnectMonitoring" ] } ], @@ -27,6 +30,9 @@ "EventBusArn": { "default": "Arn for the EventBridge bus in the monitoring account" }, + "SpokeSnsRegion": { + "default": "Region in which the spoke SNS stack exists in this account" + }, "NotificationThreshold": { "default": "At what quota utilization do you want notifications?" }, @@ -35,6 +41,12 @@ }, "ReportOKNotifications": { "default": "Report OK Notifications" + }, + "SageMakerMonitoring": { + "default": "Enable monitoring for SageMaker quotas" + }, + "ConnectMonitoring": { + "default": "Enable monitoring for Connect quotas" } } } @@ -43,21 +55,25 @@ "EventBusArn": { "Type": "String" }, + "SpokeSnsRegion": { + "Type": "String", + "Default": "", + "Description": "The region in which the spoke SNS stack exists in this account. Leave blank if the spoke SNS is not needed." + }, "NotificationThreshold": { "Type": "String", "Default": "80", - "AllowedValues": [ - "60", - "70", - "80" - ] + "AllowedPattern": "^([1-9]|[1-9][0-9])$", + "ConstraintDescription": "Threshold must be a whole number between 0 and 100", + "Description": "Threshold percentage for quota utilization alerts (0-100)" }, "MonitoringFrequency": { "Type": "String", "Default": "rate(12 hours)", "AllowedValues": [ "rate(6 hours)", - "rate(12 hours)" + "rate(12 hours)", + "rate(1 day)" ] }, "ReportOKNotifications": { @@ -67,6 +83,43 @@ "Yes", "No" ] + }, + "SageMakerMonitoring": { + "Type": "String", + "Default": "Yes", + "AllowedValues": [ + "Yes", + "No" + ] + }, + "ConnectMonitoring": { + "Type": "String", + "Default": "Yes", + "AllowedValues": [ + "Yes", + "No" + ] + } + }, + "Mappings": { + "QuotaMonitorMap": { + "SSMParameters": { + "NotificationMutingConfig": "/QuotaMonitor/spoke/NotificationConfiguration" + } + } + }, + "Conditions": { + "SpokeSnsRegionExists": { + "Fn::Not": [ + { + "Fn::Equals": [ + { + "Ref": "SpokeSnsRegion" + }, + "" + ] + } + ] } }, "Resources": { @@ -89,13 +142,13 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset11e8019ce0550d340c842c1a14bc8797872e01186f9f5b7ba02745ff143d145a.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/assete8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip" }, "LayerName": "QM-UtilsLayer-quota-monitor-sq-spoke" }, "Metadata": { "aws:cdk:path": "quota-monitor-sq-spoke/QM-UtilsLayer-quota-monitor-sq-spoke/QM-UtilsLayer-quota-monitor-sq-spoke-Layer/Resource", - "aws:asset:path": "asset.11e8019ce0550d340c842c1a14bc8797872e01186f9f5b7ba02745ff143d145a.zip", + "aws:asset:path": "asset.e8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Content" } @@ -129,7 +182,12 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete", "Metadata": { - "aws:cdk:path": "quota-monitor-sq-spoke/SQ-ServiceTable/Resource" + "aws:cdk:path": "quota-monitor-sq-spoke/SQ-ServiceTable/Resource", + "guard": { + "SuppressedRules": [ + "DYNAMODB_TABLE_ENCRYPTED_KMS" + ] + } } }, "SQQuotaTableD0BC5741": { @@ -166,7 +224,12 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete", "Metadata": { - "aws:cdk:path": "quota-monitor-sq-spoke/SQ-QuotaTable/Resource" + "aws:cdk:path": "quota-monitor-sq-spoke/SQ-QuotaTable/Resource", + "guard": { + "SuppressedRules": [ + "DYNAMODB_TABLE_ENCRYPTED_KMS" + ] + } } }, "QMListManagerQMListManagerFunctionServiceRole12D19CB7": { @@ -323,7 +386,7 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset342732eb7e556ffc25fba169c334315dec6049e2c695bcda829c862c9ac1ebcc.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/asset3701f2abae7e46f2ca278d27abfbafbf17499950bb5782fed31eb776c07ad072.zip" }, "Description": "SO0005 quota-monitor-for-aws - QM-ListManager-Function", "Environment": { @@ -337,8 +400,8 @@ "PARTITION_KEY": "ServiceCode", "SORT": "QuotaCode", "LOG_LEVEL": "info", - "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.2.11", - "VERSION": "v6.2.11", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", "SOLUTION_ID": "SO0005" } }, @@ -364,7 +427,7 @@ ], "Metadata": { "aws:cdk:path": "quota-monitor-sq-spoke/QM-ListManager/QM-ListManager-Function/Resource", - "aws:asset:path": "asset.342732eb7e556ffc25fba169c334315dec6049e2c695bcda829c862c9ac1ebcc.zip", + "aws:asset:path": "asset.3701f2abae7e46f2ca278d27abfbafbf17499950bb5782fed31eb776c07ad072.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Code", "cdk_nag": { @@ -374,6 +437,14 @@ "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK", + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } } }, @@ -535,7 +606,7 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/asset7382a0addb9f34974a1ea6c6c9b063882af874828f366f5c93b2b7b64db15c94.zip" }, "Description": "AWS CDK resource provider framework - onEvent (quota-monitor-sq-spoke/QM-ListManager/QM-ListManager-Provider)", "Environment": { @@ -582,6 +653,12 @@ "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } } }, @@ -594,7 +671,13 @@ "Arn" ] }, - "VERSION": "v6.2.11" + "VERSION": "v6.3.0", + "SageMakerMonitoring": { + "Ref": "SageMakerMonitoring" + }, + "ConnectMonitoring": { + "Ref": "ConnectMonitoring" + } }, "DependsOn": [ "QMUtilizationErr3AEC9915", @@ -894,7 +977,7 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/assete3fcc4fd4830a156b44e04bc61dce9ede2885443fb9be7f623e18947f9e92342.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/asset4ae69af36e954d598ae25d7f2f8f5ea5ecb93bf4ba61963aa7d8d571cf71ecce.zip" }, "DeadLetterConfig": { "TargetArn": { @@ -926,8 +1009,8 @@ "Ref": "ReportOKNotifications" }, "LOG_LEVEL": "info", - "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.2.11", - "VERSION": "v6.2.11", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", "SOLUTION_ID": "SO0005" } }, @@ -953,7 +1036,7 @@ ], "Metadata": { "aws:cdk:path": "quota-monitor-sq-spoke/QM-CWPoller/QM-CWPoller-Lambda/Resource", - "aws:asset:path": "asset.e3fcc4fd4830a156b44e04bc61dce9ede2885443fb9be7f623e18947f9e92342.zip", + "aws:asset:path": "asset.4ae69af36e954d598ae25d7f2f8f5ea5ecb93bf4ba61963aa7d8d571cf71ecce.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Code", "cdk_nag": { @@ -963,6 +1046,12 @@ "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } } }, @@ -1260,6 +1349,132 @@ "aws:cdk:path": "quota-monitor-sq-spoke/QM-Utilization-Err/EventsRole/DefaultPolicy/Resource" } }, + "SpokeSnsRule5A40CA85": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - quota-monitor-sq-spoke-SpokeSnsEventsRule", + "EventBusName": { + "Ref": "QMSpokeBus1D13B121" + }, + "EventPattern": { + "detail": { + "status": [ + "WARN", + "ERROR" + ] + }, + "detail-type": [ + "Trusted Advisor Check Item Refresh Notification", + "Service Quotas Utilization Notification" + ], + "source": [ + "aws.trustedadvisor", + "aws-solutions.quota-monitor" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "SpokeSnsRegion" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":event-bus/QuotaMonitorSnsSpokeBus" + ] + ] + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "SpokeSnsRuleEventsRole851D8C25", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke/SpokeSnsRule/Resource" + }, + "Condition": "SpokeSnsRegionExists" + }, + "SpokeSnsRuleEventsRole851D8C25": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke/SpokeSnsRule/EventsRole/Resource" + }, + "Condition": "SpokeSnsRegionExists" + }, + "SpokeSnsRuleEventsRoleDefaultPolicyC16FF840": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "events:PutEvents", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "SpokeSnsRegion" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":event-bus/QuotaMonitorSnsSpokeBus" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "SpokeSnsRuleEventsRoleDefaultPolicyC16FF840", + "Roles": [ + { + "Ref": "SpokeSnsRuleEventsRole851D8C25" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-sq-spoke/SpokeSnsRule/EventsRole/DefaultPolicy/Resource" + }, + "Condition": "SpokeSnsRegionExists" + }, "SQSpokeAppRegistryApplicationB3787B2B": { "Type": "AWS::ServiceCatalogAppRegistry::Application", "Properties": { @@ -1282,7 +1497,7 @@ "ApplicationType": "AWS-Solutions", "SolutionID": "SO0005-SQ", "SolutionName": "quota-monitor-for-aws", - "SolutionVersion": "v6.2.11" + "SolutionVersion": "v6.3.0" } }, "Metadata": { @@ -1295,7 +1510,7 @@ "Attributes": { "solutionID": "SO0005-SQ", "solutionName": "quota-monitor-for-aws", - "version": "v6.2.11", + "version": "v6.3.0", "applicationType": "AWS-Solutions" }, "Description": "Attribute group for application information", @@ -1317,7 +1532,7 @@ "ApplicationType": "AWS-Solutions", "SolutionID": "SO0005-SQ", "SolutionName": "quota-monitor-for-aws", - "SolutionVersion": "v6.2.11" + "SolutionVersion": "v6.3.0" } }, "Metadata": { diff --git a/deployment/quota-monitor-ta-spoke-cn.template b/deployment/quota-monitor-ta-spoke-cn.template new file mode 100644 index 0000000..c81e1f4 --- /dev/null +++ b/deployment/quota-monitor-ta-spoke-cn.template @@ -0,0 +1,680 @@ +{ + "Description": "(SO0005-TA) - quota-monitor-for-aws - Trusted Advisor Template. Version v6.3.0", + "AWSTemplateFormatVersion": "2010-09-09", + "Metadata": { + "AWS::CloudFormation::Interface": { + "ParameterGroups": [ + { + "Label": { + "default": "Monitoring Account Configuration" + }, + "Parameters": [ + "EventBusArn" + ] + }, + { + "Label": { + "default": "Refresh Configuration" + }, + "Parameters": [ + "TARefreshRate" + ] + } + ], + "ParameterLabels": { + "EventBusArn": { + "default": "Arn for the EventBridge bus in the monitoring account" + }, + "TARefreshRate": { + "default": "Trusted Advisor Refresh Rate" + } + } + } + }, + "Parameters": { + "EventBusArn": { + "Type": "String" + }, + "TARefreshRate": { + "Type": "String", + "Default": "rate(12 hours)", + "AllowedValues": [ + "rate(6 hours)", + "rate(12 hours)", + "rate(1 day)" + ], + "Description": "The rate at which to refresh Trusted Advisor checks" + } + }, + "Resources": { + "TAOkRule3B6A3866": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Quota Monitor Solution - Spoke - Rule for TA OK events", + "EventPattern": { + "account": [ + { + "Ref": "AWS::AccountId" + } + ], + "detail": { + "status": [ + "OK" + ], + "check-item-detail": { + "Service": [ + "AutoScaling", + "CloudFormation", + "DynamoDB", + "EBS", + "EC2", + "ELB", + "IAM", + "Kinesis", + "RDS", + "Route53", + "SES", + "VPC" + ] + } + }, + "detail-type": [ + "Trusted Advisor Check Item Refresh Notification" + ], + "source": [ + "aws.trustedadvisor" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Ref": "EventBusArn" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "TAOkRuleEventsRole78AEFB32", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/TAOkRule/Resource" + } + }, + "TAOkRuleEventsRole78AEFB32": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/TAOkRule/EventsRole/Resource" + } + }, + "TAOkRuleEventsRoleDefaultPolicyFAB70645": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "events:PutEvents", + "Effect": "Allow", + "Resource": { + "Ref": "EventBusArn" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TAOkRuleEventsRoleDefaultPolicyFAB70645", + "Roles": [ + { + "Ref": "TAOkRuleEventsRole78AEFB32" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/TAOkRule/EventsRole/DefaultPolicy/Resource" + } + }, + "TAWarnRule4E0A6126": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Quota Monitor Solution - Spoke - Rule for TA WARN events", + "EventPattern": { + "account": [ + { + "Ref": "AWS::AccountId" + } + ], + "detail": { + "status": [ + "WARN" + ], + "check-item-detail": { + "Service": [ + "AutoScaling", + "CloudFormation", + "DynamoDB", + "EBS", + "EC2", + "ELB", + "IAM", + "Kinesis", + "RDS", + "Route53", + "SES", + "VPC" + ] + } + }, + "detail-type": [ + "Trusted Advisor Check Item Refresh Notification" + ], + "source": [ + "aws.trustedadvisor" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Ref": "EventBusArn" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "TAWarnRuleEventsRole92C70288", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/TAWarnRule/Resource" + } + }, + "TAWarnRuleEventsRole92C70288": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/TAWarnRule/EventsRole/Resource" + } + }, + "TAWarnRuleEventsRoleDefaultPolicyB0AE7261": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "events:PutEvents", + "Effect": "Allow", + "Resource": { + "Ref": "EventBusArn" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TAWarnRuleEventsRoleDefaultPolicyB0AE7261", + "Roles": [ + { + "Ref": "TAWarnRuleEventsRole92C70288" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/TAWarnRule/EventsRole/DefaultPolicy/Resource" + } + }, + "TAErrorRule6720C8C4": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "Quota Monitor Solution - Spoke - Rule for TA ERROR events", + "EventPattern": { + "account": [ + { + "Ref": "AWS::AccountId" + } + ], + "detail": { + "status": [ + "ERROR" + ], + "check-item-detail": { + "Service": [ + "AutoScaling", + "CloudFormation", + "DynamoDB", + "EBS", + "EC2", + "ELB", + "IAM", + "Kinesis", + "RDS", + "Route53", + "SES", + "VPC" + ] + } + }, + "detail-type": [ + "Trusted Advisor Check Item Refresh Notification" + ], + "source": [ + "aws.trustedadvisor" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Ref": "EventBusArn" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "TAErrorRuleEventsRoleB879CF53", + "Arn" + ] + } + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/TAErrorRule/Resource" + } + }, + "TAErrorRuleEventsRoleB879CF53": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/TAErrorRule/EventsRole/Resource" + } + }, + "TAErrorRuleEventsRoleDefaultPolicy270A14C5": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "events:PutEvents", + "Effect": "Allow", + "Resource": { + "Ref": "EventBusArn" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TAErrorRuleEventsRoleDefaultPolicy270A14C5", + "Roles": [ + { + "Ref": "TAErrorRuleEventsRoleB879CF53" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/TAErrorRule/EventsRole/DefaultPolicy/Resource" + } + }, + "QMUtilsLayerQMUtilsLayerLayer80D5D993": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "CompatibleRuntimes": [ + "nodejs18.x" + ], + "Content": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assete8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip" + }, + "LayerName": "QM-UtilsLayer" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/QM-UtilsLayer/QM-UtilsLayer-Layer/Resource", + "aws:asset:path": "asset.e8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Content" + } + }, + "QMTARefresherQMTARefresherEventsRuleDCF4B340": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "SO0005 quota-monitor-for-aws - QM-TA-Refresher-EventsRule", + "ScheduleExpression": { + "Ref": "TARefreshRate" + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "QMTARefresherQMTARefresherLambdaEE100499", + "Arn" + ] + }, + "Id": "Target0" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/QM-TA-Refresher/QM-TA-Refresher-EventsRule/Resource" + } + }, + "QMTARefresherQMTARefresherEventsRuleAllowEventRulequotamonitortaspokecnQMTARefresherQMTARefresherLambda2C62934B2104198B": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "QMTARefresherQMTARefresherLambdaEE100499", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "QMTARefresherQMTARefresherEventsRuleDCF4B340", + "Arn" + ] + } + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/QM-TA-Refresher/QM-TA-Refresher-EventsRule/AllowEventRulequotamonitortaspokecnQMTARefresherQMTARefresherLambda2C62934B" + } + }, + "QMTARefresherQMTARefresherLambdaDeadLetterQueueC938ED3A": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": "alias/aws/sqs" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/QM-TA-Refresher/QM-TA-Refresher-Lambda-Dead-Letter-Queue/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "Queue itself is dead-letter queue", + "id": "AwsSolutions-SQS3" + } + ] + } + } + }, + "QMTARefresherQMTARefresherLambdaDeadLetterQueuePolicy61A9C7A5": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Fn::GetAtt": [ + "QMTARefresherQMTARefresherLambdaDeadLetterQueueC938ED3A", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "QMTARefresherQMTARefresherLambdaDeadLetterQueueC938ED3A" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/QM-TA-Refresher/QM-TA-Refresher-Lambda-Dead-Letter-Queue/Policy/Resource" + } + }, + "QMTARefresherQMTARefresherLambdaServiceRole95E5A974": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/QM-TA-Refresher/QM-TA-Refresher-Lambda/ServiceRole/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMTARefresherQMTARefresherLambdaServiceRoleDefaultPolicyF0E3A261": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QMTARefresherQMTARefresherLambdaDeadLetterQueueC938ED3A", + "Arn" + ] + } + }, + { + "Action": "support:RefreshTrustedAdvisorCheck", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QMTARefresherQMTARefresherLambdaServiceRoleDefaultPolicyF0E3A261", + "Roles": [ + { + "Ref": "QMTARefresherQMTARefresherLambdaServiceRole95E5A974" + } + ] + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/QM-TA-Refresher/QM-TA-Refresher-Lambda/ServiceRole/DefaultPolicy/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + "id": "AwsSolutions-IAM4" + }, + { + "reason": "Actions restricted on kms key ARN. Only actions that do not support resource-level permissions have * in resource", + "id": "AwsSolutions-IAM5" + }, + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + }, + "QMTARefresherQMTARefresherLambdaEE100499": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "solutions-${AWS::Region}" + }, + "S3Key": "quota-monitor-for-aws/v6.3.0/assete062344a6a45f8d5d2900b99e0126935391d50d4577da563c08475673a012f4c.zip" + }, + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "QMTARefresherQMTARefresherLambdaDeadLetterQueueC938ED3A", + "Arn" + ] + } + }, + "Description": "SO0005 quota-monitor-for-aws - QM-TA-Refresher-Lambda", + "Environment": { + "Variables": { + "AWS_SERVICES": "AutoScaling,CloudFormation,DynamoDB,EBS,EC2,ELB,IAM,Kinesis,RDS,Route53,SES,VPC", + "LOG_LEVEL": "info", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", + "SOLUTION_ID": "SO0005" + } + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "QMUtilsLayerQMUtilsLayerLayer80D5D993" + } + ], + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "QMTARefresherQMTARefresherLambdaServiceRole95E5A974", + "Arn" + ] + }, + "Runtime": "nodejs18.x", + "Timeout": 60 + }, + "DependsOn": [ + "QMTARefresherQMTARefresherLambdaServiceRoleDefaultPolicyF0E3A261", + "QMTARefresherQMTARefresherLambdaServiceRole95E5A974" + ], + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/QM-TA-Refresher/QM-TA-Refresher-Lambda/Resource", + "aws:asset:path": "asset.e062344a6a45f8d5d2900b99e0126935391d50d4577da563c08475673a012f4c.zip", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] + } + } + }, + "QMTARefresherQMTARefresherLambdaEventInvokeConfig4EDB1B2A": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "QMTARefresherQMTARefresherLambdaEE100499" + }, + "MaximumEventAgeInSeconds": 14400, + "Qualifier": "$LATEST" + }, + "Metadata": { + "aws:cdk:path": "quota-monitor-ta-spoke-cn/QM-TA-Refresher/QM-TA-Refresher-Lambda/EventInvokeConfig/Resource", + "cdk_nag": { + "rules_to_suppress": [ + { + "reason": "GovCloud regions support only up to nodejs 16, risk is tolerable", + "id": "AwsSolutions-L1" + } + ] + } + } + } + }, + "Outputs": { + "ServiceChecks": { + "Description": "service limit checks monitored in the account", + "Value": "AutoScaling,CloudFormation,DynamoDB,EBS,EC2,ELB,IAM,Kinesis,RDS,Route53,SES,VPC" + } + } +} \ No newline at end of file diff --git a/deployment/quota-monitor-ta-spoke.template b/deployment/quota-monitor-ta-spoke.template index f24dda6..67e88cf 100644 --- a/deployment/quota-monitor-ta-spoke.template +++ b/deployment/quota-monitor-ta-spoke.template @@ -1,5 +1,5 @@ { - "Description": "(SO0005-TA) - quota-monitor-for-aws - Trusted Advisor Template. Version v6.2.11", + "Description": "(SO0005-TA) - quota-monitor-for-aws - Trusted Advisor Template. Version v6.3.0", "AWSTemplateFormatVersion": "2010-09-09", "Metadata": { "AWS::CloudFormation::Interface": { @@ -11,11 +11,22 @@ "Parameters": [ "EventBusArn" ] + }, + { + "Label": { + "default": "Refresh Configuration" + }, + "Parameters": [ + "TARefreshRate" + ] } ], "ParameterLabels": { "EventBusArn": { "default": "Arn for the EventBridge bus in the monitoring account" + }, + "TARefreshRate": { + "default": "Trusted Advisor Refresh Rate" } } } @@ -23,13 +34,16 @@ "Parameters": { "EventBusArn": { "Type": "String" - } - }, - "Mappings": { - "QuotaMonitorMap": { - "RefreshRate": { - "Default": "rate(1 day)" - } + }, + "TARefreshRate": { + "Type": "String", + "Default": "rate(12 hours)", + "AllowedValues": [ + "rate(6 hours)", + "rate(12 hours)", + "rate(1 day)" + ], + "Description": "The rate at which to refresh Trusted Advisor checks" } }, "Resources": { @@ -355,13 +369,13 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset11e8019ce0550d340c842c1a14bc8797872e01186f9f5b7ba02745ff143d145a.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/assete8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip" }, "LayerName": "QM-UtilsLayer" }, "Metadata": { "aws:cdk:path": "quota-monitor-ta-spoke/QM-UtilsLayer/QM-UtilsLayer-Layer/Resource", - "aws:asset:path": "asset.11e8019ce0550d340c842c1a14bc8797872e01186f9f5b7ba02745ff143d145a.zip", + "aws:asset:path": "asset.e8b91b89616aa81e100a9f9ce53981ad5df4ba7439cebca83d5dc68349ed3703.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Content" } @@ -371,11 +385,7 @@ "Properties": { "Description": "SO0005 quota-monitor-for-aws - QM-TA-Refresher-EventsRule", "ScheduleExpression": { - "Fn::FindInMap": [ - "QuotaMonitorMap", - "RefreshRate", - "Default" - ] + "Ref": "TARefreshRate" }, "State": "ENABLED", "Targets": [ @@ -578,7 +588,7 @@ "S3Bucket": { "Fn::Sub": "solutions-${AWS::Region}" }, - "S3Key": "quota-monitor-for-aws/v6.2.11/asset9d1357b48fce6634e0e92251da8fe109bfcae89e65bbe743c54f656994065056.zip" + "S3Key": "quota-monitor-for-aws/v6.3.0/assete062344a6a45f8d5d2900b99e0126935391d50d4577da563c08475673a012f4c.zip" }, "DeadLetterConfig": { "TargetArn": { @@ -593,8 +603,8 @@ "Variables": { "AWS_SERVICES": "AutoScaling,CloudFormation,DynamoDB,EBS,EC2,ELB,IAM,Kinesis,RDS,Route53,SES,VPC", "LOG_LEVEL": "info", - "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.2.11", - "VERSION": "v6.2.11", + "CUSTOM_SDK_USER_AGENT": "AwsSolution/SO0005/v6.3.0", + "VERSION": "v6.3.0", "SOLUTION_ID": "SO0005" } }, @@ -620,7 +630,7 @@ ], "Metadata": { "aws:cdk:path": "quota-monitor-ta-spoke/QM-TA-Refresher/QM-TA-Refresher-Lambda/Resource", - "aws:asset:path": "asset.9d1357b48fce6634e0e92251da8fe109bfcae89e65bbe743c54f656994065056.zip", + "aws:asset:path": "asset.e062344a6a45f8d5d2900b99e0126935391d50d4577da563c08475673a012f4c.zip", "aws:asset:is-bundled": false, "aws:asset:property": "Code", "cdk_nag": { @@ -630,6 +640,12 @@ "id": "AwsSolutions-L1" } ] + }, + "guard": { + "SuppressedRules": [ + "LAMBDA_INSIDE_VPC", + "LAMBDA_CONCURRENCY_CHECK" + ] } } }, @@ -676,7 +692,7 @@ "ApplicationType": "AWS-Solutions", "SolutionID": "SO0005-TA", "SolutionName": "quota-monitor-for-aws", - "SolutionVersion": "v6.2.11" + "SolutionVersion": "v6.3.0" } }, "Metadata": { @@ -689,7 +705,7 @@ "Attributes": { "solutionID": "SO0005-TA", "solutionName": "quota-monitor-for-aws", - "version": "v6.2.11", + "version": "v6.3.0", "applicationType": "AWS-Solutions" }, "Description": "Attribute group for application information", @@ -711,7 +727,7 @@ "ApplicationType": "AWS-Solutions", "SolutionID": "SO0005-TA", "SolutionName": "quota-monitor-for-aws", - "SolutionVersion": "v6.2.11" + "SolutionVersion": "v6.3.0" } }, "Metadata": { diff --git a/deployment/run-unit-tests.sh b/deployment/run-unit-tests.sh index 4ee829b..ac5892b 100755 --- a/deployment/run-unit-tests.sh +++ b/deployment/run-unit-tests.sh @@ -13,7 +13,6 @@ resource_dir="$template_dir/../source/resources" source_dir="$template_dir/../source/lambda/services" utils_dir="$template_dir/../source/lambda/utilsLayer" root_dir="$template_dir/.." -maxrc=0 # function to print headers function headline(){ diff --git a/package-lock.json b/package-lock.json index fc2a1d5..f93e9e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "quota-monitor-for-aws", - "version": "6.2.11", + "version": "6.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quota-monitor-for-aws", - "version": "6.2.11", + "version": "6.3.0", "license": "Apache-2.0", "devDependencies": { "@types/uuid": "^9.0.0", @@ -575,10 +575,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", diff --git a/package.json b/package.json index fdda3df..d96f4a0 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "quota-monitor-for-aws", - "version": "6.2.11", + "version": "6.3.0", "description": "Quota Monitor for AWS", "author": "aws-solutions", "license": "Apache-2.0", "scripts": { "lint": "./node_modules/eslint/bin/eslint.js . --ext .ts", + "precommit": "pre-commit run --all-files", "prettier-format": "./node_modules/prettier/bin-prettier.js --config .prettierrc.yml '**/*.ts' --write", "build:cwPoller": "cd source/lambda/services/cwPoller && npm run build:all", "build:deploymentManager": "cd source/lambda/services/deploymentManager && npm run build:all", diff --git a/solution-manifest.yaml b/solution-manifest.yaml index 0d2e4e2..60338fb 100644 --- a/solution-manifest.yaml +++ b/solution-manifest.yaml @@ -1,7 +1,7 @@ --- id: SO0005 name: quota-monitor-for-aws -version: v6.2.11 +version: v6.3.0 cloudformation_templates: - template: quota-monitor-hub.template main_template: true @@ -11,4 +11,3 @@ cloudformation_templates: - template: quota-monitor-prerequisite.template build_environment: build_image: 'aws/codebuild/standard:7.0' - diff --git a/source/lambda/services/cwPoller/__tests__/cw-poller.spec.ts b/source/lambda/services/cwPoller/__tests__/cw-poller.spec.ts index 5ef6195..739cfa3 100644 --- a/source/lambda/services/cwPoller/__tests__/cw-poller.spec.ts +++ b/source/lambda/services/cwPoller/__tests__/cw-poller.spec.ts @@ -11,15 +11,9 @@ import { MetricQueryIdToQuotaMap, } from "../exports"; -import { - UnsupportedEventException, - ServiceQuotasHelper, -} from "solutions-utils"; +import { UnsupportedEventException, ServiceQuotasHelper } from "solutions-utils"; import { handler } from "../index"; -import { - MetricDataQuery, - MetricDataResult -} from "@aws-sdk/client-cloudwatch"; +import { MetricDataQuery, MetricDataResult } from "@aws-sdk/client-cloudwatch"; const getMetricDataMock = jest.fn(); const queryQuotasForServiceMock = jest.fn(); @@ -103,22 +97,14 @@ const metric1: MetricDataResult = { Label: "data label", Values: [100, 81, 10], StatusCode: "Complete", - Timestamps: [ - new Date(1664386148), - new Date(1664390148), - new Date(1664396148), - ], + Timestamps: [new Date(1664386148), new Date(1664390148), new Date(1664396148)], }; const metric2: MetricDataResult = { Id: "service2_resource2_none_resource_quota2_pct_utilization", Label: "data label", Values: [100, 81, 10], StatusCode: "Complete", - Timestamps: [ - new Date(1664386148), - new Date(1664390148), - new Date(1664396148), - ], + Timestamps: [new Date(1664386148), new Date(1664390148), new Date(1664396148)], }; const metricQueryIdToQuotaMap: MetricQueryIdToQuotaMap = {}; const metricId1 = metric1?.Id ?? ""; @@ -179,9 +165,7 @@ describe("CWPoller", () => { putEventMock.mockResolvedValue({}); getAllEnabledServicesMock.mockResolvedValue(serviceCodes); - jest - .spyOn(ServiceQuotasHelper.prototype, "generateCWQuery") - .mockReturnValue([usageQuery, percentageUsageQuery]); + jest.spyOn(ServiceQuotasHelper.prototype, "generateCWQuery").mockReturnValue([usageQuery, percentageUsageQuery]); }); beforeEach(() => { @@ -218,24 +202,16 @@ describe("CWPoller", () => { }); it("should create quota utilization events", () => { - const events = createQuotaUtilizationEvents( - metric1, - metricQueryIdToQuotaMap - ); + const events = createQuotaUtilizationEvents(metric1, metricQueryIdToQuotaMap); expect(events).toEqual(utilizationEvents); }); it("should create only WARN AND ERROR quota utilization events when REPORT_OK_NOTIFICATIONS = No", () => { process.env.SQ_REPORT_OK_NOTIFICATIONS = "No"; - const events = createQuotaUtilizationEvents( - metric1, - metricQueryIdToQuotaMap - ); - - expect(events).toEqual( - utilizationEvents.filter((m) => m.status != QUOTA_STATUS.OK) - ); + const events = createQuotaUtilizationEvents(metric1, metricQueryIdToQuotaMap); + + expect(events).toEqual(utilizationEvents.filter((m) => m.status != QUOTA_STATUS.OK)); }); it("should send quota utilization events to bridge", async () => { @@ -250,6 +226,15 @@ describe("CWPoller", () => { expect(putEventMock).toHaveBeenCalled(); }); + it("should handle a scheduled event with 1-day frequency", async () => { + process.env.POLLER_FREQUENCY = "rate(1 day)"; + await handler(event); + + expect(putEventMock).toHaveBeenCalled(); + // Check if the frequency is correctly interpreted as 24 hours + expect(getMetricDataMock).toHaveBeenCalledWith(expect.any(Date), expect.any(Date), expect.anything()); + }); + it("should return if no quotas are found", async () => { queryQuotasForServiceMock.mockResolvedValue([]); await handler(event); diff --git a/source/lambda/services/cwPoller/exports.ts b/source/lambda/services/cwPoller/exports.ts index bd56b6f..cf5266b 100644 --- a/source/lambda/services/cwPoller/exports.ts +++ b/source/lambda/services/cwPoller/exports.ts @@ -24,6 +24,7 @@ export const METRIC_STATS_PERIOD = 3600; export enum FREQUENCY { "06_HOUR" = "rate(6 hours)", "12_HOUR" = "rate(12 hours)", + "24_HOUR" = "rate(1 day)", } /** @@ -57,9 +58,7 @@ interface IQuotaUtilizationEvent { * @param rate * @returns */ -function getFrequencyInHours( - rate: string = process.env.POLLER_FREQUENCY -) { +function getFrequencyInHours(rate: string = process.env.POLLER_FREQUENCY) { if (rate == FREQUENCY["06_HOUR"]) return 6; if (rate == FREQUENCY["12_HOUR"]) return 12; else return 24; // default frequency 24 hours @@ -104,10 +103,7 @@ export function generateMetricQueryIdMap(quotas: ServiceQuota[]) { const sq = new ServiceQuotasHelper(); const dict: MetricQueryIdToQuotaMap = {}; for (const quota of quotas) { - const metricQueryId = sq.generateMetricQueryId( - quota.UsageMetric, - quota.QuotaCode - ); + const metricQueryId = sq.generateMetricQueryId(quota.UsageMetric, quota.QuotaCode); dict[metricQueryId] = quota; } return dict; @@ -145,9 +141,7 @@ export async function getCWDataForQuotaUtilization(queries: MetricDataQuery[]) { allDataPoints.push(...dataPoints); } catch (error) { if (error.name === "CloudWatchServiceException") { - logger.error( - `Error occurred while getting metric data: ${error.message}` - ); + logger.error(`Error occurred while getting metric data: ${error.message}`); } else { throw error; } @@ -161,9 +155,7 @@ export async function getCWDataForQuotaUtilization(queries: MetricDataQuery[]) { * @description returns the metric query id from the result query id * @param metricData */ -function getMetricQueryIdFromMetricData( - metricData: Omit -) { +function getMetricQueryIdFromMetricData(metricData: Omit) { return (metricData.Id).split("_pct_utilization")[0]; } @@ -182,10 +174,7 @@ export function createQuotaUtilizationEvents( const items: IQuotaUtilizationEvent[] = []; - const sendOKNotifications = stringEqualsIgnoreCase( - process.env.SQ_REPORT_OK_NOTIFICATIONS, - "Yes" - ); + const sendOKNotifications = stringEqualsIgnoreCase(process.env.SQ_REPORT_OK_NOTIFICATIONS, "Yes"); utilizationValues.forEach((value, index) => { const quotaEvents: IQuotaUtilizationEvent = { status: QUOTA_STATUS.OK, @@ -207,9 +196,7 @@ export function createQuotaUtilizationEvents( quotaEvents.status = QUOTA_STATUS.OK; } quotaEvents["check-item-detail"]["Current Usage"] = "" + value + "%"; - quotaEvents["check-item-detail"].Timestamp = (( - metricData.Timestamps - ))[index]; + quotaEvents["check-item-detail"].Timestamp = (metricData.Timestamps)[index]; if (sendOKNotifications || quotaEvents.status != QUOTA_STATUS.OK) { items.push(quotaEvents); } @@ -218,6 +205,32 @@ export function createQuotaUtilizationEvents( return items; } +export function createTestQuotaUtilizationEvents(testStatus: QUOTA_STATUS) { + let usage: string; + + if (testStatus == QUOTA_STATUS.WARN) { + usage = process.env.THRESHOLD + "%"; + } else { + usage = "100%"; + } + const quotaEvents: IQuotaUtilizationEvent[] = [ + { + status: testStatus, + "check-item-detail": { + "Limit Code": "L-testquota", + "Limit Name": "QM Test Quota", + Resource: "QM test resource", + Service: "QmTestService", + Region: "qm-test-region", + "Current Usage": usage, + "Limit Amount": "100%", // max utilization is 100% + Timestamp: new Date(), + }, + }, + ]; + + return quotaEvents; +} /** * @description send events to spoke event bridge for quota utilization * @param eventBridge event bridge to receive the events diff --git a/source/lambda/services/cwPoller/index.ts b/source/lambda/services/cwPoller/index.ts index a95d1df..0cd5a4b 100644 --- a/source/lambda/services/cwPoller/index.ts +++ b/source/lambda/services/cwPoller/index.ts @@ -1,15 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - DynamoDBHelper, - LambdaTriggers, - logger, - UnsupportedEventException, -} from "solutions-utils"; +import { DynamoDBHelper, LambdaTriggers, logger, UnsupportedEventException } from "solutions-utils"; import { ServiceQuota } from "@aws-sdk/client-service-quotas"; import { createQuotaUtilizationEvents, + createTestQuotaUtilizationEvents, generateCWQueriesForAllQuotas, generateMetricQueryIdMap, getCWDataForQuotaUtilization, @@ -31,13 +27,20 @@ export const handler = async (event: any) => { message: JSON.stringify(event), }); - if (!LambdaTriggers.isScheduledEvent(event)) - throw new UnsupportedEventException("this event type is not supported"); + if (LambdaTriggers.isQMLambdaTestEvent(event)) { + const testStatus = event["test-type"]; + const utilizationEvents = createTestQuotaUtilizationEvents(testStatus); + logger.debug({ + label: `${MODULE_NAME}/handler/UtilizationEvents`, + message: JSON.stringify(utilizationEvents), + }); + await sendQuotaUtilizationEventsToBridge(process.env.SPOKE_EVENT_BUS, utilizationEvents); + return; + } + if (!LambdaTriggers.isScheduledEvent(event)) throw new UnsupportedEventException("this event type is not supported"); const ddb = new DynamoDBHelper(); - const serviceCodes = await ddb.getAllEnabledServices( - process.env.SQ_SERVICE_TABLE - ); + const serviceCodes = await ddb.getAllEnabledServices(process.env.SQ_SERVICE_TABLE); logger.debug({ label: `${MODULE_NAME}/handler/serviceCodes`, message: JSON.stringify(serviceCodes), @@ -54,30 +57,19 @@ export const handler = async (event: any) => { }; async function handleQuotasForService(service: string) { - const quotaItems = await getQuotasForService( - process.env.SQ_QUOTA_TABLE, - service - ); + const quotaItems = await getQuotasForService(process.env.SQ_QUOTA_TABLE, service); if (!quotaItems || quotaItems.length == 0) return; // no quota items found const queries = generateCWQueriesForAllQuotas(quotaItems); - const metricQueryIdToQuotaMap = generateMetricQueryIdMap( - quotaItems - ); + const metricQueryIdToQuotaMap = generateMetricQueryIdMap(quotaItems); const metrics = await getCWDataForQuotaUtilization(queries); await Promise.allSettled( metrics.map(async (metric) => { - const utilizationEvents = createQuotaUtilizationEvents( - metric, - metricQueryIdToQuotaMap - ); + const utilizationEvents = createQuotaUtilizationEvents(metric, metricQueryIdToQuotaMap); logger.debug({ label: `${MODULE_NAME}/handler/UtilizationEvents`, message: JSON.stringify(utilizationEvents), }); - await sendQuotaUtilizationEventsToBridge( - process.env.SPOKE_EVENT_BUS, - utilizationEvents - ); + await sendQuotaUtilizationEventsToBridge(process.env.SPOKE_EVENT_BUS, utilizationEvents); }) ); logger.debug(`${service} utilizationEvents sent to spoke event bridge bus`); diff --git a/source/lambda/services/cwPoller/jest.config.ts b/source/lambda/services/cwPoller/jest.config.ts index 27552e5..6f12863 100644 --- a/source/lambda/services/cwPoller/jest.config.ts +++ b/source/lambda/services/cwPoller/jest.config.ts @@ -38,12 +38,7 @@ const config: Config = { verbose: true, // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: [ - "**/*.ts", - "!**/*.spec.ts", - "!./jest.config.ts", - "!./jest.setup.ts", - ], + collectCoverageFrom: ["**/*.ts", "!**/*.spec.ts", "!./jest.config.ts", "!./jest.setup.ts"], // A list of paths to modules that run some code to configure or set up the testing environment setupFiles: ["./jest.setup.ts"], diff --git a/source/lambda/services/cwPoller/package-lock.json b/source/lambda/services/cwPoller/package-lock.json index 1e9b41f..0035c03 100644 --- a/source/lambda/services/cwPoller/package-lock.json +++ b/source/lambda/services/cwPoller/package-lock.json @@ -1,12 +1,12 @@ { "name": "cw-poller", - "version": "6.2.11", + "version": "6.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cw-poller", - "version": "6.2.11", + "version": "6.3.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -3000,10 +3000,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", diff --git a/source/lambda/services/cwPoller/package.json b/source/lambda/services/cwPoller/package.json index 6470c90..1f19225 100644 --- a/source/lambda/services/cwPoller/package.json +++ b/source/lambda/services/cwPoller/package.json @@ -1,6 +1,6 @@ { "name": "cw-poller", - "version": "6.2.11", + "version": "6.3.0", "description": "microservice to poll for utilization metrics", "author": { "name": "Amazon Web Services", diff --git a/source/lambda/services/deploymentManager/__tests__/deployment-manager.spec.ts b/source/lambda/services/deploymentManager/__tests__/deployment-manager.spec.ts index bce99b4..e631fe2 100644 --- a/source/lambda/services/deploymentManager/__tests__/deployment-manager.spec.ts +++ b/source/lambda/services/deploymentManager/__tests__/deployment-manager.spec.ts @@ -3,10 +3,7 @@ import { handler } from "../index"; import { DEPLOYMENT_MODEL } from "../lib/deployment-manager"; -import { - IParameterChangeEvent, - IncorrectConfigurationException, -} from "solutions-utils"; +import { IParameterChangeEvent, IncorrectConfigurationException } from "solutions-utils"; const getParameterMock = jest.fn(); const getOrganizationIdMock = jest.fn(); @@ -95,8 +92,12 @@ enum TestScenarios { Account = "Account", HybridSingleOU = "HybridSingleOU", HybridMultiOU = "HybridMultiOU", + HybridOUNOP = "HybridOUNOP", + HybridAccountNOP = "HybridAccountNOP", SingleOUInvalid = "SingleOUInvalid", + SingleOUNOP = "SingleOUNOP", AccountInvalid = "AccountInvalid", + AccountNOP = "AccountNOP", SingleOUSelectedRegions = "SingleOUSelectedRegions", MultiOUSelectedRegions = "MultiOUSelectedRegions", HybridSelectedRegions = "HybridSelectedRegions", @@ -105,25 +106,25 @@ enum TestScenarios { //populate the objects to be used for the different test scenarios const orgsMap: { [key: string]: string[] } = {}; orgsMap[TestScenarios.SingleOU] = ["o-0000000000"]; +orgsMap[TestScenarios.SingleOUNOP] = ["NOP"]; orgsMap[TestScenarios.MultiOU] = ["ou-0000-00000000", "ou-0000-00000001"]; orgsMap[TestScenarios.HybridSingleOU] = ["o-0000000000"]; orgsMap[TestScenarios.HybridMultiOU] = ["ou-0000-00000000", "ou-0000-00000001"]; -orgsMap[TestScenarios.SingleOUInvalid] = ["NOP"]; +orgsMap[TestScenarios.HybridOUNOP] = ["NOP"]; +orgsMap[TestScenarios.HybridAccountNOP] = ["o-0000000000"]; +orgsMap[TestScenarios.SingleOUInvalid] = ["x-0000"]; orgsMap[TestScenarios.SingleOUSelectedRegions] = ["o-0000000000"]; -orgsMap[TestScenarios.MultiOUSelectedRegions] = [ - "ou-0000-00000000", - "ou-0000-00000001", -]; -orgsMap[TestScenarios.HybridSelectedRegions] = [ - "ou-0000-00000000", - "ou-0000-00000001", -]; +orgsMap[TestScenarios.MultiOUSelectedRegions] = ["ou-0000-00000000", "ou-0000-00000001"]; +orgsMap[TestScenarios.HybridSelectedRegions] = ["ou-0000-00000000", "ou-0000-00000001"]; const accountsMap: { [key: string]: string[] } = {}; accountsMap[TestScenarios.Account] = ["000000000000"]; accountsMap[TestScenarios.HybridSingleOU] = ["000000000000"]; accountsMap[TestScenarios.HybridMultiOU] = ["000000000000"]; -accountsMap[TestScenarios.AccountInvalid] = ["NOP"]; +accountsMap[TestScenarios.HybridOUNOP] = ["000000000000"]; +accountsMap[TestScenarios.HybridAccountNOP] = ["NOP"]; +accountsMap[TestScenarios.AccountNOP] = ["NOP"]; +accountsMap[TestScenarios.AccountInvalid] = ["1234"]; accountsMap[TestScenarios.HybridSelectedRegions] = ["000000000000"]; const regionsMap: { [key: string]: string[] } = {}; @@ -132,6 +133,10 @@ regionsMap[TestScenarios.MultiOU] = ["ALL"]; regionsMap[TestScenarios.Account] = ["ALL"]; regionsMap[TestScenarios.HybridSingleOU] = ["ALL"]; regionsMap[TestScenarios.HybridMultiOU] = ["ALL"]; +regionsMap[TestScenarios.HybridOUNOP] = ["ALL"]; +regionsMap[TestScenarios.HybridAccountNOP] = ["ALL"]; +regionsMap[TestScenarios.SingleOUNOP] = ["us-east-1", "us-west-2"]; +regionsMap[TestScenarios.AccountNOP] = ["ALL"]; regionsMap[TestScenarios.SingleOUSelectedRegions] = ["us-east-1", "us-west-2"]; regionsMap[TestScenarios.MultiOUSelectedRegions] = ["us-east-1", "us-west-2"]; regionsMap[TestScenarios.HybridSelectedRegions] = ["us-east-1", "us-west-2"]; @@ -142,12 +147,9 @@ regionsMap[TestScenarios.HybridSelectedRegions] = ["us-east-1", "us-west-2"]; */ const getParameterMockGenerator = (testType: TestScenarios) => { return (paramName: string) => { - if (paramName === "/QuotaMonitor/OUs") - return Promise.resolve(orgsMap[testType]); - else if (paramName === "/QuotaMonitor/Accounts") - return Promise.resolve(accountsMap[testType]); - else if (paramName === "/QuotaMonitor/RegionsToDeploy") - return Promise.resolve(regionsMap[testType]); + if (paramName === "/QuotaMonitor/OUs") return Promise.resolve(orgsMap[testType]); + else if (paramName === "/QuotaMonitor/Accounts") return Promise.resolve(accountsMap[testType]); + else if (paramName === "/QuotaMonitor/RegionsToDeploy") return Promise.resolve(regionsMap[testType]); else return Promise.reject("Error"); }; }; @@ -201,21 +203,14 @@ describe("Deployment Manager", () => { process.env.REGIONS_CONCURRENCY_TYPE = testConcurrncyType; process.env.MAX_CONCURRENT_PERCENTAGE = "" + testMaxConcurrentPercentage; - process.env.FAILURE_TOLERANCE_PERCENTAGE = - "" + testFailureTolerancePercentage; + process.env.FAILURE_TOLERANCE_PERCENTAGE = "" + testFailureTolerancePercentage; process.env.SQ_NOTIFICATION_THRESHOLD = testSQNotificationThreshold; process.env.SQ_MONITORING_FREQUENCY = testSQMonitoringFequency; process.env.SQ_REPORT_OK_NOTIFICATIONS = testSQReportOKNotifications; }); function assertCreateStackInstancesCallOrgIdMode() { - expect(createStackSetInstancesMock).toHaveBeenNthCalledWith( - 1, - ["r-0000"], - [], - testStackSetOpsPrefs, - undefined - ); + expect(createStackSetInstancesMock).toHaveBeenNthCalledWith(1, ["r-0000"], [], testStackSetOpsPrefs, undefined); expect(createStackSetInstancesMock).toHaveBeenLastCalledWith( ["r-0000"], ["us-east-2"], @@ -225,13 +220,7 @@ describe("Deployment Manager", () => { } function assertCreateStackInstancesCallOrgIdModeSelectedRegions() { - expect(createStackSetInstancesMock).toHaveBeenNthCalledWith( - 1, - ["r-0000"], - [], - testStackSetOpsPrefs, - undefined - ); + expect(createStackSetInstancesMock).toHaveBeenNthCalledWith(1, ["r-0000"], [], testStackSetOpsPrefs, undefined); expect(createStackSetInstancesMock).toHaveBeenLastCalledWith( ["r-0000"], ["us-west-2"], @@ -301,9 +290,7 @@ describe("Deployment Manager", () => { } it("should manage deployments in Organization deployment mode with single Org Id", async () => { - getParameterMock.mockImplementation( - getParameterMockGenerator(TestScenarios.SingleOU) - ); + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.SingleOU)); process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.ORG; await handler(event); @@ -323,9 +310,7 @@ describe("Deployment Manager", () => { }); it("should manage deployments in Organization deployment mode with single Org Id, with SEND_METRICS yes", async () => { - getParameterMock.mockImplementation( - getParameterMockGenerator(TestScenarios.SingleOU) - ); + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.SingleOU)); process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.ORG; process.env.SEND_METRIC = "Yes"; await handler(event); @@ -346,9 +331,7 @@ describe("Deployment Manager", () => { }); it("should manage deployments in Organization deployment mode with single Org Id with selected regions", async () => { - getParameterMock.mockImplementation( - getParameterMockGenerator(TestScenarios.SingleOUSelectedRegions) - ); + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.SingleOUSelectedRegions)); process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.ORG; await handler(event); @@ -368,9 +351,7 @@ describe("Deployment Manager", () => { }); it("should manage deployments in Organization deployment mode with single Org Id with TA not available", async () => { - getParameterMock.mockImplementation( - getParameterMockGenerator(TestScenarios.SingleOU) - ); + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.SingleOU)); isTAAvailableMock.mockResolvedValue(false); process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.ORG; await handler(event); @@ -396,9 +377,7 @@ describe("Deployment Manager", () => { }); it("should manage deployments in Organization deployment mode with multiple OU-Ids", async () => { - getParameterMock.mockImplementation( - getParameterMockGenerator(TestScenarios.MultiOU) - ); + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.MultiOU)); process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.ORG; await handler(event); @@ -418,9 +397,7 @@ describe("Deployment Manager", () => { }); it("should manage deployments in Organization deployment mode with multiple OU-Ids, with SEND_METRICS Yes", async () => { - getParameterMock.mockImplementation( - getParameterMockGenerator(TestScenarios.MultiOU) - ); + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.MultiOU)); process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.ORG; process.env.SEND_METRIC = "Yes"; await handler(event); @@ -441,9 +418,7 @@ describe("Deployment Manager", () => { }); it("should manage deployments in Organization deployment mode with multiple OU-Ids with selected regions", async () => { - getParameterMock.mockImplementation( - getParameterMockGenerator(TestScenarios.MultiOUSelectedRegions) - ); + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.MultiOUSelectedRegions)); process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.ORG; await handler(event); @@ -463,9 +438,7 @@ describe("Deployment Manager", () => { }); it("should manage deployments in Organization deployment mode with multiple OU-Ids with TA not available", async () => { - getParameterMock.mockImplementation( - getParameterMockGenerator(TestScenarios.MultiOU) - ); + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.MultiOU)); isTAAvailableMock.mockResolvedValue(false); process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.ORG; await handler(event); @@ -498,9 +471,7 @@ describe("Deployment Manager", () => { }); it("should manage deployments in Account deployment mode", async () => { - getParameterMock.mockImplementation( - getParameterMockGenerator(TestScenarios.Account) - ); + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.Account)); process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.ACCOUNT; await handler(event); @@ -519,9 +490,7 @@ describe("Deployment Manager", () => { }); it("should manage deployments in Hybrid single OU mode", async () => { - getParameterMock.mockImplementation( - getParameterMockGenerator(TestScenarios.HybridSingleOU) - ); + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.HybridSingleOU)); process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.HYBRID; await handler(event); @@ -540,9 +509,7 @@ describe("Deployment Manager", () => { }); it("should manage deployments in Hybrid multi OU mode", async () => { - getParameterMock.mockImplementation( - getParameterMockGenerator(TestScenarios.HybridMultiOU) - ); + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.HybridMultiOU)); process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.HYBRID; await handler(event); @@ -561,9 +528,7 @@ describe("Deployment Manager", () => { }); it("should manage deployments in Hybrid multi OU mode with selected regions", async () => { - getParameterMock.mockImplementation( - getParameterMockGenerator(TestScenarios.HybridSelectedRegions) - ); + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.HybridSelectedRegions)); process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.HYBRID; await handler(event); @@ -582,13 +547,8 @@ describe("Deployment Manager", () => { }); it("should delete unused stacksets when updating", async () => { - getParameterMock.mockImplementation( - getParameterMockGenerator(TestScenarios.MultiOU) - ); - getDeploymentTargetsMock.mockResolvedValue([ - "ou-0000-00000002", - "ou-0000-00000003", - ]); + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.MultiOU)); + getDeploymentTargetsMock.mockResolvedValue(["ou-0000-00000002", "ou-0000-00000003"]); process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.ORG; await handler(event); @@ -607,9 +567,7 @@ describe("Deployment Manager", () => { }); it("should throw an exception when the org id is malformed", async () => { - getParameterMock.mockImplementation( - getParameterMockGenerator(TestScenarios.SingleOUInvalid) - ); + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.SingleOUInvalid)); process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.ORG; const testCase = async () => { @@ -620,9 +578,7 @@ describe("Deployment Manager", () => { }); it("should throw an exception when the account id is malformed", async () => { - getParameterMock.mockImplementation( - getParameterMockGenerator(TestScenarios.AccountInvalid) - ); + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.AccountInvalid)); process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.ACCOUNT; const testCase = async () => { @@ -633,14 +589,9 @@ describe("Deployment Manager", () => { }); it("should throw an exception when trying to install in a partition where Trusted Advisor isn't available", async () => { - getParameterMock.mockImplementation( - getParameterMockGenerator(TestScenarios.SingleOU) - ); + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.SingleOU)); process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.ORG; - getEnabledRegionNamesMock.mockResolvedValue([ - "us-iso-east-1", - "us-iso-west-1", - ]); + getEnabledRegionNamesMock.mockResolvedValue(["us-iso-east-1", "us-iso-west-1"]); const testCase = async () => { await handler(event); @@ -648,4 +599,95 @@ describe("Deployment Manager", () => { await expect(testCase).rejects.toThrow(IncorrectConfigurationException); }); + + it("should skip StackSet operations when deploymentTargets is empty", async () => { + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.HybridOUNOP)); + process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.HYBRID; + process.env.SEND_METRIC = "Yes"; + await handler(event); + + expect(getParameterMock).toHaveBeenCalledTimes(3); + expect(getOrganizationIdMock).toHaveBeenCalledTimes(1); + expect(getRootIdMock).toHaveBeenCalledTimes(0); + expect(createEventBusPolicyMock).toHaveBeenCalledTimes(1); + expect(getEnabledRegionNamesMock).toHaveBeenCalledTimes(0); + expect(createStackSetInstancesMock).toHaveBeenCalledTimes(0); + expect(deleteStackSetInstancesMock).toHaveBeenCalledTimes(0); + expect(getDeploymentTargetsMock).toHaveBeenCalledTimes(3); + expect(getDeployedRegionsMock).toHaveBeenCalledTimes(0); + expect(getNumberOfAccountsInOrgMock).toHaveBeenCalledTimes(0); + expect(getNumberOfAccountsInOUMock).toHaveBeenCalledTimes(0); + expect(sendAnonymizedMetricMock).toHaveBeenCalledTimes(1); + }); + + it("should handle 'NOP' in Organization deployment mode", async () => { + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.SingleOUNOP)); + process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.ORG; + process.env.SEND_METRIC = "Yes"; + await handler(event); + + expect(getParameterMock).toHaveBeenCalledTimes(2); + expect(getOrganizationIdMock).toHaveBeenCalledTimes(1); + expect(getRootIdMock).toHaveBeenCalledTimes(0); + expect(createEventBusPolicyMock).toHaveBeenCalledTimes(1); + expect(getEnabledRegionNamesMock).toHaveBeenCalledTimes(0); + expect(createStackSetInstancesMock).toHaveBeenCalledTimes(0); + expect(deleteStackSetInstancesMock).toHaveBeenCalledTimes(0); + expect(getDeploymentTargetsMock).toHaveBeenCalledTimes(3); + expect(getDeployedRegionsMock).toHaveBeenCalledTimes(0); + expect(sendAnonymizedMetricMock).toHaveBeenCalledTimes(1); + }); + + it("should handle 'NOP' in Account deployment mode", async () => { + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.AccountNOP)); + process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.ACCOUNT; + process.env.SEND_METRIC = "Yes"; + await handler(event); + + expect(getParameterMock).toHaveBeenCalledTimes(1); + expect(getOrganizationIdMock).toHaveBeenCalledTimes(0); + expect(createEventBusPolicyMock).toHaveBeenCalledTimes(1); + expect(getEnabledRegionNamesMock).toHaveBeenCalledTimes(0); + expect(createStackSetInstancesMock).toHaveBeenCalledTimes(0); + expect(deleteStackSetInstancesMock).toHaveBeenCalledTimes(0); + expect(getDeploymentTargetsMock).toHaveBeenCalledTimes(0); + expect(getDeployedRegionsMock).toHaveBeenCalledTimes(0); + expect(sendAnonymizedMetricMock).toHaveBeenCalledTimes(0); + }); + + it("should delete all stack instances when OU is reset to 'NOP'", async () => { + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.SingleOUNOP)); + getDeploymentTargetsMock.mockResolvedValue(["ou-0000-00000000"]); + process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.ORG; + process.env.SEND_METRIC = "Yes"; + await handler(event); + + expect(getParameterMock).toHaveBeenCalledTimes(2); + expect(getOrganizationIdMock).toHaveBeenCalledTimes(1); + expect(createEventBusPolicyMock).toHaveBeenCalledTimes(1); + expect(getEnabledRegionNamesMock).toHaveBeenCalledTimes(0); + expect(createStackSetInstancesMock).toHaveBeenCalledTimes(0); + expect(deleteStackSetInstancesMock).toHaveBeenCalledTimes(3); + expect(getDeploymentTargetsMock).toHaveBeenCalledTimes(6); + expect(getDeployedRegionsMock).toHaveBeenCalledTimes(3); + expect(sendAnonymizedMetricMock).toHaveBeenCalledTimes(1); + }); + + it("should handle when Trusted Advisor is not available and OU is reset to 'NOP'", async () => { + getParameterMock.mockImplementation(getParameterMockGenerator(TestScenarios.HybridOUNOP)); + getDeploymentTargetsMock.mockResolvedValue(["ou-0000-00000000"]); + isTAAvailableMock.mockResolvedValue(false); + process.env.DEPLOYMENT_MODEL = DEPLOYMENT_MODEL.ORG; + await handler(event); + + expect(getParameterMock).toHaveBeenCalledTimes(2); + expect(getOrganizationIdMock).toHaveBeenCalledTimes(1); + expect(createEventBusPolicyMock).toHaveBeenCalledTimes(1); + expect(getEnabledRegionNamesMock).toHaveBeenCalledTimes(0); + expect(createStackSetInstancesMock).toHaveBeenCalledTimes(0); + expect(deleteStackSetInstancesMock).toHaveBeenCalledTimes(2); + expect(getDeploymentTargetsMock).toHaveBeenCalledTimes(5); + expect(getDeployedRegionsMock).toHaveBeenCalledTimes(2); + expect(sendAnonymizedMetricMock).toHaveBeenCalledTimes(0); + }); }); diff --git a/source/lambda/services/deploymentManager/jest.config.ts b/source/lambda/services/deploymentManager/jest.config.ts index 27552e5..6f12863 100644 --- a/source/lambda/services/deploymentManager/jest.config.ts +++ b/source/lambda/services/deploymentManager/jest.config.ts @@ -38,12 +38,7 @@ const config: Config = { verbose: true, // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: [ - "**/*.ts", - "!**/*.spec.ts", - "!./jest.config.ts", - "!./jest.setup.ts", - ], + collectCoverageFrom: ["**/*.ts", "!**/*.spec.ts", "!./jest.config.ts", "!./jest.setup.ts"], // A list of paths to modules that run some code to configure or set up the testing environment setupFiles: ["./jest.setup.ts"], diff --git a/source/lambda/services/deploymentManager/lib/deployment-manager.ts b/source/lambda/services/deploymentManager/lib/deployment-manager.ts index 957f6e7..6e61932 100644 --- a/source/lambda/services/deploymentManager/lib/deployment-manager.ts +++ b/source/lambda/services/deploymentManager/lib/deployment-manager.ts @@ -52,14 +52,8 @@ export class DeploymentManager { this.moduleName = __filename.split("/").pop(); this.stackSetOpsPrefs = { RegionConcurrencyType: process.env.REGIONS_CONCURRENCY_TYPE, - MaxConcurrentPercentage: parseInt( - process.env.MAX_CONCURRENT_PERCENTAGE, - 10 - ), - FailureTolerancePercentage: parseInt( - process.env.FAILURE_TOLERANCE_PERCENTAGE, - 10 - ), + MaxConcurrentPercentage: parseInt(process.env.MAX_CONCURRENT_PERCENTAGE, 10), + FailureTolerancePercentage: parseInt(process.env.FAILURE_TOLERANCE_PERCENTAGE, 10), }; this.sqParameterOverrides = [ { @@ -85,6 +79,8 @@ export class DeploymentManager { await this.manageStackSets(); } + private isOnlyNOP = (arr: string[]): boolean => arr.length === 1 && arr[0] === "NOP"; + private async getPrincipals() { const accountParameter = process.env.QM_ACCOUNT_PARAMETER; const ouParameter = process.env.QM_OU_PARAMETER; @@ -94,21 +90,34 @@ export class DeploymentManager { switch (process.env.DEPLOYMENT_MODEL) { case DEPLOYMENT_MODEL.ORG: { principals = await this.ssm.getParameter(ouParameter); - validateOrgInput(principals); + if (!this.isOnlyNOP(principals)) { + validateOrgInput(principals); + } break; } case DEPLOYMENT_MODEL.ACCOUNT: { principals = await this.ssm.getParameter(accountParameter); - validateAccountInput(principals); + if (!this.isOnlyNOP(principals)) { + validateAccountInput(principals); + } break; } case DEPLOYMENT_MODEL.HYBRID: { const org_principals = await this.ssm.getParameter(ouParameter); - validateOrgInput(org_principals); - const account_principals = await this.ssm.getParameter( - accountParameter - ); - validateAccountInput(account_principals); + const account_principals = await this.ssm.getParameter(accountParameter); + + if (this.isOnlyNOP(org_principals)) { + logger.warn("OU list contains only 'NOP' in hybrid mode. Proceeding with only account list."); + } else { + validateOrgInput(org_principals); + } + + if (this.isOnlyNOP(account_principals)) { + logger.warn("Account list contains only 'NOP' in hybrid mode. Proceeding with only OU list."); + } else { + validateAccountInput(account_principals); + } + principals = [...org_principals, ...account_principals]; break; } @@ -129,10 +138,7 @@ export class DeploymentManager { return organizationId; } - private async updatePermissions( - principals: string[], - organizationId: string - ) { + private async updatePermissions(principals: string[], organizationId: string) { await this.events.createEventBusPolicy( principals, organizationId, @@ -165,9 +171,7 @@ export class DeploymentManager { } private async getUserSelectedRegions(): Promise { - const regionsFromCfnTemplate = (process.env.REGIONS_LIST).split( - "," - ); + const regionsFromCfnTemplate = (process.env.REGIONS_LIST).split(","); const ssmParamName = process.env.QM_REGIONS_LIST_PARAMETER; const regionsFromSSMParamStore = await this.ssm.getParameter(ssmParamName); logger.debug({ @@ -182,40 +186,36 @@ export class DeploymentManager { } private async manageStackSets() { - const ouParameter = process.env.QM_OU_PARAMETER; - if ( - process.env.DEPLOYMENT_MODEL === DEPLOYMENT_MODEL.ORG || - process.env.DEPLOYMENT_MODEL === DEPLOYMENT_MODEL.HYBRID + process.env.DEPLOYMENT_MODEL !== DEPLOYMENT_MODEL.ORG && + process.env.DEPLOYMENT_MODEL !== DEPLOYMENT_MODEL.HYBRID ) { - const cfnTA = new CloudFormationHelper( - process.env.TA_STACKSET_ID - ); - const cfnSQ = new CloudFormationHelper( - process.env.SQ_STACKSET_ID - ); - const isTAAvailable = - await new SupportHelper().isTrustedAdvisorAvailable(); - const deploymentTargets = await this.ssm.getParameter(ouParameter); - const sqRegions = []; - const userSelectedRegions = await this.getUserSelectedRegions(); - const spokeDeploymentMetricData: SpokeDeploymentMetricData = {}; - if ( - userSelectedRegions.length === 0 || - arrayIncludesIgnoreCase(userSelectedRegions, "ALL") - ) { - sqRegions.push(...(await this.ec2.getEnabledRegionNames())); - spokeDeploymentMetricData.RegionsList = "ALL"; - } else { - sqRegions.push(...userSelectedRegions); - spokeDeploymentMetricData.RegionsList = userSelectedRegions.join(","); - } + return; + } + + const cfnSns = new CloudFormationHelper(process.env.SNS_STACKSET_ID); + const cfnTA = new CloudFormationHelper(process.env.TA_STACKSET_ID); + const cfnSQ = new CloudFormationHelper(process.env.SQ_STACKSET_ID); + const isTAAvailable = await new SupportHelper().isTrustedAdvisorAvailable(); + + const deploymentTargets = await this.ssm.getParameter(process.env.QM_OU_PARAMETER); + + const isOUResetToNOP = this.isOnlyNOP(deploymentTargets); + + if (isOUResetToNOP) { + await this.handleOUResetToNOP(cfnTA, cfnSQ, cfnSns, isTAAvailable); + } else { + const { sqRegions, spokeDeploymentMetricData } = await this.getRegionsForDeployment(); + logger.debug({ label: `${this.moduleName}/handler/manageStackSets`, - message: `StackSet Operation Preferences = ${JSON.stringify( - this.stackSetOpsPrefs - )}`, + message: `StackSet Operation Preferences = ${JSON.stringify(this.stackSetOpsPrefs)}`, }); + + if (process.env.SNS_SPOKE_REGION) { + const snsRegion = process.env.SNS_SPOKE_REGION; + await this.manageStackSetInstances(cfnSns, deploymentTargets, [snsRegion], undefined, []); + } if (isTAAvailable) { const taRegions = this.getTARegions(sqRegions); await this.manageStackSetInstances(cfnTA, deploymentTargets, taRegions); @@ -225,6 +225,7 @@ export class DeploymentManager { message: "Not deploying Trusted Advisor stacks", }); } + await this.manageStackSetInstances( cfnSQ, deploymentTargets, @@ -241,6 +242,71 @@ export class DeploymentManager { } } + private async getRegionsForDeployment() { + const userSelectedRegions = await this.getUserSelectedRegions(); + const sqRegions: string[] = []; + const spokeDeploymentMetricData: SpokeDeploymentMetricData = {}; + + if (userSelectedRegions.length === 0 || arrayIncludesIgnoreCase(userSelectedRegions, "ALL")) { + sqRegions.push(...(await this.ec2.getEnabledRegionNames())); + spokeDeploymentMetricData.RegionsList = "ALL"; + } else { + sqRegions.push(...userSelectedRegions); + spokeDeploymentMetricData.RegionsList = userSelectedRegions.join(","); + } + + return { sqRegions, spokeDeploymentMetricData }; + } + + private async handleOUResetToNOP( + cfnTA: CloudFormationHelper, + cfnSQ: CloudFormationHelper, + cfnSns: CloudFormationHelper, + isTAAvailable: boolean + ) { + logger.info("OU targets set to NOP. Removing existing OU-based stack instances if any."); + const existingTAInstances = await cfnTA.getDeploymentTargets(); + const existingSQInstances = await cfnSQ.getDeploymentTargets(); + const existingSnsInstances = await cfnSns.getDeploymentTargets(); + + // Send metric even when skipping StackSet operations + if (stringEqualsIgnoreCase(process.env.SEND_METRIC, "Yes")) { + await this.sendMetric( + { + SpokeCount: 0, + SpokeDeploymentRegions: "", + }, + "Spoke Deployment Metric" + ); + } + + if (isTAAvailable && existingTAInstances.length > 0) { + await this.deleteAllStackInstances(cfnTA); + } else { + logger.info("No existing Trusted Advisor stack instances found. No deletion needed."); + } + + if (existingSQInstances.length > 0) { + await this.deleteAllStackInstances(cfnSQ); + } else { + logger.info("No existing Service Quota stack instances found. No deletion needed."); + } + + if (existingSnsInstances.length > 0) { + await this.deleteAllStackInstances(cfnSns); + } else { + logger.info("No existing SNS stack instances found. No deletion needed."); + } + } + + private async deleteAllStackInstances(stackSet: CloudFormationHelper) { + const deployedRegions = await stackSet.getDeployedRegions(); + const deployedTargets = await stackSet.getDeploymentTargets(); + if (deployedTargets.length > 0 && deployedRegions.length > 0) { + await stackSet.deleteStackSetInstances(deployedTargets, deployedRegions, this.stackSetOpsPrefs); + } + } + /** *

creates or deletes stacks in the stackset based on the difference between the deployed and desired instances * the difference is determined by the following criteria

@@ -277,25 +343,13 @@ export class DeploymentManager { label: `${this.moduleName}/handler/manageStackSetInstances ${stackSet.stackSetName}`, message: `regionsNetNew: ${JSON.stringify(regionsNetNew)}`, }); - const sendMetric = - stringEqualsIgnoreCase(process.env.SEND_METRIC, "Yes") && - spokeDeploymentMetricData; + const sendMetric = stringEqualsIgnoreCase(process.env.SEND_METRIC, "Yes") && spokeDeploymentMetricData; if (deploymentTargets[0].match(ORG_REGEX)) { const root = await this.org.getRootId(); - await stackSet.deleteStackSetInstances( - [root], - regionsToRemove, - this.stackSetOpsPrefs - ); - await stackSet.createStackSetInstances( - [root], - regionsNetNew, - this.stackSetOpsPrefs, - parameterOverrides - ); + await stackSet.deleteStackSetInstances([root], regionsToRemove, this.stackSetOpsPrefs); + await stackSet.createStackSetInstances([root], regionsNetNew, this.stackSetOpsPrefs, parameterOverrides); if (sendMetric) { - spokeDeploymentMetricData.SpokeCount = - (await this.org.getNumberOfAccountsInOrg()) - 1; //minus the management account + spokeDeploymentMetricData.SpokeCount = (await this.org.getNumberOfAccountsInOrg()) - 1; //minus the management account } } else { const deployedTargets = await stackSet.getDeploymentTargets(); @@ -309,22 +363,9 @@ export class DeploymentManager { label: `${this.moduleName}/handler/manageStackSetInstances ${stackSet.stackSetName}`, message: `targetsNetNew: ${JSON.stringify(targetsNetNew)}`, }); - await stackSet.deleteStackSetInstances( - targetsToRemove, - deployedRegions, - this.stackSetOpsPrefs - ); - await stackSet.deleteStackSetInstances( - deployedTargets, - regionsToRemove, - this.stackSetOpsPrefs - ); - await stackSet.createStackSetInstances( - targetsNetNew, - regions, - this.stackSetOpsPrefs, - parameterOverrides - ); + await stackSet.deleteStackSetInstances(targetsToRemove, deployedRegions, this.stackSetOpsPrefs); + await stackSet.deleteStackSetInstances(deployedTargets, regionsToRemove, this.stackSetOpsPrefs); + await stackSet.createStackSetInstances(targetsNetNew, regions, this.stackSetOpsPrefs, parameterOverrides); await stackSet.createStackSetInstances( deploymentTargets, regionsNetNew, @@ -353,10 +394,7 @@ export class DeploymentManager { } } - private async sendMetric( - data: { [key: string]: string | number | boolean }, - message = "" - ) { + private async sendMetric(data: { [key: string]: string | number | boolean }, message = "") { const metric = { UUID: process.env.SOLUTION_UUID, Solution: process.env.SOLUTION_ID, diff --git a/source/lambda/services/deploymentManager/package-lock.json b/source/lambda/services/deploymentManager/package-lock.json index 57b022e..98549c6 100644 --- a/source/lambda/services/deploymentManager/package-lock.json +++ b/source/lambda/services/deploymentManager/package-lock.json @@ -1,12 +1,12 @@ { "name": "deployment-manager", - "version": "6.2.11", + "version": "6.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "deployment-manager", - "version": "6.2.11", + "version": "6.3.0", "hasInstallScript": true, "license": "Apache-2.0", "devDependencies": { @@ -1642,10 +1642,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", diff --git a/source/lambda/services/deploymentManager/package.json b/source/lambda/services/deploymentManager/package.json index c8e7bb5..a1ce96d 100644 --- a/source/lambda/services/deploymentManager/package.json +++ b/source/lambda/services/deploymentManager/package.json @@ -1,6 +1,6 @@ { "name": "deployment-manager", - "version": "6.2.11", + "version": "6.3.0", "description": "microservice to manage event bridge permission and stackset deployments", "main": "./index.js", "author": { diff --git a/source/lambda/services/helper/__tests__/helper.spec.ts b/source/lambda/services/helper/__tests__/helper.spec.ts index 9f5e650..0cbc585 100644 --- a/source/lambda/services/helper/__tests__/helper.spec.ts +++ b/source/lambda/services/helper/__tests__/helper.spec.ts @@ -123,9 +123,7 @@ describe("Helper", function () { }); it("should handle a failure to send a metric", async () => { - sendAnonymizedMetricMock.mockRejectedValueOnce( - new Error("Failed to send metrics") - ); + sendAnonymizedMetricMock.mockRejectedValueOnce(new Error("Failed to send metrics")); const response = await handler(mockLaunchEvent, {}); expect(sendAnonymizedMetricMock).toHaveBeenCalledTimes(1); expect(response.Data.Data).toEqual("NOV"); diff --git a/source/lambda/services/helper/index.ts b/source/lambda/services/helper/index.ts index 91455b1..2f6f1da 100644 --- a/source/lambda/services/helper/index.ts +++ b/source/lambda/services/helper/index.ts @@ -1,11 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { v4 as uuidv4 } from "uuid"; -import { - logger, - sendAnonymizedMetric, - stringEqualsIgnoreCase -} from "solutions-utils"; +import { logger, sendAnonymizedMetric, stringEqualsIgnoreCase } from "solutions-utils"; /** * @description interface for cloudformation events @@ -27,10 +23,7 @@ export interface IEvent { */ const MODULE_NAME = __filename.split("/").pop(); -export const handler = async ( - event: IEvent, - context: { [key: string]: string } -) => { +export const handler = async (event: IEvent, context: { [key: string]: string }) => { logger.debug({ label: `${MODULE_NAME}/handler`, message: `received event: ${JSON.stringify(event)}`, @@ -44,10 +37,7 @@ export const handler = async ( const properties = event.ResourceProperties; // Generate UUID - if ( - event.ResourceType === "Custom::CreateUUID" && - event.RequestType === "Create" - ) { + if (event.ResourceType === "Custom::CreateUUID" && event.RequestType === "Create") { responseData = { UUID: uuidv4(), }; @@ -73,6 +63,8 @@ export const handler = async ( Stack: process.env.QM_STACK_ID, SlackNotification: process.env.QM_SLACK_NOTIFICATION, EmailNotification: process.env.QM_EMAIL_NOTIFICATION, + SagemakerMonitoring: process.env.SAGEMAKER_MONITORING, + ConnectMonitoring: process.env.CONNECT_MONITORING, }, }; try { @@ -112,9 +104,7 @@ async function sendResponse( const responseBody = { Status: responseStatus, Reason: `${JSON.stringify(responseData)}`, - PhysicalResourceId: event.PhysicalResourceId - ? event.PhysicalResourceId - : logStreamName, + PhysicalResourceId: event.PhysicalResourceId ? event.PhysicalResourceId : logStreamName, StackId: event.StackId, RequestId: event.RequestId, LogicalResourceId: event.LogicalResourceId, diff --git a/source/lambda/services/helper/jest.config.ts b/source/lambda/services/helper/jest.config.ts index 27552e5..6f12863 100644 --- a/source/lambda/services/helper/jest.config.ts +++ b/source/lambda/services/helper/jest.config.ts @@ -38,12 +38,7 @@ const config: Config = { verbose: true, // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: [ - "**/*.ts", - "!**/*.spec.ts", - "!./jest.config.ts", - "!./jest.setup.ts", - ], + collectCoverageFrom: ["**/*.ts", "!**/*.spec.ts", "!./jest.config.ts", "!./jest.setup.ts"], // A list of paths to modules that run some code to configure or set up the testing environment setupFiles: ["./jest.setup.ts"], diff --git a/source/lambda/services/helper/package-lock.json b/source/lambda/services/helper/package-lock.json index 9ad1572..be8e262 100644 --- a/source/lambda/services/helper/package-lock.json +++ b/source/lambda/services/helper/package-lock.json @@ -1,12 +1,12 @@ { "name": "quota-monitor-helper", - "version": "6.2.11", + "version": "6.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quota-monitor-helper", - "version": "6.2.11", + "version": "6.3.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -1652,10 +1652,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", diff --git a/source/lambda/services/helper/package.json b/source/lambda/services/helper/package.json index b2e101b..d2f01e4 100644 --- a/source/lambda/services/helper/package.json +++ b/source/lambda/services/helper/package.json @@ -1,6 +1,6 @@ { "name": "quota-monitor-helper", - "version": "6.2.11", + "version": "6.3.0", "description": "microservice with helper modules for quota monitor solution", "author": { "name": "Amazon Web Services", diff --git a/source/lambda/services/preReqManager/__tests__/prereqManager.spec.ts b/source/lambda/services/preReqManager/__tests__/prereqManager.spec.ts index 641387e..a6d753e 100644 --- a/source/lambda/services/preReqManager/__tests__/prereqManager.spec.ts +++ b/source/lambda/services/preReqManager/__tests__/prereqManager.spec.ts @@ -176,9 +176,7 @@ describe("PreReqManager", function () { }); it("should throw an exception if it fails to register the delegated admin due to a service failure", async () => { - registerDelegatedAdministratorMock.mockRejectedValueOnce( - new IncorrectConfigurationException("error") - ); + registerDelegatedAdministratorMock.mockRejectedValueOnce(new IncorrectConfigurationException("error")); const testCase = async () => { await preReqManager.registerDelegatedAdministrator(MASTER_ACCOUNT_ID); }; @@ -186,9 +184,7 @@ describe("PreReqManager", function () { }); it("should throw an exception if it fails to check org details due to a service failure", async () => { - getOrgDetailsMock.mockRejectedValueOnce( - new IncorrectConfigurationException("error") - ); + getOrgDetailsMock.mockRejectedValueOnce(new IncorrectConfigurationException("error")); const testCase = async () => { await preReqManager.throwIfOrgMisconfigured(); }; diff --git a/source/lambda/services/preReqManager/index.ts b/source/lambda/services/preReqManager/index.ts index aecb30b..325b95f 100644 --- a/source/lambda/services/preReqManager/index.ts +++ b/source/lambda/services/preReqManager/index.ts @@ -2,11 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { PreReqManager } from "./lib/preReqManager"; -import { - LambdaTriggers, - logger, - UnsupportedEventException, -} from "solutions-utils"; +import { LambdaTriggers, logger, UnsupportedEventException } from "solutions-utils"; const moduleName = __filename.split("/").pop(); @@ -40,9 +36,7 @@ async function handleCreateOrUpdate(properties: Record) { const preReqManager = new PreReqManager(properties.AccountId); await preReqManager.throwIfOrgMisconfigured(); await preReqManager.enableTrustedAccess(); - await preReqManager.registerDelegatedAdministrator( - properties.QMMonitoringAccountId - ); + await preReqManager.registerDelegatedAdministrator(properties.QMMonitoringAccountId); logger.info({ label: moduleName, message: `All pre-requisites validated & installed`, diff --git a/source/lambda/services/preReqManager/jest.config.ts b/source/lambda/services/preReqManager/jest.config.ts index 27552e5..6f12863 100644 --- a/source/lambda/services/preReqManager/jest.config.ts +++ b/source/lambda/services/preReqManager/jest.config.ts @@ -38,12 +38,7 @@ const config: Config = { verbose: true, // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: [ - "**/*.ts", - "!**/*.spec.ts", - "!./jest.config.ts", - "!./jest.setup.ts", - ], + collectCoverageFrom: ["**/*.ts", "!**/*.spec.ts", "!./jest.config.ts", "!./jest.setup.ts"], // A list of paths to modules that run some code to configure or set up the testing environment setupFiles: ["./jest.setup.ts"], diff --git a/source/lambda/services/preReqManager/lib/preReqManager.ts b/source/lambda/services/preReqManager/lib/preReqManager.ts index 8a6dd42..0fc2223 100644 --- a/source/lambda/services/preReqManager/lib/preReqManager.ts +++ b/source/lambda/services/preReqManager/lib/preReqManager.ts @@ -1,11 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - IncorrectConfigurationException, - logger, - OrganizationsHelper, -} from "solutions-utils"; +import { IncorrectConfigurationException, logger, OrganizationsHelper } from "solutions-utils"; /** * @description @@ -47,8 +43,7 @@ export class PreReqManager { // checking monitoring account is not same as management account if (organization && organization.MasterAccountId !== this.accountId) { - const message = - "The template must be deployed in Organization Management account"; + const message = "The template must be deployed in Organization Management account"; logger.error({ label: this.moduleName, message: message, @@ -61,9 +56,7 @@ export class PreReqManager { * @description enable trusted access for aws services */ async enableTrustedAccess() { - await this.orgHelper.enableAWSServiceAccess( - "member.org.stacksets.cloudformation.amazonaws.com" - ); + await this.orgHelper.enableAWSServiceAccess("member.org.stacksets.cloudformation.amazonaws.com"); } /** @@ -71,8 +64,7 @@ export class PreReqManager { */ async registerDelegatedAdministrator(monitortingAccountId: string) { if (this.accountId === monitortingAccountId) { - const message = - "Cannot register Management account as a delegated StackSet administrator"; + const message = "Cannot register Management account as a delegated StackSet administrator"; logger.error({ label: this.moduleName, message: message, diff --git a/source/lambda/services/preReqManager/package-lock.json b/source/lambda/services/preReqManager/package-lock.json index 009df91..6c230c0 100644 --- a/source/lambda/services/preReqManager/package-lock.json +++ b/source/lambda/services/preReqManager/package-lock.json @@ -1,12 +1,12 @@ { "name": "pre-req-manager", - "version": "6.2.11", + "version": "6.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pre-req-manager", - "version": "6.2.11", + "version": "6.3.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -3016,10 +3016,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", diff --git a/source/lambda/services/preReqManager/package.json b/source/lambda/services/preReqManager/package.json index 81cc742..25c76bc 100644 --- a/source/lambda/services/preReqManager/package.json +++ b/source/lambda/services/preReqManager/package.json @@ -1,6 +1,6 @@ { "name": "pre-req-manager", - "version": "6.2.11", + "version": "6.3.0", "description": "microservice to validate pre-requisites for using quota monitor", "main": "./index.js", "author": { @@ -34,4 +34,4 @@ "jestSonar": { "reportPath": "coverage" } -} \ No newline at end of file +} diff --git a/source/lambda/services/quotaListManager/__tests__/quota-list-manager.spec.ts b/source/lambda/services/quotaListManager/__tests__/quota-list-manager.spec.ts index bc71f0c..7c9e790 100644 --- a/source/lambda/services/quotaListManager/__tests__/quota-list-manager.spec.ts +++ b/source/lambda/services/quotaListManager/__tests__/quota-list-manager.spec.ts @@ -11,10 +11,7 @@ import { handleDynamoDBStreamEvent, } from "../exports"; -import { - IncorrectConfigurationException, - UnsupportedEventException, -} from "solutions-utils"; +import { IncorrectConfigurationException, UnsupportedEventException } from "solutions-utils"; const getItemMock = jest.fn(); const putItemMock = jest.fn(); @@ -50,7 +47,7 @@ jest.mock("solutions-utils", () => { }; }); -const serviceCodes = ["monitoring", "dynamodb", "ec2", "ecr", "firehose"]; +const serviceCodes = ["monitoring", "dynamodb", "ec2", "sagemaker", "connect"]; const quota1 = { QuotaName: "Quota 1", @@ -78,7 +75,25 @@ const insertEvent = { S: "ec2", }, Monitored: { - BOOL: "Bool", + BOOL: true, + }, + }, + }, + }, + ], +}; + +const insertEventNotMonitored = { + Records: [ + { + eventName: "INSERT", + dynamodb: { + NewImage: { + ServiceCode: { + S: "ec2", + }, + Monitored: { + BOOL: false, }, }, }, @@ -144,17 +159,31 @@ describe("Quota List Manager Exports", () => { jest.clearAllMocks(); }); - it("should should put the service monitoring status if it doesn't exist", async () => { - await putServiceMonitoringStatus(); + it("should put the service monitoring status if it doesn't exist", async () => { + await putServiceMonitoringStatus({ + serviceTable: "dbTable", + refresh: false, + sageMakerMonitoring: true, + connectMonitoring: false, + }); expect(getServiceCodesMock).toHaveBeenCalledTimes(1); expect(getItemMock).toHaveBeenCalledTimes(5); - expect(putItemMock).toHaveBeenCalledTimes(5); + expect(putItemMock).toHaveBeenCalledWith("dbTable", { + ServiceCode: "sagemaker", + Monitored: true, + }); + expect(putItemMock).toHaveBeenCalledWith("dbTable", { + ServiceCode: "connect", + Monitored: false, + }); }); it("should should not put the service monitoring status if it already exists", async () => { getItemMock.mockResolvedValueOnce({}); - await putServiceMonitoringStatus(); + await putServiceMonitoringStatus({ + serviceTable: "dbTable", + }); expect(getItemMock).toHaveBeenCalledTimes(5); expect(putItemMock).toHaveBeenCalledTimes(4); @@ -321,6 +350,12 @@ describe("Quota List Manager Exports", () => { expect(batchWriteMock).toHaveBeenCalledTimes(1); }); + it("should not add quotas for Dynamo DB Stream INSERT Event when Monitored is false", async () => { + await handleDynamoDBStreamEvent(insertEventNotMonitored); + expect(batchDeleteMock).toHaveBeenCalledTimes(0); + expect(putItemMock).toHaveBeenCalledTimes(0); + }); + it("should handle Dynamo DB Stream MODIFY Event", async () => { await handleDynamoDBStreamEvent(modifyEvent); expect(batchDeleteMock).toHaveBeenCalledTimes(1); @@ -332,6 +367,40 @@ describe("Quota List Manager Exports", () => { expect(batchDeleteMock).toHaveBeenCalledTimes(1); expect(batchWriteMock).toHaveBeenCalledTimes(0); }); + + it("should update only SageMaker monitoring status", async () => { + await putServiceMonitoringStatus({ + serviceTable: "dbTable", + refresh: false, + sageMakerMonitoring: true, + }); + + expect(putItemMock).toHaveBeenCalledWith("dbTable", { + ServiceCode: "sagemaker", + Monitored: true, + }); + expect(putItemMock).not.toHaveBeenCalledWith("dbTable", { + ServiceCode: "connect", + Monitored: expect.anything(), + }); + }); + + it("should update only Connect monitoring status", async () => { + await putServiceMonitoringStatus({ + serviceTable: "dbTable", + refresh: false, + connectMonitoring: false, + }); + + expect(putItemMock).not.toHaveBeenCalledWith("dbTable", { + ServiceCode: "sagemaker", + Monitored: expect.anything(), + }); + expect(putItemMock).toHaveBeenCalledWith("dbTable", { + ServiceCode: "connect", + Monitored: false, + }); + }); }); describe("Handler", () => { @@ -343,11 +412,22 @@ describe("Handler", () => { const event = { RequestType: "Create", ResourceType: "", + ResourceProperties: { + SageMakerMonitoring: "Yes", + ConnectMonitoring: "No", + }, }; await handler(event); expect(getItemMock).toHaveBeenCalledTimes(5); - expect(putItemMock).toHaveBeenCalledTimes(0); + expect(putItemMock).toHaveBeenCalledWith("dbTable", { + ServiceCode: "sagemaker", + Monitored: true, + }); + expect(putItemMock).toHaveBeenCalledWith("dbTable", { + ServiceCode: "connect", + Monitored: false, + }); }); it("should handle an update event on a clean slate", async () => { @@ -379,6 +459,32 @@ describe("Handler", () => { expect(putItemMock).toHaveBeenCalledTimes(0); }); + it("should handle an Update event with changes to SageMaker and Connect", async () => { + const event = { + RequestType: "Update", + ResourceType: "", + OldResourceProperties: { + SageMakerMonitoring: "Yes", + ConnectMonitoring: "No", + }, + ResourceProperties: { + SageMakerMonitoring: "No", + ConnectMonitoring: "Yes", + }, + }; + + await handler(event); + expect(getItemMock).toHaveBeenCalledTimes(5); + expect(putItemMock).toHaveBeenCalledWith("dbTable", { + ServiceCode: "sagemaker", + Monitored: false, + }); + expect(putItemMock).toHaveBeenCalledWith("dbTable", { + ServiceCode: "connect", + Monitored: true, + }); + }); + it("should handle a dynamo db stream event", async () => { await handler(insertEvent); expect(batchDeleteMock).toHaveBeenCalledTimes(0); @@ -436,4 +542,60 @@ describe("Handler", () => { await expect(testCase()).rejects.toThrow(UnsupportedEventException); }); + + it("should preserve existing values for SageMaker and Connect during refresh", async () => { + getServiceCodesMock.mockResolvedValue(["sagemaker", "connect"]); + getItemMock.mockResolvedValueOnce({ ServiceCode: "sagemaker", Monitored: true }); + getItemMock.mockResolvedValueOnce({ ServiceCode: "connect", Monitored: false }); + + await putServiceMonitoringStatus({ + serviceTable: "dbTable", + refresh: true, + }); + // Check that SageMaker is toggled off and then on + expect(putItemMock).toHaveBeenCalledWith("dbTable", { + ServiceCode: "sagemaker", + Monitored: false, + }); + expect(putItemMock).toHaveBeenCalledWith("dbTable", { + ServiceCode: "sagemaker", + Monitored: true, + }); + + // Connect should not be toggled because it was initially false + expect(putItemMock).not.toHaveBeenCalledWith("dbTable", { + ServiceCode: "connect", + Monitored: true, + }); + + // Check the order and number of calls + expect(putItemMock.mock.calls).toEqual([ + ["dbTable", { ServiceCode: "sagemaker", Monitored: false }], + ["dbTable", { ServiceCode: "sagemaker", Monitored: true }], + ]); + + expect(putItemMock).toHaveBeenCalledTimes(2); + }); + + it("should update SageMaker and Connect when values are provided", async () => { + getServiceCodesMock.mockResolvedValue(["sagemaker", "connect"]); + getItemMock.mockResolvedValueOnce({ ServiceCode: "sagemaker", Monitored: true }); + getItemMock.mockResolvedValueOnce({ ServiceCode: "connect", Monitored: false }); + + await putServiceMonitoringStatus({ + serviceTable: "dbTable", + refresh: false, + sageMakerMonitoring: false, + connectMonitoring: true, + }); + + expect(putItemMock).toHaveBeenCalledWith("dbTable", { + ServiceCode: "sagemaker", + Monitored: false, + }); + expect(putItemMock).toHaveBeenCalledWith("dbTable", { + ServiceCode: "connect", + Monitored: true, + }); + }); }); diff --git a/source/lambda/services/quotaListManager/exports.ts b/source/lambda/services/quotaListManager/exports.ts index 02b6019..4fdc06e 100644 --- a/source/lambda/services/quotaListManager/exports.ts +++ b/source/lambda/services/quotaListManager/exports.ts @@ -25,15 +25,31 @@ interface IServiceTableItem extends Record { Monitored: boolean; } +/** + * @description Interface for the properties of the putServiceMonitoringStatus function + * @property {string} [serviceTable] - DynamoDB table name for service table + * @property {boolean} [refresh] - If true, forces re-populating of the quotas for services + * @property {boolean} [sageMakerMonitoring] - Optional. Specifies the monitoring status for SageMaker + * @property {boolean} [connectMonitoring] - Optional. Specifies the monitoring status for Connect + */ +interface PutServiceMonitoringStatusProps { + serviceTable?: string; + refresh?: boolean; + sageMakerMonitoring?: boolean; + connectMonitoring?: boolean; +} + /** * @description performs put on service table, updates monitoring status - * @param {string} serviceTable - dynamodb table name for service table - * @param {boolean} refresh - if true forces re-populating of the quotas for services + * @param {PutServiceMonitoringStatusProps} props - The properties for the function */ -export async function putServiceMonitoringStatus( - serviceTable: string = process.env.SQ_SERVICE_TABLE, - refresh = false -) { +export async function putServiceMonitoringStatus(props: PutServiceMonitoringStatusProps) { + const { + serviceTable = process.env.SQ_SERVICE_TABLE, + refresh = false, + sageMakerMonitoring, + connectMonitoring, + } = props; const ddb = new DynamoDBHelper(); const sq = new ServiceQuotasHelper(); const serviceCodes: string[] = await sq.getServiceCodes(); @@ -41,18 +57,20 @@ export async function putServiceMonitoringStatus( const disabledServices: string[] = []; const newServices: string[] = []; + logger.debug({ + label: `${MODULE_NAME}/putServiceMonitoringStatus`, + message: `Starting with refresh=${refresh}, sageMakerMonitoring=${sageMakerMonitoring}, connectMonitoring=${connectMonitoring}`, + }); + logger.debug({ label: `${MODULE_NAME}/serviceCodes`, message: JSON.stringify(serviceCodes), }); await Promise.allSettled( serviceCodes.map(async (service) => { - const getItemResponse = await ddb.getItem( - serviceTable, - { - ServiceCode: service, - } - ); + const getItemResponse = await ddb.getItem(serviceTable, { + ServiceCode: service, + }); if (!getItemResponse) newServices.push(service); else if (getItemResponse.Monitored) monitoredServices.push(service); else disabledServices.push(service); @@ -77,9 +95,19 @@ export async function putServiceMonitoringStatus( }); await Promise.allSettled( newServices.map(async (service) => { + logger.debug({ + label: `${MODULE_NAME}/putServiceMonitoringStatus`, + message: `Adding new services`, + }); + let monitoringStatus = true; + if (service === "sagemaker" && sageMakerMonitoring !== undefined) { + monitoringStatus = sageMakerMonitoring; + } else if (service === "connect" && connectMonitoring !== undefined) { + monitoringStatus = connectMonitoring; + } await ddb.putItem(serviceTable, { ServiceCode: service, - Monitored: true, + Monitored: monitoringStatus, }); }) ); @@ -110,27 +138,43 @@ export async function putServiceMonitoringStatus( }) ); } + // Update Monitoring status for SageMaker and Connect services + if (sageMakerMonitoring !== undefined) { + await ddb.putItem(serviceTable, { + ServiceCode: "sagemaker", + Monitored: sageMakerMonitoring, + }); + logger.info({ + label: `${MODULE_NAME}/putServiceMonitoringStatus`, + message: `Updated SageMaker monitoring status: ${sageMakerMonitoring}`, + }); + } + if (connectMonitoring !== undefined) { + await ddb.putItem(serviceTable, { + ServiceCode: "connect", + Monitored: connectMonitoring, + }); + logger.info({ + label: `${MODULE_NAME}/putServiceMonitoringStatus`, + message: `Updated Connect monitoring status: ${connectMonitoring}`, + }); + } } /** * @description performs get on service table to retrieve monitoring status * @param {string} serviceTable - dynamodb table name for service table */ -export async function getServiceMonitoringStatus( - serviceTable: string = process.env.SQ_SERVICE_TABLE -) { +export async function getServiceMonitoringStatus(serviceTable: string = process.env.SQ_SERVICE_TABLE) { const ddb = new DynamoDBHelper(); const statusItems: IServiceTableItem[] = []; const sq = new ServiceQuotasHelper(); const serviceCodes = await sq.getServiceCodes(); await Promise.allSettled( serviceCodes.map(async (service) => { - const getItemResponse = await ddb.getItem( - serviceTable, - { - ServiceCode: service, - } - ); + const getItemResponse = await ddb.getItem(serviceTable, { + ServiceCode: service, + }); if (getItemResponse) statusItems.push(getItemResponse); }) ); @@ -144,32 +188,25 @@ export async function getServiceMonitoringStatus( */ export function readDynamoDBStreamEvent(event: Record) { if (LambdaTriggers.isDynamoDBStreamEvent(event) && event.Records.length > 1) - throw new IncorrectConfigurationException( - "batch size more than 1 not supported" - ); + throw new IncorrectConfigurationException("batch size more than 1 not supported"); const streamRecord = event.Records[0]; // service monitoring turned ON if ( streamRecord.eventName == "INSERT" && streamRecord.dynamodb?.NewImage?.ServiceCode?.S && - streamRecord.dynamodb?.NewImage?.Monitored?.BOOL + "BOOL" in streamRecord.dynamodb?.NewImage?.Monitored ) return "INSERT"; // service monitoring toggled if ( streamRecord.eventName == "MODIFY" && - streamRecord.dynamodb?.NewImage?.Monitored?.BOOL != - streamRecord.dynamodb?.OldImage?.Monitored?.BOOL + streamRecord.dynamodb?.NewImage?.Monitored?.BOOL != streamRecord.dynamodb?.OldImage?.Monitored?.BOOL ) return "MODIFY"; // service monitoring turned OFF - if ( - streamRecord.eventName == "REMOVE" && - streamRecord.dynamodb?.OldImage?.ServiceCode?.S - ) + if (streamRecord.eventName == "REMOVE" && streamRecord.dynamodb?.OldImage?.ServiceCode?.S) return "REMOVE"; - else - throw new IncorrectConfigurationException("incorrect stream record format"); + else throw new IncorrectConfigurationException("incorrect stream record format"); } /** @@ -189,10 +226,7 @@ export async function putQuotasForService(serviceCode: string) { async function _getQuotasWithUtilizationMetrics(serviceCode: string) { const sq = new ServiceQuotasHelper(); const quotas = (await sq.getQuotaList(serviceCode)) || []; - const quotasWithMetric = await sq.getQuotasWithUtilizationMetrics( - quotas, - serviceCode - ); + const quotasWithMetric = await sq.getQuotasWithUtilizationMetrics(quotas, serviceCode); return quotasWithMetric; } @@ -223,9 +257,7 @@ async function _putMonitoredQuotas(quotas: ServiceQuota[], table: string) { } else { logger.warn({ label: `${MODULE_NAME}/_putMonitoredQuotas`, - message: `Some items were not processed: ${JSON.stringify( - result.UnprocessedItems - )}`, + message: `Some items were not processed: ${JSON.stringify(result.UnprocessedItems)}`, }); } } catch (error) { @@ -243,13 +275,8 @@ async function _putMonitoredQuotas(quotas: ServiceQuota[], table: string) { */ export async function deleteQuotasForService(serviceCode: string) { const ddb = new DynamoDBHelper(); - const quotaItems = await ddb.queryQuotasForService( - process.env.SQ_QUOTA_TABLE, - serviceCode - ); - const deleteRequestChunks = _getChunkedDeleteQuotasRequests( - quotaItems - ); + const quotaItems = await ddb.queryQuotasForService(process.env.SQ_QUOTA_TABLE, serviceCode); + const deleteRequestChunks = _getChunkedDeleteQuotasRequests(quotaItems); await Promise.allSettled( deleteRequestChunks.map(async (chunk) => { await ddb.batchDelete(process.env.SQ_QUOTA_TABLE, chunk); @@ -284,25 +311,19 @@ export async function handleDynamoDBStreamEvent(event: Record) { const _record = <_Record>event.Records[0]; switch (readDynamoDBStreamEvent(event)) { case "INSERT": { - await putQuotasForService( - _record.dynamodb?.NewImage?.ServiceCode.S - ); + if (_record.dynamodb?.NewImage?.Monitored?.BOOL === true) { + await putQuotasForService(_record.dynamodb?.NewImage?.ServiceCode.S); + } break; } case "MODIFY": { - await deleteQuotasForService( - _record.dynamodb?.NewImage?.ServiceCode.S - ); + await deleteQuotasForService(_record.dynamodb?.NewImage?.ServiceCode.S); if (_record.dynamodb?.NewImage?.Monitored?.BOOL) - await putQuotasForService( - _record.dynamodb?.NewImage?.ServiceCode.S - ); + await putQuotasForService(_record.dynamodb?.NewImage?.ServiceCode.S); break; } case "REMOVE": { - await deleteQuotasForService( - _record.dynamodb?.OldImage?.ServiceCode.S - ); + await deleteQuotasForService(_record.dynamodb?.OldImage?.ServiceCode.S); break; } } diff --git a/source/lambda/services/quotaListManager/index.ts b/source/lambda/services/quotaListManager/index.ts index 321cfd7..e44433b 100644 --- a/source/lambda/services/quotaListManager/index.ts +++ b/source/lambda/services/quotaListManager/index.ts @@ -20,22 +20,85 @@ * */ -import { - logger, - UnsupportedEventException, - LambdaTriggers, - sleep, -} from "solutions-utils"; -import { - putServiceMonitoringStatus, - handleDynamoDBStreamEvent, -} from "./exports"; +import { logger, UnsupportedEventException, LambdaTriggers, sleep } from "solutions-utils"; +import { putServiceMonitoringStatus, handleDynamoDBStreamEvent } from "./exports"; +import { CloudFormationCustomResourceEvent } from "aws-lambda"; /** * @description executing module name */ const MODULE_NAME = __filename.split("/").pop(); +type MonitoringStatus = "Yes" | "No"; + +type CustomResourceEvent = CloudFormationCustomResourceEvent & { + OldResourceProperties?: { + SageMakerMonitoring?: MonitoringStatus; + ConnectMonitoring?: MonitoringStatus; + }; +}; + +const handleCreateEvent = async (event: CustomResourceEvent) => { + const newProps = event.ResourceProperties || {}; + const sageMakerMonitoring = newProps.SageMakerMonitoring === "Yes"; + const connectMonitoring = newProps.ConnectMonitoring === "Yes"; + + logger.info({ + label: `${MODULE_NAME}/handleCreateEvent`, + message: `Start putting supported services. SageMaker: ${sageMakerMonitoring ? "Enabled" : "Disabled"}, Connect: ${ + connectMonitoring ? "Enabled" : "Disabled" + }`, + }); + + await putServiceMonitoringStatus({ + serviceTable: process.env.SQ_SERVICE_TABLE, + refresh: false, + sageMakerMonitoring: sageMakerMonitoring, + connectMonitoring: connectMonitoring, + }); +}; + +const handleUpdateEvent = async (event: CustomResourceEvent) => { + const newProps = event.ResourceProperties || {}; + const oldProps = event.OldResourceProperties || {}; + + const sageMakerMonitoring = newProps.SageMakerMonitoring === "Yes"; + const connectMonitoring = newProps.ConnectMonitoring === "Yes"; + const sageMakerChanged = oldProps.SageMakerMonitoring !== newProps.SageMakerMonitoring; + const connectChanged = oldProps.ConnectMonitoring !== newProps.ConnectMonitoring; + + await putServiceMonitoringStatus({ + serviceTable: process.env.SQ_SERVICE_TABLE, + refresh: false, + sageMakerMonitoring: sageMakerChanged ? sageMakerMonitoring : undefined, + connectMonitoring: connectChanged ? connectMonitoring : undefined, + }); +}; + +const sleepForResourceProvisioning = async () => { + // we had a bug where the quota table was sometimes not populated + // it was not populated because the stream change events from the services table weren't firing + // this happened only in this function right after the stack is deployed + // waiting on the related resources didn't work + // rather than re-architecting, this sleep is added in v6.2.0 + const delay = parseInt(process.env.RESOURCES_WAIT_TIME_SECONDS ?? "120") * 1000; + logger.info({ + label: `${MODULE_NAME}/handler`, + message: `Sleeping for ${delay / 1000} seconds to make sure all resources are provisioned`, + }); + await sleep(delay); +}; + +const handleCloudFormationEvent = async (event: any) => { + await sleepForResourceProvisioning(); + + if (event.RequestType === "Create") { + await handleCreateEvent(event); + } else if (event.RequestType === "Update") { + await handleUpdateEvent(event); + } +}; + /** * @description entry point for microservice */ @@ -46,34 +109,19 @@ export const handler = async (event: any) => { }); if (LambdaTriggers.isCfnEvent(event)) { - if (event.RequestType === "Create" || event.RequestType === "Update") { - // we had a bug where the quota table was sometimes not populated - // it was not populated because the stream change events from the services table weren't firing - // this happened only in this function right after the stack is deployed - // waiting on the related resources didn't work - // rather than re-architecting, this sleep is added in v6.2.0 - const delay = - parseInt(process.env.RESOURCES_WAIT_TIME_SECONDS ?? "120") * - 1000; - logger.info({ - label: `${MODULE_NAME}/handler`, - message: `Sleeping for ${ - delay / 1000 - } seconds to make sure all resources are provisioned`, - }); - await sleep(delay); - logger.info({ - label: `${MODULE_NAME}/handler`, - message: "Start putting supported services", - }); - await putServiceMonitoringStatus(process.env.SQ_SERVICE_TABLE); - } + await handleCloudFormationEvent(event); } else if (LambdaTriggers.isDynamoDBStreamEvent(event)) { await handleDynamoDBStreamEvent(event); } else if (LambdaTriggers.isScheduledEvent(event)) { - await putServiceMonitoringStatus( - process.env.SQ_SERVICE_TABLE, - true - ); - } else throw new UnsupportedEventException("this event type is not support"); + await putServiceMonitoringStatus({ + serviceTable: process.env.SQ_SERVICE_TABLE, + refresh: true, + }); + } else { + logger.error({ + label: `${MODULE_NAME}/handler`, + message: `Unsupported event type: ${JSON.stringify(event)}`, + }); + throw new UnsupportedEventException("this event type is not supported"); + } }; diff --git a/source/lambda/services/quotaListManager/jest.config.ts b/source/lambda/services/quotaListManager/jest.config.ts index 27552e5..6f12863 100644 --- a/source/lambda/services/quotaListManager/jest.config.ts +++ b/source/lambda/services/quotaListManager/jest.config.ts @@ -38,12 +38,7 @@ const config: Config = { verbose: true, // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: [ - "**/*.ts", - "!**/*.spec.ts", - "!./jest.config.ts", - "!./jest.setup.ts", - ], + collectCoverageFrom: ["**/*.ts", "!**/*.spec.ts", "!./jest.config.ts", "!./jest.setup.ts"], // A list of paths to modules that run some code to configure or set up the testing environment setupFiles: ["./jest.setup.ts"], diff --git a/source/lambda/services/quotaListManager/package-lock.json b/source/lambda/services/quotaListManager/package-lock.json index f12b6ba..4e13c8e 100644 --- a/source/lambda/services/quotaListManager/package-lock.json +++ b/source/lambda/services/quotaListManager/package-lock.json @@ -1,17 +1,18 @@ { "name": "quota-list-manager", - "version": "6.2.11", + "version": "6.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quota-list-manager", - "version": "6.2.11", + "version": "6.3.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/client-dynamodb-streams": "^3.621.0", - "@aws-sdk/client-service-quotas": "^3.621.0" + "@aws-sdk/client-service-quotas": "^3.621.0", + "@types/aws-lambda": "^8.10.145" }, "devDependencies": { "@types/jest": "^29.5.11", @@ -2337,6 +2338,12 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/aws-lambda": { + "version": "8.10.145", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.145.tgz", + "integrity": "sha512-dtByW6WiFk5W5Jfgz1VM+YPA21xMXTuSFoLYIDY0L44jDLLflVPtZkYuu3/YxpGcvjzKFBZLU+GyKjR0HOYtyw==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2911,10 +2918,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", diff --git a/source/lambda/services/quotaListManager/package.json b/source/lambda/services/quotaListManager/package.json index 0e97f8a..ae3b9ec 100644 --- a/source/lambda/services/quotaListManager/package.json +++ b/source/lambda/services/quotaListManager/package.json @@ -1,6 +1,6 @@ { "name": "quota-list-manager", - "version": "6.2.11", + "version": "6.3.0", "description": "microservice to manage quota list to monitor", "main": "./index.js", "author": { @@ -10,7 +10,8 @@ "license": "Apache-2.0", "dependencies": { "@aws-sdk/client-dynamodb-streams": "^3.621.0", - "@aws-sdk/client-service-quotas": "^3.621.0" + "@aws-sdk/client-service-quotas": "^3.621.0", + "@types/aws-lambda": "^8.10.145" }, "devDependencies": { "@types/jest": "^29.5.11", diff --git a/source/lambda/services/reporter/__tests__/limit-report.spec.ts b/source/lambda/services/reporter/__tests__/limit-report.spec.ts index e03369c..b51f730 100644 --- a/source/lambda/services/reporter/__tests__/limit-report.spec.ts +++ b/source/lambda/services/reporter/__tests__/limit-report.spec.ts @@ -126,6 +126,7 @@ describe("limitreport", function () { it("should delete sqs message if all APIs successful", async () => { await limitReport.readQueueAsync(); + expect(receiveMessagesMock.mock.calls[0][1]).toBe(2); expect(receiveMessagesMock).toHaveBeenCalledTimes(2); expect(deleteMessageMock).toHaveBeenCalledTimes(4); expect(putItemMock).toHaveBeenCalledTimes(4); @@ -142,9 +143,7 @@ describe("limitreport", function () { }); it("should handle some message loops returning empty arrays", async () => { - receiveMessagesMock - .mockResolvedValueOnce(data.Messages) - .mockResolvedValueOnce(emptyData); + receiveMessagesMock.mockResolvedValueOnce(data.Messages).mockResolvedValueOnce(emptyData); await limitReport.readQueueAsync(); @@ -189,5 +188,53 @@ describe("limitreport", function () { expect(putItemMock).toHaveBeenCalledTimes(0); expect(destroyMock).toHaveBeenCalledTimes(0); }); + + it("should handle MAX_MESSAGES > 10", async () => { + process.env.MAX_MESSAGES = "11"; + + await limitReport.readQueueAsync(); + + expect(receiveMessagesMock.mock.calls[0][1]).toBe(10); + expect(receiveMessagesMock).toHaveBeenCalledTimes(2); + expect(deleteMessageMock).toHaveBeenCalledTimes(4); + expect(putItemMock).toHaveBeenCalledTimes(4); + expect(destroyMock).toHaveBeenCalledTimes(2); + }); + + it("should handle MAX_MESSAGES == 0", async () => { + process.env.MAX_MESSAGES = "0"; + + await limitReport.readQueueAsync(); + + expect(receiveMessagesMock.mock.calls[0][1]).toBe(10); + expect(receiveMessagesMock).toHaveBeenCalledTimes(2); + expect(deleteMessageMock).toHaveBeenCalledTimes(4); + expect(putItemMock).toHaveBeenCalledTimes(4); + expect(destroyMock).toHaveBeenCalledTimes(2); + }); + + it("should handle MAX_MESSAGES < 0", async () => { + process.env.MAX_MESSAGES = "-1"; + + await limitReport.readQueueAsync(); + + expect(receiveMessagesMock.mock.calls[0][1]).toBe(10); + expect(receiveMessagesMock).toHaveBeenCalledTimes(2); + expect(deleteMessageMock).toHaveBeenCalledTimes(4); + expect(putItemMock).toHaveBeenCalledTimes(4); + expect(destroyMock).toHaveBeenCalledTimes(2); + }); + + it("should handle MAX_MESSAGES being undefined", async () => { + process.env.MAX_MESSAGES = undefined; + + await limitReport.readQueueAsync(); + + expect(receiveMessagesMock.mock.calls[0][1]).toBe(10); + expect(receiveMessagesMock).toHaveBeenCalledTimes(2); + expect(deleteMessageMock).toHaveBeenCalledTimes(4); + expect(putItemMock).toHaveBeenCalledTimes(4); + expect(destroyMock).toHaveBeenCalledTimes(2); + }); }); }); diff --git a/source/lambda/services/reporter/index.ts b/source/lambda/services/reporter/index.ts index 6fc4f1c..d9512f7 100644 --- a/source/lambda/services/reporter/index.ts +++ b/source/lambda/services/reporter/index.ts @@ -1,11 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - LambdaTriggers, - logger, - UnsupportedEventException, -} from "solutions-utils"; +import { LambdaTriggers, logger, UnsupportedEventException } from "solutions-utils"; import { LimitReport } from "./lib/limit-report"; const moduleName = __filename.split("/").pop(); diff --git a/source/lambda/services/reporter/jest.config.ts b/source/lambda/services/reporter/jest.config.ts index 27552e5..6f12863 100644 --- a/source/lambda/services/reporter/jest.config.ts +++ b/source/lambda/services/reporter/jest.config.ts @@ -38,12 +38,7 @@ const config: Config = { verbose: true, // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: [ - "**/*.ts", - "!**/*.spec.ts", - "!./jest.config.ts", - "!./jest.setup.ts", - ], + collectCoverageFrom: ["**/*.ts", "!**/*.spec.ts", "!./jest.config.ts", "!./jest.setup.ts"], // A list of paths to modules that run some code to configure or set up the testing environment setupFiles: ["./jest.setup.ts"], diff --git a/source/lambda/services/reporter/lib/limit-report.ts b/source/lambda/services/reporter/lib/limit-report.ts index 4a72520..da8d528 100644 --- a/source/lambda/services/reporter/lib/limit-report.ts +++ b/source/lambda/services/reporter/lib/limit-report.ts @@ -35,17 +35,14 @@ export class LimitReport { */ async processMessages() { const sqsHelper = new SQSHelper(); - const messages = await sqsHelper.receiveMessages( - process.env.SQS_URL, - parseInt(process.env.MAX_MESSAGES) ?? 10 - ); + // SQS has a limit of 10 max messages processed + let maxMessages = parseInt(process.env.MAX_MESSAGES) || 10; + maxMessages = maxMessages > 10 || maxMessages < 0 ? 10 : maxMessages; + const messages = await sqsHelper.receiveMessages(process.env.SQS_URL, maxMessages); await Promise.allSettled( messages.map(async (message) => { await this.putUsageItemOnDDB(message); - await sqsHelper.deleteMessage( - process.env.SQS_URL, - message.ReceiptHandle - ); + await sqsHelper.deleteMessage(process.env.SQS_URL, message.ReceiptHandle); }) ); logger.info({ @@ -67,17 +64,14 @@ export class LimitReport { const item = { MessageId: message.MessageId, AccountId: usageMessage.account, - TimeStamp: - usageMessage.detail["check-item-detail"]["Timestamp"] ?? - usageMessage.time, + TimeStamp: usageMessage.detail["check-item-detail"]["Timestamp"] ?? usageMessage.time, Region: usageMessage.detail["check-item-detail"]["Region"], Source: usageMessage.source, Service: usageMessage.detail["check-item-detail"]["Service"], Resource: usageMessage.detail["check-item-detail"]["Resource"] ?? "", LimitCode: usageMessage.detail["check-item-detail"]["Limit Code"] ?? "", LimitName: usageMessage.detail["check-item-detail"]["Limit Name"], - CurrentUsage: - usageMessage.detail["check-item-detail"]["Current Usage"] ?? "0", + CurrentUsage: usageMessage.detail["check-item-detail"]["Current Usage"] ?? "0", LimitAmount: usageMessage.detail["check-item-detail"]["Limit Amount"], Status: usageMessage.detail["status"], ExpiryTime: Math.floor((new Date().getTime() + 15 * 24 * 3600 * 1000) / 1000), //1️⃣5️⃣ days diff --git a/source/lambda/services/reporter/package-lock.json b/source/lambda/services/reporter/package-lock.json index 6392495..0d99877 100644 --- a/source/lambda/services/reporter/package-lock.json +++ b/source/lambda/services/reporter/package-lock.json @@ -1,12 +1,12 @@ { "name": "quota-reporter", - "version": "6.2.11", + "version": "6.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quota-reporter", - "version": "6.2.11", + "version": "6.3.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -2888,10 +2888,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", diff --git a/source/lambda/services/reporter/package.json b/source/lambda/services/reporter/package.json index 53b0567..19122e7 100644 --- a/source/lambda/services/reporter/package.json +++ b/source/lambda/services/reporter/package.json @@ -7,7 +7,7 @@ "url": "https://aws.amazon.com/solutions" }, "license": "Apache-2.0", - "version": "6.2.11", + "version": "6.3.0", "private": "true", "dependencies": { "@aws-sdk/client-sqs": "^3.621.0" diff --git a/source/lambda/services/slackNotifier/__tests__/slack-notify.spec.ts b/source/lambda/services/slackNotifier/__tests__/slack-notify.spec.ts index c496d72..831f53a 100644 --- a/source/lambda/services/slackNotifier/__tests__/slack-notify.spec.ts +++ b/source/lambda/services/slackNotifier/__tests__/slack-notify.spec.ts @@ -110,9 +110,7 @@ describe("slacknotify", function () { }); it("should return error when the call to ssm fails", async () => { - getParameterMock.mockRejectedValueOnce( - new ResourceNotFoundException("error") - ); + getParameterMock.mockRejectedValueOnce(new ResourceNotFoundException("error")); const result = await slackNotifier.sendNotification(errorEvent); expect(requestMock).toHaveBeenCalledTimes(0); @@ -132,9 +130,7 @@ describe("slacknotify", function () { statusMessage = "error"; const result = await slackNotifier.sendNotification(errorEvent); - expect(result.result).toEqual( - "Server error when processing message: 503 - error" - ); + expect(result.result).toEqual("Server error when processing message: 503 - error"); }); it("should succeed when called from handler", async () => { @@ -177,6 +173,5 @@ describe("slacknotify", function () { await handler(eventForNotification); expect(requestMock).toHaveBeenCalledTimes(1); }); - }); }); diff --git a/source/lambda/services/slackNotifier/index.ts b/source/lambda/services/slackNotifier/index.ts index a5760f2..3e27ada 100644 --- a/source/lambda/services/slackNotifier/index.ts +++ b/source/lambda/services/slackNotifier/index.ts @@ -2,11 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { SlackNotifier } from "./lib/slack-notify"; -import { - getNotificationMutingStatus, - logger, - SSMHelper, -} from "solutions-utils"; +import { getNotificationMutingStatus, logger, SSMHelper } from "solutions-utils"; export const handler = async (event: any) => { // Log a message to the console, you can view this text in the Monitoring tab in the Lambda console @@ -14,26 +10,19 @@ export const handler = async (event: any) => { logger.debug(`Received event: ${JSON.stringify(event)}`); const ssm = new SSMHelper(); - const ssmNotificationMutingConfigParamName = ( - process.env.QM_NOTIFICATION_MUTING_CONFIG_PARAMETER - ); - const mutingConfiguration: string[] = await ssm.getParameter( - ssmNotificationMutingConfigParamName - ); + const ssmNotificationMutingConfigParamName = process.env.QM_NOTIFICATION_MUTING_CONFIG_PARAMETER; + const mutingConfiguration: string[] = await ssm.getParameter(ssmNotificationMutingConfigParamName); logger.debug(`mutingConfiguration ${JSON.stringify(mutingConfiguration)}`); const service = event["detail"]["check-item-detail"]["Service"]; const limitName = event["detail"]["check-item-detail"]["Limit Name"]; const limitCode = event["detail"]["check-item-detail"]["Limit Code"]; const resource = event["detail"]["check-item-detail"]["Resource"]; - const notificationMutingStatus = getNotificationMutingStatus( - mutingConfiguration, - { - service: service, - quotaName: limitName, - quotaCode: limitCode, - resource: resource, - } - ); + const notificationMutingStatus = getNotificationMutingStatus(mutingConfiguration, { + service: service, + quotaName: limitName, + quotaCode: limitCode, + resource: resource, + }); if (!notificationMutingStatus.muted) { const slackNotifier = new SlackNotifier(); try { diff --git a/source/lambda/services/slackNotifier/jest.config.ts b/source/lambda/services/slackNotifier/jest.config.ts index 27552e5..6f12863 100644 --- a/source/lambda/services/slackNotifier/jest.config.ts +++ b/source/lambda/services/slackNotifier/jest.config.ts @@ -38,12 +38,7 @@ const config: Config = { verbose: true, // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: [ - "**/*.ts", - "!**/*.spec.ts", - "!./jest.config.ts", - "!./jest.setup.ts", - ], + collectCoverageFrom: ["**/*.ts", "!**/*.spec.ts", "!./jest.config.ts", "!./jest.setup.ts"], // A list of paths to modules that run some code to configure or set up the testing environment setupFiles: ["./jest.setup.ts"], diff --git a/source/lambda/services/slackNotifier/lib/slack-notify.ts b/source/lambda/services/slackNotifier/lib/slack-notify.ts index eacf4fe..5ffcb60 100644 --- a/source/lambda/services/slackNotifier/lib/slack-notify.ts +++ b/source/lambda/services/slackNotifier/lib/slack-notify.ts @@ -31,15 +31,10 @@ export class SlackNotifier { */ async sendNotification(event: any) { try { - const slackUrl = ( - await this.ssmHelper.getParameter(this.slackHookParameter, true) - )[0]; + const slackUrl = (await this.ssmHelper.getParameter(this.slackHookParameter, true))[0]; const slackMessage = this.slackMessageBuilder(event); - const processEventResponse = await this.processEvent( - slackUrl, - slackMessage - ); + const processEventResponse = await this.processEvent(slackUrl, slackMessage); return { result: processEventResponse, @@ -88,8 +83,7 @@ export class SlackNotifier { }, { title: "TimeStamp", - value: - event.detail["check-item-detail"]["Timestamp"] ?? event.time, + value: event.detail["check-item-detail"]["Timestamp"] ?? event.time, short: true, }, { @@ -122,14 +116,13 @@ export class SlackNotifier { fallback: "new notification from Quota Monitor for AWS", author_name: "@quota-monitor-for-aws", title: "Quota Monitor for AWS Documentation", - title_link: - "https://aws.amazon.com/solutions/implementations/quota-monitor/", + title_link: "https://aws.amazon.com/solutions/implementations/quota-monitor/", footer: "Take Action?", actions: [ { - text: "AWS Console", + text: "Request Limit Increase", type: "button", - url: "https://console.aws.amazon.com/support/home?region=us-east-1#", + url: event.quotaIncreaseLink || "https://console.aws.amazon.com/servicequotas/home", }, ], }, @@ -143,10 +136,7 @@ export class SlackNotifier { * @param {Function} callback [description] * @return {[type]} [description] */ - async postMessage( - slackUrl: string, - message: string - ): Promise { + async postMessage(slackUrl: string, message: string): Promise { const messageBody = JSON.stringify(message); const url = new URL(slackUrl); const options = { @@ -187,9 +177,7 @@ export class SlackNotifier { if (response.statusCode && response.statusCode < 400) { return "Message posted successfully"; } else if (response.statusCode && response.statusCode < 500) { - logger.warn( - `Error posting message to Slack API: ${response.statusCode} - ${response.statusMessage}` - ); + logger.warn(`Error posting message to Slack API: ${response.statusCode} - ${response.statusMessage}`); return response.statusMessage; } else { // Let Lambda retry diff --git a/source/lambda/services/slackNotifier/package-lock.json b/source/lambda/services/slackNotifier/package-lock.json index bfced5a..7582e48 100644 --- a/source/lambda/services/slackNotifier/package-lock.json +++ b/source/lambda/services/slackNotifier/package-lock.json @@ -1,12 +1,12 @@ { "name": "quota-monitor-notifier", - "version": "6.2.11", + "version": "6.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quota-monitor-notifier", - "version": "6.2.11", + "version": "6.3.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -2889,10 +2889,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", diff --git a/source/lambda/services/slackNotifier/package.json b/source/lambda/services/slackNotifier/package.json index 481cc19..291f2f9 100644 --- a/source/lambda/services/slackNotifier/package.json +++ b/source/lambda/services/slackNotifier/package.json @@ -7,7 +7,7 @@ "url": "https://aws.amazon.com/solutions" }, "license": "Apache-2.0", - "version": "6.2.11", + "version": "6.3.0", "private": "true", "dependencies": { "@aws-sdk/client-ssm": "^3.621.0", diff --git a/source/lambda/services/snsPublisher/__tests__/sns-publish.spec.ts b/source/lambda/services/snsPublisher/__tests__/sns-publish.spec.ts index 0dceada..d1e108c 100644 --- a/source/lambda/services/snsPublisher/__tests__/sns-publish.spec.ts +++ b/source/lambda/services/snsPublisher/__tests__/sns-publish.spec.ts @@ -64,6 +64,23 @@ describe("SNS publisher", function () { expect(snsPublishMock).toHaveBeenCalledTimes(1); }); + it("should call sns publisher with pretty-printed JSON", async () => { + getSSMParameterMock.mockResolvedValue(["NOP"]); + await handler(sampleEvent); + expect(snsPublishMock).toHaveBeenCalledTimes(1); + + const publishArgs = snsPublishMock.mock.calls[0]; + expect(publishArgs).toHaveLength(2); + expect(typeof publishArgs[1]).toBe("string"); + expect(publishArgs[1]).toContain('{\n "version": "0"'); + + const publishedMessage = publishArgs[1]; + expect(publishedMessage).toMatch(/^{/); + expect(publishedMessage).toMatch(/}$/); + expect(publishedMessage.split("\n").length).toBeGreaterThan(5); + expect(JSON.parse(publishedMessage)).toEqual(sampleEvent); + }); + it("should publish the message successfully if service isn't muted", async () => { getSSMParameterMock.mockResolvedValue(["NOP"]); await handler(sampleEvent); @@ -150,6 +167,4 @@ describe("SNS publisher", function () { expect(snsPublishMock).toHaveBeenCalledTimes(1); expect(getSSMParameterMock).toHaveBeenCalledTimes(1); }); - - }); diff --git a/source/lambda/services/snsPublisher/index.ts b/source/lambda/services/snsPublisher/index.ts index 9044aef..44453e1 100644 --- a/source/lambda/services/snsPublisher/index.ts +++ b/source/lambda/services/snsPublisher/index.ts @@ -12,38 +12,52 @@ import { SNSPublisher } from "./lib/sns-publish"; const moduleName = __filename.split("/").pop(); +function getQuotaIncreaseLink(event: any): string { + const region = event.detail["check-item-detail"].Region; + const service = event.detail["check-item-detail"].Service.toLowerCase(); + const quotaCode = event.detail["check-item-detail"]["Limit Code"]; + + if (quotaCode) { + if (quotaCode == "L-testquota") { + return `https://${region}.console.aws.amazon.com/servicequotas/home/services/`; + } else { + return `https://${region}.console.aws.amazon.com/servicequotas/home/services/${service}/quotas/${quotaCode}`; + } + } else { + return `https://${region}.console.aws.amazon.com/servicequotas/home/services/${service}/quotas`; + } +} + export const handler = async (event: any) => { - const eventText = JSON.stringify(event); + const eventText = JSON.stringify(event, null, 2); logger.debug(`Received event: ${eventText}`); const ssm = new SSMHelper(); - const ssmNotificationMutingConfigParamName = ( - process.env.QM_NOTIFICATION_MUTING_CONFIG_PARAMETER - ); - const mutingConfiguration: string[] = await ssm.getParameter( - ssmNotificationMutingConfigParamName - ); + const ssmNotificationMutingConfigParamName = process.env.QM_NOTIFICATION_MUTING_CONFIG_PARAMETER; + const mutingConfiguration: string[] = await ssm.getParameter(ssmNotificationMutingConfigParamName); logger.debug(`mutingConfiguration ${JSON.stringify(mutingConfiguration)}`); const service = event["detail"]["check-item-detail"]["Service"]; const limitName = event["detail"]["check-item-detail"]["Limit Name"]; const limitCode = event["detail"]["check-item-detail"]["Limit Code"]; const resource = event["detail"]["check-item-detail"]["Resource"]; - const notificationMutingStatus = getNotificationMutingStatus( - mutingConfiguration, - { - service: service, - quotaName: limitName, - quotaCode: limitCode, - resource: resource, - } - ); + const notificationMutingStatus = getNotificationMutingStatus(mutingConfiguration, { + service: service, + quotaName: limitName, + quotaCode: limitCode, + resource: resource, + }); if (!notificationMutingStatus.muted) { const snsPublisher = new SNSPublisher(); try { - await snsPublisher.publish(eventText); + const quotaIncreaseLink = getQuotaIncreaseLink(event); + event.quotaIncreaseLink = quotaIncreaseLink; + + const enrichedEventText = JSON.stringify(event, null, 2); + await snsPublisher.publish(enrichedEventText); const message = "Successfully published to topic"; logger.debug(message); if (stringEqualsIgnoreCase(process.env.SEND_METRIC, "Yes")) { - await sendMetric({ + await sendMetric( + { Region: event["detail"]["check-item-detail"]["Region"], Service: service, LimitName: limitName, @@ -66,10 +80,7 @@ export const handler = async (event: any) => { }; } - async function sendMetric( - data: { [key: string]: string | number | boolean }, - message = "" - ) { + async function sendMetric(data: { [key: string]: string | number | boolean }, message = "") { const metric = { UUID: process.env.SOLUTION_UUID, Solution: process.env.SOLUTION_ID, diff --git a/source/lambda/services/snsPublisher/jest.config.ts b/source/lambda/services/snsPublisher/jest.config.ts index 27552e5..6f12863 100644 --- a/source/lambda/services/snsPublisher/jest.config.ts +++ b/source/lambda/services/snsPublisher/jest.config.ts @@ -38,12 +38,7 @@ const config: Config = { verbose: true, // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: [ - "**/*.ts", - "!**/*.spec.ts", - "!./jest.config.ts", - "!./jest.setup.ts", - ], + collectCoverageFrom: ["**/*.ts", "!**/*.spec.ts", "!./jest.config.ts", "!./jest.setup.ts"], // A list of paths to modules that run some code to configure or set up the testing environment setupFiles: ["./jest.setup.ts"], diff --git a/source/lambda/services/snsPublisher/package-lock.json b/source/lambda/services/snsPublisher/package-lock.json index 83c33b4..42edd6c 100644 --- a/source/lambda/services/snsPublisher/package-lock.json +++ b/source/lambda/services/snsPublisher/package-lock.json @@ -1,12 +1,12 @@ { "name": "quota-monitor-sns-publisher", - "version": "6.2.11", + "version": "6.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quota-monitor-sns-publisher", - "version": "6.2.11", + "version": "6.3.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -2874,10 +2874,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", diff --git a/source/lambda/services/snsPublisher/package.json b/source/lambda/services/snsPublisher/package.json index 957d4cb..79be2eb 100644 --- a/source/lambda/services/snsPublisher/package.json +++ b/source/lambda/services/snsPublisher/package.json @@ -7,7 +7,7 @@ "url": "https://aws.amazon.com/solutions" }, "license": "Apache-2.0", - "version": "6.2.11", + "version": "6.3.0", "private": "true", "dependencies": { "@aws-sdk/client-ssm": "^3.621.0" diff --git a/source/lambda/services/taRefresher/__tests__/ta-refresh.spec.ts b/source/lambda/services/taRefresher/__tests__/ta-refresh.spec.ts index cb20b4a..e6bd5ca 100644 --- a/source/lambda/services/taRefresher/__tests__/ta-refresh.spec.ts +++ b/source/lambda/services/taRefresher/__tests__/ta-refresh.spec.ts @@ -65,17 +65,8 @@ describe("tarefresh", () => { process.env.AWS_SERVICES = "AutoScaling,CloudFormation"; handler(mockEvent); - expect(refreshTrustedAdvisorCheckMock).toHaveBeenNthCalledWith( - 1, - serviceIds[0] - ); - expect(refreshTrustedAdvisorCheckMock).toHaveBeenNthCalledWith( - 2, - serviceIds[1] - ); - expect(refreshTrustedAdvisorCheckMock).toHaveBeenNthCalledWith( - 3, - serviceIds[2] - ); + expect(refreshTrustedAdvisorCheckMock).toHaveBeenNthCalledWith(1, serviceIds[0]); + expect(refreshTrustedAdvisorCheckMock).toHaveBeenNthCalledWith(2, serviceIds[1]); + expect(refreshTrustedAdvisorCheckMock).toHaveBeenNthCalledWith(3, serviceIds[2]); }); }); diff --git a/source/lambda/services/taRefresher/index.ts b/source/lambda/services/taRefresher/index.ts index de1bd1d..b551fe4 100644 --- a/source/lambda/services/taRefresher/index.ts +++ b/source/lambda/services/taRefresher/index.ts @@ -35,9 +35,7 @@ export const handler = async (event: IEvent) => { }); //user provided services for TA refresh - const _services = (process.env.AWS_SERVICES) - .replace(/"/g, "") - .split(","); + const _services = (process.env.AWS_SERVICES).replace(/"/g, "").split(","); const taRefresh = new TAHelper(); await taRefresh.refreshChecks(_services); diff --git a/source/lambda/services/taRefresher/jest.config.ts b/source/lambda/services/taRefresher/jest.config.ts index 27552e5..6f12863 100644 --- a/source/lambda/services/taRefresher/jest.config.ts +++ b/source/lambda/services/taRefresher/jest.config.ts @@ -38,12 +38,7 @@ const config: Config = { verbose: true, // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: [ - "**/*.ts", - "!**/*.spec.ts", - "!./jest.config.ts", - "!./jest.setup.ts", - ], + collectCoverageFrom: ["**/*.ts", "!**/*.spec.ts", "!./jest.config.ts", "!./jest.setup.ts"], // A list of paths to modules that run some code to configure or set up the testing environment setupFiles: ["./jest.setup.ts"], diff --git a/source/lambda/services/taRefresher/lib/ta-helper.ts b/source/lambda/services/taRefresher/lib/ta-helper.ts index 4b5169c..1407bad 100644 --- a/source/lambda/services/taRefresher/lib/ta-helper.ts +++ b/source/lambda/services/taRefresher/lib/ta-helper.ts @@ -41,14 +41,7 @@ serviceChecks[TAServices.EBS] = [ ]; serviceChecks[TAServices.EC2] = ["0Xc6LMYG8P", "iH7PP0l7J9", "aW9HH0l8J6"]; serviceChecks[TAServices.ELB] = ["iK7OO0l7J9", "EM8b3yLRTr", "8wIqYSt25K"]; -serviceChecks[TAServices.IAM] = [ - "sU7XX0l7J9", - "nO7SS0l7J9", - "pR7UU0l7J9", - "oQ7TT0l7J9", - "rT7WW0l7J9", - "qS7VV0l7J9", -]; +serviceChecks[TAServices.IAM] = ["sU7XX0l7J9", "nO7SS0l7J9", "pR7UU0l7J9", "oQ7TT0l7J9", "rT7WW0l7J9", "qS7VV0l7J9"]; serviceChecks[TAServices.KINESIS] = ["bW7HH0l7J9"]; serviceChecks[TAServices.RDS] = [ "jtlIMO3qZM", @@ -67,13 +60,7 @@ serviceChecks[TAServices.RDS] = [ "jEhCtdJKOY", "P1jhKWEmLa", ]; -serviceChecks[TAServices.ROUTE53] = [ - "dx3xfcdfMr", - "ru4xfcdfMr", - "ty3xfcdfMr", - "dx3xfbjfMr", - "dx8afcdfMr", -]; +serviceChecks[TAServices.ROUTE53] = ["dx3xfcdfMr", "ru4xfcdfMr", "ty3xfcdfMr", "dx3xfbjfMr", "dx8afcdfMr"]; serviceChecks[TAServices.SES] = ["hJ7NN0l7J9"]; serviceChecks[TAServices.VPC] = ["lN7RR0l7J9", "kM7QQ0l7J9", "jL7PP0l7J9"]; diff --git a/source/lambda/services/taRefresher/package-lock.json b/source/lambda/services/taRefresher/package-lock.json index aa78994..91a5655 100644 --- a/source/lambda/services/taRefresher/package-lock.json +++ b/source/lambda/services/taRefresher/package-lock.json @@ -1,12 +1,12 @@ { "name": "quota-monitor-refresher", - "version": "6.2.11", + "version": "6.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quota-monitor-refresher", - "version": "6.2.11", + "version": "6.3.0", "hasInstallScript": true, "license": "Apache-2.0", "devDependencies": { @@ -1642,10 +1642,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", diff --git a/source/lambda/services/taRefresher/package.json b/source/lambda/services/taRefresher/package.json index 7110ff8..c7680cb 100644 --- a/source/lambda/services/taRefresher/package.json +++ b/source/lambda/services/taRefresher/package.json @@ -7,7 +7,7 @@ "url": "https://aws.amazon.com/solutions" }, "license": "Apache-2.0", - "version": "6.2.11", + "version": "6.3.0", "private": "true", "devDependencies": { "@types/jest": "^29.5.11", diff --git a/source/lambda/utilsLayer/__tests__/cloudformation.spec.ts b/source/lambda/utilsLayer/__tests__/cloudformation.spec.ts index c0c8494..350c310 100644 --- a/source/lambda/utilsLayer/__tests__/cloudformation.spec.ts +++ b/source/lambda/utilsLayer/__tests__/cloudformation.spec.ts @@ -12,11 +12,7 @@ import { DescribeStackSetCommand, StackSetOperationPreferences, } from "@aws-sdk/client-cloudformation"; -import { - CloudFormationHelper, - defaultOpsPercentagePrefs, - StackSetOpsPercentagePrefs -} from "../lib/cloudformation"; +import { CloudFormationHelper, defaultOpsPercentagePrefs, StackSetOpsPercentagePrefs } from "../lib/cloudformation"; import { IncorrectConfigurationException } from "../lib/error"; describe("Cloud Formation Helper", () => { @@ -188,7 +184,6 @@ describe("Cloud Formation Helper", () => { await cfHelper.createStackSetInstances(target, regions, opsPrefs); }; await expect(testCase).rejects.toThrow(IncorrectConfigurationException); - }); it("should throw an exception when createStackSetInstances fails", async () => { @@ -278,5 +273,4 @@ describe("Cloud Formation Helper", () => { await cfHelper.deleteStackSetInstances(target, regions); expect(cfMock).toHaveReceivedCommandTimes(DeleteStackInstancesCommand, 0); }); - }); diff --git a/source/lambda/utilsLayer/__tests__/cloudwatch.spec.ts b/source/lambda/utilsLayer/__tests__/cloudwatch.spec.ts index 9dba77b..98c58ff 100644 --- a/source/lambda/utilsLayer/__tests__/cloudwatch.spec.ts +++ b/source/lambda/utilsLayer/__tests__/cloudwatch.spec.ts @@ -4,10 +4,7 @@ import { mockClient } from "aws-sdk-client-mock"; import "aws-sdk-client-mock-jest"; -import { - CloudWatchClient, - CloudWatchServiceException, -} from "@aws-sdk/client-cloudwatch"; +import { CloudWatchClient, CloudWatchServiceException } from "@aws-sdk/client-cloudwatch"; import { CloudWatchHelper } from "../lib/cloudwatch"; describe("Cloud Watch Helper", () => { @@ -25,9 +22,7 @@ describe("Cloud Watch Helper", () => { it("should get metric data", async () => { cwMock.onAnyCommand().resolves({ MetricDataResults: [{}] }); - const metricDataResults = await cwHelper.getMetricData(startTime, endTime, [ - { Id: "1" }, - ]); + const metricDataResults = await cwHelper.getMetricData(startTime, endTime, [{ Id: "1" }]); expect(metricDataResults).toEqual([{}]); }); diff --git a/source/lambda/utilsLayer/__tests__/dynamodb.spec.ts b/source/lambda/utilsLayer/__tests__/dynamodb.spec.ts index 6b7d71b..06ca7b4 100644 --- a/source/lambda/utilsLayer/__tests__/dynamodb.spec.ts +++ b/source/lambda/utilsLayer/__tests__/dynamodb.spec.ts @@ -54,14 +54,9 @@ describe("Dynamo DB", () => { }); it("should query for service items", async () => { - ddbDocMock - .on(QueryCommand) - .resolves({ Items: [{ data: "data1" }, { data: "data2" }] }); + ddbDocMock.on(QueryCommand).resolves({ Items: [{ data: "data1" }, { data: "data2" }] }); - const response = await ddbHelper.queryQuotasForService( - tableName, - "dynamodb" - ); + const response = await ddbHelper.queryQuotasForService(tableName, "dynamodb"); expect(ddbDocMock).toHaveReceivedCommandTimes(QueryCommand, 1); expect(response).toEqual([{ data: "data1" }, { data: "data2" }]); @@ -173,10 +168,7 @@ describe("Dynamo DB", () => { [tableName]: [unprocessedItem], }, }); - const writeRequests = [ - { PutRequest: { Item: { id: "1", data: "test1" } } }, - unprocessedItem, - ]; + const writeRequests = [{ PutRequest: { Item: { id: "1", data: "test1" } } }, unprocessedItem]; await ddbHelper.batchWrite(tableName, writeRequests); expect(ddbDocMock).toHaveReceivedCommandTimes(BatchWriteCommand, 5); expect(sleep).toHaveBeenCalledTimes(5); diff --git a/source/lambda/utilsLayer/__tests__/ec2.spec.ts b/source/lambda/utilsLayer/__tests__/ec2.spec.ts index 6a7a00c..b7e8350 100644 --- a/source/lambda/utilsLayer/__tests__/ec2.spec.ts +++ b/source/lambda/utilsLayer/__tests__/ec2.spec.ts @@ -4,11 +4,7 @@ import { mockClient } from "aws-sdk-client-mock"; import "aws-sdk-client-mock-jest"; -import { - EC2Client, - DescribeRegionsCommand, - EC2ServiceException, -} from "@aws-sdk/client-ec2"; +import { EC2Client, DescribeRegionsCommand, EC2ServiceException } from "@aws-sdk/client-ec2"; import { EC2Helper } from "../lib/ec2"; describe("EC2 Helper", () => { diff --git a/source/lambda/utilsLayer/__tests__/events.spec.ts b/source/lambda/utilsLayer/__tests__/events.spec.ts index 819d485..67b2724 100644 --- a/source/lambda/utilsLayer/__tests__/events.spec.ts +++ b/source/lambda/utilsLayer/__tests__/events.spec.ts @@ -55,16 +55,8 @@ describe("Event Helper", () => { }), }; - await eventsHelper.createEventBusPolicy( - [principalOrg], - orgId, - eventBusArn, - eventBusName - ); - expect(eventsClient).toHaveReceivedCommandWith( - PutPermissionCommand, - expectedCommand - ); + await eventsHelper.createEventBusPolicy([principalOrg], orgId, eventBusArn, eventBusName); + expect(eventsClient).toHaveReceivedCommandWith(PutPermissionCommand, expectedCommand); }); it("should create a resource based policy for an ou", async () => { @@ -90,16 +82,8 @@ describe("Event Helper", () => { }), }; - await eventsHelper.createEventBusPolicy( - [principalOU], - orgId, - eventBusArn, - eventBusName - ); - expect(eventsClient).toHaveReceivedCommandWith( - PutPermissionCommand, - expectedCommand - ); + await eventsHelper.createEventBusPolicy([principalOU], orgId, eventBusArn, eventBusName); + expect(eventsClient).toHaveReceivedCommandWith(PutPermissionCommand, expectedCommand); }); it("should create a resource based policy for multiple ous", async () => { @@ -117,10 +101,7 @@ describe("Event Helper", () => { Resource: "arn:aws:events:us-east-1:000000000000:event-bus/MyBus", Condition: { "ForAnyValue:StringLike": { - "aws:PrincipalOrgPaths": [ - orgId + "/*/" + principalOU + "/*", - orgId + "/*/" + principalOU2 + "/*", - ], + "aws:PrincipalOrgPaths": [orgId + "/*/" + principalOU + "/*", orgId + "/*/" + principalOU2 + "/*"], }, }, }, @@ -128,16 +109,8 @@ describe("Event Helper", () => { }), }; - await eventsHelper.createEventBusPolicy( - [principalOU, principalOU2], - orgId, - eventBusArn, - eventBusName - ); - expect(eventsClient).toHaveReceivedCommandWith( - PutPermissionCommand, - expectedCommand - ); + await eventsHelper.createEventBusPolicy([principalOU, principalOU2], orgId, eventBusArn, eventBusName); + expect(eventsClient).toHaveReceivedCommandWith(PutPermissionCommand, expectedCommand); }); it("should create a resource based policy for an account", async () => { @@ -160,16 +133,8 @@ describe("Event Helper", () => { }), }; - await eventsHelper.createEventBusPolicy( - [principalAccount], - orgId, - eventBusArn, - eventBusName - ); - expect(eventsClient).toHaveReceivedCommandWith( - PutPermissionCommand, - expectedCommand - ); + await eventsHelper.createEventBusPolicy([principalAccount], orgId, eventBusArn, eventBusName); + expect(eventsClient).toHaveReceivedCommandWith(PutPermissionCommand, expectedCommand); }); it("should create a resource based policy for multiple accounts", async () => { @@ -192,16 +157,8 @@ describe("Event Helper", () => { }), }; - await eventsHelper.createEventBusPolicy( - [principalAccount, principalAccount2], - orgId, - eventBusArn, - eventBusName - ); - expect(eventsClient).toHaveReceivedCommandWith( - PutPermissionCommand, - expectedCommand - ); + await eventsHelper.createEventBusPolicy([principalAccount, principalAccount2], orgId, eventBusArn, eventBusName); + expect(eventsClient).toHaveReceivedCommandWith(PutPermissionCommand, expectedCommand); }); it("should create a resource based policy for multiple ous and accounts", async () => { @@ -219,10 +176,7 @@ describe("Event Helper", () => { Resource: "arn:aws:events:us-east-1:000000000000:event-bus/MyBus", Condition: { "ForAnyValue:StringLike": { - "aws:PrincipalOrgPaths": [ - orgId + "/*/" + principalOU + "/*", - orgId + "/*/" + principalOU2 + "/*", - ], + "aws:PrincipalOrgPaths": [orgId + "/*/" + principalOU + "/*", orgId + "/*/" + principalOU2 + "/*"], }, }, }, @@ -245,10 +199,7 @@ describe("Event Helper", () => { eventBusArn, eventBusName ); - expect(eventsClient).toHaveReceivedCommandWith( - PutPermissionCommand, - expectedCommand - ); + expect(eventsClient).toHaveReceivedCommandWith(PutPermissionCommand, expectedCommand); }); it("should create a resource based policy for org and accounts", async () => { @@ -289,10 +240,7 @@ describe("Event Helper", () => { eventBusArn, eventBusName ); - expect(eventsClient).toHaveReceivedCommandWith( - PutPermissionCommand, - expectedCommand - ); + expect(eventsClient).toHaveReceivedCommandWith(PutPermissionCommand, expectedCommand); }); it("should try to remove existing policy for empty/invalid inputs", async () => { @@ -303,17 +251,12 @@ describe("Event Helper", () => { }; await eventsHelper.createEventBusPolicy( - [principalOrg, principalAccount, principalAccount2].map( - (s) => "INVALID_" + s - ), + [principalOrg, principalAccount, principalAccount2].map((s) => "INVALID_" + s), orgId, eventBusArn, eventBusName ); - expect(eventsClient).toHaveReceivedCommandWith( - RemovePermissionCommand, - expectedRemoveCommand - ); + expect(eventsClient).toHaveReceivedCommandWith(RemovePermissionCommand, expectedRemoveCommand); }); it("should throw an exception if PutPermissionCommand fails", async () => { @@ -326,12 +269,7 @@ describe("Event Helper", () => { ); const testCase = async () => { - await eventsHelper.createEventBusPolicy( - [principalOrg], - orgId, - eventBusArn, - eventBusName - ); + await eventsHelper.createEventBusPolicy([principalOrg], orgId, eventBusArn, eventBusName); }; await expect(testCase).rejects.toThrow(CloudWatchEventsServiceException); diff --git a/source/lambda/utilsLayer/__tests__/exports.spec.ts b/source/lambda/utilsLayer/__tests__/exports.spec.ts index 0e12c84..38c587a 100644 --- a/source/lambda/utilsLayer/__tests__/exports.spec.ts +++ b/source/lambda/utilsLayer/__tests__/exports.spec.ts @@ -102,29 +102,17 @@ describe("Exports", () => { describe("array contains elements of another array", () => { it("should perform a case-insensitive contains on the array for elements of another array", () => { expect(arrayIncludesAnyIgnoreCase(["js", "ts"], ["java"])).toEqual(false); - expect(arrayIncludesAnyIgnoreCase(["js", "ts"], ["java", "ts"])).toEqual( - true - ); - expect(arrayIncludesAnyIgnoreCase(["js", "ts"], ["JS", "java"])).toEqual( - true - ); - expect( - arrayIncludesAnyIgnoreCase(["js", "ts"], ["python", "java"]) - ).toEqual(false); + expect(arrayIncludesAnyIgnoreCase(["js", "ts"], ["java", "ts"])).toEqual(true); + expect(arrayIncludesAnyIgnoreCase(["js", "ts"], ["JS", "java"])).toEqual(true); + expect(arrayIncludesAnyIgnoreCase(["js", "ts"], ["python", "java"])).toEqual(false); }); }); describe("array difference", () => { it("should perform a case-insensitive contains on the array", () => { - expect( - arrayDiff(["us-east-1", "us-west-1"], ["us-east-1", "us-west-1"]) - ).toEqual([]); - expect( - arrayDiff(["us-east-1", "us-west-1"], ["us-east-1-x", "us-west-1-s"]) - ).toEqual(["us-east-1", "us-west-1"]); - expect( - arrayDiff(["us-east-1", "us-west-1"], ["us-east-1-x", "us-west-1"]) - ).toEqual(["us-east-1"]); + expect(arrayDiff(["us-east-1", "us-west-1"], ["us-east-1", "us-west-1"])).toEqual([]); + expect(arrayDiff(["us-east-1", "us-west-1"], ["us-east-1-x", "us-west-1-s"])).toEqual(["us-east-1", "us-west-1"]); + expect(arrayDiff(["us-east-1", "us-west-1"], ["us-east-1-x", "us-west-1"])).toEqual(["us-east-1"]); }); }); @@ -187,8 +175,7 @@ describe("Exports", () => { expect( getNotificationMutingStatus(testNotificationString, { service: "ec2", - quotaName: - "Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances", + quotaName: "Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances", }) ).toEqual({ muted: true, @@ -198,8 +185,7 @@ describe("Exports", () => { expect( getNotificationMutingStatus(testNotificationString, { service: "ec2", - quotaName: - "Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances", + quotaName: "Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances", quotaCode: "CODE123", }) ).toEqual({ @@ -208,11 +194,9 @@ describe("Exports", () => { "ec2:L-1216C47A,Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances in the notification muting configuration; those quotas/limits are muted", }); expect( - getNotificationMutingStatus( - testNotificationString, { + getNotificationMutingStatus(testNotificationString, { service: "ec2", - quotaName: - "Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances", + quotaName: "Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances", quotaCode: "CODE123", resource: "resource1", }) @@ -222,11 +206,9 @@ describe("Exports", () => { "ec2:L-1216C47A,Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances in the notification muting configuration; those quotas/limits are muted", }); expect( - getNotificationMutingStatus( - testNotificationString, { + getNotificationMutingStatus(testNotificationString, { service: "EC2", - quotaName: - "Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances", + quotaName: "Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances", quotaCode: "CODE123", resource: "resource1", }) @@ -243,8 +225,7 @@ describe("Exports", () => { }) ).toEqual({ muted: false }); expect( - getNotificationMutingStatus( - testNotificationString,{ + getNotificationMutingStatus(testNotificationString, { service: "ec2", quotaName: "ABC", quotaCode: "ABC", @@ -258,8 +239,7 @@ describe("Exports", () => { }) ).toEqual({ muted: true, - message: - "dynamodb in the notification muting configuration; all quotas/limits in dynamodb muted", + message: "dynamodb in the notification muting configuration; all quotas/limits in dynamodb muted", }); expect( getNotificationMutingStatus(testNotificationString, { @@ -269,12 +249,10 @@ describe("Exports", () => { }) ).toEqual({ muted: true, - message: - "dynamodb in the notification muting configuration; all quotas/limits in dynamodb muted", + message: "dynamodb in the notification muting configuration; all quotas/limits in dynamodb muted", }); expect( - getNotificationMutingStatus( - testNotificationString,{ + getNotificationMutingStatus(testNotificationString, { service: "dynamodb", quotaName: "ABC", quotaCode: "ABC", @@ -282,8 +260,7 @@ describe("Exports", () => { }) ).toEqual({ muted: true, - message: - "dynamodb in the notification muting configuration; all quotas/limits in dynamodb muted", + message: "dynamodb in the notification muting configuration; all quotas/limits in dynamodb muted", }); expect( getNotificationMutingStatus(testNotificationString, { @@ -292,18 +269,16 @@ describe("Exports", () => { }) ).toEqual({ muted: true, - message: - "logs:* in the notification muting configuration, all quotas/limits in logs muted", + message: "logs:* in the notification muting configuration, all quotas/limits in logs muted", }); expect( getNotificationMutingStatus(testNotificationString, { service: "Logs", - quotaName: "ABC" + quotaName: "ABC", }) ).toEqual({ muted: true, - message: - "logs:* in the notification muting configuration, all quotas/limits in logs muted", + message: "logs:* in the notification muting configuration, all quotas/limits in logs muted", }); expect( getNotificationMutingStatus(testNotificationString, { @@ -313,8 +288,7 @@ describe("Exports", () => { }) ).toEqual({ muted: true, - message: - "logs:* in the notification muting configuration, all quotas/limits in logs muted", + message: "logs:* in the notification muting configuration, all quotas/limits in logs muted", }); expect( getNotificationMutingStatus(testNotificationString, { @@ -325,8 +299,7 @@ describe("Exports", () => { }) ).toEqual({ muted: true, - message: - "logs:* in the notification muting configuration, all quotas/limits in logs muted", + message: "logs:* in the notification muting configuration, all quotas/limits in logs muted", }); expect( getNotificationMutingStatus(testNotificationString, { @@ -336,8 +309,7 @@ describe("Exports", () => { }) ).toEqual({ muted: true, - message: - "geo:L-05EFD12D in the notification muting configuration; those quotas/limits are muted", + message: "geo:L-05EFD12D in the notification muting configuration; those quotas/limits are muted", }); expect( getNotificationMutingStatus(testNotificationString, { @@ -353,13 +325,12 @@ describe("Exports", () => { }) ).toEqual({ muted: false }); expect( - getNotificationMutingStatus( - testNotificationString, { - service: "geo", - quotaName: "ABC", - quotaCode: "ABC", - resource: "ABC", - }) + getNotificationMutingStatus(testNotificationString, { + service: "geo", + quotaName: "ABC", + quotaCode: "ABC", + resource: "ABC", + }) ).toEqual({ muted: false }); expect( getNotificationMutingStatus(testNotificationString, { @@ -368,16 +339,14 @@ describe("Exports", () => { }) ).toEqual({ muted: false }); expect( - getNotificationMutingStatus( - testNotificationString, { + getNotificationMutingStatus(testNotificationString, { service: "lambda", quotaName: "ABC", quotaCode: "ABC", }) ).toEqual({ muted: false }); expect( - getNotificationMutingStatus( - testNotificationString, { + getNotificationMutingStatus(testNotificationString, { service: "lambda", quotaName: "ABC", quotaCode: "ABC", @@ -385,11 +354,9 @@ describe("Exports", () => { }) ).toEqual({ muted: false }); expect( - getNotificationMutingStatus( - testNotificationString, { + getNotificationMutingStatus(testNotificationString, { service: "lambda", - quotaName: - "Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances", + quotaName: "Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances", quotaCode: "L-1216C47A", resource: "L-05EFD12D", }) @@ -401,16 +368,14 @@ describe("Exports", () => { }) ).toEqual({ muted: false }); expect( - getNotificationMutingStatus( - testNotificationString, { + getNotificationMutingStatus(testNotificationString, { service: "cloudwatch", quotaName: "ABC", quotaCode: "ABC", }) ).toEqual({ muted: false }); expect( - getNotificationMutingStatus( - testNotificationString, { + getNotificationMutingStatus(testNotificationString, { service: "cloudwatch", quotaName: "ABC", quotaCode: "ABC", diff --git a/source/lambda/utilsLayer/__tests__/organizations.spec.ts b/source/lambda/utilsLayer/__tests__/organizations.spec.ts index bc4af87..bb90ec3 100644 --- a/source/lambda/utilsLayer/__tests__/organizations.spec.ts +++ b/source/lambda/utilsLayer/__tests__/organizations.spec.ts @@ -110,9 +110,7 @@ describe("Organizations Helper", () => { }); const awsServiceTestCase = async () => { - await orgHelper.enableAWSServiceAccess( - "member.org.stacksets.cloudformation.amazonaws.com" - ); + await orgHelper.enableAWSServiceAccess("member.org.stacksets.cloudformation.amazonaws.com"); }; it("should enable AWS Service Access", async () => { @@ -130,9 +128,7 @@ describe("Organizations Helper", () => { }) ); - await expect(awsServiceTestCase).rejects.toThrow( - OrganizationsServiceException - ); + await expect(awsServiceTestCase).rejects.toThrow(OrganizationsServiceException); }); it("should register a delegated administrator", async () => { @@ -223,5 +219,4 @@ describe("Organizations Helper", () => { await expect(testCase).rejects.toThrow(OrganizationsServiceException); }); - }); diff --git a/source/lambda/utilsLayer/__tests__/servicequotas.spec.ts b/source/lambda/utilsLayer/__tests__/servicequotas.spec.ts index 7119b62..aeb2702 100644 --- a/source/lambda/utilsLayer/__tests__/servicequotas.spec.ts +++ b/source/lambda/utilsLayer/__tests__/servicequotas.spec.ts @@ -187,9 +187,7 @@ describe("Service Quotas Helper", () => { }, }); - const processBatchSpy = jest - .spyOn(sqHelper as any, "processBatch") - .mockResolvedValue(undefined); + const processBatchSpy = jest.spyOn(sqHelper as any, "processBatch").mockResolvedValue(undefined); await sqHelper.getQuotasWithUtilizationMetrics(quotas, "testService"); @@ -197,10 +195,7 @@ describe("Service Quotas Helper", () => { }); it("should handle empty quota list", async () => { - const result = await sqHelper.getQuotasWithUtilizationMetrics( - [], - "testService" - ); + const result = await sqHelper.getQuotasWithUtilizationMetrics([], "testService"); expect(result).toEqual([]); }); }); @@ -211,17 +206,9 @@ describe("Service Quotas Helper", () => { const validatedQuotas: ServiceQuota[] = []; const cwMock = new CloudWatchHelper(); - const generateQueriesSpy = jest - .spyOn(sqHelper as any, "generateCWQueriesForQuotas") - .mockReturnValue([]); + const generateQueriesSpy = jest.spyOn(sqHelper as any, "generateCWQueriesForQuotas").mockReturnValue([]); - await (sqHelper as any).processBatch( - batch, - validatedQuotas, - cwMock, - 1, - "testService" - ); + await (sqHelper as any).processBatch(batch, validatedQuotas, cwMock, 1, "testService"); expect(generateQueriesSpy).toHaveBeenCalledTimes(1); expect(cwMock.getMetricData).not.toHaveBeenCalled(); @@ -234,20 +221,12 @@ describe("Service Quotas Helper", () => { const cwMock = new CloudWatchHelper(); const error = new Error("Non-validation error"); - jest - .spyOn(sqHelper as any, "generateCWQueriesForQuotas") - .mockReturnValue([{}]); + jest.spyOn(sqHelper as any, "generateCWQueriesForQuotas").mockReturnValue([{}]); cwMock.getMetricData = jest.fn().mockRejectedValue(error); const loggerErrorSpy = jest.spyOn(logger, "error"); - await (sqHelper as any).processBatch( - batch, - validatedQuotas, - cwMock, - 1, - "testService" - ); + await (sqHelper as any).processBatch(batch, validatedQuotas, cwMock, 1, "testService"); expect(loggerErrorSpy).toHaveBeenCalledWith({ label: expect.any(String), @@ -274,9 +253,7 @@ describe("Service Quotas Helper", () => { ]; const validatedQuotas: ServiceQuota[] = []; - const validationError = new Error( - "Error in expression 'test_metric_pct_utilization': Test error" - ); + const validationError = new Error("Error in expression 'test_metric_pct_utilization': Test error"); validationError.name = "ValidationError"; const getMetricDataMock = jest.fn().mockRejectedValue(validationError); @@ -285,37 +262,22 @@ describe("Service Quotas Helper", () => { getMetricData: getMetricDataMock, } as any; - jest - .spyOn(sqHelper as any, "generateCWQueriesForQuotas") - .mockReturnValue([{}]); - const handleValidationErrorSpy = jest.spyOn( - sqHelper as any, - "handleValidationError" - ); - jest - .spyOn(sqHelper as any, "extractProblematicMetric") - .mockReturnValue("test_metric"); + jest.spyOn(sqHelper as any, "generateCWQueriesForQuotas").mockReturnValue([{}]); + const handleValidationErrorSpy = jest.spyOn(sqHelper as any, "handleValidationError"); + jest.spyOn(sqHelper as any, "extractProblematicMetric").mockReturnValue("test_metric"); jest.spyOn(sqHelper as any, "removeProblematicQuota").mockReturnValue({ problematicQuota: batch[0], updatedBatch: [], }); - await (sqHelper as any).processBatch( - batch, - validatedQuotas, - cwMock, - 1, - "testService" - ); + await (sqHelper as any).processBatch(batch, validatedQuotas, cwMock, 1, "testService"); expect(getMetricDataMock).toHaveBeenCalledTimes(1); expect(handleValidationErrorSpy).toHaveBeenCalledTimes(1); expect(handleValidationErrorSpy).toHaveBeenCalledWith( expect.objectContaining({ name: "ValidationError", - message: expect.stringContaining( - "Error in expression 'test_metric_pct_utilization'" - ), + message: expect.stringContaining("Error in expression 'test_metric_pct_utilization'"), }), batch, validatedQuotas, @@ -335,17 +297,13 @@ describe("Service Quotas Helper", () => { }); it("should extract the problematic metric from the error message with _pct_utilization", () => { - const errorMessage = - "Error in expression 'service_resource_none_type_quotacode_pct_utilization': Some error"; + const errorMessage = "Error in expression 'service_resource_none_type_quotacode_pct_utilization': Some error"; const result = (sqHelper as any).extractProblematicMetric(errorMessage); - expect(result).toBe( - "service_resource_none_type_quotacode_pct_utilization" - ); + expect(result).toBe("service_resource_none_type_quotacode_pct_utilization"); }); it("should extract the problematic metric from the error message without _pct_utilization", () => { - const errorMessage = - "Error in expression 'service_resource_none_type_quotacode': Some error"; + const errorMessage = "Error in expression 'service_resource_none_type_quotacode': Some error"; const result = (sqHelper as any).extractProblematicMetric(errorMessage); expect(result).toBe("service_resource_none_type_quotacode"); }); @@ -354,9 +312,7 @@ describe("Service Quotas Helper", () => { const errorMessage = "Error in expression 'service_resource_none_type_quotacode_pct_utilization' and 'another_service_resource_none_type_quotacode': Some error"; const result = (sqHelper as any).extractProblematicMetric(errorMessage); - expect(result).toBe( - "service_resource_none_type_quotacode_pct_utilization" - ); + expect(result).toBe("service_resource_none_type_quotacode_pct_utilization"); }); }); @@ -375,9 +331,7 @@ describe("Service Quotas Helper", () => { const validatedQuotas: ServiceQuota[] = []; const cwMock = new CloudWatchHelper(); - jest - .spyOn(sqHelper as any, "extractProblematicMetric") - .mockReturnValue("bad_metric"); + jest.spyOn(sqHelper as any, "extractProblematicMetric").mockReturnValue("bad_metric"); jest.spyOn(sqHelper as any, "removeProblematicQuota").mockReturnValue({ problematicQuota: { QuotaCode: "BadQuota", @@ -390,9 +344,7 @@ describe("Service Quotas Helper", () => { }, ], }); - const processBatchSpy = jest - .spyOn(sqHelper as any, "processBatch") - .mockResolvedValue(undefined); + const processBatchSpy = jest.spyOn(sqHelper as any, "processBatch").mockResolvedValue(undefined); await (sqHelper as any).handleValidationError( new Error("Test error"), @@ -429,9 +381,7 @@ describe("Service Quotas Helper", () => { const validatedQuotas: ServiceQuota[] = []; const cwMock = new CloudWatchHelper(); - jest - .spyOn(sqHelper as any, "extractProblematicMetric") - .mockReturnValue("bad_metric"); + jest.spyOn(sqHelper as any, "extractProblematicMetric").mockReturnValue("bad_metric"); jest.spyOn(sqHelper as any, "removeProblematicQuota").mockReturnValue({ problematicQuota: { QuotaCode: "BadQuota", @@ -460,9 +410,7 @@ describe("Service Quotas Helper", () => { const cwMock = new CloudWatchHelper(); const error = new Error("Test error"); - jest - .spyOn(sqHelper as any, "extractProblematicMetric") - .mockReturnValue("test_metric"); + jest.spyOn(sqHelper as any, "extractProblematicMetric").mockReturnValue("test_metric"); jest.spyOn(sqHelper as any, "removeProblematicQuota").mockReturnValue({ problematicQuota: undefined, updatedBatch: batch, @@ -470,15 +418,7 @@ describe("Service Quotas Helper", () => { const loggerWarnSpy = jest.spyOn(logger, "warn"); - await (sqHelper as any).handleValidationError( - error, - batch, - validatedQuotas, - cwMock, - 1, - "testService", - 0 - ); + await (sqHelper as any).handleValidationError(error, batch, validatedQuotas, cwMock, 1, "testService", 0); expect(loggerWarnSpy).toHaveBeenCalledWith({ label: expect.any(String), @@ -493,26 +433,15 @@ describe("Service Quotas Helper", () => { const cwMock = new CloudWatchHelper(); const error = new Error("Test error"); - jest - .spyOn(sqHelper as any, "extractProblematicMetric") - .mockReturnValue(null); + jest.spyOn(sqHelper as any, "extractProblematicMetric").mockReturnValue(null); const loggerWarnSpy = jest.spyOn(logger, "warn"); - await (sqHelper as any).handleValidationError( - error, - batch, - validatedQuotas, - cwMock, - 1, - "testService", - 0 - ); + await (sqHelper as any).handleValidationError(error, batch, validatedQuotas, cwMock, 1, "testService", 0); expect(loggerWarnSpy).toHaveBeenCalledWith({ label: expect.any(String), - message: - "Unable to extract problematic metric. Skipping remaining quotas in this batch for testService.", + message: "Unable to extract problematic metric. Skipping remaining quotas in this batch for testService.", }); }); }); @@ -544,18 +473,13 @@ describe("Service Quotas Helper", () => { }, ]; - jest - .spyOn(sqHelper, "generateMetricQueryId") - .mockImplementation((_metricInfo, quotaCode) => { - if (quotaCode === "GoodQuota") return "good_metric"; - if (quotaCode === "BadQuota") return "bad_metric"; - return ""; - }); + jest.spyOn(sqHelper, "generateMetricQueryId").mockImplementation((_metricInfo, quotaCode) => { + if (quotaCode === "GoodQuota") return "good_metric"; + if (quotaCode === "BadQuota") return "bad_metric"; + return ""; + }); - const result = (sqHelper as any).removeProblematicQuota( - batch, - "bad_metric" - ); + const result = (sqHelper as any).removeProblematicQuota(batch, "bad_metric"); expect(result.problematicQuota).toBeDefined(); expect(result.problematicQuota?.QuotaCode).toBe("BadQuota"); @@ -589,14 +513,9 @@ describe("Service Quotas Helper", () => { }, ]; - jest - .spyOn(sqHelper as any, "generateMetricQueryId") - .mockReturnValue("good_metric"); + jest.spyOn(sqHelper as any, "generateMetricQueryId").mockReturnValue("good_metric"); - const result = (sqHelper as any).removeProblematicQuota( - batch, - "bad_metric" - ); + const result = (sqHelper as any).removeProblematicQuota(batch, "bad_metric"); expect(result.problematicQuota).toBeUndefined(); expect(result.updatedBatch).toHaveLength(2); @@ -617,9 +536,7 @@ describe("Service Quotas Helper", () => { const result = sqHelper.generateMetricQueryId(metricInfo, quotaCode); - expect(result).toBe( - "testservice_testresource_testclass_testtype_testquotacode" - ); + expect(result).toBe("testservice_testresource_testclass_testtype_testquotacode"); }); it("should handle missing dimensions", () => { @@ -660,12 +577,8 @@ describe("Service Quotas Helper", () => { // UsageMetric is missing }; - expect(() => - (sqHelper as any).validateQuotaHasUsageMetrics(invalidQuota) - ).toThrow(UnsupportedQuotaException); - expect(() => - (sqHelper as any).validateQuotaHasUsageMetrics(invalidQuota) - ).toThrow( + expect(() => (sqHelper as any).validateQuotaHasUsageMetrics(invalidQuota)).toThrow(UnsupportedQuotaException); + expect(() => (sqHelper as any).validateQuotaHasUsageMetrics(invalidQuota)).toThrow( "TestQuota for TestService does not currently support utilization monitoring" ); }); @@ -684,9 +597,7 @@ describe("Service Quotas Helper", () => { }, }; - expect(() => - (sqHelper as any).validateQuotaHasUsageMetrics(validQuota) - ).not.toThrow(); + expect(() => (sqHelper as any).validateQuotaHasUsageMetrics(validQuota)).not.toThrow(); }); }); }); diff --git a/source/lambda/utilsLayer/__tests__/sns.spec.ts b/source/lambda/utilsLayer/__tests__/sns.spec.ts index 16c8d97..f117beb 100644 --- a/source/lambda/utilsLayer/__tests__/sns.spec.ts +++ b/source/lambda/utilsLayer/__tests__/sns.spec.ts @@ -4,11 +4,8 @@ import { mockClient } from "aws-sdk-client-mock"; import "aws-sdk-client-mock-jest"; -import { SNSHelper} from "../lib"; -import { - PublishCommand, - SNSClient -} from "@aws-sdk/client-sns"; +import { SNSHelper } from "../lib"; +import { PublishCommand, SNSClient } from "@aws-sdk/client-sns"; describe("SNSHelper", () => { const snsMock = mockClient(SNSClient); @@ -19,4 +16,4 @@ describe("SNSHelper", () => { await snsHelper.publish("ARN:123", "{}"); expect(snsMock).toHaveReceivedCommand(PublishCommand); }); -}); \ No newline at end of file +}); diff --git a/source/lambda/utilsLayer/__tests__/sqs.spec.ts b/source/lambda/utilsLayer/__tests__/sqs.spec.ts index a1ea314..6ce76fa 100644 --- a/source/lambda/utilsLayer/__tests__/sqs.spec.ts +++ b/source/lambda/utilsLayer/__tests__/sqs.spec.ts @@ -3,12 +3,7 @@ import { mockClient } from "aws-sdk-client-mock"; import "aws-sdk-client-mock-jest"; -import { - DeleteMessageCommand, - ReceiveMessageCommand, - SQSClient, - SQSServiceException, -} from "@aws-sdk/client-sqs"; +import { DeleteMessageCommand, ReceiveMessageCommand, SQSClient, SQSServiceException } from "@aws-sdk/client-sqs"; import { SQSHelper } from "../lib/sqs"; diff --git a/source/lambda/utilsLayer/__tests__/ssm.spec.ts b/source/lambda/utilsLayer/__tests__/ssm.spec.ts index 2edd8a5..26a5ccf 100644 --- a/source/lambda/utilsLayer/__tests__/ssm.spec.ts +++ b/source/lambda/utilsLayer/__tests__/ssm.spec.ts @@ -3,12 +3,7 @@ import { mockClient } from "aws-sdk-client-mock"; import "aws-sdk-client-mock-jest"; -import { - GetParameterCommand, - ParameterType, - SSMClient, - SSMServiceException, -} from "@aws-sdk/client-ssm"; +import { GetParameterCommand, ParameterType, SSMClient, SSMServiceException } from "@aws-sdk/client-ssm"; import { SSMHelper } from "../lib/ssm"; diff --git a/source/lambda/utilsLayer/lib/catch.ts b/source/lambda/utilsLayer/lib/catch.ts index 65e8ef1..0f2cd32 100644 --- a/source/lambda/utilsLayer/lib/catch.ts +++ b/source/lambda/utilsLayer/lib/catch.ts @@ -9,10 +9,7 @@ import { logger } from "./logger"; * @param raiseException - whether or not raise exception * @returns */ -export const catchDecorator = ( - errorType: any, - raiseException: boolean -): any => { +export const catchDecorator = (errorType: any, raiseException: boolean): any => { return (_: any, key: string | symbol, descriptor?: PropertyDescriptor) => { if (!descriptor) { logger.warn({ @@ -46,12 +43,7 @@ export const catchDecorator = ( * @param errorType * @param raiseException */ -const _handleError = ( - error: any, - key: string | symbol, - errorType: any, - raiseException: boolean -) => { +const _handleError = (error: any, key: string | symbol, errorType: any, raiseException: boolean) => { if (error instanceof errorType) logger.warn({ label: key, diff --git a/source/lambda/utilsLayer/lib/cloudformation.ts b/source/lambda/utilsLayer/lib/cloudformation.ts index 7c7afbd..1a8c3ae 100644 --- a/source/lambda/utilsLayer/lib/cloudformation.ts +++ b/source/lambda/utilsLayer/lib/cloudformation.ts @@ -12,10 +12,7 @@ import { import { catchDecorator } from "./catch"; import { ServiceHelper } from "./exports"; import { logger } from "./logger"; -import { - StackSetOperationPreferences, - Parameter, -} from "@aws-sdk/client-cloudformation/dist-types/models/models_0"; +import { StackSetOperationPreferences, Parameter } from "@aws-sdk/client-cloudformation/dist-types/models/models_0"; import { IncorrectConfigurationException } from "./error"; export interface StackSetOpsPercentagePrefs { @@ -30,22 +27,17 @@ export const defaultOpsPercentagePrefs: StackSetOpsPercentagePrefs = { FailureTolerancePercentage: 0, }; -function validateStackSetOpsPercentagePrefs( - opsPrefs: StackSetOpsPercentagePrefs -) { +function validateStackSetOpsPercentagePrefs(opsPrefs: StackSetOpsPercentagePrefs) { if ( - ![ - RegionConcurrencyType.PARALLEL, - RegionConcurrencyType.SEQUENTIAL, - ].includes(opsPrefs.RegionConcurrencyType) || - opsPrefs.MaxConcurrentPercentage < 1 || - opsPrefs.MaxConcurrentPercentage > 100 || - opsPrefs.FailureTolerancePercentage < 0 || - opsPrefs.FailureTolerancePercentage > 100 + ![RegionConcurrencyType.PARALLEL, RegionConcurrencyType.SEQUENTIAL].includes( + opsPrefs.RegionConcurrencyType + ) || + opsPrefs.MaxConcurrentPercentage < 1 || + opsPrefs.MaxConcurrentPercentage > 100 || + opsPrefs.FailureTolerancePercentage < 0 || + opsPrefs.FailureTolerancePercentage > 100 ) - throw new IncorrectConfigurationException( - "Invalid StackSetOperationPreferences" - ); + throw new IncorrectConfigurationException("Invalid StackSetOperationPreferences"); } /** @@ -110,11 +102,7 @@ export class CloudFormationHelper extends ServiceHelper { }) ); //remove duplicates coming from different OUs - return Array.from( - new Set( - result?.Summaries?.map((summary) => summary.Region) - ).values() - ); + return Array.from(new Set(result?.Summaries?.map((summary) => summary.Region)).values()); } /** @@ -131,18 +119,14 @@ export class CloudFormationHelper extends ServiceHelper { validateStackSetOpsPercentagePrefs(opsPrefs); logger.debug({ label: this.moduleName, - message: `creating stackset instances for ${ - this.stackSetName - }; regions: ${JSON.stringify(regions)}; targets :${JSON.stringify( - target - )}`, + message: `creating stackset instances for ${this.stackSetName}; regions: ${JSON.stringify( + regions + )}; targets :${JSON.stringify(target)}`, }); if (target.length === 0 || regions.length === 0) { logger.debug({ label: this.moduleName, - message: `creating stackset instances aborted because ${ - target.length === 0 ? "targets" : "regions" - } is empty`, + message: `creating stackset instances aborted because ${target.length === 0 ? "targets" : "regions"} is empty`, }); return; } @@ -171,18 +155,14 @@ export class CloudFormationHelper extends ServiceHelper { validateStackSetOpsPercentagePrefs(opsPrefs); logger.debug({ label: this.moduleName, - message: `deleting stackset instances for ${ - this.stackSetName - }; regions: ${JSON.stringify(regions)}; targets :${JSON.stringify( - target - )}`, + message: `deleting stackset instances for ${this.stackSetName}; regions: ${JSON.stringify( + regions + )}; targets :${JSON.stringify(target)}`, }); if (target.length === 0 || regions.length === 0) { logger.debug({ label: this.moduleName, - message: `deleting stackset instances aborted because ${ - target.length === 0 ? "targets" : "regions" - } is empty`, + message: `deleting stackset instances aborted because ${target.length === 0 ? "targets" : "regions"} is empty`, }); return; } diff --git a/source/lambda/utilsLayer/lib/cloudwatch.ts b/source/lambda/utilsLayer/lib/cloudwatch.ts index 66c8f25..613daee 100644 --- a/source/lambda/utilsLayer/lib/cloudwatch.ts +++ b/source/lambda/utilsLayer/lib/cloudwatch.ts @@ -34,16 +34,10 @@ export class CloudWatchHelper extends ServiceHelper { * @param queries - metric data query to fetch percentage utilization */ @catchDecorator(CloudWatchServiceException, true) - async getMetricData( - startTime: Date, - endTime: Date, - queries: MetricDataQuery[] - ) { + async getMetricData(startTime: Date, endTime: Date, queries: MetricDataQuery[]) { logger.debug({ label: this.moduleName, - message: `getting cloudwatch metric data for queries: ${JSON.stringify( - queries - )}`, + message: `getting cloudwatch metric data for queries: ${JSON.stringify(queries)}`, }); const paginator = paginateGetMetricData( { client: this.client }, diff --git a/source/lambda/utilsLayer/lib/dynamodb.ts b/source/lambda/utilsLayer/lib/dynamodb.ts index 1a76813..0414101 100644 --- a/source/lambda/utilsLayer/lib/dynamodb.ts +++ b/source/lambda/utilsLayer/lib/dynamodb.ts @@ -1,10 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - DynamoDBClient, - DynamoDBServiceException, -} from "@aws-sdk/client-dynamodb"; +import { DynamoDBClient, DynamoDBServiceException } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, PutCommand, @@ -54,9 +51,7 @@ export class DynamoDBHelper extends ServiceHelper { label: this.moduleName, message: `putting JSON item on ${tableName}: ${JSON.stringify(item)}`, }); - await this.ddbDocClient.send( - new PutCommand({ TableName: tableName, Item: item }) - ); + await this.ddbDocClient.send(new PutCommand({ TableName: tableName, Item: item })); } /** @@ -187,9 +182,7 @@ export class DynamoDBHelper extends ServiceHelper { ); if (response.Items) { allItems.push( - ...response.Items.filter((item) => item["Monitored"] === true).map( - (item) => item["ServiceCode"] - ) + ...response.Items.filter((item) => item["Monitored"] === true).map((item) => item["ServiceCode"]) ); } } while (response.LastEvaluatedKey); diff --git a/source/lambda/utilsLayer/lib/ec2.ts b/source/lambda/utilsLayer/lib/ec2.ts index ab0f5c7..029322e 100644 --- a/source/lambda/utilsLayer/lib/ec2.ts +++ b/source/lambda/utilsLayer/lib/ec2.ts @@ -1,10 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - EC2Client, - EC2ServiceException, - DescribeRegionsCommand, -} from "@aws-sdk/client-ec2"; +import { EC2Client, EC2ServiceException, DescribeRegionsCommand } from "@aws-sdk/client-ec2"; import { catchDecorator } from "./catch"; import { ServiceHelper } from "./exports"; import { logger } from "./logger"; diff --git a/source/lambda/utilsLayer/lib/events.ts b/source/lambda/utilsLayer/lib/events.ts index b008723..81625ac 100644 --- a/source/lambda/utilsLayer/lib/events.ts +++ b/source/lambda/utilsLayer/lib/events.ts @@ -6,16 +6,10 @@ import { PutEventsCommand, PutEventsRequestEntry, PutPermissionCommand, - RemovePermissionCommand + RemovePermissionCommand, } from "@aws-sdk/client-cloudwatch-events"; import { catchDecorator } from "./catch"; -import { - ServiceHelper, - ORG_REGEX, - ACCOUNT_REGEX, - OU_REGEX, - createChunksFromArray, -} from "./exports"; +import { ServiceHelper, ORG_REGEX, ACCOUNT_REGEX, OU_REGEX, createChunksFromArray } from "./exports"; import { logger } from "./logger"; /** @@ -43,18 +37,11 @@ export class EventsHelper extends ServiceHelper { * @param eventBusName */ @catchDecorator(CloudWatchEventsServiceException, true) - async createEventBusPolicy( - principals: string[], - orgId: string, - eventBusArn: string, - eventBusName: string - ) { + async createEventBusPolicy(principals: string[], orgId: string, eventBusArn: string, eventBusName: string) { const policyStatements = []; const orgIds = principals.filter((principal) => principal.match(ORG_REGEX)); const ouIds = principals.filter((principal) => principal.match(OU_REGEX)); - const accountIds = principals.filter((principal) => - principal.match(ACCOUNT_REGEX) - ); + const accountIds = principals.filter((principal) => principal.match(ACCOUNT_REGEX)); if (orgIds.length > 0) { //the caller shouldn't provide multiple orgIds, this is checked upstream policyStatements.push({ diff --git a/source/lambda/utilsLayer/lib/exports.ts b/source/lambda/utilsLayer/lib/exports.ts index 7ad8b65..e33b494 100644 --- a/source/lambda/utilsLayer/lib/exports.ts +++ b/source/lambda/utilsLayer/lib/exports.ts @@ -71,13 +71,9 @@ export function validateOrgInput(list: string[]) { // iterate over list list.forEach((item) => { if (!(item.match(OU_REGEX) || item.match(ORG_REGEX))) - throw new IncorrectConfigurationException( - `valid values include OU-Ids or Org-Id ${item}` - ); + throw new IncorrectConfigurationException(`valid values include OU-Ids or Org-Id ${item}`); if (item.match(ORG_REGEX) && list.length > 1) - throw new IncorrectConfigurationException( - `when providing Org-Id, provide single Org-Id ${item}'` - ); + throw new IncorrectConfigurationException(`when providing Org-Id, provide single Org-Id ${item}'`); }); return true; } @@ -89,9 +85,7 @@ export function validateOrgInput(list: string[]) { export function validateAccountInput(accounts: string[]) { accounts.forEach((account) => { if (!account.match(ACCOUNT_REGEX)) - throw new IncorrectConfigurationException( - `invalid Account Id provided:${account}` - ); + throw new IncorrectConfigurationException(`invalid Account Id provided:${account}`); }); } @@ -100,10 +94,7 @@ export function validateAccountInput(accounts: string[]) { * @param array * @returns */ -export function createChunksFromArray( - array: Record[], - chunkSize: number -) { +export function createChunksFromArray(array: Record[], chunkSize: number) { const chunks = []; for (let i = 0; i < array.length; i += chunkSize) { const chunk = array.slice(i, i + chunkSize); @@ -178,11 +169,7 @@ function getMutedNotificationMap(mutingConfig: string[], separator: string) { * @param separator * @param service */ -function getMutedQuotasForService( - mutingConfig: string[], - separator: string, - service: string -) { +function getMutedQuotasForService(mutingConfig: string[], separator: string, service: string) { // using the whole return map is more readable than getting the array for a specific // service and differentiating between when a service not included in the config, and // when the service specified with no quotas to mute all quotas in that service @@ -221,11 +208,7 @@ export function getNotificationMutingStatus( const SEPARATOR = ":"; const WILD_CARD = "*"; const lowerCaseServiceCode = mutingDetail.service.toLowerCase(); - const mutedQuotas = getMutedQuotasForService( - mutingConfig, - SEPARATOR, - mutingDetail.service - ); + const mutedQuotas = getMutedQuotasForService(mutingConfig, SEPARATOR, mutingDetail.service); if (!mutedQuotas) { //service not included in the muting config return { muted: false }; @@ -241,11 +224,9 @@ export function getNotificationMutingStatus( arrayIncludesAnyIgnoreCase( mutedQuotas, ( - [ - mutingDetail.quotaCode, - mutingDetail.quotaName, - mutingDetail.resource, - ].filter((s) => s !== undefined && s !== "") + [mutingDetail.quotaCode, mutingDetail.quotaName, mutingDetail.resource].filter( + (s) => s !== undefined && s !== "" + ) ) ) ) { diff --git a/source/lambda/utilsLayer/lib/logger.ts b/source/lambda/utilsLayer/lib/logger.ts index 35b53a0..dc452c9 100644 --- a/source/lambda/utilsLayer/lib/logger.ts +++ b/source/lambda/utilsLayer/lib/logger.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -/** +/** * { emerg: 0, alert: 1, diff --git a/source/lambda/utilsLayer/lib/organizations.ts b/source/lambda/utilsLayer/lib/organizations.ts index baa4d5c..39ee9b7 100644 --- a/source/lambda/utilsLayer/lib/organizations.ts +++ b/source/lambda/utilsLayer/lib/organizations.ts @@ -67,10 +67,7 @@ export class OrganizationsHelper extends ServiceHelper { } @catchDecorator(OrganizationsServiceException, true) - async registerDelegatedAdministrator( - accountId: string, - servicePrincipal: string - ) { + async registerDelegatedAdministrator(accountId: string, servicePrincipal: string) { logger.debug({ label: this.moduleName, message: `registering delegated administrator`, @@ -97,7 +94,8 @@ export class OrganizationsHelper extends ServiceHelper { @catchDecorator(OrganizationsServiceException, true) async getNumberOfAccountsInOrg(): Promise { let count = 0; - const paginator = paginateListAccounts({ + const paginator = paginateListAccounts( + { client: this.client, }, {} diff --git a/source/lambda/utilsLayer/lib/servicequotas.ts b/source/lambda/utilsLayer/lib/servicequotas.ts index 4386e7b..ab576c3 100644 --- a/source/lambda/utilsLayer/lib/servicequotas.ts +++ b/source/lambda/utilsLayer/lib/servicequotas.ts @@ -1,9 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - CloudWatchServiceException, - MetricDataQuery, -} from "@aws-sdk/client-cloudwatch"; +import { CloudWatchServiceException, MetricDataQuery } from "@aws-sdk/client-cloudwatch"; import { MetricInfo, paginateListServiceQuotas, @@ -88,11 +85,7 @@ export class ServiceQuotasHelper extends ServiceHelper { // get list of quotas which support usage metric for await (const page of paginator) { if (page.Quotas) { - quotasSupportingUsage.push( - ...page.Quotas.filter( - (Quota) => Quota.UsageMetric?.MetricNamespace == "AWS/Usage" - ) - ); + quotasSupportingUsage.push(...page.Quotas.filter((Quota) => Quota.UsageMetric?.MetricNamespace == "AWS/Usage")); } await sleep(1000); } @@ -110,10 +103,7 @@ export class ServiceQuotasHelper extends ServiceHelper { * @param serviceCode optional parameter needed only for logging */ @catchDecorator(CloudWatchServiceException, true) - async getQuotasWithUtilizationMetrics( - quotas: ServiceQuota[], - serviceCode?: string - ) { + async getQuotasWithUtilizationMetrics(quotas: ServiceQuota[], serviceCode?: string) { logger.debug({ label: this.moduleName, message: `Starting to process ${quotas.length} quotas for ${serviceCode}`, @@ -125,13 +115,7 @@ export class ServiceQuotasHelper extends ServiceHelper { for (let i = 0; i < quotas.length; i += BATCH_SIZE) { batchCount++; const batch = quotas.slice(i, i + BATCH_SIZE); - await this.processBatch( - batch, - validatedQuotas, - cw, - batchCount, - serviceCode - ); + await this.processBatch(batch, validatedQuotas, cw, batchCount, serviceCode); await sleep(1000); } logger.debug({ @@ -165,11 +149,7 @@ export class ServiceQuotasHelper extends ServiceHelper { return; } try { - await cw.getMetricData( - new Date(Date.now() - 15 * 60 * 1000), - new Date(), - queries - ); + await cw.getMetricData(new Date(Date.now() - 15 * 60 * 1000), new Date(), queries); validatedQuotas.push(...batch); logger.debug({ label: this.moduleName, @@ -177,15 +157,7 @@ export class ServiceQuotasHelper extends ServiceHelper { }); } catch (error) { if (error instanceof Error && error.name === "ValidationError") { - await this.handleValidationError( - error, - batch, - validatedQuotas, - cw, - batchCount, - serviceCode, - retryCount - ); + await this.handleValidationError(error, batch, validatedQuotas, cw, batchCount, serviceCode, retryCount); } else { logger.error({ label: this.moduleName, @@ -224,10 +196,7 @@ export class ServiceQuotasHelper extends ServiceHelper { message: `Extracted problematic metric: ${problematicMetric}`, }); if (problematicMetric) { - const { problematicQuota, updatedBatch } = this.removeProblematicQuota( - batch, - problematicMetric - ); + const { problematicQuota, updatedBatch } = this.removeProblematicQuota(batch, problematicMetric); // Log the skipping of the problematic quota and process the updated batch if (problematicQuota) { logger.info({ @@ -239,18 +208,9 @@ export class ServiceQuotasHelper extends ServiceHelper { label: this.moduleName, message: `Retrying batch ${batchCount} for ${serviceCode} with ${ updatedBatch.length - } remaining quotas. Retry attempt ${ - retryCount + 1 - } of ${MAX_RETRIES}`, + } remaining quotas. Retry attempt ${retryCount + 1} of ${MAX_RETRIES}`, }); - await this.processBatch( - updatedBatch, - validatedQuotas, - cw, - batchCount, - serviceCode, - retryCount + 1 - ); + await this.processBatch(updatedBatch, validatedQuotas, cw, batchCount, serviceCode, retryCount + 1); } else { logger.warn({ label: this.moduleName, @@ -310,22 +270,14 @@ export class ServiceQuotasHelper extends ServiceHelper { const problematicQuota = batch.find( (q) => q.UsageMetric && - (this.generateMetricQueryId(q.UsageMetric, q.QuotaCode) === - problematicMetric || - `${this.generateMetricQueryId( - q.UsageMetric, - q.QuotaCode - )}_pct_utilization` === problematicMetric) + (this.generateMetricQueryId(q.UsageMetric, q.QuotaCode) === problematicMetric || + `${this.generateMetricQueryId(q.UsageMetric, q.QuotaCode)}_pct_utilization` === problematicMetric) ); const updatedBatch = batch.filter( (q) => q.UsageMetric && - this.generateMetricQueryId(q.UsageMetric, q.QuotaCode) !== - problematicMetric && - `${this.generateMetricQueryId( - q.UsageMetric, - q.QuotaCode - )}_pct_utilization` !== problematicMetric + this.generateMetricQueryId(q.UsageMetric, q.QuotaCode) !== problematicMetric && + `${this.generateMetricQueryId(q.UsageMetric, q.QuotaCode)}_pct_utilization` !== problematicMetric ); return { problematicQuota, updatedBatch }; } @@ -335,9 +287,7 @@ export class ServiceQuotasHelper extends ServiceHelper { * @param quotas An array of ServiceQuota objects to generate queries for * @returns An array of MetricDataQuery objects for CloudWatch */ - private generateCWQueriesForQuotas( - quotas: ServiceQuota[] - ): MetricDataQuery[] { + private generateCWQueriesForQuotas(quotas: ServiceQuota[]): MetricDataQuery[] { const queries: MetricDataQuery[] = []; for (const quota of quotas) { if (quota.UsageMetric) { @@ -361,14 +311,8 @@ export class ServiceQuotasHelper extends ServiceHelper { }); this.validateQuotaHasUsageMetrics(quota); - const usageQuery = this.generateUsageQuery( - quota.UsageMetric, - period, - quota.QuotaCode - ); - const percentageUsageQuery = this.generatePercentageUtilizationQuery( - usageQuery.Id - ); + const usageQuery = this.generateUsageQuery(quota.UsageMetric, period, quota.QuotaCode); + const percentageUsageQuery = this.generatePercentageUtilizationQuery(usageQuery.Id); logger.debug({ label: this.moduleName, message: `${JSON.stringify({ @@ -401,26 +345,15 @@ export class ServiceQuotasHelper extends ServiceHelper { * @param metricInfo * @param quotaCode */ - public generateMetricQueryId( - metricInfo: MetricInfo, - quotaCode: string | undefined - ): string { + public generateMetricQueryId(metricInfo: MetricInfo, quotaCode: string | undefined): string { logger.debug({ label: `generateMetricQueryId/metricInfo`, message: JSON.stringify(metricInfo), }); - const service = (metricInfo.MetricDimensions?.Service || "") - .toLowerCase() - .replace(/[^a-z0-9_]/g, ""); - const resource = (metricInfo.MetricDimensions?.Resource || "") - .toLowerCase() - .replace(/[^a-z0-9_]/g, ""); - const classValue = (metricInfo.MetricDimensions?.Class || "") - .toLowerCase() - .replace(/[^a-z0-9_]/g, ""); - const type = (metricInfo.MetricDimensions?.Type || "") - .toLowerCase() - .replace(/[^a-z0-9_]/g, ""); + const service = (metricInfo.MetricDimensions?.Service || "").toLowerCase().replace(/[^a-z0-9_]/g, ""); + const resource = (metricInfo.MetricDimensions?.Resource || "").toLowerCase().replace(/[^a-z0-9_]/g, ""); + const classValue = (metricInfo.MetricDimensions?.Class || "").toLowerCase().replace(/[^a-z0-9_]/g, ""); + const type = (metricInfo.MetricDimensions?.Type || "").toLowerCase().replace(/[^a-z0-9_]/g, ""); const code = (quotaCode || "").toLowerCase().replace(/[^a-z0-9_]/g, ""); const metricQueryId = `${service}_${resource}_${classValue}_${type}_${code}`; @@ -440,20 +373,14 @@ export class ServiceQuotasHelper extends ServiceHelper { * @param quotaCode - the code which identifies the quota. * @returns */ - private generateUsageQuery( - metricInfo: MetricInfo, - period: number, - quotaCode: string | undefined - ) { + private generateUsageQuery(metricInfo: MetricInfo, period: number, quotaCode: string | undefined) { const usageQuery: MetricDataQuery = { Id: this.generateMetricQueryId(metricInfo, quotaCode), MetricStat: { Metric: { Namespace: metricInfo.MetricNamespace, MetricName: metricInfo.MetricName, - Dimensions: Object.entries( - metricInfo.MetricDimensions as Record - ).map(([key, value]) => { + Dimensions: Object.entries(metricInfo.MetricDimensions as Record).map(([key, value]) => { return { Name: key, Value: value }; }), }, diff --git a/source/lambda/utilsLayer/lib/sns.ts b/source/lambda/utilsLayer/lib/sns.ts index 759b16a..f8919ff 100644 --- a/source/lambda/utilsLayer/lib/sns.ts +++ b/source/lambda/utilsLayer/lib/sns.ts @@ -1,11 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - SNSClient, - PublishCommand, - SNSServiceException, -} from "@aws-sdk/client-sns"; +import { SNSClient, PublishCommand, SNSServiceException } from "@aws-sdk/client-sns"; import { ServiceHelper } from "./exports"; import { catchDecorator } from "./catch"; @@ -41,4 +37,4 @@ export class SNSHelper extends ServiceHelper { const command = new PublishCommand(input); await this.client.send(command); } -} \ No newline at end of file +} diff --git a/source/lambda/utilsLayer/lib/sqs.ts b/source/lambda/utilsLayer/lib/sqs.ts index c0510a6..9e45075 100644 --- a/source/lambda/utilsLayer/lib/sqs.ts +++ b/source/lambda/utilsLayer/lib/sqs.ts @@ -5,7 +5,7 @@ import { QueueAttributeName, ReceiveMessageCommand, SQSClient, - SQSServiceException + SQSServiceException, } from "@aws-sdk/client-sqs"; import { catchDecorator } from "./catch"; import { ServiceHelper } from "./exports"; diff --git a/source/lambda/utilsLayer/lib/ssm.ts b/source/lambda/utilsLayer/lib/ssm.ts index 589d0ab..082f854 100644 --- a/source/lambda/utilsLayer/lib/ssm.ts +++ b/source/lambda/utilsLayer/lib/ssm.ts @@ -38,14 +38,10 @@ export class SSMHelper extends ServiceHelper { label: this.moduleName, message: `getting ssm parameter ${name}`, }); - const response = await this.client.send( - new GetParameterCommand({ Name: name, WithDecryption: withDecrpytion }) - ); - if (!response.Parameter || !response.Parameter.Value) - throw ParameterNotFound; + const response = await this.client.send(new GetParameterCommand({ Name: name, WithDecryption: withDecrpytion })); + if (!response.Parameter || !response.Parameter.Value) throw ParameterNotFound; else { - if (response.Parameter.Type === ParameterType.STRING_LIST) - return response.Parameter.Value.split(","); + if (response.Parameter.Type === ParameterType.STRING_LIST) return response.Parameter.Value.split(","); else return [response.Parameter.Value]; } } diff --git a/source/lambda/utilsLayer/lib/triggers.ts b/source/lambda/utilsLayer/lib/triggers.ts index 06d0ea8..7773a18 100644 --- a/source/lambda/utilsLayer/lib/triggers.ts +++ b/source/lambda/utilsLayer/lib/triggers.ts @@ -23,10 +23,7 @@ interface IScheduledEvent extends Record {} /** * @description supported triggering events */ -type TriggerEvent = - | IDynamoDBStreamEvent - | ICloudFormationEvent - | IScheduledEvent; +type TriggerEvent = IDynamoDBStreamEvent | ICloudFormationEvent | IScheduledEvent; /** * @description class with methods to check incoming trigger event @@ -52,4 +49,8 @@ export class LambdaTriggers { static isScheduledEvent(event: TriggerEvent) { return "detail-type" in event && event["detail-type"] == "Scheduled Event"; } + + static isQMLambdaTestEvent(event: TriggerEvent) { + return "detail-type" in event && event["detail-type"] == "QM Lambda Test Event"; + } } diff --git a/source/lambda/utilsLayer/package-lock.json b/source/lambda/utilsLayer/package-lock.json index f0466bd..408b7a0 100644 --- a/source/lambda/utilsLayer/package-lock.json +++ b/source/lambda/utilsLayer/package-lock.json @@ -1,12 +1,12 @@ { "name": "utils-layer", - "version": "6.2.11", + "version": "6.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "utils-layer", - "version": "6.2.11", + "version": "6.3.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -4025,10 +4025,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", diff --git a/source/lambda/utilsLayer/package.json b/source/lambda/utilsLayer/package.json index 14ae9f9..74555f0 100644 --- a/source/lambda/utilsLayer/package.json +++ b/source/lambda/utilsLayer/package.json @@ -1,6 +1,6 @@ { "name": "utils-layer", - "version": "6.2.11", + "version": "6.3.0", "description": "utils layer for aws-solutions", "author": { "name": "Amazon Web Services", @@ -44,4 +44,4 @@ "ts-node": "^10.9.2", "typescript": "^5.3.3" } -} \ No newline at end of file +} diff --git a/source/resources/__tests__/hub-no-ou.spec.ts b/source/resources/__tests__/hub-no-ou.spec.ts index f16e587..296cdac 100644 --- a/source/resources/__tests__/hub-no-ou.spec.ts +++ b/source/resources/__tests__/hub-no-ou.spec.ts @@ -11,10 +11,12 @@ describe("==Hub No OU Stack Tests==", () => { const app = new App({ context: TestContext, }); - const stack = new QuotaMonitorHubNoOU(app, "QMHubStackNoOU"); + const stack = new QuotaMonitorHubNoOU(app, "QMHubStackNoOU", { targetPartition: "Commercial" }); + const stack_cn = new QuotaMonitorHubNoOU(app, "QMHubStackNoOUChina", { targetPartition: "China" }); const template = Template.fromStack(stack); + const template_cn = Template.fromStack(stack_cn); - describe("hub stack resources", () => { + describe("No ou hub stack resources", () => { TestsCommon.assertCommonHubResources(template); it("should have SSM Parameters for SlackHook, Accounts and Muted Services", () => { @@ -32,28 +34,31 @@ describe("==Hub No OU Stack Tests==", () => { }); it("should have Service Catalog AppRegistry Resource Association, ", () => { - template.resourceCountIs( - "AWS::ServiceCatalogAppRegistry::ResourceAssociation", - 1 - ); - template.hasResource( - "AWS::ServiceCatalogAppRegistry::ResourceAssociation", - { - Properties: { - Application: { - "Fn::GetAtt": ["HubNoOUAppRegistryApplication11687F81", "Id"], - }, - Resource: { - Ref: "AWS::StackId", - }, - ResourceType: "CFN_STACK", + template.resourceCountIs("AWS::ServiceCatalogAppRegistry::ResourceAssociation", 1); + template.hasResource("AWS::ServiceCatalogAppRegistry::ResourceAssociation", { + Properties: { + Application: { + "Fn::GetAtt": ["HubNoOUAppRegistryApplication11687F81", "Id"], }, - } - ); + Resource: { + Ref: "AWS::StackId", + }, + ResourceType: "CFN_STACK", + }, + }); }); }); - describe("hub stack outputs", () => { + describe("No ou hub stack outputs", () => { TestsCommon.assertCommonHubOutputs(template); }); + + describe("China partition no ou hub stack resources", () => { + it("should not have Service Catalog AppRegistry resources", () => { + template_cn.resourceCountIs("AWS::ServiceCatalogAppRegistry::Application", 0); + template_cn.resourceCountIs("AWS::ServiceCatalogAppRegistry::AttributeGroup", 0); + template_cn.resourceCountIs("AWS::ServiceCatalogAppRegistry::ResourceAssociation", 0); + template_cn.resourceCountIs("AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation", 0); + }); + }); }); diff --git a/source/resources/__tests__/hub-tests-common.ts b/source/resources/__tests__/hub-tests-common.ts index 62c668a..171df7d 100644 --- a/source/resources/__tests__/hub-tests-common.ts +++ b/source/resources/__tests__/hub-tests-common.ts @@ -146,17 +146,11 @@ export function assertCommonHubResources(template: Template) { }); it("should have Service Catalog AppRegistry AttributeGroup, ", () => { - template.resourceCountIs( - "AWS::ServiceCatalogAppRegistry::AttributeGroup", - 1 - ); + template.resourceCountIs("AWS::ServiceCatalogAppRegistry::AttributeGroup", 1); }); it("should have Service Catalog AppRegistry AttributeGroup Association, ", () => { - template.resourceCountIs( - "AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation", - 1 - ); + template.resourceCountIs("AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation", 1); }); } diff --git a/source/resources/__tests__/hub.spec.ts b/source/resources/__tests__/hub.spec.ts index f3fa78a..526b36d 100644 --- a/source/resources/__tests__/hub.spec.ts +++ b/source/resources/__tests__/hub.spec.ts @@ -11,8 +11,10 @@ describe("==Hub Stack Tests==", () => { const app = new App({ context: TestContext, }); - const stack = new QuotaMonitorHub(app, "QMHubStack"); + const stack = new QuotaMonitorHub(app, "QMHubStack", { targetPartition: "Commercial" }); + const stack_cn = new QuotaMonitorHub(app, "QMHubStackChina", { targetPartition: "China" }); const template = Template.fromStack(stack); + const template_cn = Template.fromStack(stack_cn); describe("hub stack resources", () => { TestsCommon.assertCommonHubResources(template); @@ -34,12 +36,12 @@ describe("==Hub Stack Tests==", () => { ).toEqual({}); }); - it("should have SQ and TA StackSets, ", () => { - template.resourceCountIs("AWS::CloudFormation::StackSet", 2); + it("should have SQ and TA and SNS StackSets, ", () => { + template.resourceCountIs("AWS::CloudFormation::StackSet", 3); }); it("should have parameters", () => { - const allParams = template.findParameters("*", {}); + const allParams = template.findParameters("*", {}); expect(allParams).toHaveProperty("SNSEmail"); expect(allParams).toHaveProperty("SlackNotification"); expect(allParams).toHaveProperty("DeploymentModel"); @@ -62,28 +64,41 @@ describe("==Hub Stack Tests==", () => { }); it("should have Service Catalog AppRegistry Resource Association, ", () => { - template.resourceCountIs( - "AWS::ServiceCatalogAppRegistry::ResourceAssociation", - 1 - ); - template.hasResource( - "AWS::ServiceCatalogAppRegistry::ResourceAssociation", - { - Properties: { - Application: { - "Fn::GetAtt": ["HubAppRegistryApplication3E8980C3", "Id"], - }, - Resource: { - Ref: "AWS::StackId", - }, - ResourceType: "CFN_STACK", + template.resourceCountIs("AWS::ServiceCatalogAppRegistry::ResourceAssociation", 1); + template.hasResource("AWS::ServiceCatalogAppRegistry::ResourceAssociation", { + Properties: { + Application: { + "Fn::GetAtt": ["HubAppRegistryApplication3E8980C3", "Id"], + }, + Resource: { + Ref: "AWS::StackId", }, - } - ); + ResourceType: "CFN_STACK", + }, + }); + }); + + it("should have a parameter for SQ notification threshold", () => { + template.hasParameter("SQNotificationThreshold", { + Type: "String", + Default: "80", + AllowedPattern: "^([1-9]|[1-9][0-9])$", + Description: "Threshold percentage for quota utilization alerts (0-100)", + ConstraintDescription: "Threshold must be a whole number between 0 and 100", + }); }); }); describe("hub stack outputs", () => { TestsCommon.assertCommonHubOutputs(template); }); + + describe("China partition hub stack resources", () => { + it("should not have Service Catalog AppRegistry resources", () => { + template_cn.resourceCountIs("AWS::ServiceCatalogAppRegistry::Application", 0); + template_cn.resourceCountIs("AWS::ServiceCatalogAppRegistry::AttributeGroup", 0); + template_cn.resourceCountIs("AWS::ServiceCatalogAppRegistry::ResourceAssociation", 0); + template_cn.resourceCountIs("AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation", 0); + }); + }); }); diff --git a/source/resources/__tests__/prereq.spec.ts b/source/resources/__tests__/prereq.spec.ts index c9d8f7c..ce88f34 100644 --- a/source/resources/__tests__/prereq.spec.ts +++ b/source/resources/__tests__/prereq.spec.ts @@ -7,7 +7,7 @@ import { App } from "aws-cdk-lib"; describe("==Pre-requisite Stack Tests==", () => { const app = new App(); - const stack = new PreReqStack(app, "PreReqStack"); + const stack = new PreReqStack(app, "PreReqStack", { targetPartition: "Commercial" }); const template = Template.fromStack(stack); describe("Pre-requisite stack resources", () => { @@ -18,8 +18,7 @@ describe("==Pre-requisite Stack Tests==", () => { }); }); - it("should have helper, pre-req and provider lambda functions " + - "with nodejs18.x runtime", () => { + it("should have helper, pre-req and provider lambda functions " + "with nodejs18.x runtime", () => { template.resourceCountIs("AWS::Lambda::Function", 4); template.hasResourceProperties("AWS::Lambda::Function", { Runtime: "nodejs18.x", diff --git a/source/resources/__tests__/sq-spoke.spec.ts b/source/resources/__tests__/sq-spoke.spec.ts index 435c74c..f539178 100644 --- a/source/resources/__tests__/sq-spoke.spec.ts +++ b/source/resources/__tests__/sq-spoke.spec.ts @@ -10,8 +10,10 @@ describe("==SQ-Spoke Stack Tests==", () => { const app = new App({ context: TestContext, }); - const stack = new QuotaMonitorSQSpoke(app, "SQSpokeStack", {}); + const stack = new QuotaMonitorSQSpoke(app, "SQSpokeStackCommercial", { targetPartition: "Commercial" }); + const stack_cn = new QuotaMonitorSQSpoke(app, "SQSpokeStackChina", { targetPartition: "China" }); const template = Template.fromStack(stack); + const template_cn = Template.fromStack(stack_cn); describe("sq-spoke stack resources", () => { it("should have a Lambda Utils Layer with nodejs18.x runtime", () => { @@ -92,8 +94,7 @@ describe("==SQ-Spoke Stack Tests==", () => { }); it( - "should have lambda functions for QMListManager, CWPoller, and provider frameworks " + - "with nodejs18.x runtime", + "should have lambda functions for QMListManager, CWPoller, and provider frameworks " + "with nodejs18.x runtime", () => { template.resourceCountIs("AWS::Lambda::Function", 3); template.hasResourceProperties("AWS::Lambda::Function", { @@ -103,7 +104,7 @@ describe("==SQ-Spoke Stack Tests==", () => { ); it("should have events rules for the pollers", () => { - template.resourceCountIs("AWS::Events::Rule", 5); + template.resourceCountIs("AWS::Events::Rule", 6); }); it("should have DeadLetterQueues for Lambda Functions ", () => { @@ -122,45 +123,75 @@ describe("==SQ-Spoke Stack Tests==", () => { }); it("should have Service Catalog AppRegistry Application, ", () => { - template.resourceCountIs( - "AWS::ServiceCatalogAppRegistry::Application", - 1 - ); + template.resourceCountIs("AWS::ServiceCatalogAppRegistry::Application", 1); }); it("should have Service Catalog AppRegistry AttributeGroup, ", () => { - template.resourceCountIs( - "AWS::ServiceCatalogAppRegistry::AttributeGroup", - 1 - ); + template.resourceCountIs("AWS::ServiceCatalogAppRegistry::AttributeGroup", 1); }); it("should have Service Catalog AppRegistry Resource Association, ", () => { - template.resourceCountIs( - "AWS::ServiceCatalogAppRegistry::ResourceAssociation", - 1 - ); - template.hasResource( - "AWS::ServiceCatalogAppRegistry::ResourceAssociation", - { - Properties: { - Application: { - "Fn::GetAtt": ["SQSpokeAppRegistryApplicationB3787B2B", "Id"], - }, - Resource: { - Ref: "AWS::StackId", - }, - ResourceType: "CFN_STACK", + template.resourceCountIs("AWS::ServiceCatalogAppRegistry::ResourceAssociation", 1); + template.hasResource("AWS::ServiceCatalogAppRegistry::ResourceAssociation", { + Properties: { + Application: { + "Fn::GetAtt": ["SQSpokeAppRegistryApplicationB3787B2B", "Id"], }, - } - ); + Resource: { + Ref: "AWS::StackId", + }, + ResourceType: "CFN_STACK", + }, + }); }); it("should have Service Catalog AppRegistry AttributeGroup Association, ", () => { - template.resourceCountIs( - "AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation", - 1 - ); + template.resourceCountIs("AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation", 1); + }); + + it("should have a MonitoringFrequency parameter", () => { + template.hasParameter("MonitoringFrequency", { + Type: "String", + Default: "rate(12 hours)", + AllowedValues: ["rate(6 hours)", "rate(12 hours)", "rate(1 day)"], + }); + }); + + it("should use the MonitoringFrequency parameter in the CW Poller event rule", () => { + template.hasResourceProperties("AWS::Events::Rule", { + ScheduleExpression: { + Ref: "MonitoringFrequency", + }, + }); + }); + + it("should have a parameter for notification threshold with correct properties", () => { + template.hasParameter("NotificationThreshold", { + Type: "String", + Default: "80", + AllowedPattern: "^([1-9]|[1-9][0-9])$", + Description: "Threshold percentage for quota utilization alerts (0-100)", + ConstraintDescription: "Threshold must be a whole number between 0 and 100", + }); + }); + + it("should pass the threshold to the CW Poller Lambda", () => { + template.hasResourceProperties("AWS::Lambda::Function", { + Environment: Match.objectLike({ + Variables: Match.objectLike({ + THRESHOLD: { Ref: "NotificationThreshold" }, + }), + }), + }); + }); + }); + + describe("China partition sq-spoke stack resources", () => { + it("should not have Service Catalog AppRegistry resources", () => { + template_cn.resourceCountIs("AWS::ServiceCatalogAppRegistry::Application", 0); + template_cn.resourceCountIs("AWS::ServiceCatalogAppRegistry::AttributeGroup", 0); + template_cn.resourceCountIs("AWS::ServiceCatalogAppRegistry::ResourceAssociation", 0); + template_cn.resourceCountIs("AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation", 0); }); }); }); diff --git a/source/resources/__tests__/ta-spoke.spec.ts b/source/resources/__tests__/ta-spoke.spec.ts index 98d202f..e1048ed 100644 --- a/source/resources/__tests__/ta-spoke.spec.ts +++ b/source/resources/__tests__/ta-spoke.spec.ts @@ -10,8 +10,10 @@ describe("==TA-Spoke Stack Tests==", () => { const app = new App({ context: TestContext, }); - const stack = new QuotaMonitorTASpoke(app, "TASpokeStack"); + const stack = new QuotaMonitorTASpoke(app, "TASpokeStackCommerical", { targetPartition: "Commercial" }); + const stack_cn = new QuotaMonitorTASpoke(app, "TASpokeStackChina", { targetPartition: "China" }); const template = Template.fromStack(stack); + const template_cn = Template.fromStack(stack_cn); describe("ta-spoke stack resources", () => { it("should have a Lambda Utils Layer with nodejs18.x runtime", () => { @@ -47,45 +49,46 @@ describe("==TA-Spoke Stack Tests==", () => { }); it("should have Service Catalog AppRegistry Application, ", () => { - template.resourceCountIs( - "AWS::ServiceCatalogAppRegistry::Application", - 1 - ); + template.resourceCountIs("AWS::ServiceCatalogAppRegistry::Application", 1); }); it("should have Service Catalog AppRegistry AttributeGroup, ", () => { - template.resourceCountIs( - "AWS::ServiceCatalogAppRegistry::AttributeGroup", - 1 - ); + template.resourceCountIs("AWS::ServiceCatalogAppRegistry::AttributeGroup", 1); }); it("should have Service Catalog AppRegistry Resource Association, ", () => { - template.resourceCountIs( - "AWS::ServiceCatalogAppRegistry::ResourceAssociation", - 1 - ); - template.hasResource( - "AWS::ServiceCatalogAppRegistry::ResourceAssociation", - { - Properties: { - Application: { - "Fn::GetAtt": ["TASpokeAppRegistryApplicationAEA2BFDF", "Id"], - }, - Resource: { - Ref: "AWS::StackId", - }, - ResourceType: "CFN_STACK", + template.resourceCountIs("AWS::ServiceCatalogAppRegistry::ResourceAssociation", 1); + template.hasResource("AWS::ServiceCatalogAppRegistry::ResourceAssociation", { + Properties: { + Application: { + "Fn::GetAtt": ["TASpokeAppRegistryApplicationAEA2BFDF", "Id"], }, - } - ); + Resource: { + Ref: "AWS::StackId", + }, + ResourceType: "CFN_STACK", + }, + }); }); it("should have Service Catalog AppRegistry AttributeGroup Association, ", () => { - template.resourceCountIs( - "AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation", - 1 - ); + template.resourceCountIs("AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation", 1); + }); + + it("should have a TARefreshRate parameter", () => { + template.hasParameter("TARefreshRate", { + Type: "String", + Default: "rate(12 hours)", + AllowedValues: ["rate(6 hours)", "rate(12 hours)", "rate(1 day)"], + }); + }); + + it("should use the TARefreshRate parameter in the refresher event rule", () => { + template.hasResourceProperties("AWS::Events::Rule", { + ScheduleExpression: { + Ref: "TARefreshRate", + }, + }); }); }); @@ -94,4 +97,12 @@ describe("==TA-Spoke Stack Tests==", () => { template.hasOutput("ServiceChecks", {}); }); }); + describe("China partition ta-spoke stack resources", () => { + it("should not have Service Catalog AppRegistry resources", () => { + template_cn.resourceCountIs("AWS::ServiceCatalogAppRegistry::Application", 0); + template_cn.resourceCountIs("AWS::ServiceCatalogAppRegistry::AttributeGroup", 0); + template_cn.resourceCountIs("AWS::ServiceCatalogAppRegistry::ResourceAssociation", 0); + template_cn.resourceCountIs("AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation", 0); + }); + }); }); diff --git a/source/resources/bin/app.ts b/source/resources/bin/app.ts index 7817875..07e3d2c 100644 --- a/source/resources/bin/app.ts +++ b/source/resources/bin/app.ts @@ -8,34 +8,74 @@ import { QuotaMonitorHub } from "../lib/hub.stack"; import { QuotaMonitorTASpoke } from "../lib/ta-spoke.stack"; import { QuotaMonitorSQSpoke } from "../lib/sq-spoke.stack"; import { QuotaMonitorHubNoOU } from "../lib/hub-no-ou.stack"; +import { QuotaMonitorSnsSpoke } from "../lib/sns-spoke-stack"; -const app = new App(); -new PreReqStack(app, "quota-monitor-prerequisite", { - synthesizer: new DefaultStackSynthesizer({ - generateBootstrapVersionRule: false, - }), -}); -new QuotaMonitorHub(app, "quota-monitor-hub", { - synthesizer: new DefaultStackSynthesizer({ - generateBootstrapVersionRule: false, - }), -}); -new QuotaMonitorHubNoOU(app, "quota-monitor-hub-no-ou", { - synthesizer: new DefaultStackSynthesizer({ - generateBootstrapVersionRule: false, - }), -}); -new QuotaMonitorTASpoke(app, "quota-monitor-ta-spoke", { - synthesizer: new DefaultStackSynthesizer({ - generateBootstrapVersionRule: false, - }), - analyticsReporting: false, -}); -new QuotaMonitorSQSpoke(app, "quota-monitor-sq-spoke", { - synthesizer: new DefaultStackSynthesizer({ +interface AppProps { + targetPartition: "Commercial" | "China"; +} + +function addAppStacks(app: App, props: AppProps): void { + /** + * MODIFY_TEMPLATES customizes asset handling for orgHub:deploy script: + * - Uses SOLUTION_BUCKET, disables default encryption, modifies synthesizer. + * - Workaround for spoke account deployments: Uses actual bucket instead of + * ${ACCOUNT_ID} and ${ACCOUNT_REGION}, fixing S3 reference issues. + */ + const MODIFY_TEMPLATES = process.env.MODIFY_TEMPLATES === "true"; + const solutionBucket = MODIFY_TEMPLATES ? process.env.SOLUTION_BUCKET : undefined; + + const synthesizerProps = { generateBootstrapVersionRule: false, - }), - analyticsReporting: false, -}); + ...(solutionBucket ? { fileAssetsBucketName: solutionBucket } : {}), + }; + const synthesizer = new DefaultStackSynthesizer(synthesizerProps); + + new PreReqStack(app, `quota-monitor-prerequisite${props.targetPartition === "China" ? "-cn" : ""}`, { + synthesizer, + targetPartition: props.targetPartition, + }); + + new QuotaMonitorHub(app, `quota-monitor-hub${props.targetPartition === "China" ? "-cn" : ""}`, { + synthesizer, + targetPartition: props.targetPartition, + }); + + new QuotaMonitorHubNoOU(app, `quota-monitor-hub-no-ou${props.targetPartition === "China" ? "-cn" : ""}`, { + synthesizer, + targetPartition: props.targetPartition, + }); + + new QuotaMonitorTASpoke(app, `quota-monitor-ta-spoke${props.targetPartition === "China" ? "-cn" : ""}`, { + synthesizer, + targetPartition: props.targetPartition, + analyticsReporting: false, + }); + + new QuotaMonitorSQSpoke(app, `quota-monitor-sq-spoke${props.targetPartition === "China" ? "-cn" : ""}`, { + synthesizer, + targetPartition: props.targetPartition, + analyticsReporting: false, + }); + + new QuotaMonitorSnsSpoke(app, `quota-monitor-sns-spoke${props.targetPartition === "China" ? "-cn" : ""}`, { + synthesizer, + targetPartition: props.targetPartition, + analyticsReporting: false, + }); +} + +function main(): void { + const app = new App(); + const MODIFY_TEMPLATES = process.env.MODIFY_TEMPLATES === "true"; + + if (MODIFY_TEMPLATES) { + app.node.setContext("@aws-cdk/aws-s3-assets:disableDefaultEncryption", true); + } + + Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true })); + + addAppStacks(app, { targetPartition: "Commercial" }); + addAppStacks(app, { targetPartition: "China" }); +} -Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true })); +main(); diff --git a/source/resources/cdk.json b/source/resources/cdk.json old mode 100755 new mode 100644 diff --git a/source/resources/jest.config.ts b/source/resources/jest.config.ts index 22cf266..f3076a0 100644 --- a/source/resources/jest.config.ts +++ b/source/resources/jest.config.ts @@ -38,13 +38,7 @@ const config: Config = { verbose: true, // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: [ - "**/*.ts", - "!**/*.spec.ts", - "!./jest.config.ts", - "!./jest.setup.ts", - "!**/app.ts", - ], + collectCoverageFrom: ["**/*.ts", "!**/*.spec.ts", "!./jest.config.ts", "!./jest.setup.ts", "!**/app.ts"], coverageReporters: [["lcov", { projectRoot: "../" }], "text"], diff --git a/source/resources/lib/app-registry-application.ts b/source/resources/lib/app-registry-application.ts index 0c14c18..e04e7e2 100644 --- a/source/resources/lib/app-registry-application.ts +++ b/source/resources/lib/app-registry-application.ts @@ -6,45 +6,38 @@ import * as appreg from "@aws-cdk/aws-servicecatalogappregistry-alpha"; import { Construct } from "constructs"; export interface AppRegistryApplicationProps extends cdk.StackProps { - appRegistryApplicationName: string; - solutionId: string + appRegistryApplicationName: string; + solutionId: string; } export class AppRegistryApplication extends Construct { - constructor(scope: Construct, id: string, props: AppRegistryApplicationProps ) { + constructor(scope: Construct, id: string, props: AppRegistryApplicationProps) { super(scope, id); - + const application = new appreg.Application(this, "AppRegistryApplication", { - applicationName: Fn.join("-", [ - props.appRegistryApplicationName, - Aws.REGION, - Aws.ACCOUNT_ID, - ]), - description: `Service Catalog application to track and manage all your resources for the solution ${this.node.tryGetContext("SOLUTION_NAME")}`, + applicationName: Fn.join("-", [props.appRegistryApplicationName, Aws.REGION, Aws.ACCOUNT_ID]), + description: `Service Catalog application to track and manage all your resources for the solution ${this.node.tryGetContext( + "SOLUTION_NAME" + )}`, }); - application.associateApplicationWithStack(cdk.Stack.of(this)) + application.associateApplicationWithStack(cdk.Stack.of(this)); - application.addAttributeGroup('ApplicationAttributeGroup', { - attributeGroupName: Fn.join("-", [ - props.appRegistryApplicationName, - Aws.REGION, - Aws.ACCOUNT_ID, - ]), - description: "Attribute group for application information", - attributes: { - solutionID: props.solutionId, - solutionName: this.node.tryGetContext("SOLUTION_NAME"), - version: this.node.tryGetContext("SOLUTION_VERSION"), - applicationType: this.node.tryGetContext("APPLICATION_TYPE"), - } - }) + application.addAttributeGroup("ApplicationAttributeGroup", { + attributeGroupName: Fn.join("-", [props.appRegistryApplicationName, Aws.REGION, Aws.ACCOUNT_ID]), + description: "Attribute group for application information", + attributes: { + solutionID: props.solutionId, + solutionName: this.node.tryGetContext("SOLUTION_NAME"), + version: this.node.tryGetContext("SOLUTION_VERSION"), + applicationType: this.node.tryGetContext("APPLICATION_TYPE"), + }, + }); // Tags for application Tags.of(application).add("SolutionID", props.solutionId); Tags.of(application).add("SolutionName", this.node.tryGetContext("SOLUTION_NAME")); Tags.of(application).add("SolutionVersion", this.node.tryGetContext("SOLUTION_VERSION")); Tags.of(application).add("ApplicationType", this.node.tryGetContext("APPLICATION_TYPE")); - } + } } - diff --git a/source/resources/lib/cfn-guard-utils.ts b/source/resources/lib/cfn-guard-utils.ts new file mode 100644 index 0000000..fc2d342 --- /dev/null +++ b/source/resources/lib/cfn-guard-utils.ts @@ -0,0 +1,47 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { CfnResource, Stack } from "aws-cdk-lib"; +import { IConstruct } from "constructs"; +import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; + +export function addCfnGuardSuppression(construct: IConstruct, suppressions: string[]) { + let cfnResource: CfnResource | undefined; + + if (construct instanceof CfnResource) { + cfnResource = construct; + } else { + cfnResource = construct.node.defaultChild as CfnResource; + } + + if (!cfnResource || !cfnResource.cfnOptions) { + console.warn(`Unable to add cfn-guard suppression for ${construct.node.id}: not a CFN resource or no cfnOptions`); + return; + } + + const existingSuppressions: string[] = cfnResource.cfnOptions.metadata?.guard?.SuppressedRules || []; + cfnResource.cfnOptions.metadata = { + ...cfnResource.cfnOptions.metadata, + guard: { + SuppressedRules: [...existingSuppressions, ...suppressions], + }, + }; +} + +export function addCfnGuardSuppressionToNestedResources(construct: IConstruct, suppressions: string[]) { + const stack = Stack.of(construct); + stack.node.findAll().forEach((child) => { + if (child instanceof CfnResource && child.cfnResourceType === "AWS::Lambda::Function") { + addCfnGuardSuppression(child, suppressions); + } + }); +} + +export function addDynamoDbSuppressions(table: dynamodb.Table) { + const cfnTable = table.node.defaultChild as CfnResource; + if (cfnTable && cfnTable.cfnOptions) { + addCfnGuardSuppression(cfnTable, ["DYNAMODB_TABLE_ENCRYPTED_KMS"]); + } else { + console.warn(`Unable to add cfn-guard suppression for DynamoDB table: ${table.tableName}`); + } +} diff --git a/source/resources/lib/condition.utils.ts b/source/resources/lib/condition.utils.ts index 3ef5d6e..ee23a96 100644 --- a/source/resources/lib/condition.utils.ts +++ b/source/resources/lib/condition.utils.ts @@ -37,13 +37,9 @@ export class ConditionAspect implements IAspect { * @param resource - resource on which to apply condition, * @param condition - condition to apply */ -function applyCondition( - resource: Resource | CfnResource, - condition: CfnCondition -) { +function applyCondition(resource: Resource | CfnResource, condition: CfnCondition) { if (resource) { - if (resource instanceof Resource) - resource = resource.node.defaultChild as CfnResource; + if (resource instanceof Resource) resource = resource.node.defaultChild as CfnResource; resource.cfnOptions.condition = condition; } } diff --git a/source/resources/lib/custom-resource-lambda.construct.ts b/source/resources/lib/custom-resource-lambda.construct.ts index 21080bc..56767b6 100644 --- a/source/resources/lib/custom-resource-lambda.construct.ts +++ b/source/resources/lib/custom-resource-lambda.construct.ts @@ -1,13 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - aws_lambda as lambda, - Duration, - Stack, - custom_resources as cr, - CustomResource, -} from "aws-cdk-lib"; +import { aws_lambda as lambda, Duration, Stack, custom_resources as cr, CustomResource } from "aws-cdk-lib"; import { NagSuppressions } from "cdk-nag"; import { Construct, IConstruct } from "constructs"; import { LambdaProps, LAMBDA_RUNTIME_NODE, LOG_LEVEL } from "./exports"; @@ -27,10 +21,7 @@ interface ICRLambda { * @param properties - key,value pairs * @returns */ - addCustomResource( - identifier: string, - properties?: { [key: string]: string } - ): CustomResource; + addCustomResource(identifier: string, properties?: { [key: string]: string }): CustomResource; } /** @@ -46,9 +37,9 @@ export class CustomResourceLambda extends Construct implements ICRLambda { super(scope, id); this.function = new lambda.Function(this, `${id}-Function`, { - description: `${this.node.tryGetContext( - "SOLUTION_ID" - )} ${this.node.tryGetContext("SOLUTION_NAME")} - ${id}-Function`, + description: `${this.node.tryGetContext("SOLUTION_ID")} ${this.node.tryGetContext( + "SOLUTION_NAME" + )} - ${id}-Function`, code: lambda.Code.fromAsset(props.assetLocation), memorySize: props.memorySize || 128, timeout: props.timeout ? props.timeout : Duration.seconds(5), @@ -57,9 +48,9 @@ export class CustomResourceLambda extends Construct implements ICRLambda { environment: { ...props.environment, LOG_LEVEL: this.node.tryGetContext("LOG_LEVEL") || LOG_LEVEL.INFO, //change as needed - CUSTOM_SDK_USER_AGENT: `AwsSolution/${this.node.tryGetContext( - "SOLUTION_ID" - )}/${this.node.tryGetContext("SOLUTION_VERSION")}`, + CUSTOM_SDK_USER_AGENT: `AwsSolution/${this.node.tryGetContext("SOLUTION_ID")}/${this.node.tryGetContext( + "SOLUTION_VERSION" + )}`, VERSION: this.node.tryGetContext("SOLUTION_VERSION"), SOLUTION_ID: this.node.tryGetContext("SOLUTION_ID"), }, @@ -74,11 +65,9 @@ export class CustomResourceLambda extends Construct implements ICRLambda { // permission to use kms-cmk if (props.encryptionKey) { - KMS.getIAMPolicyStatementsToAccessKey(props.encryptionKey.keyArn).forEach( - (policyStatement) => { - this.function.addToRolePolicy(policyStatement); - } - ); + KMS.getIAMPolicyStatementsToAccessKey(props.encryptionKey.keyArn).forEach((policyStatement) => { + this.function.addToRolePolicy(policyStatement); + }); } // cdk-nag suppressions @@ -87,8 +76,7 @@ export class CustomResourceLambda extends Construct implements ICRLambda { [ { id: "AwsSolutions-IAM4", - reason: - "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + reason: "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", }, { id: "AwsSolutions-IAM5", @@ -103,8 +91,7 @@ export class CustomResourceLambda extends Construct implements ICRLambda { [ { id: "AwsSolutions-L1", - reason: - "GovCloud regions support only up to nodejs 16, risk is tolerable", + reason: "GovCloud regions support only up to nodejs 16, risk is tolerable", }, ], true @@ -114,28 +101,22 @@ export class CustomResourceLambda extends Construct implements ICRLambda { [ { id: "AwsSolutions-IAM4", - reason: - "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + reason: "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", }, { id: "AwsSolutions-IAM5", - reason: - "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", + reason: "IAM policy is appropriated scoped, ARN is provided in policy resource, false warning", }, { id: "AwsSolutions-L1", - reason: - "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", + reason: "Lambda function created by Provider L2 construct uses nodejs 14, risk is tolerable", }, ], true ); } - addCustomResource( - identifier: string, - properties?: { [key: string]: string } - ) { + addCustomResource(identifier: string, properties?: { [key: string]: string }) { const cr = new CustomResource(this, identifier, { resourceType: `Custom::${identifier}`, serviceToken: this.provider.serviceToken, diff --git a/source/resources/lib/depends.utils.ts b/source/resources/lib/depends.utils.ts index c197419..83eac5f 100644 --- a/source/resources/lib/depends.utils.ts +++ b/source/resources/lib/depends.utils.ts @@ -8,13 +8,9 @@ import { CfnResource, Resource } from "aws-cdk-lib"; * @param dependee * @param parent */ -export function applyDependsOn( - dependee: Resource | CfnResource, - parent: Resource -) { +export function applyDependsOn(dependee: Resource | CfnResource, parent: Resource) { if (dependee) { - if (dependee instanceof Resource) - dependee = dependee.node.defaultChild as CfnResource; + if (dependee instanceof Resource) dependee = dependee.node.defaultChild as CfnResource; dependee.addDependency(parent.node.defaultChild as CfnResource); } } diff --git a/source/resources/lib/enforce-SSL.utils.ts b/source/resources/lib/enforce-SSL.utils.ts index 6e837b6..a99ca98 100644 --- a/source/resources/lib/enforce-SSL.utils.ts +++ b/source/resources/lib/enforce-SSL.utils.ts @@ -15,9 +15,7 @@ export function enforceSSL(resource: Queue | Topic) { effect: iam.Effect.DENY, principals: [new iam.AnyPrincipal()], actions: [resource instanceof Queue ? "sqs:*" : "sns:Publish"], - resources: [ - resource instanceof Queue ? resource.queueArn : resource.topicArn, - ], + resources: [resource instanceof Queue ? resource.queueArn : resource.topicArn], conditions: { Bool: { "aws:SecureTransport": "false" }, }, diff --git a/source/resources/lib/events-lambda-sns.construct.ts b/source/resources/lib/events-lambda-sns.construct.ts index c1c3f32..0405ccf 100644 --- a/source/resources/lib/events-lambda-sns.construct.ts +++ b/source/resources/lib/events-lambda-sns.construct.ts @@ -1,21 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - IRuleToTarget, - LambdaProps, - QuotaMonitorEvent, - RuleTargetProps, -} from "./exports"; -import { - aws_events as events, - aws_iam as iam, - aws_lambda as lambda, - aws_sns as sns, - Stack, -} from "aws-cdk-lib"; +import { IRuleToTarget, LambdaProps, QuotaMonitorEvent, RuleTargetProps } from "./exports"; +import { aws_events as events, aws_iam as iam, aws_lambda as lambda, aws_sns as sns, Stack } from "aws-cdk-lib"; import { Construct } from "constructs"; import { EventsToLambda } from "./events-lambda.construct"; +import { Key } from "aws-cdk-lib/aws-kms"; /** * @description construct for events rule to lambda to sns @@ -37,11 +27,7 @@ export class EventsToLambdaToSNS * @param {string} id - unique id for the construct * @param {RuleTargetProps & LambdaProps} props - constructor props */ - constructor( - scope: Stack, - id: string, - props: RuleTargetProps & LambdaProps - ) { + constructor(scope: Stack, id: string, props: RuleTargetProps & LambdaProps) { /** * @description sns topic */ @@ -49,21 +35,17 @@ export class EventsToLambdaToSNS this.snsTopic = new sns.Topic(this, `${id}-SNSTopic`, { masterKey: props.encryptionKey, }); - const eventsToLambda = new EventsToLambda( - scope, - `${id}Function`, - { - assetLocation: props.assetLocation, - environment: { - ...props.environment, - TOPIC_ARN: this.snsTopic.topicArn, - }, - layers: props.layers, - eventRule: props.eventRule, - encryptionKey: props.encryptionKey, - eventBus: props.eventBus, - } - ); + const eventsToLambda = new EventsToLambda(scope, `${id}Function`, { + assetLocation: props.assetLocation, + environment: { + ...props.environment, + TOPIC_ARN: this.snsTopic.topicArn, + }, + layers: props.layers, + eventRule: props.eventRule, + ...(props.encryptionKey instanceof Key && { encryptionKey: props.encryptionKey }), + eventBus: props.eventBus, + }); this.target = eventsToLambda.target; this.rule = eventsToLambda.rule; diff --git a/source/resources/lib/events-lambda.construct.ts b/source/resources/lib/events-lambda.construct.ts index 66c1f5d..c063b59 100644 --- a/source/resources/lib/events-lambda.construct.ts +++ b/source/resources/lib/events-lambda.construct.ts @@ -26,10 +26,7 @@ import { KMS } from "./kms.construct"; /** * @description construct for events rule to lambda as target */ -export class EventsToLambda - extends Construct - implements IRuleToTarget -{ +export class EventsToLambda extends Construct implements IRuleToTarget { readonly rule: events.Rule; /** * @description target lambda function @@ -42,51 +39,36 @@ export class EventsToLambda * @param {string} id - unique id for the construct * @param {RuleTargetProps & LambdaProps} props - constructor props */ - constructor( - scope: Stack, - id: string, - props: RuleTargetProps & LambdaProps - ) { + constructor(scope: Stack, id: string, props: RuleTargetProps & LambdaProps) { super(scope, id); this.rule = new events.Rule(this, `${id}-EventsRule`, { - description: `${this.node.tryGetContext( - "SOLUTION_ID" - )} ${this.node.tryGetContext("SOLUTION_NAME")} - ${id}-EventsRule`, - schedule: - props.eventRule instanceof events.Schedule - ? props.eventRule - : undefined, - eventPattern: !(props.eventRule instanceof events.Schedule) - ? props.eventRule - : undefined, + description: `${this.node.tryGetContext("SOLUTION_ID")} ${this.node.tryGetContext( + "SOLUTION_NAME" + )} - ${id}-EventsRule`, + schedule: props.eventRule instanceof events.Schedule ? props.eventRule : undefined, + eventPattern: !(props.eventRule instanceof events.Schedule) ? props.eventRule : undefined, eventBus: props.eventBus, }); - const deadLetterQueue = new sqs.Queue( - this, - `${id}-Lambda-Dead-Letter-Queue`, - { - encryption: props.encryptionKey - ? QueueEncryption.KMS - : QueueEncryption.KMS_MANAGED, - encryptionMasterKey: props.encryptionKey, - } - ); + const deadLetterQueue = new sqs.Queue(this, `${id}-Lambda-Dead-Letter-Queue`, { + encryption: props.encryptionKey ? QueueEncryption.KMS : QueueEncryption.KMS_MANAGED, + encryptionMasterKey: props.encryptionKey, + }); enforceSSL(deadLetterQueue); this.target = new lambda.Function(this, `${id}-Lambda`, { - description: `${this.node.tryGetContext( - "SOLUTION_ID" - )} ${this.node.tryGetContext("SOLUTION_NAME")} - ${id}-Lambda`, + description: `${this.node.tryGetContext("SOLUTION_ID")} ${this.node.tryGetContext( + "SOLUTION_NAME" + )} - ${id}-Lambda`, runtime: LAMBDA_RUNTIME_NODE, code: lambda.Code.fromAsset(props.assetLocation), handler: "index.handler", environment: { ...props.environment, LOG_LEVEL: this.node.tryGetContext("LOG_LEVEL") || LOG_LEVEL.INFO, //change as needed - CUSTOM_SDK_USER_AGENT: `AwsSolution/${this.node.tryGetContext( - "SOLUTION_ID" - )}/${this.node.tryGetContext("SOLUTION_VERSION")}`, + CUSTOM_SDK_USER_AGENT: `AwsSolution/${this.node.tryGetContext("SOLUTION_ID")}/${this.node.tryGetContext( + "SOLUTION_VERSION" + )}`, VERSION: this.node.tryGetContext("SOLUTION_VERSION"), SOLUTION_ID: this.node.tryGetContext("SOLUTION_ID"), }, @@ -103,11 +85,9 @@ export class EventsToLambda // permissions to access KMS key if (props.encryptionKey) { - KMS.getIAMPolicyStatementsToAccessKey(props.encryptionKey.keyArn).forEach( - (policyStatement) => { - this.target.addToRolePolicy(policyStatement); - } - ); + KMS.getIAMPolicyStatementsToAccessKey(props.encryptionKey.keyArn).forEach((policyStatement) => { + this.target.addToRolePolicy(policyStatement); + }); } // cdk-nag suppressions @@ -116,8 +96,7 @@ export class EventsToLambda [ { id: "AwsSolutions-IAM4", - reason: - "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", + reason: "AWSLambdaBasicExecutionRole added by cdk only gives write permissions for CW logs", }, { id: "AwsSolutions-IAM5", @@ -132,8 +111,7 @@ export class EventsToLambda [ { id: "AwsSolutions-L1", - reason: - "GovCloud regions support only up to nodejs 16, risk is tolerable", + reason: "GovCloud regions support only up to nodejs 16, risk is tolerable", }, ], true diff --git a/source/resources/lib/events-sns.construct.ts b/source/resources/lib/events-sns.construct.ts index 8c2c57c..831c42b 100644 --- a/source/resources/lib/events-sns.construct.ts +++ b/source/resources/lib/events-sns.construct.ts @@ -1,12 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - aws_events as events, - aws_events_targets as targets, - aws_sns as sns, - Stack, -} from "aws-cdk-lib"; +import { aws_events as events, aws_events_targets as targets, aws_sns as sns, Stack } from "aws-cdk-lib"; import { Construct } from "constructs"; import { enforceSSL } from "./enforce-SSL.utils"; import { IRuleToTarget, QuotaMonitorEvent, RuleTargetProps } from "./exports"; @@ -14,10 +9,7 @@ import { IRuleToTarget, QuotaMonitorEvent, RuleTargetProps } from "./exports"; /** * @description construct for events rule to lambda as target */ -export class EventsToSNS - extends Construct - implements IRuleToTarget -{ +export class EventsToSNS extends Construct implements IRuleToTarget { readonly rule: events.Rule; /** * @description target sns topic @@ -32,16 +24,11 @@ export class EventsToSNS constructor(scope: Stack, id: string, props: RuleTargetProps) { super(scope, id); this.rule = new events.Rule(this, `${id}-EventsRule`, { - description: `${this.node.tryGetContext( - "SOLUTION_ID" - )} ${this.node.tryGetContext("SOLUTION_NAME")} - ${id}-EventsRule`, - schedule: - props.eventRule instanceof events.Schedule - ? props.eventRule - : undefined, - eventPattern: !(props.eventRule instanceof events.Schedule) - ? props.eventRule - : undefined, + description: `${this.node.tryGetContext("SOLUTION_ID")} ${this.node.tryGetContext( + "SOLUTION_NAME" + )} - ${id}-EventsRule`, + schedule: props.eventRule instanceof events.Schedule ? props.eventRule : undefined, + eventPattern: !(props.eventRule instanceof events.Schedule) ? props.eventRule : undefined, eventBus: props.eventBus, }); diff --git a/source/resources/lib/events-sqs.construct.ts b/source/resources/lib/events-sqs.construct.ts index f639536..eff4e80 100644 --- a/source/resources/lib/events-sqs.construct.ts +++ b/source/resources/lib/events-sqs.construct.ts @@ -1,13 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - aws_events as events, - aws_events_targets as targets, - aws_sqs as sqs, - Duration, - Stack, -} from "aws-cdk-lib"; +import { aws_events as events, aws_events_targets as targets, aws_sqs as sqs, Duration, Stack } from "aws-cdk-lib"; import { NagSuppressions } from "cdk-nag"; import { Construct } from "constructs"; import { enforceSSL } from "./enforce-SSL.utils"; @@ -16,10 +10,7 @@ import { IRuleToTarget, QuotaMonitorEvent, RuleTargetProps } from "./exports"; /** * @description construct for events rule to sqs to lambda as target */ -export class EventsToSQS - extends Construct - implements IRuleToTarget -{ +export class EventsToSQS extends Construct implements IRuleToTarget { readonly rule: events.Rule; /** * @description target sqs queue @@ -34,23 +25,16 @@ export class EventsToSQS constructor(scope: Stack, id: string, props: RuleTargetProps) { super(scope, id); this.rule = new events.Rule(this, `${id}-EventsRule`, { - description: `${this.node.tryGetContext( - "SOLUTION_ID" - )} ${this.node.tryGetContext("SOLUTION_NAME")} - ${id}-EventsRule`, - schedule: - props.eventRule instanceof events.Schedule - ? props.eventRule - : undefined, - eventPattern: !(props.eventRule instanceof events.Schedule) - ? props.eventRule - : undefined, + description: `${this.node.tryGetContext("SOLUTION_ID")} ${this.node.tryGetContext( + "SOLUTION_NAME" + )} - ${id}-EventsRule`, + schedule: props.eventRule instanceof events.Schedule ? props.eventRule : undefined, + eventPattern: !(props.eventRule instanceof events.Schedule) ? props.eventRule : undefined, eventBus: props.eventBus, }); this.target = new sqs.Queue(this, `${id}-Queue`, { - encryption: props.encryptionKey - ? sqs.QueueEncryption.KMS - : sqs.QueueEncryption.KMS_MANAGED, + encryption: props.encryptionKey ? sqs.QueueEncryption.KMS : sqs.QueueEncryption.KMS_MANAGED, encryptionMasterKey: props.encryptionKey, visibilityTimeout: Duration.seconds(60), }); @@ -63,8 +47,7 @@ export class EventsToSQS [ { id: "AwsSolutions-SQS3", - reason: - "dlq not implemented on sqs, will evaluate in future if there is need", + reason: "dlq not implemented on sqs, will evaluate in future if there is need", }, ], false diff --git a/source/resources/lib/exports.ts b/source/resources/lib/exports.ts index 61986ec..bd44f1a 100644 --- a/source/resources/lib/exports.ts +++ b/source/resources/lib/exports.ts @@ -8,6 +8,7 @@ import { aws_dynamodb as dynamodb, Duration, } from "aws-cdk-lib"; +import { IAlias } from "aws-cdk-lib/aws-kms"; import { ILayerVersion } from "aws-cdk-lib/aws-lambda"; /** @@ -54,13 +55,7 @@ export const TA_CHECKS_SERVICES = [ "VPC", ]; -export const SQ_CHECKS_SERVICES = [ - "monitoring", - "dynamodb", - "ec2", - "ecr", - "firehose", -]; +export const SQ_CHECKS_SERVICES = ["monitoring", "dynamodb", "ec2", "ecr", "firehose"]; /** * @description supported event rule types @@ -92,7 +87,7 @@ export interface RuleTargetProps { /** * @description kms key to be used for encryption */ - encryptionKey?: kms.Key; + encryptionKey?: kms.Key | IAlias; /** * @description event bus on to attach the rule to, if undefined rule will be attached to default bus */ @@ -136,7 +131,7 @@ export interface LambdaProps { /** * @description kms key to be used for encryption */ - encryptionKey?: kms.Key; + encryptionKey?: kms.Key | IAlias; /** * @description layers to apply for this function */ @@ -147,6 +142,12 @@ export interface LambdaProps { memorySize?: number; } +export interface LambdaTestSchemaProps { + lambdaFunctionName: string; + type: "OpenApi3" | "JSONSchemaDraft4"; + description?: string; + content: string; +} export interface DynamoDBProps { /** * @description pre-provisioned dynamodb table to use diff --git a/source/resources/lib/hub-no-ou.stack.ts b/source/resources/lib/hub-no-ou.stack.ts old mode 100755 new mode 100644 index 34b252a..f827555 --- a/source/resources/lib/hub-no-ou.stack.ts +++ b/source/resources/lib/hub-no-ou.stack.ts @@ -19,15 +19,13 @@ import { StackProps, } from "aws-cdk-lib"; import { Subscription } from "aws-cdk-lib/aws-sns"; +import { addCfnGuardSuppression, addCfnGuardSuppressionToNestedResources } from "./cfn-guard-utils"; import * as path from "path"; import { ConditionAspect } from "./condition.utils"; import { CustomResourceLambda } from "./custom-resource-lambda.construct"; import { EventsToLambda } from "./events-lambda.construct"; import { EventsToSQS } from "./events-sqs.construct"; -import { - EVENT_NOTIFICATION_DETAIL_TYPE, - EVENT_NOTIFICATION_SOURCES, -} from "./exports"; +import { EVENT_NOTIFICATION_DETAIL_TYPE, EVENT_NOTIFICATION_SOURCES } from "./exports"; import { Layer } from "./lambda-layer.construct"; import { EventsToLambdaToSNS } from "./events-lambda-sns.construct"; import { KMS } from "./kms.construct"; @@ -41,12 +39,16 @@ import { AppRegistryApplication } from "./app-registry-application"; * @author aws-solutions */ +interface QuotaMonitorHubNoOUProps extends StackProps { + targetPartition: "Commercial" | "China"; +} + export class QuotaMonitorHubNoOU extends Stack { /** * @param {App} scope - parent of the construct * @param {string} id - identifier for the object */ - constructor(scope: App, id: string, props?: StackProps) { + constructor(scope: App, id: string, props: QuotaMonitorHubNoOUProps) { super(scope, id, props); //============================================================================================= @@ -67,28 +69,14 @@ export class QuotaMonitorHubNoOU extends Stack { // Mapping & Conditions //============================================================================================= const map = new CfnMapping(this, "QuotaMonitorMap"); - map.setValue( - "Metrics", - "SendAnonymizedData", - this.node.tryGetContext("SEND_METRICS") - ); - map.setValue( - "Metrics", - "MetricsEndpoint", - this.node.tryGetContext("METRICS_ENDPOINT") - ); + map.setValue("Metrics", "SendAnonymizedData", this.node.tryGetContext("SEND_METRICS")); + map.setValue("Metrics", "MetricsEndpoint", this.node.tryGetContext("METRICS_ENDPOINT")); map.setValue("SSMParameters", "SlackHook", "/QuotaMonitor/SlackHook"); map.setValue("SSMParameters", "Accounts", "/QuotaMonitor/Accounts"); - map.setValue( - "SSMParameters", - "NotificationMutingConfig", - "/QuotaMonitor/NotificationConfiguration" - ); + map.setValue("SSMParameters", "NotificationMutingConfig", "/QuotaMonitor/NotificationConfiguration"); const emailTrue = new CfnCondition(this, "EmailTrueCondition", { - expression: Fn.conditionNot( - Fn.conditionEquals(snsEmail.valueAsString, "") - ), + expression: Fn.conditionNot(Fn.conditionEquals(snsEmail.valueAsString, "")), }); const slackTrue = new CfnCondition(this, "SlackTrueCondition", { @@ -118,9 +106,7 @@ export class QuotaMonitorHubNoOU extends Stack { }, }, }; - this.templateOptions.description = `(${this.node.tryGetContext( - "SOLUTION_ID" - )}-NoOU) - ${this.node.tryGetContext( + this.templateOptions.description = `(${this.node.tryGetContext("SOLUTION_ID")}-NoOU) - ${this.node.tryGetContext( "SOLUTION_NAME" )} - Hub Template, use it when you are not using AWS Organizations. Version ${this.node.tryGetContext( "SOLUTION_VERSION" @@ -172,20 +158,13 @@ export class QuotaMonitorHubNoOU extends Stack { * @description list of muted services and limits (quotas) for quota monitoring * value could be list of serviceCode[:quota_name|quota_code|resource] */ - const ssmNotificationMutingConfig = new ssm.StringListParameter( - this, - "QM-NotificationMutingConfig", - { - parameterName: map.findInMap( - "SSMParameters", - "NotificationMutingConfig" - ), - stringListValue: ["NOP"], - description: - "Muting configuration for services, limits e.g. ec2:L-1216C47A,ec2:Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances,dynamodb,logs:*,geo:L-05EFD12D", - simpleName: false, - } - ); + const ssmNotificationMutingConfig = new ssm.StringListParameter(this, "QM-NotificationMutingConfig", { + parameterName: map.findInMap("SSMParameters", "NotificationMutingConfig"), + stringListValue: ["NOP"], + description: + "Muting configuration for services, limits e.g. ec2:L-1216C47A,ec2:Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances,dynamodb,logs:*,geo:L-05EFD12D", + simpleName: false, + }); /** * @description utility layer for solution microservices @@ -206,14 +185,8 @@ export class QuotaMonitorHubNoOU extends Stack { detail: { status: ["WARN", "ERROR"], }, - detailType: [ - EVENT_NOTIFICATION_DETAIL_TYPE.TRUSTED_ADVISOR, - EVENT_NOTIFICATION_DETAIL_TYPE.SERVICE_QUOTA, - ], - source: [ - EVENT_NOTIFICATION_SOURCES.TRUSTED_ADVISOR, - EVENT_NOTIFICATION_SOURCES.SERVICE_QUOTA, - ], + detailType: [EVENT_NOTIFICATION_DETAIL_TYPE.TRUSTED_ADVISOR, EVENT_NOTIFICATION_DETAIL_TYPE.SERVICE_QUOTA], + source: [EVENT_NOTIFICATION_SOURCES.TRUSTED_ADVISOR, EVENT_NOTIFICATION_SOURCES.SERVICE_QUOTA], }; /** @@ -222,38 +195,54 @@ export class QuotaMonitorHubNoOU extends Stack { const slackNotifierSSMReadPolicy = new iam.PolicyStatement({ actions: ["ssm:GetParameter"], effect: iam.Effect.ALLOW, - resources: [ - ssmSlackHook.parameterArn, - ssmNotificationMutingConfig.parameterArn, - ], + resources: [ssmSlackHook.parameterArn, ssmNotificationMutingConfig.parameterArn], }); /** * @description construct for events-lambda */ - const slackNotifier = new EventsToLambda( - this, - "QM-SlackNotifier", - { - assetLocation: `${path.dirname( - __dirname - )}/../lambda/services/slackNotifier/dist/slack-notifier.zip`, - environment: { - SLACK_HOOK: map.findInMap("SSMParameters", "SlackHook"), - QM_NOTIFICATION_MUTING_CONFIG_PARAMETER: - ssmNotificationMutingConfig.parameterName, - }, - layers: [utilsLayer.layer], - eventRule: slackRulePattern, - eventBus: quotaMonitorBus, - encryptionKey: kms.key, - } - ); + const slackNotifier = new EventsToLambda(this, "QM-SlackNotifier", { + assetLocation: `${path.dirname(__dirname)}/../lambda/services/slackNotifier/dist/slack-notifier.zip`, + environment: { + SLACK_HOOK: map.findInMap("SSMParameters", "SlackHook"), + QM_NOTIFICATION_MUTING_CONFIG_PARAMETER: ssmNotificationMutingConfig.parameterName, + }, + layers: [utilsLayer.layer], + eventRule: slackRulePattern, + eventBus: quotaMonitorBus, + encryptionKey: kms.key, + }); + addCfnGuardSuppression(slackNotifier.target, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); + slackNotifier.target.addToRolePolicy(slackNotifierSSMReadPolicy); // applying condition on all child nodes Aspects.of(slackNotifier).add(new ConditionAspect(slackTrue)); + //=========================== + // Solution helper components + //=========================== + /** + * @description construct to deploy lambda backed custom resource + */ + const helper = new CustomResourceLambda(this, "QM-Helper", { + assetLocation: `${path.dirname(__dirname)}/../lambda/services/helper/dist/helper.zip`, + layers: [utilsLayer.layer], + environment: { + METRICS_ENDPOINT: map.findInMap("Metrics", "MetricsEndpoint"), + SEND_METRIC: map.findInMap("Metrics", "SendAnonymizedData"), + QM_STACK_ID: id, + }, + }); + addCfnGuardSuppression(helper.function, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); + addCfnGuardSuppressionToNestedResources(helper, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); + + // Custom resources + const createUUID = helper.addCustomResource("CreateUUID"); + helper.addCustomResource("LaunchData", { + SOLUTION_UUID: createUUID.getAttString("UUID"), + }); + //======================= // SNS workflow component //======================= @@ -264,14 +253,8 @@ export class QuotaMonitorHubNoOU extends Stack { detail: { status: ["WARN", "ERROR"], }, - detailType: [ - EVENT_NOTIFICATION_DETAIL_TYPE.TRUSTED_ADVISOR, - EVENT_NOTIFICATION_DETAIL_TYPE.SERVICE_QUOTA, - ], - source: [ - EVENT_NOTIFICATION_SOURCES.TRUSTED_ADVISOR, - EVENT_NOTIFICATION_SOURCES.SERVICE_QUOTA, - ], + detailType: [EVENT_NOTIFICATION_DETAIL_TYPE.TRUSTED_ADVISOR, EVENT_NOTIFICATION_DETAIL_TYPE.SERVICE_QUOTA], + source: [EVENT_NOTIFICATION_SOURCES.TRUSTED_ADVISOR, EVENT_NOTIFICATION_SOURCES.SERVICE_QUOTA], }; /** @@ -287,23 +270,20 @@ export class QuotaMonitorHubNoOU extends Stack { * @description construct for events-lambda */ - const snsPublisher = new EventsToLambdaToSNS( - this, - "QM-SNSPublisher", - { - assetLocation: `${path.dirname( - __dirname - )}/../lambda/services/snsPublisher/dist/sns-publisher.zip`, - environment: { - QM_NOTIFICATION_MUTING_CONFIG_PARAMETER: - ssmNotificationMutingConfig.parameterName, - }, - layers: [utilsLayer.layer], - eventRule: snsRulePattern, - eventBus: quotaMonitorBus, - encryptionKey: kms.key, - } - ); + const snsPublisher = new EventsToLambdaToSNS(this, "QM-SNSPublisher", { + assetLocation: `${path.dirname(__dirname)}/../lambda/services/snsPublisher/dist/sns-publisher.zip`, + environment: { + QM_NOTIFICATION_MUTING_CONFIG_PARAMETER: ssmNotificationMutingConfig.parameterName, + SOLUTION_UUID: createUUID.getAttString("UUID"), + METRICS_ENDPOINT: map.findInMap("Metrics", "MetricsEndpoint"), + SEND_METRIC: map.findInMap("Metrics", "SendAnonymizedData"), + }, + layers: [utilsLayer.layer], + eventRule: snsRulePattern, + eventBus: quotaMonitorBus, + encryptionKey: kms.key, + }); + addCfnGuardSuppression(snsPublisher.target, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); snsPublisher.target.addToRolePolicy(snsPublisherSSMReadPolicy); @@ -329,28 +309,18 @@ export class QuotaMonitorHubNoOU extends Stack { detail: { status: ["OK", "WARN", "ERROR"], }, - detailType: [ - EVENT_NOTIFICATION_DETAIL_TYPE.TRUSTED_ADVISOR, - EVENT_NOTIFICATION_DETAIL_TYPE.SERVICE_QUOTA, - ], - source: [ - EVENT_NOTIFICATION_SOURCES.TRUSTED_ADVISOR, - EVENT_NOTIFICATION_SOURCES.SERVICE_QUOTA, - ], + detailType: [EVENT_NOTIFICATION_DETAIL_TYPE.TRUSTED_ADVISOR, EVENT_NOTIFICATION_DETAIL_TYPE.SERVICE_QUOTA], + source: [EVENT_NOTIFICATION_SOURCES.TRUSTED_ADVISOR, EVENT_NOTIFICATION_SOURCES.SERVICE_QUOTA], }; /** * @description construct for event-sqs */ - const summarizerEventQueue = new EventsToSQS( - this, - "QM-Summarizer-EventQueue", - { - eventRule: summarizerRulePattern, - encryptionKey: kms.key, - eventBus: quotaMonitorBus, - } - ); + const summarizerEventQueue = new EventsToSQS(this, "QM-Summarizer-EventQueue", { + eventRule: summarizerRulePattern, + encryptionKey: kms.key, + eventBus: quotaMonitorBus, + }); /** * @description quota summary dynamodb table @@ -374,26 +344,21 @@ export class QuotaMonitorHubNoOU extends Stack { /** * @description event-lambda construct for capturing quota summary */ - const summarizer = new EventsToLambda( - this, - "QM-Reporter", - { - eventRule: events.Schedule.rate(Duration.minutes(5)), - encryptionKey: kms.key, - assetLocation: `${path.dirname( - __dirname - )}/../lambda/services/reporter/dist/reporter.zip`, - environment: { - QUOTA_TABLE: summaryTable.tableName, - SQS_URL: summarizerEventQueue.target.queueUrl, - MAX_MESSAGES: "10", //100 messages can be read with each invocation, change as needed - MAX_LOOPS: "10", - }, - memorySize: 512, - timeout: Duration.seconds(10), - layers: [utilsLayer.layer], - } - ); + const summarizer = new EventsToLambda(this, "QM-Reporter", { + eventRule: events.Schedule.rate(Duration.minutes(5)), + encryptionKey: kms.key, + assetLocation: `${path.dirname(__dirname)}/../lambda/services/reporter/dist/reporter.zip`, + environment: { + QUOTA_TABLE: summaryTable.tableName, + SQS_URL: summarizerEventQueue.target.queueUrl, + MAX_MESSAGES: "10", //100 messages can be read with each invocation, change as needed + MAX_LOOPS: "10", + }, + memorySize: 512, + timeout: Duration.seconds(10), + layers: [utilsLayer.layer], + }); + addCfnGuardSuppression(summarizer.target, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); // adding queue permissions to summarizer lambda function summarizer.target.addToRolePolicy( @@ -428,25 +393,20 @@ export class QuotaMonitorHubNoOU extends Stack { /** * @description construct for events-lambda */ - const deploymentManager = new EventsToLambda( - this, - "QM-Deployment-Manager", - { - eventRule: ssmRulePattern, - encryptionKey: kms.key, - assetLocation: `${path.dirname( - __dirname - )}/../lambda/services/deploymentManager/dist/deployment-manager.zip`, - environment: { - EVENT_BUS_NAME: quotaMonitorBus.eventBusName, - EVENT_BUS_ARN: quotaMonitorBus.eventBusArn, - QM_ACCOUNT_PARAMETER: ssmQMAccounts.parameterName, - DEPLOYMENT_MODEL: "Accounts", - }, - layers: [utilsLayer.layer], - memorySize: 512, - } - ); + const deploymentManager = new EventsToLambda(this, "QM-Deployment-Manager", { + eventRule: ssmRulePattern, + encryptionKey: kms.key, + assetLocation: `${path.dirname(__dirname)}/../lambda/services/deploymentManager/dist/deployment-manager.zip`, + environment: { + EVENT_BUS_NAME: quotaMonitorBus.eventBusName, + EVENT_BUS_ARN: quotaMonitorBus.eventBusArn, + QM_ACCOUNT_PARAMETER: ssmQMAccounts.parameterName, + DEPLOYMENT_MODEL: "Accounts", + }, + layers: [utilsLayer.layer], + memorySize: 512, + }); + addCfnGuardSuppression(deploymentManager.target, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); /** * @description policy statement to allow CRUD on event bus permissions @@ -482,44 +442,18 @@ export class QuotaMonitorHubNoOU extends Stack { actions: ["support:DescribeTrustedAdvisorChecks"], resources: ["*"], // does not allow resource-level permissions }); - deploymentManager.target.addToRolePolicy( - taDescribeTrustedAdvisorChecksPolicy - ); - - //=========================== - // Solution helper components - //=========================== - /** - * @description construct to deploy lambda backed custom resource - */ - const helper = new CustomResourceLambda(this, "QM-Helper", { - assetLocation: `${path.dirname( - __dirname - )}/../lambda/services/helper/dist/helper.zip`, - layers: [utilsLayer.layer], - environment: { - METRICS_ENDPOINT: map.findInMap("Metrics", "MetricsEndpoint"), - SEND_METRIC: map.findInMap("Metrics", "SendAnonymizedData"), - QM_STACK_ID: id, - }, - }); - - // Custom resources - const createUUID = helper.addCustomResource("CreateUUID"); - helper.addCustomResource("LaunchData", { - SOLUTION_UUID: createUUID.getAttString("UUID"), - }); + deploymentManager.target.addToRolePolicy(taDescribeTrustedAdvisorChecksPolicy); /** * app registry application for hub-no-ou-stack */ - new AppRegistryApplication(this, "HubNoOUAppRegistryApplication", { - appRegistryApplicationName: this.node.tryGetContext( - "APP_REG_HUB_NO_OU_APPLICATION_NAME" - ), - solutionId: `${this.node.tryGetContext("SOLUTION_ID")}-NoOU`, - }); + if (props.targetPartition !== "China") { + new AppRegistryApplication(this, "HubNoOUAppRegistryApplication", { + appRegistryApplicationName: this.node.tryGetContext("APP_REG_HUB_NO_OU_APPLICATION_NAME"), + solutionId: `${this.node.tryGetContext("SOLUTION_ID")}-NoOU`, + }); + } //============================================================================================= // Outputs @@ -527,8 +461,7 @@ export class QuotaMonitorHubNoOU extends Stack { new CfnOutput(this, "SlackHookKey", { condition: slackTrue, value: map.findInMap("SSMParameters", "SlackHook"), - description: - "SSM parameter for Slack Web Hook, change the value for your slack workspace", + description: "SSM parameter for Slack Web Hook, change the value for your slack workspace", }); new CfnOutput(this, "UUID", { diff --git a/source/resources/lib/hub.stack.ts b/source/resources/lib/hub.stack.ts old mode 100755 new mode 100644 index 20fbaa6..2fe73f3 --- a/source/resources/lib/hub.stack.ts +++ b/source/resources/lib/hub.stack.ts @@ -24,14 +24,12 @@ import { } from "aws-cdk-lib"; import { Subscription } from "aws-cdk-lib/aws-sns"; import * as path from "path"; +import { addCfnGuardSuppression, addCfnGuardSuppressionToNestedResources } from "./cfn-guard-utils"; import { ConditionAspect } from "./condition.utils"; import { CustomResourceLambda } from "./custom-resource-lambda.construct"; import { EventsToLambda } from "./events-lambda.construct"; import { EventsToSQS } from "./events-sqs.construct"; -import { - EVENT_NOTIFICATION_DETAIL_TYPE, - EVENT_NOTIFICATION_SOURCES, -} from "./exports"; +import { EVENT_NOTIFICATION_DETAIL_TYPE, EVENT_NOTIFICATION_SOURCES } from "./exports"; import { Layer } from "./lambda-layer.construct"; import { EventsToLambdaToSNS } from "./events-lambda-sns.construct"; import { KMS } from "./kms.construct"; @@ -44,12 +42,17 @@ import { AppRegistryApplication } from "./app-registry-application"; * @author aws-solutions */ +interface QuotaMonitorHubProps extends StackProps { + targetPartition: "Commercial" | "China"; +} + export class QuotaMonitorHub extends Stack { /** * @param {App} scope - parent of the construct * @param {string} id - identifier for the object */ - constructor(scope: App, id: string, props?: StackProps) { + private isChinaPartition: CfnCondition; + constructor(scope: App, id: string, props: QuotaMonitorHubProps) { super(scope, id, props); //============================================================================================= @@ -72,132 +75,111 @@ export class QuotaMonitorHub extends Stack { }); const managementAccountId = new CfnParameter(this, "ManagementAccountId", { - description: - "AWS Account Id for the organization's management account or *", + description: "AWS Account Id for the organization's management account or *", type: "String", allowedPattern: "^([0-9]{1}\\d{11})|\\*$", default: "*", }); const regionsListCfnParam = new CfnParameter(this, "RegionsList", { - description: - "Comma separated list of regions like us-east-1,us-east-2 or ALL or leave it blank for ALL", + description: "Comma separated list of regions like us-east-1,us-east-2 or ALL or leave it blank for ALL", default: "ALL", }); - const stackSetRegionConcurrencyType = new CfnParameter( - this, - "RegionConcurrency", - { - allowedValues: ["PARALLEL", "SEQUENTIAL"], - default: "PARALLEL", - description: - "Choose to deploy StackSets into regions sequentially or in parallel", - } - ); + const snsSpokeRegion = new CfnParameter(this, "SnsSpokeRegion", { + description: + "The region in which to launch the SNS stack in each spoke account. Leave blank if the spoke SNS is not needed", + type: "String", + default: "", + }); - const stackSetMaxConcurrentPercentage = new CfnParameter( - this, - "MaxConcurrentPercentage", - { - type: "Number", - default: 100, - minValue: 1, - maxValue: 100, - description: - "Percentage of accounts per region to which you can deploy stacks at one time. The higher the number, the faster the operation", - } - ); + const stackSetRegionConcurrencyType = new CfnParameter(this, "RegionConcurrency", { + allowedValues: ["PARALLEL", "SEQUENTIAL"], + default: "PARALLEL", + description: "Choose to deploy StackSets into regions sequentially or in parallel", + }); - const stackSetFailureTolerancePercentage = new CfnParameter( - this, - "FailureTolerancePercentage", - { - type: "Number", - default: 0, - minValue: 0, - maxValue: 100, - description: - "Percentage of account, per region, for which stacks can fail before CloudFormation stops the operation in that region. If the operation is stopped in one region, it does not continue in other regions. The lower the number the safer the operation", - } - ); + const stackSetMaxConcurrentPercentage = new CfnParameter(this, "MaxConcurrentPercentage", { + type: "Number", + default: 100, + minValue: 1, + maxValue: 100, + description: + "Percentage of accounts per region to which you can deploy stacks at one time. The higher the number, the faster the operation", + }); - const sqNotificationThreshold = new CfnParameter( - this, - "SQNotificationThreshold", - { - type: "String", - default: "80", - allowedValues: ["60", "70", "80"], - } - ); + const stackSetFailureTolerancePercentage = new CfnParameter(this, "FailureTolerancePercentage", { + type: "Number", + default: 0, + minValue: 0, + maxValue: 100, + description: + "Percentage of account, per region, for which stacks can fail before CloudFormation stops the operation in that region. If the operation is stopped in one region, it does not continue in other regions. The lower the number the safer the operation", + }); - const sqMonitoringFrequency = new CfnParameter( - this, - "SQMonitoringFrequency", - { - type: "String", - default: "rate(12 hours)", - allowedValues: ["rate(6 hours)", "rate(12 hours)"], - } - ); + const sqNotificationThreshold = new CfnParameter(this, "SQNotificationThreshold", { + type: "String", + default: "80", + description: "Threshold percentage for quota utilization alerts (0-100)", + allowedPattern: "^([1-9]|[1-9][0-9])$", + constraintDescription: "Threshold must be a whole number between 0 and 100", + }); - const sqReportOKNotifications = new CfnParameter( - this, - "SQReportOKNotifications", - { - type: "String", - default: "No", - allowedValues: ["Yes", "No"], - } - ); + const sqMonitoringFrequency = new CfnParameter(this, "SQMonitoringFrequency", { + type: "String", + default: "rate(12 hours)", + allowedValues: ["rate(6 hours)", "rate(12 hours)", "rate(1 day)"], + }); + + const sqReportOKNotifications = new CfnParameter(this, "SQReportOKNotifications", { + type: "String", + default: "No", + allowedValues: ["Yes", "No"], + }); + + const sageMakerMonitoring = new CfnParameter(this, "SageMakerMonitoring", { + type: "String", + default: "Yes", + allowedValues: ["Yes", "No"], + description: + "Enable monitoring for SageMaker quotas. NOTE: (1) SageMaker monitoring consumes a high number of quotas, potentially resulting in higher usage cost. (2) Changing this value during a stack update will affect all spoke accounts but if left unchanged, it preserves existing spoke accounts customizations.", + }); + + const connectMonitoring = new CfnParameter(this, "ConnectMonitoring", { + type: "String", + default: "Yes", + allowedValues: ["Yes", "No"], + description: + "Enable monitoring for Connect quotas. NOTE: (1) Connect monitoring consumes a high number of quotas, potentially resulting in higher usage cost. (2) Changing this value during a stack update will affect all spoke accounts but if left unchanged, it preserves existing spoke accounts customizations.", + }); //============================================================================================= // Mapping & Conditions //============================================================================================= const map = new CfnMapping(this, "QuotaMonitorMap"); - map.setValue( - "Metrics", - "SendAnonymizedData", - this.node.tryGetContext("SEND_METRICS") - ); - map.setValue( - "Metrics", - "MetricsEndpoint", - this.node.tryGetContext("METRICS_ENDPOINT") - ); + map.setValue("Metrics", "SendAnonymizedData", this.node.tryGetContext("SEND_METRICS")); + map.setValue("Metrics", "MetricsEndpoint", this.node.tryGetContext("METRICS_ENDPOINT")); map.setValue("SSMParameters", "SlackHook", "/QuotaMonitor/SlackHook"); map.setValue("SSMParameters", "Accounts", "/QuotaMonitor/Accounts"); map.setValue("SSMParameters", "OrganizationalUnits", "/QuotaMonitor/OUs"); - map.setValue( - "SSMParameters", - "NotificationMutingConfig", - "/QuotaMonitor/NotificationConfiguration" - ); - map.setValue( - "SSMParameters", - "RegionsList", - "/QuotaMonitor/RegionsToDeploy" - ); + map.setValue("SSMParameters", "NotificationMutingConfig", "/QuotaMonitor/NotificationConfiguration"); + map.setValue("SSMParameters", "RegionsList", "/QuotaMonitor/RegionsToDeploy"); const emailTrue = new CfnCondition(this, "EmailTrueCondition", { - expression: Fn.conditionNot( - Fn.conditionEquals(snsEmail.valueAsString, "") - ), + expression: Fn.conditionNot(Fn.conditionEquals(snsEmail.valueAsString, "")), }); const slackTrue = new CfnCondition(this, "SlackTrueCondition", { expression: Fn.conditionEquals(slackNotification.valueAsString, "Yes"), }); - const accountDeployCondition = new CfnCondition( - this, - "AccountDeployCondition", - { - expression: Fn.conditionEquals(deploymentModel, "Hybrid"), - } - ); + const accountDeployCondition = new CfnCondition(this, "AccountDeployCondition", { + expression: Fn.conditionEquals(deploymentModel, "Hybrid"), + }); + this.isChinaPartition = new CfnCondition(this, "IsChinaPartition", { + expression: Fn.conditionEquals(Aws.PARTITION, "aws-cn"), + }); //============================================================================================= // Metadata //============================================================================================= @@ -208,21 +190,13 @@ export class QuotaMonitorHub extends Stack { Label: { default: "Deployment Configuration", }, - Parameters: [ - "DeploymentModel", - "RegionsList", - "ManagementAccountId", - ], + Parameters: ["DeploymentModel", "RegionsList", "SnsSpokeRegion", "ManagementAccountId"], }, { Label: { default: "Stackset Deployment Options", }, - Parameters: [ - "RegionConcurrency", - "MaxConcurrentPercentage", - "FailureTolerancePercentage", - ], + Parameters: ["RegionConcurrency", "MaxConcurrentPercentage", "FailureTolerancePercentage"], }, { Label: { @@ -238,13 +212,14 @@ export class QuotaMonitorHub extends Stack { "SQNotificationThreshold", "SQMonitoringFrequency", "SQReportOKNotifications", + "SageMakerMonitoring", + "ConnectMonitoring", ], }, ], ParameterLabels: { DeploymentModel: { - default: - "Do you want to monitor quotas across Organizational Units, Accounts or both?", + default: "Do you want to monitor quotas across Organizational Units, Accounts or both?", }, SNSEmail: { default: "Email address for notifications", @@ -253,12 +228,13 @@ export class QuotaMonitorHub extends Stack { default: "Do you want slack notifications?", }, ManagementAccountId: { - default: - "Organization's management Id to scope permissions down for Stackset creation", + default: "Organization's management Id to scope permissions down for Stackset creation", }, RegionsList: { - default: - "List of regions to deploy resources to monitor service quotas", + default: "List of regions to deploy resources to monitor service quotas", + }, + SnsSpokeRegion: { + default: "Region in which to launch the SNS stack in the spoke accounts.", }, RegionConcurrencyType: { default: "Region Concurrency", @@ -278,12 +254,16 @@ export class QuotaMonitorHub extends Stack { SQReportOKNotifications: { default: "Report OK Notifications", }, + SageMakerMonitoring: { + default: "Enable monitoring for SageMaker quotas", + }, + ConnectMonitoring: { + default: "Enable monitoring for Connect quotas", + }, }, }, }; - this.templateOptions.description = `(${this.node.tryGetContext( - "SOLUTION_ID" - )}) - ${this.node.tryGetContext( + this.templateOptions.description = `(${this.node.tryGetContext("SOLUTION_ID")}) - ${this.node.tryGetContext( "SOLUTION_NAME" )} - Hub Template. Version ${this.node.tryGetContext("SOLUTION_VERSION")}`; this.templateOptions.templateFormatVersion = "2010-09-09"; @@ -345,28 +325,20 @@ export class QuotaMonitorHub extends Stack { * @description list of muted services and limits (quotas) for quota monitoring * value could be list of serviceCode[:quota_name|quota_code|resource] */ - const ssmNotificationMutingConfig = new ssm.StringListParameter( - this, - "QM-NotificationMutingConfig", - { - parameterName: map.findInMap( - "SSMParameters", - "NotificationMutingConfig" - ), - stringListValue: ["NOP"], - description: - "Muting configuration for services, limits e.g. ec2:L-1216C47A,ec2:Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances,dynamodb,logs:*,geo:L-05EFD12D", - simpleName: false, - } - ); + const ssmNotificationMutingConfig = new ssm.StringListParameter(this, "QM-NotificationMutingConfig", { + parameterName: map.findInMap("SSMParameters", "NotificationMutingConfig"), + stringListValue: ["NOP"], + description: + "Muting configuration for services, limits e.g. ec2:L-1216C47A,ec2:Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances,dynamodb,logs:*,geo:L-05EFD12D", + simpleName: false, + }); /** * @description list of regions to deploy spoke resources */ const ssmRegionsList = new ssm.StringListParameter(this, "QM-RegionsList", { parameterName: map.findInMap("SSMParameters", "RegionsList"), - description: - "list of regions to deploy spoke resources (eg. us-east-1,us-west-2)", + description: "list of regions to deploy spoke resources (eg. us-east-1,us-west-2)", stringListValue: regionsListCfnParam.valueAsString.split(","), //initialize it with the template parameter simpleName: false, }); @@ -387,22 +359,20 @@ export class QuotaMonitorHub extends Stack { * @description construct to deploy lambda backed custom resource */ const helper = new CustomResourceLambda(this, "QM-Helper", { - assetLocation: `${path.dirname( - __dirname - )}/../lambda/services/helper/dist/helper.zip`, + assetLocation: `${path.dirname(__dirname)}/../lambda/services/helper/dist/helper.zip`, layers: [utilsLayer.layer], environment: { METRICS_ENDPOINT: map.findInMap("Metrics", "MetricsEndpoint"), SEND_METRIC: map.findInMap("Metrics", "SendAnonymizedData"), QM_STACK_ID: id, QM_SLACK_NOTIFICATION: slackNotification.valueAsString, - QM_EMAIL_NOTIFICATION: Fn.conditionIf( - "EmailTrueCondition", - "Yes", - "No" - ).toString(), + QM_EMAIL_NOTIFICATION: Fn.conditionIf("EmailTrueCondition", "Yes", "No").toString(), + SAGEMAKER_MONITORING: sageMakerMonitoring.valueAsString, + CONNECT_MONITORING: connectMonitoring.valueAsString, }, }); + addCfnGuardSuppression(helper.function, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); + addCfnGuardSuppressionToNestedResources(helper, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); // Custom resources const createUUID = helper.addCustomResource("CreateUUID"); @@ -420,14 +390,8 @@ export class QuotaMonitorHub extends Stack { detail: { status: ["WARN", "ERROR"], }, - detailType: [ - EVENT_NOTIFICATION_DETAIL_TYPE.TRUSTED_ADVISOR, - EVENT_NOTIFICATION_DETAIL_TYPE.SERVICE_QUOTA, - ], - source: [ - EVENT_NOTIFICATION_SOURCES.TRUSTED_ADVISOR, - EVENT_NOTIFICATION_SOURCES.SERVICE_QUOTA, - ], + detailType: [EVENT_NOTIFICATION_DETAIL_TYPE.TRUSTED_ADVISOR, EVENT_NOTIFICATION_DETAIL_TYPE.SERVICE_QUOTA], + source: [EVENT_NOTIFICATION_SOURCES.TRUSTED_ADVISOR, EVENT_NOTIFICATION_SOURCES.SERVICE_QUOTA], }; /** @@ -436,33 +400,24 @@ export class QuotaMonitorHub extends Stack { const slackNotifierSSMReadPolicy = new iam.PolicyStatement({ actions: ["ssm:GetParameter"], effect: iam.Effect.ALLOW, - resources: [ - ssmSlackHook.parameterArn, - ssmNotificationMutingConfig.parameterArn, - ], + resources: [ssmSlackHook.parameterArn, ssmNotificationMutingConfig.parameterArn], }); /** * @description construct for events-lambda */ - const slackNotifier = new EventsToLambda( - this, - "QM-SlackNotifier", - { - assetLocation: `${path.dirname( - __dirname - )}/../lambda/services/slackNotifier/dist/slack-notifier.zip`, - environment: { - SLACK_HOOK: map.findInMap("SSMParameters", "SlackHook"), - QM_NOTIFICATION_MUTING_CONFIG_PARAMETER: - ssmNotificationMutingConfig.parameterName, - }, - layers: [utilsLayer.layer], - eventRule: slackRulePattern, - eventBus: quotaMonitorBus, - encryptionKey: kms.key, - } - ); + const slackNotifier = new EventsToLambda(this, "QM-SlackNotifier", { + assetLocation: `${path.dirname(__dirname)}/../lambda/services/slackNotifier/dist/slack-notifier.zip`, + environment: { + SLACK_HOOK: map.findInMap("SSMParameters", "SlackHook"), + QM_NOTIFICATION_MUTING_CONFIG_PARAMETER: ssmNotificationMutingConfig.parameterName, + }, + layers: [utilsLayer.layer], + eventRule: slackRulePattern, + eventBus: quotaMonitorBus, + encryptionKey: kms.key, + }); + addCfnGuardSuppression(slackNotifier.target, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); slackNotifier.target.addToRolePolicy(slackNotifierSSMReadPolicy); // applying condition on all child nodes @@ -478,14 +433,8 @@ export class QuotaMonitorHub extends Stack { detail: { status: ["WARN", "ERROR"], }, - detailType: [ - EVENT_NOTIFICATION_DETAIL_TYPE.TRUSTED_ADVISOR, - EVENT_NOTIFICATION_DETAIL_TYPE.SERVICE_QUOTA, - ], - source: [ - EVENT_NOTIFICATION_SOURCES.TRUSTED_ADVISOR, - EVENT_NOTIFICATION_SOURCES.SERVICE_QUOTA, - ], + detailType: [EVENT_NOTIFICATION_DETAIL_TYPE.TRUSTED_ADVISOR, EVENT_NOTIFICATION_DETAIL_TYPE.SERVICE_QUOTA], + source: [EVENT_NOTIFICATION_SOURCES.TRUSTED_ADVISOR, EVENT_NOTIFICATION_SOURCES.SERVICE_QUOTA], }; /** @@ -501,26 +450,20 @@ export class QuotaMonitorHub extends Stack { * @description construct for events-lambda */ - const snsPublisher = new EventsToLambdaToSNS( - this, - "QM-SNSPublisher", - { - assetLocation: `${path.dirname( - __dirname - )}/../lambda/services/snsPublisher/dist/sns-publisher.zip`, - environment: { - QM_NOTIFICATION_MUTING_CONFIG_PARAMETER: - ssmNotificationMutingConfig.parameterName, - SOLUTION_UUID: createUUID.getAttString("UUID"), - METRICS_ENDPOINT: map.findInMap("Metrics", "MetricsEndpoint"), - SEND_METRIC: map.findInMap("Metrics", "SendAnonymizedData"), - }, - layers: [utilsLayer.layer], - eventRule: snsRulePattern, - eventBus: quotaMonitorBus, - encryptionKey: kms.key, - } - ); + const snsPublisher = new EventsToLambdaToSNS(this, "QM-SNSPublisher", { + assetLocation: `${path.dirname(__dirname)}/../lambda/services/snsPublisher/dist/sns-publisher.zip`, + environment: { + QM_NOTIFICATION_MUTING_CONFIG_PARAMETER: ssmNotificationMutingConfig.parameterName, + SOLUTION_UUID: createUUID.getAttString("UUID"), + METRICS_ENDPOINT: map.findInMap("Metrics", "MetricsEndpoint"), + SEND_METRIC: map.findInMap("Metrics", "SendAnonymizedData"), + }, + layers: [utilsLayer.layer], + eventRule: snsRulePattern, + eventBus: quotaMonitorBus, + encryptionKey: kms.key, + }); + addCfnGuardSuppression(snsPublisher.target, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); snsPublisher.target.addToRolePolicy(snsPublisherSSMReadPolicy); @@ -546,28 +489,18 @@ export class QuotaMonitorHub extends Stack { detail: { status: ["OK", "WARN", "ERROR"], }, - detailType: [ - EVENT_NOTIFICATION_DETAIL_TYPE.TRUSTED_ADVISOR, - EVENT_NOTIFICATION_DETAIL_TYPE.SERVICE_QUOTA, - ], - source: [ - EVENT_NOTIFICATION_SOURCES.TRUSTED_ADVISOR, - EVENT_NOTIFICATION_SOURCES.SERVICE_QUOTA, - ], + detailType: [EVENT_NOTIFICATION_DETAIL_TYPE.TRUSTED_ADVISOR, EVENT_NOTIFICATION_DETAIL_TYPE.SERVICE_QUOTA], + source: [EVENT_NOTIFICATION_SOURCES.TRUSTED_ADVISOR, EVENT_NOTIFICATION_SOURCES.SERVICE_QUOTA], }; /** * @description construct for event-sqs */ - const summarizerEventQueue = new EventsToSQS( - this, - "QM-Summarizer-EventQueue", - { - eventRule: summarizerRulePattern, - encryptionKey: kms.key, - eventBus: quotaMonitorBus, - } - ); + const summarizerEventQueue = new EventsToSQS(this, "QM-Summarizer-EventQueue", { + eventRule: summarizerRulePattern, + encryptionKey: kms.key, + eventBus: quotaMonitorBus, + }); /** * @description quota summary dynamodb table @@ -591,26 +524,21 @@ export class QuotaMonitorHub extends Stack { /** * @description event-lambda construct for capturing quota summary */ - const summarizer = new EventsToLambda( - this, - "QM-Reporter", - { - eventRule: events.Schedule.rate(Duration.minutes(5)), - encryptionKey: kms.key, - assetLocation: `${path.dirname( - __dirname - )}/../lambda/services/reporter/dist/reporter.zip`, - environment: { - QUOTA_TABLE: summaryTable.tableName, - SQS_URL: summarizerEventQueue.target.queueUrl, - MAX_MESSAGES: "10", //100 messages can be read with each invocation, change as needed - MAX_LOOPS: "10", - }, - memorySize: 512, - timeout: Duration.seconds(10), - layers: [utilsLayer.layer], - } - ); + const summarizer = new EventsToLambda(this, "QM-Reporter", { + eventRule: events.Schedule.rate(Duration.minutes(5)), + encryptionKey: kms.key, + assetLocation: `${path.dirname(__dirname)}/../lambda/services/reporter/dist/reporter.zip`, + environment: { + QUOTA_TABLE: summaryTable.tableName, + SQS_URL: summarizerEventQueue.target.queueUrl, + MAX_MESSAGES: "10", //100 messages can be read with each invocation, change as needed + MAX_LOOPS: "10", + }, + memorySize: 512, + timeout: Duration.seconds(10), + layers: [utilsLayer.layer], + }); + addCfnGuardSuppression(summarizer.target, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); // adding queue permissions to summarizer lambda function summarizer.target.addToRolePolicy( @@ -633,77 +561,78 @@ export class QuotaMonitorHub extends Stack { //============================================== // StackSets for Organization/Hybrid deployments //============================================== - const qmTAStackSet = new cloudformation.CfnStackSet( - this, - "QM-TA-StackSet", - { - stackSetName: "QM-TA-Spoke-StackSet", - permissionModel: "SERVICE_MANAGED", - description: - "StackSet for deploying Quota Monitor Trusted Advisor spokes in Organization", - templateUrl: Fn.sub( - "https://" + - `${this.node.tryGetContext("SOLUTION_BUCKET")}` + - "-${AWS::Region}.s3.${AWS::Region}.amazonaws.com/" + - `${this.node.tryGetContext( - "SOLUTION_NAME" - )}/${this.node.tryGetContext( - "SOLUTION_VERSION" - )}/quota-monitor-ta-spoke.template` - ), - parameters: [ - { - parameterKey: "EventBusArn", - parameterValue: quotaMonitorBus.eventBusArn, - }, - ], - autoDeployment: { - enabled: true, - retainStacksOnAccountRemoval: false, - }, - managedExecution: { - Active: true, + const qmTAStackSet = new cloudformation.CfnStackSet(this, "QM-TA-StackSet", { + stackSetName: "QM-TA-Spoke-StackSet", + permissionModel: "SERVICE_MANAGED", + description: "StackSet for deploying Quota Monitor Trusted Advisor spokes in Organization", + templateUrl: this.getTemplateUrl("quota-monitor-ta-spoke.template"), + parameters: [ + { + parameterKey: "EventBusArn", + parameterValue: quotaMonitorBus.eventBusArn, }, - capabilities: [CfnCapabilities.ANONYMOUS_IAM], - callAs: "DELEGATED_ADMIN", - } - ); + ], + autoDeployment: { + enabled: true, + retainStacksOnAccountRemoval: false, + }, + managedExecution: { + Active: true, + }, + capabilities: [CfnCapabilities.ANONYMOUS_IAM], + callAs: "DELEGATED_ADMIN", + }); - const qmSQStackSet = new cloudformation.CfnStackSet( - this, - "QM-SQ-StackSet", - { - stackSetName: "QM-SQ-Spoke-StackSet", - permissionModel: "SERVICE_MANAGED", - description: - "StackSet for deploying Quota Monitor Service Quota spokes in Organization", - templateUrl: Fn.sub( - "https://" + - `${this.node.tryGetContext("SOLUTION_BUCKET")}` + - "-${AWS::Region}.s3.${AWS::Region}.amazonaws.com/" + - `${this.node.tryGetContext( - "SOLUTION_NAME" - )}/${this.node.tryGetContext( - "SOLUTION_VERSION" - )}/quota-monitor-sq-spoke.template` - ), - parameters: [ - { - parameterKey: "EventBusArn", - parameterValue: quotaMonitorBus.eventBusArn, - }, - ], - autoDeployment: { - enabled: true, - retainStacksOnAccountRemoval: false, + const qmSQStackSet = new cloudformation.CfnStackSet(this, "QM-SQ-StackSet", { + stackSetName: "QM-SQ-Spoke-StackSet", + permissionModel: "SERVICE_MANAGED", + description: "StackSet for deploying Quota Monitor Service Quota spokes in Organization", + templateUrl: this.getTemplateUrl("quota-monitor-sq-spoke.template"), + parameters: [ + { + parameterKey: "EventBusArn", + parameterValue: quotaMonitorBus.eventBusArn, }, - managedExecution: { - Active: true, + { + parameterKey: "SpokeSnsRegion", + parameterValue: snsSpokeRegion.valueAsString, }, - capabilities: [CfnCapabilities.ANONYMOUS_IAM], - callAs: "DELEGATED_ADMIN", - } - ); + { + parameterKey: "SageMakerMonitoring", + parameterValue: sageMakerMonitoring.valueAsString, + }, + { + parameterKey: "ConnectMonitoring", + parameterValue: connectMonitoring.valueAsString, + }, + ], + autoDeployment: { + enabled: true, + retainStacksOnAccountRemoval: false, + }, + managedExecution: { + Active: true, + }, + capabilities: [CfnCapabilities.ANONYMOUS_IAM], + callAs: "DELEGATED_ADMIN", + }); + + const qmSnsStackSet = new cloudformation.CfnStackSet(this, "QM-SNS-StackSet", { + stackSetName: "QM-SNS-Spoke-StackSet", + permissionModel: "SERVICE_MANAGED", + description: "StackSet for deploying Quota Monitor notification spokes in Organization", + templateUrl: this.getTemplateUrl("quota-monitor-sns-spoke.template"), + parameters: [], + autoDeployment: { + enabled: true, + retainStacksOnAccountRemoval: false, + }, + managedExecution: { + Active: true, + }, + capabilities: [CfnCapabilities.ANONYMOUS_IAM], + callAs: "DELEGATED_ADMIN", + }); // the spoke templates which are parameters of the stacksets as assets for cdk deploy to work // use `npm run cdk:deploy quota-monitor-hub` to generate the template that can be deployed by cdk @@ -711,30 +640,20 @@ export class QuotaMonitorHub extends Stack { // (with parameterized templateUrls that are to be substituted by deployment scripts) // and once more for generating the corresponding cdk assets and updating the corresponding templateUrls try { - console.log( - "Attempting to generate cdk assets for the stackset templates" - ); - const stackSetCdkTemplateTA = new aws_s3_assets.Asset( - this, - "QM-TA-Spoke-StackSet-Template", - { - path: `${path.dirname( - __dirname - )}/cdk.out/quota-monitor-ta-spoke.template.json`, - } - ); - const stackSetCdkTemplateSQ = new aws_s3_assets.Asset( - this, - "QM-SQ-Spoke-StackSet-Template", - { - path: `${path.dirname( - __dirname - )}/cdk.out/quota-monitor-sq-spoke.template.json`, - } - ); + console.log("Attempting to generate cdk assets for the stackset templates"); + const stackSetCdkTemplateTA = new aws_s3_assets.Asset(this, "QM-TA-Spoke-StackSet-Template", { + path: `${path.dirname(__dirname)}/cdk.out/quota-monitor-ta-spoke.template.json`, + }); + const stackSetCdkTemplateSQ = new aws_s3_assets.Asset(this, "QM-SQ-Spoke-StackSet-Template", { + path: `${path.dirname(__dirname)}/cdk.out/quota-monitor-sq-spoke.template.json`, + }); + const stackSetCdkTemplateSNS = new aws_s3_assets.Asset(this, "QM-SNS-Spoke-StackSet-Template", { + path: `${path.dirname(__dirname)}/cdk.out/quota-monitor-sns-spoke.template.json`, + }); console.log("Updating stackset templateUrls for cdk deployment"); qmTAStackSet.templateUrl = stackSetCdkTemplateTA.httpUrl; qmSQStackSet.templateUrl = stackSetCdkTemplateSQ.httpUrl; + qmSnsStackSet.templateUrl = stackSetCdkTemplateSNS.httpUrl; } catch (error) { //Error is expected the first time the templates are synthesized console.log("Not updating templateUrls for cdk deployment"); @@ -748,11 +667,7 @@ export class QuotaMonitorHub extends Stack { source: ["aws.ssm"], resources: [ ssmQMOUs.parameterArn, - Fn.conditionIf( - accountDeployCondition.logicalId, - ssmQMAccounts.parameterArn, - Aws.NO_VALUE - ).toString(), + Fn.conditionIf(accountDeployCondition.logicalId, ssmQMAccounts.parameterArn, Aws.NO_VALUE).toString(), ssmRegionsList.parameterArn, ], }; @@ -760,45 +675,40 @@ export class QuotaMonitorHub extends Stack { /** * @description construct for events-lambda */ - const deploymentManager = new EventsToLambda( - this, - "QM-Deployment-Manager", - { - eventRule: ssmRulePattern, - encryptionKey: kms.key, - assetLocation: `${path.dirname( - __dirname - )}/../lambda/services/deploymentManager/dist/deployment-manager.zip`, - environment: { - EVENT_BUS_NAME: quotaMonitorBus.eventBusName, - EVENT_BUS_ARN: quotaMonitorBus.eventBusArn, - TA_STACKSET_ID: qmTAStackSet.attrStackSetId.toString(), - SQ_STACKSET_ID: qmSQStackSet.attrStackSetId.toString(), - QM_OU_PARAMETER: ssmQMOUs.parameterName.toString(), - QM_ACCOUNT_PARAMETER: Fn.conditionIf( - accountDeployCondition.logicalId, - ssmQMAccounts.parameterName, - Aws.NO_VALUE - ).toString(), - DEPLOYMENT_MODEL: deploymentModel.valueAsString, - REGIONS_LIST: regionsListCfnParam.valueAsString, - QM_REGIONS_LIST_PARAMETER: ssmRegionsList.parameterName.toString(), - REGIONS_CONCURRENCY_TYPE: stackSetRegionConcurrencyType.valueAsString, - MAX_CONCURRENT_PERCENTAGE: - stackSetMaxConcurrentPercentage.valueAsString, - FAILURE_TOLERANCE_PERCENTAGE: - stackSetFailureTolerancePercentage.valueAsString, - SQ_NOTIFICATION_THRESHOLD: sqNotificationThreshold.valueAsString, - SQ_MONITORING_FREQUENCY: sqMonitoringFrequency.valueAsString, - SQ_REPORT_OK_NOTIFICATIONS: sqReportOKNotifications.valueAsString, - SOLUTION_UUID: createUUID.getAttString("UUID"), - METRICS_ENDPOINT: map.findInMap("Metrics", "MetricsEndpoint"), - SEND_METRIC: map.findInMap("Metrics", "SendAnonymizedData"), - }, - layers: [utilsLayer.layer], - memorySize: 512, - } - ); + const deploymentManager = new EventsToLambda(this, "QM-Deployment-Manager", { + eventRule: ssmRulePattern, + encryptionKey: kms.key, + assetLocation: `${path.dirname(__dirname)}/../lambda/services/deploymentManager/dist/deployment-manager.zip`, + environment: { + EVENT_BUS_NAME: quotaMonitorBus.eventBusName, + EVENT_BUS_ARN: quotaMonitorBus.eventBusArn, + TA_STACKSET_ID: qmTAStackSet.attrStackSetId.toString(), + SQ_STACKSET_ID: qmSQStackSet.attrStackSetId.toString(), + SNS_STACKSET_ID: qmSnsStackSet.attrStackSetId.toString(), + QM_OU_PARAMETER: ssmQMOUs.parameterName.toString(), + QM_ACCOUNT_PARAMETER: Fn.conditionIf( + accountDeployCondition.logicalId, + ssmQMAccounts.parameterName, + Aws.NO_VALUE + ).toString(), + DEPLOYMENT_MODEL: deploymentModel.valueAsString, + REGIONS_LIST: regionsListCfnParam.valueAsString, + QM_REGIONS_LIST_PARAMETER: ssmRegionsList.parameterName.toString(), + SNS_SPOKE_REGION: snsSpokeRegion.valueAsString, + REGIONS_CONCURRENCY_TYPE: stackSetRegionConcurrencyType.valueAsString, + MAX_CONCURRENT_PERCENTAGE: stackSetMaxConcurrentPercentage.valueAsString, + FAILURE_TOLERANCE_PERCENTAGE: stackSetFailureTolerancePercentage.valueAsString, + SQ_NOTIFICATION_THRESHOLD: sqNotificationThreshold.valueAsString, + SQ_MONITORING_FREQUENCY: sqMonitoringFrequency.valueAsString, + SQ_REPORT_OK_NOTIFICATIONS: sqReportOKNotifications.valueAsString, + SOLUTION_UUID: createUUID.getAttString("UUID"), + METRICS_ENDPOINT: map.findInMap("Metrics", "MetricsEndpoint"), + SEND_METRIC: map.findInMap("Metrics", "SendAnonymizedData"), + }, + layers: [utilsLayer.layer], + memorySize: 512, + }); + addCfnGuardSuppression(deploymentManager.target, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); /** * @description policy statement to allow CRUD on event bus permissions @@ -824,11 +734,7 @@ export class QuotaMonitorHub extends Stack { effect: iam.Effect.ALLOW, resources: [ ssmQMOUs.parameterArn, - Fn.conditionIf( - accountDeployCondition.logicalId, - ssmQMAccounts.parameterArn, - Aws.NO_VALUE - ).toString(), + Fn.conditionIf(accountDeployCondition.logicalId, ssmQMAccounts.parameterArn, Aws.NO_VALUE).toString(), ssmRegionsList.parameterArn, ], }); @@ -838,19 +744,12 @@ export class QuotaMonitorHub extends Stack { * @description policy statement to describe organizations */ const deployerOrgReadPolicy1 = new iam.PolicyStatement({ - actions: [ - "organizations:DescribeOrganization", - "organizations:ListRoots", - "organizations:ListAccounts", - ], + actions: ["organizations:DescribeOrganization", "organizations:ListRoots", "organizations:ListAccounts"], effect: iam.Effect.ALLOW, resources: ["*"], // do not support resource-level permissions }); const deployerOrgReadPolicy2 = new iam.PolicyStatement({ - actions: [ - "organizations:ListDelegatedAdministrators", - "organizations:ListAccountsForParent", - ], + actions: ["organizations:ListDelegatedAdministrators", "organizations:ListAccountsForParent"], effect: iam.Effect.ALLOW, resources: ["*"], // documentation says can be narrowed to `arn:aws:organizations:::ou/o-*/ou-*` @@ -879,6 +778,7 @@ export class QuotaMonitorHub extends Stack { resources: [ `arn:${this.partition}:cloudformation:*:${managementAccountId.valueAsString}:stackset/QM-TA-Spoke-StackSet:*`, `arn:${this.partition}:cloudformation:*:${managementAccountId.valueAsString}:stackset/QM-SQ-Spoke-StackSet:*`, + `arn:${this.partition}:cloudformation:*:${managementAccountId.valueAsString}:stackset/QM-SNS-Spoke-StackSet:*`, ], }); const deployerStackSetPolicy2 = new iam.PolicyStatement({ @@ -891,6 +791,8 @@ export class QuotaMonitorHub extends Stack { resources: [ `arn:${this.partition}:cloudformation:*:${managementAccountId.valueAsString}:stackset/QM-TA-Spoke-StackSet:*`, `arn:${this.partition}:cloudformation:*:${managementAccountId.valueAsString}:stackset/QM-SQ-Spoke-StackSet:*`, + `arn:${this.partition}:cloudformation:*:${managementAccountId.valueAsString}:stackset/QM-SNS-Spoke-StackSet:*`, + `arn:${this.partition}:cloudformation:*:${managementAccountId.valueAsString}:stackset-target/QM-SNS-Spoke-StackSet:*/*`, `arn:${this.partition}:cloudformation:*:${managementAccountId.valueAsString}:stackset-target/QM-TA-Spoke-StackSet:*/*`, `arn:${this.partition}:cloudformation:*:${managementAccountId.valueAsString}:stackset-target/QM-SQ-Spoke-StackSet:*/*`, `arn:${this.partition}:cloudformation:*::type/resource/*`, @@ -917,20 +819,18 @@ export class QuotaMonitorHub extends Stack { actions: ["support:DescribeTrustedAdvisorChecks"], resources: ["*"], // does not allow resource-level permissions }); - deploymentManager.target.addToRolePolicy( - taDescribeTrustedAdvisorChecksPolicy - ); + deploymentManager.target.addToRolePolicy(taDescribeTrustedAdvisorChecksPolicy); /** * app registry application for hub stack */ - new AppRegistryApplication(this, "HubAppRegistryApplication", { - appRegistryApplicationName: this.node.tryGetContext( - "APP_REG_HUB_APPLICATION_NAME" - ), - solutionId: this.node.tryGetContext("SOLUTION_ID"), - }); + if (props.targetPartition !== "China") { + new AppRegistryApplication(this, "HubAppRegistryApplication", { + appRegistryApplicationName: this.node.tryGetContext("APP_REG_HUB_APPLICATION_NAME"), + solutionId: this.node.tryGetContext("SOLUTION_ID"), + }); + } //============================================================================================= // Outputs @@ -938,8 +838,7 @@ export class QuotaMonitorHub extends Stack { new CfnOutput(this, "SlackHookKey", { condition: slackTrue, value: map.findInMap("SSMParameters", "SlackHook"), - description: - "SSM parameter for Slack Web Hook, change the value for your slack workspace", + description: "SSM parameter for Slack Web Hook, change the value for your slack workspace", }); new CfnOutput(this, "UUID", { @@ -957,4 +856,23 @@ export class QuotaMonitorHub extends Stack { description: "The SNS Topic where notifications are published to", }); } + + private getTemplateUrl(templateName: string): string { + const solutionBucket = this.node.tryGetContext("SOLUTION_BUCKET"); + const solutionName = this.node.tryGetContext("SOLUTION_NAME"); + const solutionVersion = this.node.tryGetContext("SOLUTION_VERSION"); + + return Fn.join("", [ + "https://", + solutionBucket, + `-${Aws.REGION}.s3.${Aws.REGION}.amazonaws.com`, + Fn.conditionIf(this.isChinaPartition.logicalId, ".cn", ""), + "/", + solutionName, + "/", + solutionVersion, + "/", + Fn.conditionIf(this.isChinaPartition.logicalId, templateName.replace(".template", "-cn.template"), templateName), + ]); + } } diff --git a/source/resources/lib/kms.construct.ts b/source/resources/lib/kms.construct.ts index 6d7f81b..f57d3a6 100644 --- a/source/resources/lib/kms.construct.ts +++ b/source/resources/lib/kms.construct.ts @@ -22,11 +22,7 @@ export class KMS extends Construct { actions: ["kms:*"], resources: ["*"], effect: iam.Effect.ALLOW, - principals: [ - new iam.ArnPrincipal( - `arn:${Aws.PARTITION}:iam::${Aws.ACCOUNT_ID}:root` - ), - ], + principals: [new iam.ArnPrincipal(`arn:${Aws.PARTITION}:iam::${Aws.ACCOUNT_ID}:root`)], }), ], }); @@ -35,8 +31,7 @@ export class KMS extends Construct { * @description kms key for encryption in quota monitor resources */ this.key = new kms.Key(this, "QM-EncryptionKey", { - description: - "CMK for AWS resources provisioned by Quota Monitor in this account", + description: "CMK for AWS resources provisioned by Quota Monitor in this account", enabled: true, enableKeyRotation: true, policy: encryptionKeyPolicy, diff --git a/source/resources/lib/lambda-dynamodb.construct.ts b/source/resources/lib/lambda-dynamodb.construct.ts index bac5c16..dbd23f8 100644 --- a/source/resources/lib/lambda-dynamodb.construct.ts +++ b/source/resources/lib/lambda-dynamodb.construct.ts @@ -1,30 +1,15 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - aws_dynamodb as dynamodb, - aws_iam as iam, - aws_lambda as lambda, - Duration, - Stack, -} from "aws-cdk-lib"; +import { aws_dynamodb as dynamodb, aws_iam as iam, aws_lambda as lambda, Duration, Stack } from "aws-cdk-lib"; import { Construct } from "constructs"; -import { - DynamoDBProps, - LambdaProps, - LambdaToTarget, - LAMBDA_RUNTIME_NODE, - LOG_LEVEL, -} from "./exports"; +import { DynamoDBProps, LambdaProps, LambdaToTarget, LAMBDA_RUNTIME_NODE, LOG_LEVEL } from "./exports"; import { KMS } from "./kms.construct"; /** * @description construct for lambda to dynamodb pattern */ -export class LambdaToDDB - extends Construct - implements LambdaToTarget -{ +export class LambdaToDDB extends Construct implements LambdaToTarget { readonly function: lambda.Function; readonly target: dynamodb.Table; @@ -42,18 +27,18 @@ export class LambdaToDDB this.function = props.function ? props.function : new lambda.Function(this, `${id}-Function`, { - description: `${this.node.tryGetContext( - "SOLUTION_ID" - )} ${this.node.tryGetContext("SOLUTION_NAME")} - ${id}-Lambda`, + description: `${this.node.tryGetContext("SOLUTION_ID")} ${this.node.tryGetContext( + "SOLUTION_NAME" + )} - ${id}-Lambda`, runtime: LAMBDA_RUNTIME_NODE, code: lambda.Code.fromAsset(props.assetLocation), handler: "index.handler", environment: { ...props.environment, LOG_LEVEL: this.node.tryGetContext("LOG_LEVEL") || LOG_LEVEL.INFO, //change as needed - CUSTOM_SDK_USER_AGENT: `AwsSolution/${this.node.tryGetContext( - "SOLUTION_ID" - )}/${this.node.tryGetContext("SOLUTION_VERSION")}`, + CUSTOM_SDK_USER_AGENT: `AwsSolution/${this.node.tryGetContext("SOLUTION_ID")}/${this.node.tryGetContext( + "SOLUTION_VERSION" + )}`, }, timeout: props.timeout ? props.timeout : Duration.seconds(60), deadLetterQueueEnabled: true, @@ -101,11 +86,9 @@ export class LambdaToDDB // permissions to access KMS key if function created by the construct if (!props.function && props.encryptionKey) { - KMS.getIAMPolicyStatementsToAccessKey(props.encryptionKey.keyArn).forEach( - (policyStatement) => { - this.function.addToRolePolicy(policyStatement); - } - ); + KMS.getIAMPolicyStatementsToAccessKey(props.encryptionKey.keyArn).forEach((policyStatement) => { + this.function.addToRolePolicy(policyStatement); + }); } } } diff --git a/source/resources/lib/prereq.stack.ts b/source/resources/lib/prereq.stack.ts index 01cddd5..4f9d268 100644 --- a/source/resources/lib/prereq.stack.ts +++ b/source/resources/lib/prereq.stack.ts @@ -1,20 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - aws_iam as iam, - App, - Stack, - CfnOutput, - CfnParameter, - CfnMapping, - StackProps, -} from "aws-cdk-lib"; +import { aws_iam as iam, App, Stack, CfnOutput, CfnParameter, CfnMapping, StackProps } from "aws-cdk-lib"; import { NagSuppressions } from "cdk-nag"; import { IConstruct } from "constructs"; import * as path from "path"; import { CustomResourceLambda } from "./custom-resource-lambda.construct"; import { Layer } from "./lambda-layer.construct"; +import { addCfnGuardSuppression, addCfnGuardSuppressionToNestedResources } from "./cfn-guard-utils"; /** * @description @@ -22,12 +15,17 @@ import { Layer } from "./lambda-layer.construct"; * The stack should be deployed in the Organization Management account * @author aws-solutions */ + +interface PreReqStackProps extends StackProps { + targetPartition: "Commercial" | "China"; +} + export class PreReqStack extends Stack { /** * @param {Construct} scope parent of the construct * @param {string} id - identifier for the object */ - constructor(scope: App, id: string, props?: StackProps) { + constructor(scope: App, id: string, props: PreReqStackProps) { super(scope, id, props); //============================================================================================= @@ -43,16 +41,8 @@ export class PreReqStack extends Stack { // Mapping & Conditions //============================================================================================= const map = new CfnMapping(this, "QuotaMonitorMap"); - map.setValue( - "Metrics", - "SendAnonymizedData", - this.node.tryGetContext("SEND_METRICS") - ); - map.setValue( - "Metrics", - "MetricsEndpoint", - this.node.tryGetContext("METRICS_ENDPOINT") - ); + map.setValue("Metrics", "SendAnonymizedData", this.node.tryGetContext("SEND_METRICS")); + map.setValue("Metrics", "MetricsEndpoint", this.node.tryGetContext("METRICS_ENDPOINT")); //============================================================================================= // Metadata @@ -73,13 +63,9 @@ export class PreReqStack extends Stack { }, }; - this.templateOptions.description = `(${this.node.tryGetContext( - "SOLUTION_ID" - )}-PreReq) - ${this.node.tryGetContext( + this.templateOptions.description = `(${this.node.tryGetContext("SOLUTION_ID")}-PreReq) - ${this.node.tryGetContext( "SOLUTION_NAME" - )} - Prerequisite Template. Version ${this.node.tryGetContext( - "SOLUTION_VERSION" - )}`; + )} - Prerequisite Template. Version ${this.node.tryGetContext("SOLUTION_VERSION")}`; this.templateOptions.templateFormatVersion = "2010-09-09"; //============================================================================================= @@ -102,9 +88,7 @@ export class PreReqStack extends Stack { * @description construct to deploy lambda backed custom resource */ const helper = new CustomResourceLambda(this, "QM-Helper", { - assetLocation: `${path.dirname( - __dirname - )}/../lambda/services/helper/dist/helper.zip`, + assetLocation: `${path.dirname(__dirname)}/../lambda/services/helper/dist/helper.zip`, layers: [utilsLayer.layer], environment: { METRICS_ENDPOINT: map.findInMap("Metrics", "MetricsEndpoint"), @@ -112,6 +96,8 @@ export class PreReqStack extends Stack { QM_STACK_ID: id, }, }); + addCfnGuardSuppression(helper.function, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); + addCfnGuardSuppressionToNestedResources(helper, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); // Custom resources const uuid = helper.addCustomResource("CreateUUID"); @@ -126,15 +112,15 @@ export class PreReqStack extends Stack { * @description construct to deploy lambda backed custom resource */ const preReqManager = new CustomResourceLambda(this, "QM-PreReqManager", { - assetLocation: `${path.dirname( - __dirname - )}/../lambda/services/preReqManager/dist/prereq-manager.zip`, + assetLocation: `${path.dirname(__dirname)}/../lambda/services/preReqManager/dist/prereq-manager.zip`, environment: { METRICS_ENDPOINT: map.findInMap("Metrics", "MetricsEndpoint"), SEND_METRIC: map.findInMap("Metrics", "SendAnonymizedData"), }, layers: [utilsLayer.layer], }); + addCfnGuardSuppression(preReqManager.function, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); + addCfnGuardSuppressionToNestedResources(preReqManager, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); /** * @description policy to allow write permissions for pre-requisite manager lambda @@ -167,8 +153,7 @@ export class PreReqStack extends Stack { [ { id: "AwsSolutions-L1", - reason: - "GovCloud regions support only up to nodejs 16, risk is tolerable", + reason: "GovCloud regions support only up to nodejs 16, risk is tolerable", }, ], true diff --git a/source/resources/lib/sns-spoke-stack.ts b/source/resources/lib/sns-spoke-stack.ts new file mode 100644 index 0000000..210e292 --- /dev/null +++ b/source/resources/lib/sns-spoke-stack.ts @@ -0,0 +1,165 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + aws_events as events, + aws_iam as iam, + aws_ssm as ssm, + aws_kms as kms, + App, + Stack, + StackProps, + CfnMapping, + CfnOutput, +} from "aws-cdk-lib"; +import path from "path"; +import { addCfnGuardSuppression } from "./cfn-guard-utils"; +import { Layer } from "./lambda-layer.construct"; +import { EVENT_NOTIFICATION_DETAIL_TYPE, EVENT_NOTIFICATION_SOURCES } from "./exports"; +import { AppRegistryApplication } from "./app-registry-application"; +import { EventsToLambdaToSNS } from "./events-lambda-sns.construct"; + +/** + * @description + * This is the SNS Spoke Stack for Quota Monitor for AWS for AWS Organizations + * The stack should be deployed in the spoke accounts before the SQ spoke stacks + * @author aws-solutions + */ + +interface QuotaMonitorSnsSpokeProps extends StackProps { + targetPartition: "Commercial" | "China"; +} + +export class QuotaMonitorSnsSpoke extends Stack { + /** + * @param {App} scope - parent of the construct + * @param {string} id - identifier for the object + */ + constructor(scope: App, id: string, props: QuotaMonitorSnsSpokeProps) { + super(scope, id, props); + + //============================================================================================= + // Parameters + //============================================================================================= + + const map = new CfnMapping(this, "QuotaMonitorMap"); + map.setValue("SSMParameters", "NotificationMutingConfig", "/QuotaMonitor/spoke/NotificationConfiguration"); + + //============================================================================================= + // Metadata + //============================================================================================= + + this.templateOptions.description = `(${this.node.tryGetContext( + "SOLUTION_ID" + )}-SPOKE-SNS) - ${this.node.tryGetContext( + "SOLUTION_NAME" + )} - Service Quotas Template. Version ${this.node.tryGetContext("SOLUTION_VERSION")}`; + this.templateOptions.templateFormatVersion = "2010-09-09"; + + //============================================================================================= + // Resources + //============================================================================================= + + //========================= + // Common shared components + //========================= + /** + * @description local event bus for quota monitor events + */ + const snsSpokeBus = new events.EventBus(this, "QM-SNS-Spoke-Bus", { + eventBusName: "QuotaMonitorSnsSpokeBus", + }); + + const spokeBusPolicyStatement = new iam.PolicyStatement({ + sid: "allowed_accounts", + effect: iam.Effect.ALLOW, + actions: ["events:PutEvents"], + principals: [new iam.AccountPrincipal(this.account)], + resources: [snsSpokeBus.eventBusArn], + }); + + snsSpokeBus.addToResourcePolicy(spokeBusPolicyStatement); + + /** + * @description utility layer for solution microservices + */ + const utilsLayer = new Layer( + this, + `QM-UtilsLayer-${this.stackName}`, + `${path.dirname(__dirname)}/../lambda/utilsLayer/dist/utilsLayer.zip` + ); + + //======================= + // SNS workflow component + //======================= + /** + * @description event rule pattern for sns events + */ + const snsRulePattern: events.EventPattern = { + detail: { + status: ["WARN", "ERROR"], + }, + detailType: [EVENT_NOTIFICATION_DETAIL_TYPE.TRUSTED_ADVISOR, EVENT_NOTIFICATION_DETAIL_TYPE.SERVICE_QUOTA], + source: [EVENT_NOTIFICATION_SOURCES.TRUSTED_ADVISOR, EVENT_NOTIFICATION_SOURCES.SERVICE_QUOTA], + }; + + /** + * @description list of muted services and limits (quotas) for quota monitoring + * value could be list of serviceCode[:quota_name|quota_code|resource] + */ + const ssmNotificationMutingConfig = new ssm.StringListParameter(this, "sq-spoke-NotificationMutingConfig", { + parameterName: map.findInMap("SSMParameters", "NotificationMutingConfig"), + stringListValue: ["NOP"], + description: + "Muting configuration for services, limits e.g. ec2:L-1216C47A,ec2:Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances,dynamodb,logs:*,geo:L-05EFD12D", + simpleName: false, + }); + + /** + * @description use aws managed kms key for SNS + */ + const aws_sns_kms = kms.Alias.fromAliasName(this, "aws-managed-sns-kms-key", "alias/aws/sns"); + + /** + * @description construct for events-lambda + */ + const snsPublisher = new EventsToLambdaToSNS(this, "sq-spoke-SNSPublisher", { + assetLocation: `${path.dirname(__dirname)}/../lambda/services/snsPublisher/dist/sns-publisher.zip`, + environment: { + QM_NOTIFICATION_MUTING_CONFIG_PARAMETER: ssmNotificationMutingConfig.parameterName, + SEND_METRIC: "No", + }, + layers: [utilsLayer.layer], + eventRule: snsRulePattern, + eventBus: snsSpokeBus, + encryptionKey: aws_sns_kms, + }); + addCfnGuardSuppression(snsPublisher.target, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); + + /** + * @description policy statement allowing READ on SSM parameter store + */ + const snsPublisherSSMReadPolicy = new iam.PolicyStatement({ + actions: ["ssm:GetParameter"], + effect: iam.Effect.ALLOW, + resources: [ssmNotificationMutingConfig.parameterArn], + }); + + snsPublisher.target.addToRolePolicy(snsPublisherSSMReadPolicy); + + /** + * app registry application for spoke SNS stack + */ + if (props.targetPartition !== "China") { + new AppRegistryApplication(this, "SpokeSnsAppRegistryApplication", { + appRegistryApplicationName: this.node.tryGetContext("APP_REG_SPOKE_SNS_APPLICATION_NAME"), + solutionId: `${this.node.tryGetContext("SOLUTION_ID")}-SPOKE-SNS`, + }); + } + + new CfnOutput(this, "SpokeSnsEventBus", { + value: snsSpokeBus.eventBusArn, + description: "SNS Event Bus Arn in spoke account", + }); + } +} diff --git a/source/resources/lib/sq-spoke.stack.ts b/source/resources/lib/sq-spoke.stack.ts index 385dbed..c25d87a 100644 --- a/source/resources/lib/sq-spoke.stack.ts +++ b/source/resources/lib/sq-spoke.stack.ts @@ -12,25 +12,31 @@ import { StackProps, RemovalPolicy, Duration, + CfnMapping, + Fn, + CfnCondition, + Aspects, + ArnFormat, } from "aws-cdk-lib"; import path from "path"; import { LambdaToDDB } from "./lambda-dynamodb.construct"; +import { + addCfnGuardSuppression, + addCfnGuardSuppressionToNestedResources, + addDynamoDbSuppressions, +} from "./cfn-guard-utils"; import { Layer } from "./lambda-layer.construct"; import { EventsToLambda } from "./events-lambda.construct"; import { CustomResourceLambda } from "./custom-resource-lambda.construct"; import { StreamViewType } from "aws-cdk-lib/aws-dynamodb"; -import { - EVENT_NOTIFICATION_DETAIL_TYPE, - EVENT_NOTIFICATION_SOURCES, - QUOTA_TABLE, - SERVICE_TABLE, -} from "./exports"; +import { EVENT_NOTIFICATION_DETAIL_TYPE, EVENT_NOTIFICATION_SOURCES, QUOTA_TABLE, SERVICE_TABLE } from "./exports"; import { DynamoEventSource } from "aws-cdk-lib/aws-lambda-event-sources"; import { StartingPosition } from "aws-cdk-lib/aws-lambda"; import { applyDependsOn } from "./depends.utils"; import { NagSuppressions } from "cdk-nag"; import { IConstruct } from "constructs"; import { AppRegistryApplication } from "./app-registry-application"; +import { ConditionAspect } from "./condition.utils"; /** * @description @@ -38,12 +44,17 @@ import { AppRegistryApplication } from "./app-registry-application"; * The stack should be deployed in the spoke accounts * @author aws-solutions */ + +interface QuotaMonitorSQSpokeProps extends StackProps { + targetPartition: "Commercial" | "China"; +} + export class QuotaMonitorSQSpoke extends Stack { /** * @param {App} scope - parent of the construct * @param {string} id - identifier for the object */ - constructor(scope: App, id: string, props: StackProps) { + constructor(scope: App, id: string, props: QuotaMonitorSQSpokeProps) { super(scope, id, props); //============================================================================================= @@ -53,27 +64,50 @@ export class QuotaMonitorSQSpoke extends Stack { type: "String", }); + const spokeSnsRegion = new CfnParameter(this, "SpokeSnsRegion", { + type: "String", + default: "", + description: `The region in which the spoke SNS stack exists in this account. Leave blank if the spoke SNS is not needed.`, + }); + const threshold = new CfnParameter(this, "NotificationThreshold", { type: "String", default: "80", - allowedValues: ["60", "70", "80"], + description: "Threshold percentage for quota utilization alerts (0-100)", + allowedPattern: "^([1-9]|[1-9][0-9])$", + constraintDescription: "Threshold must be a whole number between 0 and 100", }); const frequency = new CfnParameter(this, "MonitoringFrequency", { type: "String", default: "rate(12 hours)", - allowedValues: ["rate(6 hours)", "rate(12 hours)"], + allowedValues: ["rate(6 hours)", "rate(12 hours)", "rate(1 day)"], }); - const reportOKNotifications = new CfnParameter( - this, - "ReportOKNotifications", - { - type: "String", - default: "No", - allowedValues: ["Yes", "No"], - } - ); + const reportOKNotifications = new CfnParameter(this, "ReportOKNotifications", { + type: "String", + default: "No", + allowedValues: ["Yes", "No"], + }); + + const sageMakerMonitoring = new CfnParameter(this, "SageMakerMonitoring", { + type: "String", + default: "Yes", + allowedValues: ["Yes", "No"], + }); + + const connectMonitoring = new CfnParameter(this, "ConnectMonitoring", { + type: "String", + default: "Yes", + allowedValues: ["Yes", "No"], + }); + + const map = new CfnMapping(this, "QuotaMonitorMap"); + map.setValue("SSMParameters", "NotificationMutingConfig", "/QuotaMonitor/spoke/NotificationConfiguration"); + + const spokeSnsRegionExists = new CfnCondition(this, "SpokeSnsRegionExists", { + expression: Fn.conditionNot(Fn.conditionEquals(spokeSnsRegion, "")), + }); //============================================================================================= // Metadata @@ -85,7 +119,7 @@ export class QuotaMonitorSQSpoke extends Stack { Label: { default: "Monitoring Account Configuration", }, - Parameters: ["EventBusArn"], + Parameters: ["EventBusArn", "SpokeSnsRegion"], }, { Label: { @@ -95,6 +129,8 @@ export class QuotaMonitorSQSpoke extends Stack { "NotificationThreshold", "MonitoringFrequency", "ReportOKNotifications", + "SageMakerMonitoring", + "ConnectMonitoring", ], }, ], @@ -102,6 +138,9 @@ export class QuotaMonitorSQSpoke extends Stack { EventBusArn: { default: "Arn for the EventBridge bus in the monitoring account", }, + SpokeSnsRegion: { + default: "Region in which the spoke SNS stack exists in this account", + }, NotificationThreshold: { default: "At what quota utilization do you want notifications?", }, @@ -111,16 +150,18 @@ export class QuotaMonitorSQSpoke extends Stack { ReportOKNotifications: { default: "Report OK Notifications", }, + SageMakerMonitoring: { + default: "Enable monitoring for SageMaker quotas", + }, + ConnectMonitoring: { + default: "Enable monitoring for Connect quotas", + }, }, }, }; - this.templateOptions.description = `(${this.node.tryGetContext( - "SOLUTION_ID" - )}-SQ) - ${this.node.tryGetContext( + this.templateOptions.description = `(${this.node.tryGetContext("SOLUTION_ID")}-SQ) - ${this.node.tryGetContext( "SOLUTION_NAME" - )} - Service Quotas Template. Version ${this.node.tryGetContext( - "SOLUTION_VERSION" - )}`; + )} - Service Quotas Template. Version ${this.node.tryGetContext("SOLUTION_VERSION")}`; this.templateOptions.templateFormatVersion = "2010-09-09"; //============================================================================================= @@ -140,11 +181,7 @@ export class QuotaMonitorSQSpoke extends Stack { /** * @description primary event bus in the monitoring account to send events to */ - const _primaryEventBus = events.EventBus.fromEventBusArn( - this, - "QM-Primary-Bus", - eventBusArn.valueAsString - ); + const _primaryEventBus = events.EventBus.fromEventBusArn(this, "QM-Primary-Bus", eventBusArn.valueAsString); const primaryEventBus = new targets.EventBus(_primaryEventBus); /** @@ -173,6 +210,7 @@ export class QuotaMonitorSQSpoke extends Stack { stream: StreamViewType.NEW_AND_OLD_IMAGES, removalPolicy: RemovalPolicy.DESTROY, }); + addDynamoDbSuppressions(serviceTable); /** * @description dynamodb table for supported quota list @@ -191,14 +229,13 @@ export class QuotaMonitorSQSpoke extends Stack { encryption: dynamodb.TableEncryption.AWS_MANAGED, removalPolicy: RemovalPolicy.DESTROY, }); + addDynamoDbSuppressions(quotaTable); /** * @description construct to deploy lambda backed custom resource for quota list manager */ const quotaListManager = new CustomResourceLambda(this, "QM-ListManager", { - assetLocation: `${path.dirname( - __dirname - )}/../lambda/services/quotaListManager/dist/quota-list-manager.zip`, + assetLocation: `${path.dirname(__dirname)}/../lambda/services/quotaListManager/dist/quota-list-manager.zip`, environment: { SQ_SERVICE_TABLE: serviceTable.tableName, SQ_QUOTA_TABLE: quotaTable.tableName, @@ -209,6 +246,8 @@ export class QuotaMonitorSQSpoke extends Stack { layers: [utilsLayer.layer], timeout: Duration.minutes(15), }); + addCfnGuardSuppression(quotaListManager.function, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); + addCfnGuardSuppressionToNestedResources(quotaListManager, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); /** * @description lambda-dynamodb construct for service list table @@ -251,19 +290,13 @@ export class QuotaMonitorSQSpoke extends Stack { quotaListManager.function.addEventSource(eventSourceMapping); // Schedule to trigger lambda - const quotaListManagerScheduleRule = new events.Rule( - this, - `QM-ListManagerSchedule`, - { - description: `${this.node.tryGetContext( - "SOLUTION_ID" - )} ${this.node.tryGetContext("SOLUTION_NAME")} - ${id}-EventsRule`, - schedule: events.Schedule.rate(Duration.days(30)), - } - ); - quotaListManagerScheduleRule.addTarget( - new targets.LambdaFunction(quotaListManager.function) - ); + const quotaListManagerScheduleRule = new events.Rule(this, `QM-ListManagerSchedule`, { + description: `${this.node.tryGetContext("SOLUTION_ID")} ${this.node.tryGetContext( + "SOLUTION_NAME" + )} - ${id}-EventsRule`, + schedule: events.Schedule.rate(Duration.days(30)), + }); + quotaListManagerScheduleRule.addTarget(new targets.LambdaFunction(quotaListManager.function)); // cdk-nag suppressions NagSuppressions.addResourceSuppressions( @@ -285,9 +318,7 @@ export class QuotaMonitorSQSpoke extends Stack { */ const cwPoller = new EventsToLambda(this, "QM-CWPoller", { eventRule: events.Schedule.expression(frequency.valueAsString), - assetLocation: `${path.dirname( - __dirname - )}/../lambda/services/cwPoller/dist/cw-poller.zip`, + assetLocation: `${path.dirname(__dirname)}/../lambda/services/cwPoller/dist/cw-poller.zip`, environment: { SQ_SERVICE_TABLE: serviceTable.tableName, SQ_QUOTA_TABLE: quotaTable.tableName, @@ -300,6 +331,7 @@ export class QuotaMonitorSQSpoke extends Stack { layers: [utilsLayer.layer], timeout: Duration.minutes(15), }); + addCfnGuardSuppression(cwPoller.target, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); // permission to query the quota table cwPoller.target.addToRolePolicy( @@ -361,9 +393,9 @@ export class QuotaMonitorSQSpoke extends Stack { * @description rule to send quota utilization OK events to centralized event bus */ new events.Rule(this, `QM-Utilization-OK`, { - description: `${this.node.tryGetContext( - "SOLUTION_ID" - )} ${this.node.tryGetContext("SOLUTION_NAME")} - ${id}-EventsRule`, + description: `${this.node.tryGetContext("SOLUTION_ID")} ${this.node.tryGetContext( + "SOLUTION_NAME" + )} - ${id}-EventsRule`, eventPattern: quotaUtilizationOkEvent, eventBus: spokeBus, targets: [primaryEventBus], @@ -385,9 +417,9 @@ export class QuotaMonitorSQSpoke extends Stack { * @description rule to send quota utilization WARN events to centralized event bus */ new events.Rule(this, `QM-Utilization-Warn`, { - description: `${this.node.tryGetContext( - "SOLUTION_ID" - )} ${this.node.tryGetContext("SOLUTION_NAME")} - ${id}-EventsRule`, + description: `${this.node.tryGetContext("SOLUTION_ID")} ${this.node.tryGetContext( + "SOLUTION_NAME" + )} - ${id}-EventsRule`, eventPattern: quotaUtilizationWarnEvent, eventBus: spokeBus, targets: [primaryEventBus], @@ -409,33 +441,69 @@ export class QuotaMonitorSQSpoke extends Stack { * @description rule to send quota utilization ERROR events to centralized event bus */ const eventsRuleError = new events.Rule(this, `QM-Utilization-Err`, { - description: `${this.node.tryGetContext( - "SOLUTION_ID" - )} ${this.node.tryGetContext("SOLUTION_NAME")} - ${id}-EventsRule`, + description: `${this.node.tryGetContext("SOLUTION_ID")} ${this.node.tryGetContext( + "SOLUTION_NAME" + )} - ${id}-EventsRule`, eventPattern: quotaUtilizationErrorEvent, eventBus: spokeBus, targets: [primaryEventBus], }); + //======================= + // SNS workflow component + //======================= /** - * app registry application for SQ stack + * @description event rule pattern for sns events */ - - new AppRegistryApplication(this, "SQSpokeAppRegistryApplication", { - appRegistryApplicationName: this.node.tryGetContext( - "APP_REG_SQ_SPOKE_APPLICATION_NAME" - ), - solutionId: `${this.node.tryGetContext("SOLUTION_ID")}-SQ`, + const snsRulePattern: events.EventPattern = { + detail: { + status: ["WARN", "ERROR"], + }, + detailType: [EVENT_NOTIFICATION_DETAIL_TYPE.TRUSTED_ADVISOR, EVENT_NOTIFICATION_DETAIL_TYPE.SERVICE_QUOTA], + source: [EVENT_NOTIFICATION_SOURCES.TRUSTED_ADVISOR, EVENT_NOTIFICATION_SOURCES.SERVICE_QUOTA], + }; + const _spokeSnsEventBus = events.EventBus.fromEventBusArn( + this, + "QM-Spoke-SNS-Bus", + Stack.of(this).formatArn({ + service: "events", + region: spokeSnsRegion.valueAsString, + resource: "event-bus", + resourceName: "QuotaMonitorSnsSpokeBus", + arnFormat: ArnFormat.SLASH_RESOURCE_NAME, + }) + ); + const spokeSnsEventBus = new targets.EventBus(_spokeSnsEventBus); + Aspects.of(_spokeSnsEventBus).add(new ConditionAspect(spokeSnsRegionExists)); + + const spokeSnsRule = new events.Rule(this, "SpokeSnsRule", { + description: `${this.node.tryGetContext("SOLUTION_ID")} ${this.node.tryGetContext( + "SOLUTION_NAME" + )} - ${id}-SpokeSnsEventsRule`, + eventPattern: snsRulePattern, + eventBus: spokeBus, + targets: [spokeSnsEventBus], }); + Aspects.of(spokeSnsRule).add(new ConditionAspect(spokeSnsRegionExists)); + + /** + * app registry application for SQ stack + */ + if (props.targetPartition !== "China") { + new AppRegistryApplication(this, "SQSpokeAppRegistryApplication", { + appRegistryApplicationName: this.node.tryGetContext("APP_REG_SQ_SPOKE_APPLICATION_NAME"), + solutionId: `${this.node.tryGetContext("SOLUTION_ID")}-SQ`, + }); + } + // add mode depends on the custom resource so that it fires the lambda function at last // Create/Update service list - const generateQuotaList = quotaListManager.addCustomResource( - "SQServiceList", - { - VERSION: this.node.tryGetContext("SOLUTION_VERSION"), // this is to trigger updates for different versions - } - ); + const generateQuotaList = quotaListManager.addCustomResource("SQServiceList", { + VERSION: this.node.tryGetContext("SOLUTION_VERSION"), // this is to trigger updates for different versions + SageMakerMonitoring: sageMakerMonitoring.valueAsString, + ConnectMonitoring: connectMonitoring.valueAsString, + }); applyDependsOn(generateQuotaList, quotaTable); applyDependsOn(generateQuotaList, serviceTable); applyDependsOn(generateQuotaList, eventsRuleError); diff --git a/source/resources/lib/ta-spoke.stack.ts b/source/resources/lib/ta-spoke.stack.ts index b3024d2..c010cd9 100644 --- a/source/resources/lib/ta-spoke.stack.ts +++ b/source/resources/lib/ta-spoke.stack.ts @@ -6,13 +6,13 @@ import { aws_events as events, App, CfnParameter, - CfnMapping, CfnOutput, Stack, aws_events_targets as targets, StackProps, } from "aws-cdk-lib"; import * as path from "path"; +import { addCfnGuardSuppression } from "./cfn-guard-utils"; import { EventsToLambda } from "./events-lambda.construct"; import { TA_CHECKS_SERVICES } from "./exports"; import { Layer } from "./lambda-layer.construct"; @@ -24,12 +24,17 @@ import { AppRegistryApplication } from "./app-registry-application"; * The stack should be deployed in the spoke accounts * @author aws-solutions */ + +interface QuotaMonitorTASpokeProps extends StackProps { + targetPartition: "Commercial" | "China"; +} + export class QuotaMonitorTASpoke extends Stack { /** * @param {App} scope - parent of the construct * @param {string} id - identifier for the object */ - constructor(scope: App, id: string, props?: StackProps) { + constructor(scope: App, id: string, props: QuotaMonitorTASpokeProps) { super(scope, id, props); //============================================================================================= @@ -39,11 +44,12 @@ export class QuotaMonitorTASpoke extends Stack { type: "String", }); - //============================================================================================= - // Mapping & Conditions - //============================================================================================= - const map = new CfnMapping(this, "QuotaMonitorMap"); - map.setValue("RefreshRate", "Default", "rate(1 day)"); + const taRefreshRate = new CfnParameter(this, "TARefreshRate", { + type: "String", + default: "rate(12 hours)", + allowedValues: ["rate(6 hours)", "rate(12 hours)", "rate(1 day)"], + description: "The rate at which to refresh Trusted Advisor checks", + }); //============================================================================================= // Metadata @@ -57,21 +63,26 @@ export class QuotaMonitorTASpoke extends Stack { }, Parameters: ["EventBusArn"], }, + { + Label: { + default: "Refresh Configuration", + }, + Parameters: ["TARefreshRate"], + }, ], ParameterLabels: { EventBusArn: { default: "Arn for the EventBridge bus in the monitoring account", }, + TARefreshRate: { + default: "Trusted Advisor Refresh Rate", + }, }, }, }; - this.templateOptions.description = `(${this.node.tryGetContext( - "SOLUTION_ID" - )}-TA) - ${this.node.tryGetContext( + this.templateOptions.description = `(${this.node.tryGetContext("SOLUTION_ID")}-TA) - ${this.node.tryGetContext( "SOLUTION_NAME" - )} - Trusted Advisor Template. Version ${this.node.tryGetContext( - "SOLUTION_VERSION" - )}`; + )} - Trusted Advisor Template. Version ${this.node.tryGetContext("SOLUTION_VERSION")}`; this.templateOptions.templateFormatVersion = "2010-09-09"; //============================================================================================= @@ -81,11 +92,7 @@ export class QuotaMonitorTASpoke extends Stack { /** * @description primary event bus to send events to */ - const primaryEventBus = events.EventBus.fromEventBusArn( - this, - "QM-EventBus", - eventBusArn.valueAsString - ); + const primaryEventBus = events.EventBus.fromEventBusArn(this, "QM-EventBus", eventBusArn.valueAsString); const target = new targets.EventBus(primaryEventBus); /** @@ -163,22 +170,15 @@ export class QuotaMonitorTASpoke extends Stack { /** * @description event-lambda construct for refreshing TA checks */ - const refresher = new EventsToLambda( - this, - "QM-TA-Refresher", - { - eventRule: events.Schedule.expression( - map.findInMap("RefreshRate", "Default") - ), - assetLocation: `${path.dirname( - __dirname - )}/../lambda/services/taRefresher/dist/ta-refresher.zip`, - environment: { - AWS_SERVICES: TA_CHECKS_SERVICES.join(","), - }, - layers: [utilsLayer.layer], - } - ); + const refresher = new EventsToLambda(this, "QM-TA-Refresher", { + eventRule: events.Schedule.expression(taRefreshRate.valueAsString), + assetLocation: `${path.dirname(__dirname)}/../lambda/services/taRefresher/dist/ta-refresher.zip`, + environment: { + AWS_SERVICES: TA_CHECKS_SERVICES.join(","), + }, + layers: [utilsLayer.layer], + }); + addCfnGuardSuppression(refresher.target, ["LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"]); // permission to refresh TA checks refresher.target.addToRolePolicy( @@ -188,15 +188,17 @@ export class QuotaMonitorTASpoke extends Stack { resources: ["*"], // does not allow resource-level permissions }) ); - + /** - * app registry application for TA stack - */ + * app registry application for TA stack + */ - new AppRegistryApplication(this, 'TASpokeAppRegistryApplication', { - appRegistryApplicationName: this.node.tryGetContext("APP_REG_TA_SPOKE_APPLICATION_NAME"), - solutionId: `${this.node.tryGetContext("SOLUTION_ID")}-TA` - }) + if (props.targetPartition !== "China") { + new AppRegistryApplication(this, "TASpokeAppRegistryApplication", { + appRegistryApplicationName: this.node.tryGetContext("APP_REG_TA_SPOKE_APPLICATION_NAME"), + solutionId: `${this.node.tryGetContext("SOLUTION_ID")}-TA`, + }); + } //============================================================================================= // Outputs diff --git a/source/resources/package-lock.json b/source/resources/package-lock.json index 6dd1dcd..8b04f76 100644 --- a/source/resources/package-lock.json +++ b/source/resources/package-lock.json @@ -1,12 +1,12 @@ { "name": "quota-monitor", - "version": "6.2.11", + "version": "6.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quota-monitor", - "version": "6.2.11", + "version": "6.3.0", "license": "Apache-2.0", "devDependencies": { "@aws-cdk/aws-servicecatalogappregistry-alpha": "^2.117.0-alpha.0", @@ -2086,10 +2086,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", diff --git a/source/resources/package.json b/source/resources/package.json old mode 100755 new mode 100644 index 42c67ed..578c9ea --- a/source/resources/package.json +++ b/source/resources/package.json @@ -1,6 +1,6 @@ { "name": "quota-monitor", - "version": "6.2.11", + "version": "6.3.0", "description": "cdk resources to provision needed infrastructure", "author": { "name": "Amazon Web Services", @@ -14,7 +14,11 @@ "pretest": "npm ci", "test": "npx jest", "cdk": "npx cdk", - "orgHub:deploy": "echo 'fixing stackset template paths for cdk assets by forcing it to synth twice' && rm -rf cdk.out && cdk synth && cdk deploy" + "get-cdk-bucket": "aws cloudformation describe-stacks --stack-name CDKToolkit --query 'Stacks[0].Outputs[?OutputKey==`BucketName`].OutputValue' --output text", + "update-bucket-encryption": "aws s3api put-bucket-encryption --bucket $SOLUTION_BUCKET --server-side-encryption-configuration '{\"Rules\": [{\"ApplyServerSideEncryptionByDefault\": {\"SSEAlgorithm\": \"AES256\"}}]}'", + "synth": "npm run cdk -- synth -c SOLUTION_NAME=quota-monitor-for-aws -c SOLUTION_VERSION=6.3.0", + "deploy": "npm run cdk -- deploy -c SOLUTION_NAME=quota-monitor-for-aws -c SOLUTION_VERSION=6.3.0", + "orgHub:deploy": "export MODIFY_TEMPLATES=true && export SOLUTION_BUCKET=$(npm run -s get-cdk-bucket) && npm run update-bucket-encryption && rm -rf cdk.out && echo 'fixing stackset template paths for cdk assets by forcing it to synth twice' && npm run synth && npm run deploy" }, "devDependencies": { "@types/jest": "^29.5.11", diff --git a/source/tsconfig.json b/source/tsconfig.json index 0bec5a0..4c01b96 100644 --- a/source/tsconfig.json +++ b/source/tsconfig.json @@ -1,39 +1,29 @@ { "compilerOptions": { - /* Basic Options */ - "target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, - "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + "target": "ES2020", + "module": "commonjs", "lib": [ "DOM", "ES2020" - ] /* Specify library files to be included in the compilation. */, - "declaration": false /* Generates corresponding '.d.ts' file. */, + ], + "declaration": false, "noEmit": true, - "removeComments": true /* Do not emit comments to output. */, - "resolveJsonModule": true /* Allows importing modules with a ‘.json’ extension */, - /* Strict Type-Checking Options */ - "strict": false /* Enable all strict type-checking options. */, - "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, - "strictNullChecks": true /* Enable strict null checks. */, - "strictFunctionTypes": true /* Enable strict checking of function types. */, - "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, - "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, - "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, - - /* Additional Checks */ - "noUnusedLocals": true /* Report errors on unused locals. */, - "noUnusedParameters": true /* Report errors on unused parameters. */, - "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, - "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, - - /* Module Resolution Options */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, - - /* Experimental Options */ - "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, - "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ - - /* Advanced Options */ - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + "removeComments": true, + "resolveJsonModule": true, + "strict": false, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "forceConsistentCasingInFileNames": true } }