Ec2Factory.java

package com.cloudforgeci.api.compute;

import com.cloudforgeci.api.core.annotation.BaseFactory;
import com.cloudforgeci.api.scaling.ScalingFactory;
import com.cloudforge.core.annotation.DeploymentContext;
import com.cloudforge.core.annotation.SystemContext;
import com.cloudforge.core.enums.AuthMode;
import com.cloudforge.core.enums.NetworkMode;
import com.cloudforge.core.enums.RuntimeType;
import com.cloudforge.core.enums.SecurityProfile;
import com.cloudforge.core.interfaces.ApplicationSpec;
import com.cloudforge.core.interfaces.OidcConfiguration;
import com.cloudforge.core.interfaces.OidcIntegration;
import io.github.cdklabs.cdknag.NagPackSuppression;
import io.github.cdklabs.cdknag.NagSuppressions;

import software.amazon.awscdk.services.autoscaling.AutoScalingGroup;
import software.amazon.awscdk.services.ec2.BlockDevice;
import software.amazon.awscdk.services.ec2.BlockDeviceVolume;
import software.amazon.awscdk.services.ec2.EbsDeviceOptions;
import software.amazon.awscdk.services.ec2.EbsDeviceVolumeType;
import software.amazon.awscdk.services.ec2.InstanceClass;
import software.amazon.awscdk.services.ec2.InstanceSize;
import software.amazon.awscdk.services.ec2.InstanceType;
import software.amazon.awscdk.services.ec2.LaunchTemplate;
import software.amazon.awscdk.services.ec2.MachineImage;
import software.amazon.awscdk.services.ec2.Peer;
import software.amazon.awscdk.services.ec2.Port;
import software.amazon.awscdk.services.ec2.SecurityGroup;
import software.amazon.awscdk.services.ec2.SubnetSelection;
import software.amazon.awscdk.services.ec2.SubnetType;
import software.amazon.awscdk.services.ec2.UserData;

import software.amazon.awscdk.services.autoscaling.NotificationConfiguration;
import software.amazon.awscdk.services.autoscaling.ScalingEvents;
import software.amazon.awscdk.services.iam.AnyPrincipal;
import software.amazon.awscdk.services.iam.Effect;
import software.amazon.awscdk.services.iam.ManagedPolicy;
import software.amazon.awscdk.services.iam.PolicyStatement;
import software.amazon.awscdk.services.iam.Role;
import software.amazon.awscdk.services.iam.ServicePrincipal;
import software.amazon.awscdk.services.kms.Key;
import software.amazon.awscdk.services.logs.LogGroup;
import software.amazon.awscdk.services.sns.Topic;

import java.util.logging.Logger;
import software.constructs.Construct;

import java.util.List;

/**
 * Factory for creating EC2-based Jenkins compute infrastructure.
 *
 * <p>This factory creates and configures EC2 instances for Jenkins deployments,
 * including auto-scaling groups, launch templates, and IAM roles. It respects
 * the network mode configuration to place instances in appropriate subnets.</p>
 *
 * <p><strong>Key Features:</strong></p>
 * <ul>
 *   <li>Auto-scaling groups with configurable min/max capacity</li>
 *   <li>Launch templates with Jenkins pre-installed</li>
 *   <li>EBS encryption and proper volume configuration</li>
 *   <li>IAM roles with EFS access (when EFS is available)</li>
 *   <li>CloudWatch logging integration</li>
 *   <li>Network mode awareness (public vs private subnets)</li>
 * </ul>
 *
 * <p><strong>Storage Options:</strong></p>
 * <ul>
 *   <li><strong>EFS:</strong> When EFS is available, instances mount EFS for persistent storage</li>
 *   <li><strong>EBS:</strong> When EFS is not available, instances use EBS volumes</li>
 * </ul>
 *
 * <p><strong>Example Usage:</strong></p>
 * <pre>{@code
 * Ec2Factory factory = new Ec2Factory(scope, "JenkinsEC2");
 * factory.create(ctx);
 *
 * // Access created resources
 * AutoScalingGroup asg = ctx.asg.get().orElseThrow();
 * Role instanceRole = ctx.ec2InstanceRole.get().orElseThrow();
 * }</pre>
 *
 * @author CloudForgeCI
 * @since 1.0.0
 * @see SystemContext
 * @see ScalingFactory
 * @see com.cloudforgeci.api.core.DeploymentContext#networkMode()
 */
