From 49b51c92daba1215db58b3448fbbbb1cdf1358ec Mon Sep 17 00:00:00 2001 From: "John \"Preston\" Mille" Date: Sun, 15 Jan 2023 16:41:17 +0000 Subject: [PATCH 1/2] CW Agent configuration & EMF improvement * Fix SSM Parameter access - CW Agent config always created. * SSM Parameters names generated by CFN * Better logical resource names in the template for SSM Parameters * Shorter JSON for CW Agent config * Fixing service IAM Role property --- .../compose_x/ecs.details/monitoring.rst | 33 ++++++++++++++++++ .../compose_x/ecs.details/prometheus.rst | 11 ++++++ ecs_composex/ecs/ecs_prometheus/__init__.py | 6 ++-- .../ecs_prometheus/config_ssm_parameters.py | 34 +++++-------------- ecs_composex/ecs/ecs_prometheus/helpers.py | 33 ++++++++++++------ .../ecs/managed_sidecars/aws_cw_agent.py | 5 ++- ecs_composex/ecs/task_iam/task_role.py | 27 +++++++++++---- ecs_composex/specs/compose-spec.json | 3 ++ .../specs/services.x-monitoring.spec.json | 15 ++++++++ 9 files changed, 121 insertions(+), 46 deletions(-) create mode 100644 docs/syntax/compose_x/ecs.details/monitoring.rst create mode 100644 ecs_composex/specs/services.x-monitoring.spec.json diff --git a/docs/syntax/compose_x/ecs.details/monitoring.rst b/docs/syntax/compose_x/ecs.details/monitoring.rst new file mode 100644 index 000000000..706163f8d --- /dev/null +++ b/docs/syntax/compose_x/ecs.details/monitoring.rst @@ -0,0 +1,33 @@ + +.. meta:: + :description: ECS Compose-X service level x-monitoring extensions + :keywords: AWS, AWS ECS, compose, monitoring + +.. _x_services_monitoring_syntax: + +====================== +services.x-monitoring +====================== + +.. code-block:: yaml + + services: + serviceA: + x-monitoring: + CWAgentCollectEmf: bool + +Shorthands for monitoring features. + + +.. _monitoring_cw_agent_emf_collection: + +CWAgentCollectEmf +=================== + +Simple boolean that will automatically add the CW Agent to the task definition and allow EMF Collection. +The ``AWS_EMF_AGENT_ENDPOINT`` environment variable for the other services is automatically set to point to the CW Agent. +A new SSM Parameter is created with the configuration necessary, and exposed to the container as ``CW_CONFIG_CONTENT`` + +See the `AWS CloudWatch agent & EMF Configuration for details`_ of what's configured under the hood. + +.. _AWS CloudWatch agent & EMF Configuration for details: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Generation_CloudWatch_Agent.html diff --git a/docs/syntax/compose_x/ecs.details/prometheus.rst b/docs/syntax/compose_x/ecs.details/prometheus.rst index 9e5023c99..e11c4ebf3 100644 --- a/docs/syntax/compose_x/ecs.details/prometheus.rst +++ b/docs/syntax/compose_x/ecs.details/prometheus.rst @@ -32,12 +32,23 @@ ContainersInsights Syntax Reference .. code-block:: yaml EnableCWAgentDebug: bool + CollectEmf: bool CollectForAppMesh: bool CollectForJavaJmx: bool|ExporterConfig CollectForNginx: bool|ExporterConfig AutoAddNginxPrometheusExporter: bool CustomRules: [ExporterConfig] +CollectEmf +------------- + +This allows to turn on EMF Collection from the CW Agent container. + +.. hint:: + + Same as :ref:`monitoring_cw_agent_emf_collection`. See for more details. + + CollectForAppMesh ------------------- diff --git a/ecs_composex/ecs/ecs_prometheus/__init__.py b/ecs_composex/ecs/ecs_prometheus/__init__.py index 29d35ad72..39c17dd7c 100644 --- a/ecs_composex/ecs/ecs_prometheus/__init__.py +++ b/ecs_composex/ecs/ecs_prometheus/__init__.py @@ -96,9 +96,9 @@ def add_cw_agent_to_family( else: prometheus_config = None cw_agent_config = set_cw_config_parameter(family, collect_emf, **prometheus_options) - cw_agent_service = define_cloudwatch_agent(prometheus_config, cw_agent_config) + cw_agent_service = define_cloudwatch_agent(cw_agent_config, prometheus_config) cw_agent_service.add_to_family(family, is_dependency=True) - set_ecs_cw_policy(family, prometheus_config, cw_agent_config) + set_ecs_cw_policy(family, cw_agent_config, prometheus_config) family.cwagent_service = cw_agent_service if collect_emf: env_var = Environment( @@ -118,5 +118,5 @@ def add_cw_agent_to_family( ) family.iam_manager.add_new_managed_policy( "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", - role_name=family.iam_manager.task_role._role_type, + role_name=family.iam_manager.task_role.role_type, ) diff --git a/ecs_composex/ecs/ecs_prometheus/config_ssm_parameters.py b/ecs_composex/ecs/ecs_prometheus/config_ssm_parameters.py index 047152ea7..23a97ca00 100644 --- a/ecs_composex/ecs/ecs_prometheus/config_ssm_parameters.py +++ b/ecs_composex/ecs/ecs_prometheus/config_ssm_parameters.py @@ -26,6 +26,7 @@ from yaml import Loader as Loader from ecs_composex.common.cfn_params import STACK_ID_SHORT +from ecs_composex.common.troposphere_tools import add_resource from ecs_composex.ecs import ecs_params from ecs_composex.ecs.ecs_prometheus.emf_processors import generate_emf_processors @@ -69,24 +70,16 @@ def set_cw_prometheus_config_parameter( } parameter = SSMParameter( - f"{family.logical_name}SSMPrometheusConfig", + f"{family.logical_name}CWAgentPrometheusScrapingConfig", Tier="Standard", Type="String", - Name=Sub( - f"/ecs/config/prometheus/${{{ecs_params.CLUSTER_NAME.title}}}/${{STACK_SHORT_ID}}" - f"/${{{ecs_params.SERVICE_NAME.title}}}", - STACK_SHORT_ID=STACK_ID_SHORT, - ), Description=Sub( - f"Prometheus Scraping SSM Parameter for ECS Cluster: ${{{ecs_params.CLUSTER_NAME.title}}}" + "CW Agent Prometheus Scraping config for " + f"ecs/${{{ecs_params.CLUSTER_NAME.title}}}/${{{ecs_params.SERVICE_NAME.title}}}" ), Value=Sub(yaml.dump(value_py, Dumper=Dumper), STACK_SHORT_ID=STACK_ID_SHORT), ) - if parameter.title not in family.template.resources: - family.template.add_resource(parameter) - return parameter - else: - return family.template.resources[parameter.title] + return add_resource(family.template, parameter) def set_cw_config_parameter( @@ -131,24 +124,15 @@ def set_cw_config_parameter( "emf_processor" ] = emf_processors parameter = SSMParameter( - f"{family.logical_name}SSMCWAgentPrometheusConfig", + f"{family.logical_name}SsmCWAgentConfig", Tier="Intelligent-Tiering", Type="String", - Name=Sub( - f"/ecs/config/cw_agent_config/${{{ecs_params.CLUSTER_NAME.title}}}/${{STACK_SHORT_ID}}" - f"/${{{ecs_params.SERVICE_NAME.title}}}", - STACK_SHORT_ID=STACK_ID_SHORT, - ), Description=Sub( - f"Prometheus Scraping SSM Parameter for ECS Cluster: ${{{ecs_params.CLUSTER_NAME.title}}}" + f"CW Agent config for ecs/${{{ecs_params.CLUSTER_NAME.title}}}/${{{ecs_params.SERVICE_NAME.title}}}" ), Value=Sub( - json.dumps(value_py, ensure_ascii=True, sort_keys=True, indent=2), + json.dumps(value_py, ensure_ascii=True, sort_keys=True), STACK_SHORT_ID=STACK_ID_SHORT, ), ) - if parameter.title not in family.template.resources: - family.template.add_resource(parameter) - return parameter - else: - return family.template.resources[parameter.title] + return add_resource(family.template, parameter) diff --git a/ecs_composex/ecs/ecs_prometheus/helpers.py b/ecs_composex/ecs/ecs_prometheus/helpers.py index 2d7b32792..129d8a974 100644 --- a/ecs_composex/ecs/ecs_prometheus/helpers.py +++ b/ecs_composex/ecs/ecs_prometheus/helpers.py @@ -22,12 +22,14 @@ from ecs_composex.ecs import ecs_params -def define_cloudwatch_agent(cw_prometheus_config, cw_agent_config) -> ManagedSidecar: +def define_cloudwatch_agent( + cw_agent_config, cw_prometheus_config=None +) -> ManagedSidecar: """ Function to define the CW Agent image task definition - :param cw_prometheus_config: :param cw_agent_config: + :param cw_prometheus_config: :return: """ from copy import deepcopy @@ -40,7 +42,7 @@ def define_cloudwatch_agent(cw_prometheus_config, cw_agent_config) -> ManagedSid Name="CW_CONFIG_CONTENT", ValueFrom=Sub( f"arn:${{{AWS_PARTITION}}}:ssm:${{{AWS_REGION}}}:${{{AWS_ACCOUNT_ID}}}" - f":parameter${{{cw_agent_config.title}}}" + f":parameter/${{{cw_agent_config.title}}}" ), ), ] @@ -50,7 +52,7 @@ def define_cloudwatch_agent(cw_prometheus_config, cw_agent_config) -> ManagedSid Name="PROMETHEUS_CONFIG_CONTENT", ValueFrom=Sub( f"arn:${{{AWS_PARTITION}}}:ssm:${{{AWS_REGION}}}:${{{AWS_ACCOUNT_ID}}}" - f":parameter${{{cw_prometheus_config.title}}}" + f":parameter/${{{cw_prometheus_config.title}}}" ), ), ) @@ -67,8 +69,8 @@ def define_cloudwatch_agent(cw_prometheus_config, cw_agent_config) -> ManagedSid def set_ecs_cw_policy( family: ComposeFamily, - prometheus_parameter: Parameter, cw_config_parameter: Parameter, + prometheus_parameter: Parameter = None, ) -> None: """ Renders the IAM policy to grant the TaskRole access to CW, ECS and SSM Parameters @@ -83,6 +85,20 @@ def set_ecs_cw_policy( PolicyDocument={ "Version": "2012-10-17", "Statement": [ + { + "Sid": "CWAgentConfigurationFromSSMParameter", + "Effect": "Allow", + "Action": ["ssm:GetParameter*"], + "Resource": [ + Sub( + "arn:aws:ssm:*:${AWS::AccountId}:parameter/AmazonCloudWatch-*" + ), + Sub( + f"arn:${{{AWS_PARTITION}}}:ssm:${{{AWS_REGION}}}:${{{AWS_ACCOUNT_ID}}}" + f":parameter${{{cw_config_parameter.title}}}" + ), + ], + }, { "Sid": "EnableCreationAndManagementOfContainerInsightsLogEvents", "Effect": "Allow", @@ -142,19 +158,14 @@ def set_ecs_cw_policy( if prometheus_parameter: ecs_sd_policy.PolicyDocument["Statement"].append( { - "Sid": "ExtractFromCloudWatchAgentServerPolicy", + "Sid": "CWAgentPrometheusScrapingConfigurationAccess", "Effect": "Allow", "Action": ["ssm:GetParameter*"], "Resource": [ - Sub("arn:aws:ssm:*:${AWS::AccountId}:parameter/AmazonCloudWatch-*"), Sub( f"arn:${{{AWS_PARTITION}}}:ssm:${{{AWS_REGION}}}:${{{AWS_ACCOUNT_ID}}}" f":parameter${{{prometheus_parameter.title}}}" ), - Sub( - f"arn:${{{AWS_PARTITION}}}:ssm:${{{AWS_REGION}}}:${{{AWS_ACCOUNT_ID}}}" - f":parameter${{{cw_config_parameter.title}}}" - ), ], }, ) diff --git a/ecs_composex/ecs/managed_sidecars/aws_cw_agent.py b/ecs_composex/ecs/managed_sidecars/aws_cw_agent.py index 2f3294265..d6e144a79 100644 --- a/ecs_composex/ecs/managed_sidecars/aws_cw_agent.py +++ b/ecs_composex/ecs/managed_sidecars/aws_cw_agent.py @@ -17,7 +17,10 @@ CW_AGENT_NAME = "cloudwatch-agent" CW_AGENT_DEFINITION = { "image": CW_IMAGE_PARAMETER.Default, - "ports": [{"target": 25888, "protocol": "tcp"}], + "ports": [ + {"target": 25888, "protocol": "tcp"}, + {"target": 25888, "protocol": "udp"}, + ], "deploy": { "resources": {"limits": {"cpus": 0.1, "memory": "256M"}}, }, diff --git a/ecs_composex/ecs/task_iam/task_role.py b/ecs_composex/ecs/task_iam/task_role.py index a2bd4b82e..3a588b37b 100644 --- a/ecs_composex/ecs/task_iam/task_role.py +++ b/ecs_composex/ecs/task_iam/task_role.py @@ -1,6 +1,12 @@ # SPDX-License-Identifier: MPL-2.0 # Copyright 2020-2022 John Mille +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ecs_composex.ecs.ecs_family import ComposeFamily from compose_x_common.compose_x_common import keyisset from troposphere import GetAtt, Output, Ref, Sub @@ -18,15 +24,12 @@ class EcsRole: Class to wrap around the AWS IAM Role """ - def __init__(self, family, role_type): + def __init__(self, family: ComposeFamily, role_type: str): """ :param family: The family the role will belong to """ - if role_type not in [TASK_ROLE_T, EXEC_ROLE_T]: - raise ValueError( - "role_type is", role_type, "expected one of", [TASK_ROLE_T, EXEC_ROLE_T] - ) - self._role_type = role_type + self._role_type = None + self.role_type = role_type self._name = None self._arn = None self.family = family @@ -55,6 +58,18 @@ def __init__(self, family, role_type): self.outputs = [] self.lookup = {} + @property + def role_type(self) -> str: + return self._role_type + + @role_type.setter + def role_type(self, role_type: str) -> None: + if role_type not in [TASK_ROLE_T, EXEC_ROLE_T]: + raise ValueError( + "role_type is", role_type, "expected one of", [TASK_ROLE_T, EXEC_ROLE_T] + ) + self._role_type = role_type + @property def name_param(self): """ diff --git a/ecs_composex/specs/compose-spec.json b/ecs_composex/specs/compose-spec.json index e586631aa..688e6bb73 100644 --- a/ecs_composex/specs/compose-spec.json +++ b/ecs_composex/specs/compose-spec.json @@ -105,6 +105,9 @@ "x-prometheus": { "$ref": "services.x-prometheus.spec.json" }, + "x-monitoring": { + "$ref": "services.x-monitoring.spec.json" + }, "x-ecs": { "$ref": "services.x-ecs.spec.json" }, diff --git a/ecs_composex/specs/services.x-monitoring.spec.json b/ecs_composex/specs/services.x-monitoring.spec.json new file mode 100644 index 000000000..bb3171475 --- /dev/null +++ b/ecs_composex/specs/services.x-monitoring.spec.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "id": "services.x-monitoring", + "$id": "services.x-monitoring.spec.json", + "type": "object", + "title": "services.x-monitoring specification", + "description": "The services.x-monitoring specification for ECS Compose-X", + "additionalProperties": false, + "properties": { + "CWAgentCollectEmf": { + "type": "boolean" + } + }, + "definitions": {} +} From 7deb1cbd4c1714a30ba6b904ac8052754abe5f57 Mon Sep 17 00:00:00 2001 From: "John \"Preston\" Mille" Date: Sun, 15 Jan 2023 16:47:54 +0000 Subject: [PATCH 2/2] Fix ARN Path to the SSM parameters --- ecs_composex/ecs/ecs_prometheus/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ecs_composex/ecs/ecs_prometheus/helpers.py b/ecs_composex/ecs/ecs_prometheus/helpers.py index 129d8a974..c630d80c6 100644 --- a/ecs_composex/ecs/ecs_prometheus/helpers.py +++ b/ecs_composex/ecs/ecs_prometheus/helpers.py @@ -95,7 +95,7 @@ def set_ecs_cw_policy( ), Sub( f"arn:${{{AWS_PARTITION}}}:ssm:${{{AWS_REGION}}}:${{{AWS_ACCOUNT_ID}}}" - f":parameter${{{cw_config_parameter.title}}}" + f":parameter/${{{cw_config_parameter.title}}}" ), ], }, @@ -164,7 +164,7 @@ def set_ecs_cw_policy( "Resource": [ Sub( f"arn:${{{AWS_PARTITION}}}:ssm:${{{AWS_REGION}}}:${{{AWS_ACCOUNT_ID}}}" - f":parameter${{{prometheus_parameter.title}}}" + f":parameter/${{{prometheus_parameter.title}}}" ), ], },