From 0f2caea57e37a1a58dc305e2f342e9c28aeb9c41 Mon Sep 17 00:00:00 2001 From: AWS Date: Thu, 21 Jul 2022 16:11:40 +0000 Subject: [PATCH] Release: 1.6.0 --- README.md | 4 + VERSION | 2 +- main.tf | 1 + .../aft-account-customizations-terraform.yml | 10 +- .../aft-global-customizations-terraform.yml | 10 +- modules/aft-ssm-parameters/ssm.tf | 15 ++ modules/aft-ssm-parameters/variables.tf | 4 + .../account_provisioning_framework.py | 2 +- .../aft_common/{types.py => aft_types.py} | 0 .../aft-lambda-layer/aft_common/aft_utils.py | 8 +- .../aft-lambda-layer/aft_common/metrics.py | 136 ++++++++++++++++++ .../aft_common/report_metrics.py | 38 +++++ sources/aft-lambda-layer/setup.py | 1 + ...provisioning_framework_get_account_info.py | 2 +- .../aft_account_request_processor.py | 27 ++++ ...voke_aft_account_provisioning_framework.py | 1 + variables.tf | 14 ++ 17 files changed, 269 insertions(+), 6 deletions(-) rename sources/aft-lambda-layer/aft_common/{types.py => aft_types.py} (100%) create mode 100644 sources/aft-lambda-layer/aft_common/metrics.py create mode 100644 sources/aft-lambda-layer/aft_common/report_metrics.py diff --git a/README.md b/README.md index f9f90a2d..b7fa54ae 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,9 @@ for more information. Now that you have configured and deployed AWS Control Tower Account Factory for Terraform, follow the steps outlined in [Post-deployment steps](https://docs.aws.amazon.com/controltower/latest/userguide/aft-post-deployment.html) and [Provision accounts with AWS Control Tower Account Factory for Terraform](https://docs.aws.amazon.com/controltower/latest/userguide/taf-account-provisioning.html) to begin using your environment. +## Collection of Operational Metrics +As of version 1.6.0, AFT collects anonymous operational metrics to help AWS improve the quality and features of the solution. For more information, including how to disable this capability, please see the [documentation here](https://docs.aws.amazon.com/controltower/latest/userguide/aft-operational-metrics.html). + ## Requirements @@ -108,6 +111,7 @@ Now that you have configured and deployed AWS Control Tower Account Factory for | [aft\_framework\_repo\_git\_ref](#input\_aft\_framework\_repo\_git\_ref) | Git branch from which the AFT framework should be sourced from | `string` | `null` | no | | [aft\_framework\_repo\_url](#input\_aft\_framework\_repo\_url) | Git repo URL where the AFT framework should be sourced from | `string` | `"https://github.com/aws-ia/terraform-aws-control_tower_account_factory.git"` | no | | [aft\_management\_account\_id](#input\_aft\_management\_account\_id) | AFT Management Account ID | `string` | n/a | yes | +| [aft\_metrics\_reporting](#input\_aft\_metrics\_reporting) | Flag toggling reporting of operational metrics | `bool` | `true` | no | | [aft\_vpc\_cidr](#input\_aft\_vpc\_cidr) | CIDR Block to allocate to the AFT VPC | `string` | `"192.168.0.0/22"` | no | | [aft\_vpc\_endpoints](#input\_aft\_vpc\_endpoints) | Flag turning VPC endpoints on/off for AFT VPC | `bool` | `true` | no | | [aft\_vpc\_private\_subnet\_01\_cidr](#input\_aft\_vpc\_private\_subnet\_01\_cidr) | CIDR Block to allocate to the Private Subnet 01 | `string` | `"192.168.0.0/24"` | no | diff --git a/VERSION b/VERSION index 4cda8f19..dc1e644a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.5.2 +1.6.0 diff --git a/main.tf b/main.tf index c7b60713..9d4b950c 100644 --- a/main.tf +++ b/main.tf @@ -243,4 +243,5 @@ module "aft_ssm_parameters" { account_provisioning_customizations_repo_branch = var.account_provisioning_customizations_repo_branch maximum_concurrent_customizations = var.maximum_concurrent_customizations github_enterprise_url = var.github_enterprise_url + aft_metrics_reporting = var.aft_metrics_reporting } diff --git a/modules/aft-customizations/buildspecs/aft-account-customizations-terraform.yml b/modules/aft-customizations/buildspecs/aft-account-customizations-terraform.yml index d8880de5..a551c188 100644 --- a/modules/aft-customizations/buildspecs/aft-account-customizations-terraform.yml +++ b/modules/aft-customizations/buildspecs/aft-account-customizations-terraform.yml @@ -87,7 +87,7 @@ phases: fi build: - on-failure: ABORT + on-failure: CONTINUE commands: # Apply Customizations - | @@ -136,6 +136,14 @@ phases: post_build: on-failure: ABORT commands: + - export PYTHONPATH="$DEFAULT_PATH/aws-aft-core-framework/sources/aft-lambda-layer:$PYTHONPATH" + - export AWS_PROFILE=aft-management + - python3 $DEFAULT_PATH/aws-aft-core-framework/sources/aft-lambda-layer/aft_common/report_metrics.py --codebuild-name "aft-account-customizations" --codebuild-status $CODEBUILD_BUILD_SUCCEEDING + - unset AWS_PROFILE + - | + if [[ $CODEBUILD_BUILD_SUCCEEDING == 0 ]]; then + exit 1 + fi - | if [[ ! -z "$CUSTOMIZATION" ]]; then source $DEFAULT_PATH/api-helpers-venv/bin/activate diff --git a/modules/aft-customizations/buildspecs/aft-global-customizations-terraform.yml b/modules/aft-customizations/buildspecs/aft-global-customizations-terraform.yml index 255fc41f..c2d2d4cc 100644 --- a/modules/aft-customizations/buildspecs/aft-global-customizations-terraform.yml +++ b/modules/aft-customizations/buildspecs/aft-global-customizations-terraform.yml @@ -69,7 +69,7 @@ phases: - unset AWS_PROFILE build: - on-failure: ABORT + on-failure: CONTINUE commands: # Apply customizations - source $DEFAULT_PATH/aft-venv/bin/activate @@ -115,6 +115,14 @@ phases: post_build: on-failure: ABORT commands: + - export PYTHONPATH="$DEFAULT_PATH/aws-aft-core-framework/sources/aft-lambda-layer:$PYTHONPATH" + - export AWS_PROFILE=aft-management + - python3 $DEFAULT_PATH/aws-aft-core-framework/sources/aft-lambda-layer/aft_common/report_metrics.py --codebuild-name "aft-global-customizations" --codebuild-status $CODEBUILD_BUILD_SUCCEEDING + - unset AWS_PROFILE + - | + if [[ $CODEBUILD_BUILD_SUCCEEDING == 0 ]]; then + exit 1 + fi - source $DEFAULT_PATH/api-helpers-venv/bin/activate - export AWS_PROFILE=aft-target - $DEFAULT_PATH/api_helpers/post-api-helpers.sh diff --git a/modules/aft-ssm-parameters/ssm.tf b/modules/aft-ssm-parameters/ssm.tf index f44bac75..2800f21c 100644 --- a/modules/aft-ssm-parameters/ssm.tf +++ b/modules/aft-ssm-parameters/ssm.tf @@ -368,3 +368,18 @@ resource "aws_ssm_parameter" "aft_maximum_concurrent_customizations" { value = var.maximum_concurrent_customizations type = "String" } + +resource "aws_ssm_parameter" "aft_metrics_reporting" { + name = "/aft/config/metrics-reporting" + value = var.aft_metrics_reporting + type = "String" +} + +resource "random_uuid" "metrics_reporting_uuid" { +} + +resource "aws_ssm_parameter" "aft_metrics_reporting_uuid" { + name = "/aft/config/metrics-reporting-uuid" + value = random_uuid.metrics_reporting_uuid.result + type = "String" +} diff --git a/modules/aft-ssm-parameters/variables.tf b/modules/aft-ssm-parameters/variables.tf index 8782b76a..3530b29b 100644 --- a/modules/aft-ssm-parameters/variables.tf +++ b/modules/aft-ssm-parameters/variables.tf @@ -249,3 +249,7 @@ variable "maximum_concurrent_customizations" { variable "aft_version" { type = string } + +variable "aft_metrics_reporting" { + type = string +} diff --git a/sources/aft-lambda-layer/aft_common/account_provisioning_framework.py b/sources/aft-lambda-layer/aft_common/account_provisioning_framework.py index d0720efb..9bc9edc2 100644 --- a/sources/aft-lambda-layer/aft_common/account_provisioning_framework.py +++ b/sources/aft-lambda-layer/aft_common/account_provisioning_framework.py @@ -9,8 +9,8 @@ import aft_common.aft_utils as utils import jsonschema +from aft_common.aft_types import AftAccountInfo from aft_common.auth import AuthClient -from aft_common.types import AftAccountInfo from boto3.session import Session from botocore.exceptions import ClientError diff --git a/sources/aft-lambda-layer/aft_common/types.py b/sources/aft-lambda-layer/aft_common/aft_types.py similarity index 100% rename from sources/aft-lambda-layer/aft_common/types.py rename to sources/aft-lambda-layer/aft_common/aft_types.py diff --git a/sources/aft-lambda-layer/aft_common/aft_utils.py b/sources/aft-lambda-layer/aft_common/aft_utils.py index ac6afb72..a06a0001 100644 --- a/sources/aft-lambda-layer/aft_common/aft_utils.py +++ b/sources/aft-lambda-layer/aft_common/aft_utils.py @@ -63,7 +63,7 @@ STSClient = object -from aft_common.types import AftAccountInfo +from aft_common.aft_types import AftAccountInfo from .logger import Logger @@ -129,6 +129,12 @@ SSM_PARAM_ACCOUNT_LOG_ARCHIVE_ACCOUNT_ID = "/aft/account/log-archive/account-id" SSM_PARAM_ACCOUNT_AFT_MANAGEMENT_ACCOUNT_ID = "/aft/account/aft-management/account-id" +SSM_PARAM_ACCOUNT_AFT_VERSION = "/aft/config/aft/version" +SSM_PARAM_ACCOUNT_TERRAFORM_VERSION = "/aft/config/terraform/version" + +SSM_PARAM_AFT_METRICS_REPORTING = "/aft/config/metrics-reporting" +SSM_PARAM_AFT_METRICS_REPORTING_UUID = "/aft/config/metrics-reporting-uuid" + # INIT def get_logger() -> Logger: diff --git a/sources/aft-lambda-layer/aft_common/metrics.py b/sources/aft-lambda-layer/aft_common/metrics.py new file mode 100644 index 00000000..8823e09d --- /dev/null +++ b/sources/aft-lambda-layer/aft_common/metrics.py @@ -0,0 +1,136 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import json +from datetime import datetime +from typing import Any, Dict, Optional, TypedDict + +import requests # type: ignore +from aft_common import aft_utils as utils +from aft_common.auth import AuthClient +from boto3.session import Session + + +class MetricsPayloadType(TypedDict): + Solution: str + TimeStamp: str + Version: Optional[str] + UUID: Optional[str] + Data: Dict[str, Any] + + +class AFTMetrics: + def __init__(self) -> None: + + self.solution_id = "SO0089-aft" + self.api_endpoint = "https://metrics.awssolutionsbuilder.com/generic" + self.auth = AuthClient() + + def _get_uuid(self, aft_management_session: Session) -> str: + + uuid = utils.get_ssm_parameter_value( + aft_management_session, utils.SSM_PARAM_AFT_METRICS_REPORTING_UUID + ) + return uuid + + def _metrics_reporting_enabled(self, aft_management_session: Session) -> bool: + + flag = utils.get_ssm_parameter_value( + aft_management_session, utils.SSM_PARAM_AFT_METRICS_REPORTING + ) + + if flag.lower() == "true": + return True + return False + + def _get_aft_deployment_config( + self, aft_management_session: Session + ) -> Dict[str, str]: + + config = {} + + config["cloud_trail_enabled"] = utils.get_ssm_parameter_value( + aft_management_session, + utils.SSM_PARAM_FEATURE_CLOUDTRAIL_DATA_EVENTS_ENABLED, + ) + config["enterprise_support_enabled"] = utils.get_ssm_parameter_value( + aft_management_session, utils.SSM_PARAM_FEATURE_ENTERPRISE_SUPPORT_ENABLED + ) + config["delete_default_vpc_enabled"] = utils.get_ssm_parameter_value( + aft_management_session, utils.SSM_PARAM_FEATURE_DEFAULT_VPCS_ENABLED + ) + + config["aft_version"] = utils.get_ssm_parameter_value( + aft_management_session, utils.SSM_PARAM_ACCOUNT_AFT_VERSION + ) + + config["terraform_version"] = utils.get_ssm_parameter_value( + aft_management_session, utils.SSM_PARAM_ACCOUNT_TERRAFORM_VERSION + ) + + config["region"] = utils.get_session_info(aft_management_session)["region"] + + return config + + def wrap_event_for_api( + self, aft_management_session: Session, event: Dict[str, Any] + ) -> MetricsPayloadType: + + payload: MetricsPayloadType = { + "Solution": self.solution_id, + "TimeStamp": datetime.utcnow().isoformat(timespec="seconds"), + "Version": None, + "UUID": None, + "Data": {}, + } + payload["Solution"] = self.solution_id + + data_body: Dict[str, Any] = {} + data_body["event"] = event + + errors = [] + + try: + payload["Version"] = utils.get_ssm_parameter_value( + aft_management_session, utils.SSM_PARAM_ACCOUNT_AFT_VERSION + ) + except Exception as e: + payload["Version"] = None + errors.append(str(e)) + + try: + payload["UUID"] = self._get_uuid(aft_management_session) + except Exception as e: + payload["UUID"] = None + errors.append(str(e)) + + try: + data_body["config"] = self._get_aft_deployment_config( + aft_management_session + ) + except Exception as e: + data_body["config"] = None + errors.append(str(e)) + + if not errors: + data_body["error"] = None + else: + data_body["error"] = " | ".join(errors) + + payload["Data"] = data_body + + return payload + + def post_event(self, action: str, status: Optional[str] = None) -> None: + + aft_management_session = self.auth.get_aft_management_session() + + if self._metrics_reporting_enabled(aft_management_session): + + event = {"action": action, "status": status} + + payload = self.wrap_event_for_api(aft_management_session, event) + + response = requests.post(self.api_endpoint, json=payload) + + return None diff --git a/sources/aft-lambda-layer/aft_common/report_metrics.py b/sources/aft-lambda-layer/aft_common/report_metrics.py new file mode 100644 index 00000000..12278a88 --- /dev/null +++ b/sources/aft-lambda-layer/aft_common/report_metrics.py @@ -0,0 +1,38 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import argparse + +from aft_common import aft_utils as utils +from aft_common.metrics import AFTMetrics + +logger = utils.get_logger() + +if __name__ == "__main__": + + parser = argparse.ArgumentParser( + description="Script called from within CodeBuild containers to report back AFT usage metrics" + ) + + parser.add_argument( + "--codebuild-name", type=str, help="Name of the build container" + ) + parser.add_argument( + "--codebuild-status", + type=int, + help="Whether the build succeeded or not (1 or 0)", + ) + + args = parser.parse_args() + + codebuild_name = args.codebuild_name + codebuild_status = "SUCCEEDED" if args.codebuild_status == 1 else "FAILED" + + try: + aft_metrics = AFTMetrics() + aft_metrics.post_event(action=args.codebuild_name, status=args.codebuild_status) + logger.info(f"Successfully logged metrics. Action: {args.codebuild_name}") + except Exception as e: + logger.info( + f"Unable to report metrics. Action: {args.codebuild_name}; Error: {e}" + ) diff --git a/sources/aft-lambda-layer/setup.py b/sources/aft-lambda-layer/setup.py index cc67ed94..fb35069b 100644 --- a/sources/aft-lambda-layer/setup.py +++ b/sources/aft-lambda-layer/setup.py @@ -63,6 +63,7 @@ "whaaaaat == 0.5.2", "Jinja2 == 3.0.3", "jsonschema == 4.3.2", + "requests == 2.28.0", ], extras_require={ "dev": [ diff --git a/src/aft_lambda/aft_account_provisioning_framework/aft_account_provisioning_framework_get_account_info.py b/src/aft_lambda/aft_account_provisioning_framework/aft_account_provisioning_framework_get_account_info.py index 699dc151..0a7aed3e 100644 --- a/src/aft_lambda/aft_account_provisioning_framework/aft_account_provisioning_framework_get_account_info.py +++ b/src/aft_lambda/aft_account_provisioning_framework/aft_account_provisioning_framework_get_account_info.py @@ -7,8 +7,8 @@ from aft_common import aft_utils as utils from aft_common import notifications from aft_common.account_provisioning_framework import ProvisionRoles, get_account_info +from aft_common.aft_types import AftAccountInfo from aft_common.auth import AuthClient -from aft_common.types import AftAccountInfo from boto3.session import Session if TYPE_CHECKING: diff --git a/src/aft_lambda/aft_account_request_framework/aft_account_request_processor.py b/src/aft_lambda/aft_account_request_framework/aft_account_request_processor.py index 01f8f729..2dbb71c8 100644 --- a/src/aft_lambda/aft_account_request_framework/aft_account_request_processor.py +++ b/src/aft_lambda/aft_account_request_framework/aft_account_request_processor.py @@ -18,6 +18,7 @@ ) from aft_common.auth import AuthClient from aft_common.exceptions import NoAccountFactoryPortfolioFound +from aft_common.metrics import AFTMetrics from boto3.session import Session if TYPE_CHECKING: @@ -31,6 +32,7 @@ def lambda_handler(event: Dict[str, Any], context: LambdaContext) -> None: aft_management_session = Session() auth = AuthClient() + try: account_request = AccountRequest(auth=auth) try: @@ -55,6 +57,9 @@ def lambda_handler(event: Dict[str, Any], context: LambdaContext) -> None: ), ) if sqs_message is not None: + + aft_metrics = AFTMetrics() + sqs_body = json.loads(sqs_message["Body"]) ct_request_is_valid = True if sqs_body["operation"] == "ADD": @@ -68,6 +73,17 @@ def lambda_handler(event: Dict[str, Any], context: LambdaContext) -> None: request=sqs_body, ) + action = "new-account-creation-invoked" + try: + aft_metrics.post_event(action=action, status="SUCCEEDED") + logger.info( + f"Successfully logged metrics. Action: {action}" + ) + except Exception as e: + logger.info( + f"Unable to report metrics. Action: {action}; Error: {e}" + ) + elif sqs_body["operation"] == "UPDATE": ct_request_is_valid = modify_ct_request_is_valid(sqs_body) if ct_request_is_valid: @@ -76,6 +92,17 @@ def lambda_handler(event: Dict[str, Any], context: LambdaContext) -> None: ct_management_session=ct_management_session, request=sqs_body, ) + + action = "existing-account-update-invoked" + try: + aft_metrics.post_event(action=action, status="SUCCEEDED") + logger.info( + f"Successfully logged metrics. Action: {action}" + ) + except Exception as e: + logger.info( + f"Unable to report metrics. Action: {action}; Error: {e}" + ) else: logger.info("Unknown operation received in message") diff --git a/src/aft_lambda/aft_account_request_framework/aft_invoke_aft_account_provisioning_framework.py b/src/aft_lambda/aft_account_request_framework/aft_invoke_aft_account_provisioning_framework.py index 71c373cd..a492b3b4 100644 --- a/src/aft_lambda/aft_account_request_framework/aft_invoke_aft_account_provisioning_framework.py +++ b/src/aft_lambda/aft_account_request_framework/aft_invoke_aft_account_provisioning_framework.py @@ -26,6 +26,7 @@ def lambda_handler(event: Dict[str, Any], context: LambdaContext) -> None: session = Session() auth = AuthClient() + try: ct_management_session = auth.get_ct_management_session( role_name=ProvisionRoles.SERVICE_ROLE_NAME diff --git a/variables.tf b/variables.tf index 153c6658..be95d136 100644 --- a/variables.tf +++ b/variables.tf @@ -362,3 +362,17 @@ variable "aft_vpc_public_subnet_02_cidr" { error_message = "Variable var: aft_vpc_public_subnet_02_cidr value must be a valid network CIDR, x.x.x.x/y." } } + +######################################### +# AFT Metrics Reporting Variables +######################################### + +variable "aft_metrics_reporting" { + description = "Flag toggling reporting of operational metrics" + type = bool + default = true + validation { + condition = contains([true, false], var.aft_metrics_reporting) + error_message = "Valid values for var: aft_metrics_reporting are (true, false)." + } +}