public class Ec2Factory extends BaseFactory {
  private static final Logger LOG = Logger.getLogger(Ec2Factory.class.getName());
  private AutoScalingGroup asg;

  @SystemContext("stackName")
  private String stackName;

  @SystemContext("runtime")
  private RuntimeType runtime;

  @SystemContext("security")
  private SecurityProfile security;

  @SystemContext("applicationSpec")
  private com.cloudforge.core.interfaces.ApplicationSpec applicationSpec;

  @DeploymentContext("minInstanceCapacity")
  private Integer minInstanceCapacity;

  @DeploymentContext("maxInstanceCapacity")
  private Integer maxInstanceCapacity;

  @DeploymentContext("networkMode")
  private NetworkMode networkMode;

  @DeploymentContext("retainStorage")
  private Boolean retainStorage;

  @DeploymentContext("instanceType")
  private String instanceType;

  @DeploymentContext("enableEncryption")
  private Boolean enableEncryption;

  // ========== Optional Port Configuration ==========
  // These flags control which optional ports are exposed in security groups
  // Ports are NOT exposed by default - must be explicitly enabled

  @DeploymentContext("enableAgents")
  private Boolean enableAgents;

  @DeploymentContext("enableSsh")
  private Boolean enableSsh;

  @DeploymentContext("enableSmtp")
  private Boolean enableSmtp;

  @DeploymentContext("enableSmtps")
  private Boolean enableSmtps;

  @DeploymentContext("enableClustering")
  private Boolean enableClustering;

  @DeploymentContext("enableDockerRegistry")
  private Boolean enableDockerRegistry;

  @DeploymentContext("enableMetrics")
  private Boolean enableMetrics;

  @DeploymentContext("enableNotary")
  private Boolean enableNotary;

  @DeploymentContext("enableTrivy")
  private Boolean enableTrivy;

  @DeploymentContext("enableSentinel")
  private Boolean enableSentinel;

  @DeploymentContext("enableCluster")
  private Boolean enableCluster;

  public Ec2Factory(Construct scope, String id) {
    super(scope, id);
    // All fields are automatically injected by BaseFactory
  }

  @Override
  public void create() {
    createEc2Infrastructure();
  }

  /**
   * Creates the complete EC2-based Jenkins infrastructure.
   *
   * <p>This method orchestrates the creation of all EC2-related resources:</p>
   * <ul>
   *   <li>IAM role for EC2 instances with appropriate permissions</li>
   *   <li>CloudWatch log group for Jenkins logs</li>
   *   <li>User data script for Jenkins installation and configuration</li>
   *   <li>Launch template with Jenkins pre-installed</li>
   *   <li>Auto-scaling group with configurable capacity</li>
   *   <li>Auto-scaling policies and CloudWatch alarms</li>
   * </ul>
   *
   * <p>The method respects the network mode setting to place instances in
   * appropriate subnets (public vs private) and configures storage based
   * on EFS availability.</p>
   *
   * @throws IllegalStateException if required resources are not available in context
   * @see SystemContext
   * @see DeploymentContext#networkMode()
   * @see DeploymentContext#minInstanceCapacity()
   * @see DeploymentContext#maxInstanceCapacity()
   */
  @SystemContext("ec2InstanceRole")
  private Role ec2InstanceRole;

  @SystemContext("instanceSg")
  private SecurityGroup instanceSg;

  @SystemContext("vpc")
  private software.amazon.awscdk.services.ec2.Vpc vpc;

  @SystemContext("albSg")
  private SecurityGroup albSg;

  @SystemContext("efs")
  private software.amazon.awscdk.services.efs.FileSystem efs;

  @SystemContext("ap")
  private software.amazon.awscdk.services.efs.AccessPoint ap;

  @DeploymentContext("authMode")
  private AuthMode authMode;

  @DeploymentContext("fqdn")
  private String fqdn;

  @DeploymentContext("enableSsl")
  private Boolean enableSsl;

  @SystemContext("applicationOidcConfig")
  private OidcConfiguration applicationOidcConfig;

