import json import os import hashlib import re import yaml from aws_cdk import ( Stack, Tags, CfnOutput, Duration, aws_route53 as route53, aws_cloudwatch as cloudwatch, aws_sns as sns, aws_sns_subscriptions as sns_subs, aws_lambda as _lambda, aws_secretsmanager as secretsmanager, ) from constructs import Construct DEFAULT_DEPLOYMENT_ID = "dns-alert" MAX_DEPLOYMENT_ID_LENGTH = 24 def sanitize_deployment_id(raw_value: str) -> str: """Return a deployment identifier safe for stack and resource names.""" normalized = re.sub(r"[^a-z0-9]+", "-", raw_value.strip().lower()) normalized = re.sub(r"-{2,}", "-", normalized).strip("-") normalized = normalized[:MAX_DEPLOYMENT_ID_LENGTH].strip("-") return normalized or DEFAULT_DEPLOYMENT_ID def stack_name_for_deployment(deployment_id: str) -> str: return f"DnsAlertStack-{sanitize_deployment_id(deployment_id)}" def project_topic_name_for_deployment(deployment_id: str) -> str: return f"dns-health-check-alerts-{sanitize_deployment_id(deployment_id)}" def project_lark_function_name_for_deployment(deployment_id: str) -> str: return f"dns-alert-lark-notifier-{sanitize_deployment_id(deployment_id)}" def health_check_tag_name_for_deployment(deployment_id: str, health_check_name: str) -> str: return f"r53-{sanitize_deployment_id(deployment_id)}-{health_check_name}" def alarm_name_for_deployment(deployment_id: str, health_check_name: str) -> str: return f"r53-{sanitize_deployment_id(deployment_id)}-{health_check_name}-health" def stable_construct_suffix(raw_name: str) -> str: """Return a CDK-safe, collision-resistant suffix for construct IDs.""" alnum_only = "".join(ch for ch in raw_name if ch.isalnum()) readable = alnum_only[:24] or "HealthCheck" digest = hashlib.sha1(raw_name.encode("utf-8")).hexdigest()[:10] return f"{readable}{digest}" def build_enterprise_alarm_description( health_check_name: str, health_check_domain: str, health_check_type: str, health_check_path: str, health_check_port: int, ) -> str: """Build static enterprise metadata for the alarm description field.""" if health_check_type == "TCP": description = ( f"Route53 health check unhealthy for {health_check_domain} over TCP port {health_check_port}" ) else: description = ( f"Route53 health check unhealthy for {health_check_domain} over " f"{health_check_type} {health_check_path}" ) return ( json.dumps( { "component": "security", "description": description, "priority": "Critical", "project": "O11Y", }, indent=0, ) + "\n" ) class DnsAlertStack(Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # --- Context variables --- deployment_id = sanitize_deployment_id( (self.node.try_get_context("deployment_id") or DEFAULT_DEPLOYMENT_ID).strip() ) enterprise_sns_topic_arn = (self.node.try_get_context("enterprise_sns_topic_arn") or "").strip() enterprise_https_endpoint_url = (self.node.try_get_context("enterprise_https_endpoint_url") or "").strip() project_lark_secret_arn = (self.node.try_get_context("project_lark_secret_arn") or "").strip() notification_mode = (self.node.try_get_context("notification_mode") or "").strip() legacy_enable_test_lark = self.node.try_get_context("enable_test_lark") == "true" if not notification_mode: if enterprise_sns_topic_arn and legacy_enable_test_lark: notification_mode = "dual" elif enterprise_sns_topic_arn: notification_mode = "enterprise" elif legacy_enable_test_lark: notification_mode = "project_lark" if notification_mode not in { "enterprise", "project_lark", "dual", "enterprise_https", }: raise ValueError( "CDK context 'notification_mode' must be one of: enterprise, project_lark, dual, " "enterprise_https." ) enterprise_enabled = notification_mode in {"enterprise", "dual"} enterprise_https_enabled = notification_mode == "enterprise_https" project_lark_enabled = notification_mode in {"project_lark", "dual"} relay_topic_enabled = ( project_lark_enabled or enterprise_https_enabled ) if enterprise_enabled and not enterprise_sns_topic_arn: raise ValueError( "CDK context 'enterprise_sns_topic_arn' is required when enterprise delivery is enabled." ) if enterprise_https_endpoint_url and not enterprise_https_endpoint_url.startswith("https://"): raise ValueError( "CDK context 'enterprise_https_endpoint_url' must start with 'https://'." ) if project_lark_enabled and not project_lark_secret_arn: raise ValueError( "CDK context 'project_lark_secret_arn' is required when project_lark delivery is enabled." ) # --- Load health checks from YAML --- config_path = os.path.join(os.path.dirname(__file__), "..", "..", "config", "health_checks.yaml") config_path = os.path.normpath(config_path) with open(config_path, "r") as f: config = yaml.safe_load(f) or {} health_check_entries = config.get("health_checks") if not health_check_entries: raise ValueError( f"No 'health_checks' entries found in {config_path}. " "Add at least one endpoint to monitor." ) # --- Validate name uniqueness --- names = [hc["name"] for hc in health_check_entries] duplicates = [n for n in names if names.count(n) > 1] if duplicates: raise ValueError( f"Duplicate health check names found: {sorted(set(duplicates))}. " "Each health check must have a unique 'name'." ) # --- Validate fields --- for hc in health_check_entries: name = str(hc.get("name") or "").strip() domain = str(hc.get("domain") or "").strip() if not name: raise ValueError("Each health check must define a non-empty 'name'.") if not domain: raise ValueError(f"Health check '{name or ''}': domain is required") interval = hc.get("request_interval", 30) if interval not in (10, 30): raise ValueError( f"Health check '{name}': request_interval must be 10 or 30, got {interval}" ) hc_type = hc.get("type", "HTTPS").upper() if hc_type not in ("HTTP", "HTTPS", "TCP"): raise ValueError( f"Health check '{name}': type must be HTTP, HTTPS, or TCP, got {hc_type}" ) port = hc.get("port", 443 if hc_type == "HTTPS" else 80) if not isinstance(port, int) or not (1 <= port <= 65535): raise ValueError( f"Health check '{name}': port must be an integer between 1 and 65535, got {port}" ) threshold = hc.get("failure_threshold", 3) if not isinstance(threshold, int) or not (1 <= threshold <= 10): raise ValueError( f"Health check '{name}': failure_threshold must be an integer between 1 and 10, got {threshold}" ) path = hc.get("path", "/") if hc_type != "TCP" else "" if hc_type != "TCP" and path and not str(path).startswith("/"): raise ValueError(f"Health check '{name}': path must start with '/'") # --- Build alarm action ARN list --- alarm_action_arns = [] if enterprise_enabled: alarm_action_arns.append(enterprise_sns_topic_arn) # --- Optional: Project SNS Relay Topic + downstream subscriptions --- project_topic = None lark_lambda = None if relay_topic_enabled: project_topic = sns.Topic( self, "ProjectSnsTopic", topic_name=project_topic_name_for_deployment(deployment_id), display_name="DNS Health Check Alerts", ) Tags.of(project_topic).add("Purpose", "project-relay") alarm_action_arns.append(project_topic.topic_arn) if project_lark_enabled: lambda_code_path = os.path.join(os.path.dirname(__file__), "..", "..", "lambda") lambda_code_path = os.path.normpath(lambda_code_path) webhook_secret = secretsmanager.Secret.from_secret_complete_arn( self, "ProjectLarkWebhookSecret", project_lark_secret_arn, ) lark_lambda = _lambda.Function( self, "LarkNotifierLambda", function_name=project_lark_function_name_for_deployment(deployment_id), runtime=_lambda.Runtime.PYTHON_3_12, handler="lark_notifier.handler", code=_lambda.Code.from_asset(lambda_code_path), environment={ "LARK_WEBHOOK_SECRET_ARN": project_lark_secret_arn, }, timeout=Duration.seconds(30), ) webhook_secret.grant_read(lark_lambda) project_topic.add_subscription(sns_subs.LambdaSubscription(lark_lambda)) if enterprise_https_enabled and enterprise_https_endpoint_url: project_topic.add_subscription(sns_subs.UrlSubscription(enterprise_https_endpoint_url)) if not alarm_action_arns: raise ValueError("At least one notification target must be configured.") # --- Create Health Checks + CloudWatch Alarms --- previous_health_check = None for hc in health_check_entries: hc_name = hc["name"] construct_suffix = stable_construct_suffix(hc_name) hc_type = hc.get("type", "HTTPS").upper() hc_port = hc.get("port", 443 if hc_type == "HTTPS" else 80) hc_path = hc.get("path", "/") hc_domain = hc["domain"] hc_interval = hc.get("request_interval", 30) hc_threshold = hc.get("failure_threshold", 3) hc_string_match = hc.get("string_match", "") hc_regions = hc.get("regions") # Build health check config health_check_config = { "type": hc_type, "fullyQualifiedDomainName": hc_domain, "port": hc_port, "requestInterval": hc_interval, "failureThreshold": hc_threshold, } # HTTP/HTTPS specific fields if hc_type in ("HTTP", "HTTPS"): health_check_config["resourcePath"] = hc_path if hc_string_match: health_check_config["searchString"] = hc_string_match health_check_config["type"] = f"{hc_type}_STR_MATCH" if hc_regions: health_check_config["regions"] = hc_regions # Enable SNI for HTTPS if hc_type in ("HTTPS", "HTTPS_STR_MATCH"): health_check_config["enableSni"] = True cfn_health_check = route53.CfnHealthCheck( self, f"HealthCheck{construct_suffix}", health_check_config=health_check_config, health_check_tags=[ route53.CfnHealthCheck.HealthCheckTagProperty( key="Name", value=health_check_tag_name_for_deployment(deployment_id, hc_name), ), ], ) # Route53 throttles bulk health check creation aggressively. Serialize creates # within the stack so large endpoint batches do not fail with Rate exceeded. if previous_health_check is not None: cfn_health_check.add_dependency(previous_health_check) previous_health_check = cfn_health_check if enterprise_https_enabled: alarm_description = build_enterprise_alarm_description( health_check_name=hc_name, health_check_domain=hc_domain, health_check_type=hc_type, health_check_path=hc_path, health_check_port=hc_port, ) else: alarm_description = ( f"Route53 health check failed for {hc_domain}{hc_path if hc_type != 'TCP' else ''}" ) # CloudWatch Alarm cloudwatch.CfnAlarm( self, f"Alarm{construct_suffix}", alarm_name=alarm_name_for_deployment(deployment_id, hc_name), alarm_description=alarm_description, namespace="AWS/Route53", metric_name="HealthCheckStatus", dimensions=[ cloudwatch.CfnAlarm.DimensionProperty( name="HealthCheckId", value=cfn_health_check.attr_health_check_id, ), ], statistic="Minimum", period=60, evaluation_periods=1, threshold=1, comparison_operator="LessThanThreshold", alarm_actions=alarm_action_arns, treat_missing_data="missing", ) # --- Tags on all resources --- Tags.of(self).add("Project", "dns-alert") Tags.of(self).add("DeploymentId", deployment_id) Tags.of(self).add("ManagedBy", "cdk") # --- Stack outputs --- CfnOutput(self, "DeploymentId", value=deployment_id) CfnOutput(self, "StackName", value=self.stack_name) CfnOutput(self, "NotificationMode", value=notification_mode) if enterprise_enabled: CfnOutput(self, "EnterpriseSnsTopicArn", value=enterprise_sns_topic_arn) if project_topic: CfnOutput(self, "ProjectSnsTopicArn", value=project_topic.topic_arn) if enterprise_https_enabled: CfnOutput( self, "EnterpriseHttpsEndpointConfigured", value="true" if enterprise_https_endpoint_url else "false", ) if enterprise_https_endpoint_url: CfnOutput(self, "EnterpriseHttpsEndpointUrl", value=enterprise_https_endpoint_url) if project_lark_enabled: CfnOutput(self, "ProjectLarkWebhookSecretArn", value=project_lark_secret_arn) if lark_lambda: CfnOutput(self, "LarkLambdaArn", value=lark_lambda.function_arn)