  private void createEc2Infrastructure() {
    // Use existing IAM role created by IAM configuration (has CloudWatch Logs permissions)
    if (ec2InstanceRole == null) {
      throw new IllegalStateException("EC2 instance role not found - IAM configuration should have created it");
    }

    // Use existing instance security group (created by JenkinsFactory)
    if (instanceSg == null) {
      throw new IllegalStateException("Instance security group not found");
    }

    // Create CloudWatch log group
    LogGroup logs = createLogGroup();
    ctx.logs.set(logs);

    // Create user data script
    UserData userData = createUserData();

    // Create launch template
    LaunchTemplate launchTemplate = createLaunchTemplate(ec2InstanceRole, instanceSg, userData);

    // Create Auto Scaling Group
    this.asg = createAutoScalingGroup(launchTemplate);
    ctx.asg.set(asg);

    // Auto-scaling configuration is handled by the orchestration layer
    // The JenkinsServiceTopologyConfiguration will add the ASG to the target group
  }

  // Note: IAM role creation is now handled by IAM configuration (IAMRules)
  // This ensures proper CloudWatch Logs permissions are included

  private SecurityGroup createInstanceSecurityGroup() {
    int appPort = applicationSpec != null ? applicationSpec.applicationPort() : 8080;
    String appId = applicationSpec != null ? applicationSpec.applicationId() : "app";

    if (vpc == null) {
      throw new IllegalStateException("VPC not available");
    }
    if (albSg == null) {
      throw new IllegalStateException("ALB security group not available");
    }

    // Check if egress should be restricted to VPC CIDR only (only for private subnets)
    boolean restrictEgress = config.isRestrictSecurityGroupEgressEnabled()
        && networkMode != NetworkMode.PUBLIC;

    SecurityGroup instanceSg = SecurityGroup.Builder.create(this, appId + "Ec2Sg")
            .vpc(vpc)
            .description(appId + " EC2 Instance Security Group")
            .allowAllOutbound(!restrictEgress)
            .build();

    // If egress is restricted, add explicit egress rule for VPC CIDR only
    // Instances need to communicate with EFS, RDS, and other VPC resources
    if (restrictEgress) {
      instanceSg.addEgressRule(
          Peer.ipv4(vpc.getVpcCidrBlock()),
          Port.allTraffic(),
          "Allow egress to VPC CIDR only"
      );
    }

    // Add ingress rule from ALB security group
    instanceSg.addIngressRule(albSg, Port.tcp(appPort), "ALB_to_" + appId);

    // Add EC23 suppression - EC2 instances may need public access for optional services
    // The suppression covers optional ports like SSH, JNLP agents that require 0.0.0.0/0 access
    NagSuppressions.addResourceSuppressions(
        instanceSg,
        List.of(
            NagPackSuppression.builder()
                .id("AwsSolutions-EC23")
                .reason("EC2 security group requires open ingress for optional service ports " +
                        "(SSH for debugging, JNLP for Jenkins agents, etc.). These ports are " +
                        "explicitly enabled via deployment configuration and are necessary " +
                        "for the application's operational requirements.")
                .build()
        ),
        Boolean.TRUE
    );

    // Add security group rules for optional inbound ports
    // These are NOT exposed by default - must be explicitly enabled via deployment config
    if (applicationSpec != null) {
      for (ApplicationSpec.OptionalPort optionalPort : applicationSpec.optionalPorts()) {
        // Only add ingress rules for inbound ports that are enabled
        if (optionalPort.inbound() && isOptionalPortEnabled(optionalPort.configKey())) {
          Port port = optionalPort.protocol().equals("udp")
              ? Port.udp(optionalPort.port())
              : Port.tcp(optionalPort.port());
          // Allow from anywhere for optional service ports (e.g., SSH, JNLP agents)
          instanceSg.addIngressRule(
              software.amazon.awscdk.services.ec2.Peer.anyIpv4(),
              port,
              optionalPort.service().replace(" ", "_") + "_inbound"
          );
          LOG.info("  ✅ Added security group rule for optional port: " +
                   optionalPort.port() + "/" + optionalPort.protocol() +
                   " (" + optionalPort.service() + ")");
        }
      }
    }

    return instanceSg;
  }

  /**
   * Check if an optional port is enabled based on the config key.
   */
  private boolean isOptionalPortEnabled(String configKey) {
    return switch (configKey) {
      case "enableAgents" -> Boolean.TRUE.equals(enableAgents);
      case "enableSsh" -> Boolean.TRUE.equals(enableSsh);
      case "enableSmtp" -> Boolean.TRUE.equals(enableSmtp);
      case "enableSmtps" -> Boolean.TRUE.equals(enableSmtps);
      case "enableClustering" -> Boolean.TRUE.equals(enableClustering);
      case "enableDockerRegistry" -> Boolean.TRUE.equals(enableDockerRegistry);
      case "enableMetrics" -> Boolean.TRUE.equals(enableMetrics);
      case "enableNotary" -> Boolean.TRUE.equals(enableNotary);
      case "enableTrivy" -> Boolean.TRUE.equals(enableTrivy);
      case "enableSentinel" -> Boolean.TRUE.equals(enableSentinel);
      case "enableCluster" -> Boolean.TRUE.equals(enableCluster);
      default -> {
        LOG.warning("Unknown optional port config key: " + configKey);
        yield false;
      }
    };
  }

  private LogGroup createLogGroup() {
    String appId = applicationSpec != null ? applicationSpec.applicationId() : "app";
    return LogGroup.Builder.create(this, appId + "Ec2Logs")
            .retention(config.getLogRetentionDays())
            .build();
  }

  private UserData createUserData() {
    UserData ud = UserData.forLinux();

    // Add bash best practices
    ud.addCommands(
        "#!/bin/bash",
        "set -euxo pipefail",
        "echo 'UserData script started' > /var/log/userdata.log"
    );

    // Create Ec2Context for application configuration
    boolean hasEfs = efs != null && ap != null;
    String efsId = hasEfs ? efs.getFileSystemId() : null;
    String accessPointId = hasEfs ? ap.getAccessPointId() : null;

    com.cloudforgeci.api.core.Ec2ContextImpl ec2Context = new com.cloudforgeci.api.core.Ec2ContextImpl(
        stackName,
        runtime.name().toLowerCase(),
        security.name().toLowerCase(),
        hasEfs,
        efsId,
        accessPointId
    );

    // Create UserDataBuilder and delegate to ApplicationSpec
    com.cloudforgeci.api.core.UserDataBuilderImpl builder =
        new com.cloudforgeci.api.core.UserDataBuilderImpl(ud);

    if (applicationSpec != null) {
      applicationSpec.configureUserData(builder, ec2Context);
    }

    // Add OIDC integration commands if application-oidc mode is enabled
    // This configures OIDC authentication (e.g., Jenkins OIDC plugin setup)
    if (authMode == AuthMode.APPLICATION_OIDC && applicationSpec != null && applicationSpec.supportsOidcIntegration()) {
      LOG.info("Ec2Factory: Configuring OIDC integration for EC2 UserData...");

      if (applicationOidcConfig != null) {
        OidcIntegration oidcIntegration = applicationSpec.getOidcIntegration();
        if (oidcIntegration != null) {
          LOG.info("  OIDC Integration: " + oidcIntegration.getIntegrationMethod());
          LOG.info("  Auth Type: " + oidcIntegration.getAuthenticationType());

          // Get OIDC UserData commands from the integration
          List<String> oidcCommands = oidcIntegration.getUserDataCommands(applicationOidcConfig, ec2Context);

          if (oidcCommands != null && !oidcCommands.isEmpty()) {
            LOG.info("  Adding " + oidcCommands.size() + " OIDC configuration commands to UserData");

            // Add marker comment for clarity
            ud.addCommands("");
            ud.addCommands("# ========================================");
            ud.addCommands("# OIDC Integration Configuration");
            ud.addCommands("# Added by Ec2Factory for " + applicationSpec.applicationId());
            ud.addCommands("# ========================================");

            // Add all OIDC commands
            for (String command : oidcCommands) {
              ud.addCommands(command);
            }

            LOG.info("✅ OIDC configuration commands added to UserData for " + applicationSpec.applicationId());
          } else {
            LOG.warning("⚠️  No OIDC commands returned from integration");
          }
        } else {
          LOG.severe("❌ OidcIntegration is NULL - cannot configure OIDC!");
        }
      } else {
        LOG.severe("❌ applicationOidcConfig NOT PRESENT - OIDC configuration NOT added to UserData!");
        LOG.severe("   Make sure ApplicationOidcFactory runs before Ec2Factory");
      }
    }

    return ud;
  }

  private LaunchTemplate createLaunchTemplate(Role ec2Role, SecurityGroup instanceSg, UserData userData) {
    String appId = applicationSpec != null ? applicationSpec.applicationId() : "app";

    // Determine instance type with priority: DeploymentConfig > ApplicationSpec > default
    String instanceTypeStr;
    if (instanceType != null && !instanceType.isEmpty()) {
      instanceTypeStr = instanceType;
    } else if (applicationSpec != null) {
      instanceTypeStr = applicationSpec.defaultInstanceType();
    } else {
      instanceTypeStr = "t3.micro";  // Fallback default
    }

    // Parse instance type string (e.g., "t3.micro" -> InstanceClass.T3, InstanceSize.MICRO)
    InstanceType parsedInstanceType = parseInstanceType(instanceTypeStr);

    LaunchTemplate.Builder ltBuilder = LaunchTemplate.Builder.create(this, appId + "Lt")
            .machineImage(MachineImage.latestAmazonLinux2023())
            .instanceType(parsedInstanceType)
            .securityGroup(instanceSg)
            .role(ec2Role)
            .userData(userData)
            .requireImdsv2(config.isImdsv2Required());  // HIPAA: IMDSv2 controlled by SecurityProfileConfiguration

    // Determine EBS retention policy based on retainStorage configuration
    // Root volume is always deleted (system disk), but data volume can be retained
    boolean deleteDataVolume = Boolean.FALSE.equals(retainStorage) || retainStorage == null;

    if (Boolean.TRUE.equals(retainStorage)) {
      LOG.info("EBS data volumes will be RETAINED after instance termination (retainStorage = true)");
      LOG.info("⚠️  You must manually delete EBS volumes from AWS Console to avoid ongoing storage costs");
    } else {
      LOG.info("EBS data volumes will be DESTROYED with instances (retainStorage = false)");
    }

    // Determine encryption setting with priority: DeploymentConfig > SecurityProfileConfiguration > SecurityProfile default
    // For production deployments, encryption defaults to true
    boolean encrypt;
    if (enableEncryption != null) {
      encrypt = enableEncryption;
      LOG.info("EBS encryption setting from deployment context: " + encrypt);
    } else if (config != null) {
      // Use SecurityProfileConfiguration setting (inherited from BaseFactory)
      encrypt = config.isEbsEncryptionEnabled();
      LOG.info("EBS encryption inherited from security profile: " + encrypt);
    } else {
      // Fallback: encrypt for PRODUCTION and STAGING, optional for DEV
      encrypt = (security == SecurityProfile.PRODUCTION || security == SecurityProfile.STAGING);
      LOG.info("EBS encryption using SecurityProfile default: " + encrypt);
    }

    // Add block devices
    ltBuilder.blockDevices(List.of(
            BlockDevice.builder()
                    .deviceName("/dev/xvda")
                    .volume(BlockDeviceVolume.ebs(20, EbsDeviceOptions.builder()
                            .encrypted(encrypt)
                            .volumeType(EbsDeviceVolumeType.STANDARD)
                            .deleteOnTermination(true)  // Always delete root volume
                            .build()))
                    .build()
    ));

    // Add data volume if not using EFS
    if (efs == null) {
      String ebsDeviceName = applicationSpec != null ? applicationSpec.ebsDeviceName() : "/dev/xvdh";
      ltBuilder.blockDevices(List.of(
              BlockDevice.builder()
                      .deviceName(ebsDeviceName)
                      .volume(BlockDeviceVolume.ebs(100, EbsDeviceOptions.builder()
                              .encrypted(encrypt)
                              .volumeType(EbsDeviceVolumeType.STANDARD)
                              .deleteOnTermination(deleteDataVolume)  // Respect retainStorage setting
                              .build()))
                      .build()
      ));
    }

    return ltBuilder.build();
  }

  /**
   * Parse EC2 instance type from string (e.g., "t3.micro").
   *
   * @param instanceTypeStr Instance type string (e.g., "t3.micro", "m5.large")
   * @return Parsed InstanceType
   */
  private static InstanceType parseInstanceType(String instanceTypeStr) {
    // Extract class and size from instance type (e.g., "t3.micro")
    String[] parts = instanceTypeStr.split("\\.");
    if (parts.length < 2) {
      return InstanceType.of(InstanceClass.BURSTABLE3, InstanceSize.MICRO);
    }

    InstanceClass instanceClass = parseInstanceClass(parts[0]);
    InstanceSize instanceSize = parseInstanceSize(parts[1]);

    return InstanceType.of(instanceClass, instanceSize);
  }

  /**
   * Parse instance class from string.
   */
  private static InstanceClass parseInstanceClass(String className) {
    return switch (className.toLowerCase()) {
      case "t3" -> InstanceClass.BURSTABLE3;
      case "t4g" -> InstanceClass.BURSTABLE4_GRAVITON;
      case "m5" -> InstanceClass.M5;
      case "m6g" -> InstanceClass.MEMORY6_GRAVITON;
      case "r5" -> InstanceClass.R5;
      case "r6g" -> InstanceClass.MEMORY6_GRAVITON;
      case "c5" -> InstanceClass.COMPUTE5;
      case "c6g" -> InstanceClass.COMPUTE6_GRAVITON2;
      default -> InstanceClass.BURSTABLE3;
    };
  }

  /**
   * Parse instance size from string.
   */
  private static InstanceSize parseInstanceSize(String size) {
    return switch (size.toLowerCase()) {
      case "micro" -> InstanceSize.MICRO;
      case "small" -> InstanceSize.SMALL;
      case "medium" -> InstanceSize.MEDIUM;
      case "large" -> InstanceSize.LARGE;
      case "xlarge" -> InstanceSize.XLARGE;
      case "2xlarge" -> InstanceSize.XLARGE2;
      case "4xlarge" -> InstanceSize.XLARGE4;
      case "8xlarge" -> InstanceSize.XLARGE8;
      case "12xlarge" -> InstanceSize.XLARGE12;
      case "16xlarge" -> InstanceSize.XLARGE16;
      case "24xlarge" -> InstanceSize.XLARGE24;
      default -> InstanceSize.MICRO;
    };
  }

  private AutoScalingGroup createAutoScalingGroup(LaunchTemplate launchTemplate) {
    // Use annotated field values for AutoScaling Group configuration
    int minCapacity = minInstanceCapacity != null ? minInstanceCapacity : 1;
    int maxCapacity = maxInstanceCapacity != null ? maxInstanceCapacity : 1;
    int desiredCapacity = Math.max(minCapacity, Math.min(maxCapacity, minCapacity)); // Start with minimum

    // Determine subnet type based on network mode
    SubnetType subnetType = networkMode == NetworkMode.PUBLIC ?
            SubnetType.PUBLIC : SubnetType.PRIVATE_WITH_EGRESS;

    if (vpc == null) {
      throw new IllegalStateException("VPC not available");
    }

    String appId = applicationSpec != null ? applicationSpec.applicationId() : "app";

    // Create SNS topic for ASG notifications (required for STAGING/PRODUCTION - AwsSolutions-AS3)
    // Notifications provide visibility into instance lifecycle events for operational monitoring
    Topic.Builder topicBuilder = Topic.Builder.create(this, appId + "AsgNotifications")
            .displayName(stackName + " Auto Scaling Notifications");

    // HIPAA/PCI-DSS requires KMS encryption for SNS topics - controlled by SecurityProfileConfiguration
    if (config.isSnsKmsEncryptionEnabled()) {
      Key asgNotificationsKey = Key.Builder.create(this, appId + "AsgNotificationsKey")
              .description("KMS key for ASG notifications SNS topic")
              .enableKeyRotation(true)
              .build();
      topicBuilder.masterKey(asgNotificationsKey);
    }

    Topic asgNotificationTopic = topicBuilder.build();

    // AwsSolutions-SNS3: Require SSL/TLS for publishers
    asgNotificationTopic.addToResourcePolicy(PolicyStatement.Builder.create()
            .sid("EnforceSSL")
            .effect(Effect.DENY)
            .principals(List.of(new AnyPrincipal()))
            .actions(List.of("sns:Publish"))
            .resources(List.of(asgNotificationTopic.getTopicArn()))
            .conditions(java.util.Map.of(
                    "Bool", java.util.Map.of("aws:SecureTransport", "false")
            ))
            .build());

    // Build ASG with notifications for all scaling events
    AutoScalingGroup asg = AutoScalingGroup.Builder.create(this, appId + "Asg")
            .vpc(vpc)
            .vpcSubnets(SubnetSelection.builder().subnetType(subnetType).build())
            .minCapacity(minCapacity)
            .desiredCapacity(desiredCapacity)
            .maxCapacity(maxCapacity)
            .launchTemplate(launchTemplate)
            .notifications(List.of(NotificationConfiguration.builder()
                    .topic(asgNotificationTopic)
                    .scalingEvents(ScalingEvents.ALL)
                    .build()))
            .build();

    LOG.info("ASG notifications configured for all scaling events -> " + asgNotificationTopic.getTopicName());
    return asg;
  }

}