diff --git a/resources/lib/constructs/spicy-ecs-cluster.ts b/resources/lib/constructs/spicy-ecs-cluster.ts index 199f9ec..3939a75 100644 --- a/resources/lib/constructs/spicy-ecs-cluster.ts +++ b/resources/lib/constructs/spicy-ecs-cluster.ts @@ -230,22 +230,22 @@ export class SpicyEcsCluster extends Construct { public readonly ec2CapacityProvider: ecs.AsgCapacityProvider; /** External (internet-facing) load balancer */ - public readonly externalLoadBalancer?: elbv2.ApplicationLoadBalancer; + public externalLoadBalancer?: elbv2.ApplicationLoadBalancer; /** Internal load balancer */ - public readonly internalLoadBalancer?: elbv2.ApplicationLoadBalancer; + public internalLoadBalancer?: elbv2.ApplicationLoadBalancer; /** External HTTPS listener */ - public readonly externalHttpsListener?: elbv2.ApplicationListener; + public externalHttpsListener?: elbv2.ApplicationListener; /** External HTTP listener */ - public readonly externalHttpListener?: elbv2.ApplicationListener; + public externalHttpListener?: elbv2.ApplicationListener; /** Internal HTTPS listener */ - public readonly internalHttpsListener?: elbv2.ApplicationListener; + public internalHttpsListener?: elbv2.ApplicationListener; /** Internal HTTP listener */ - public readonly internalHttpListener?: elbv2.ApplicationListener; + public internalHttpListener?: elbv2.ApplicationListener; /** ECS Host security group */ public readonly ecsHostSecurityGroup: ec2.SecurityGroup; @@ -374,21 +374,13 @@ export class SpicyEcsCluster extends Construct { // User data script const userData = ec2.UserData.forLinux(); userData.addCommands( - '#!/bin/bash', - 'set -e', - '', - '# Install SSM agent and other utilities', - 'yum install -y amazon-ssm-agent', - 'systemctl enable amazon-ssm-agent', - 'systemctl start amazon-ssm-agent', - '', - `# Configure ECS agent`, + '# Configure ECS agent', `echo "ECS_CLUSTER=${stack.stackName}" >> /etc/ecs/ecs.config`, 'echo "ECS_ENABLE_SPOT_INSTANCE_DRAINING=true" >> /etc/ecs/ecs.config', 'echo "ECS_ENABLE_CONTAINER_METADATA=true" >> /etc/ecs/ecs.config', 'echo \'ECS_AVAILABLE_LOGGING_DRIVERS=["json-file","awslogs","splunk"]\' >> /etc/ecs/ecs.config', '', - '# Signal success', + '# Signal CloudFormation (always runs, reports actual exit code)', `/opt/aws/bin/cfn-signal -e $? --stack ${stack.stackName} --resource AutoScalingGroup --region ${region}` ); @@ -722,8 +714,7 @@ def lambda_handler(event, context): this.loadBalancerSecurityGroup!.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'Allow HTTP'); this.loadBalancerSecurityGroup!.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443), 'Allow HTTPS'); - (this as { externalLoadBalancer: elbv2.ApplicationLoadBalancer }).externalLoadBalancer = - new elbv2.ApplicationLoadBalancer(this, 'ExternalLoadBalancer', { + this.externalLoadBalancer = new elbv2.ApplicationLoadBalancer(this, 'ExternalLoadBalancer', { vpc: props.vpc, internetFacing: true, securityGroup: this.loadBalancerSecurityGroup, @@ -737,8 +728,7 @@ def lambda_handler(event, context): } // HTTP Listener - (this as { externalHttpListener: elbv2.ApplicationListener }).externalHttpListener = - this.externalLoadBalancer!.addListener('ExternalHTTP', { + this.externalHttpListener = this.externalLoadBalancer!.addListener('ExternalHTTP', { port: 80, protocol: elbv2.ApplicationProtocol.HTTP, defaultAction: elbv2.ListenerAction.fixedResponse(404, { @@ -749,8 +739,7 @@ def lambda_handler(event, context): // HTTPS Listener (if certificate provided) if (config.certificateArn) { - (this as { externalHttpsListener: elbv2.ApplicationListener }).externalHttpsListener = - this.externalLoadBalancer!.addListener('ExternalHTTPS', { + this.externalHttpsListener = this.externalLoadBalancer!.addListener('ExternalHTTPS', { port: 443, protocol: elbv2.ApplicationProtocol.HTTPS, certificates: [elbv2.ListenerCertificate.fromArn(config.certificateArn)], @@ -769,8 +758,7 @@ def lambda_handler(event, context): props: SpicyEcsClusterProps, config: { certificateArn?: string; idleTimeout: number; enableAccessLogs: boolean } ): void { - (this as { internalLoadBalancer: elbv2.ApplicationLoadBalancer }).internalLoadBalancer = - new elbv2.ApplicationLoadBalancer(this, 'InternalLoadBalancer', { + this.internalLoadBalancer = new elbv2.ApplicationLoadBalancer(this, 'InternalLoadBalancer', { vpc: props.vpc, internetFacing: false, securityGroup: this.loadBalancerSecurityGroup, @@ -784,8 +772,7 @@ def lambda_handler(event, context): } // HTTP Listener - (this as { internalHttpListener: elbv2.ApplicationListener }).internalHttpListener = - this.internalLoadBalancer!.addListener('InternalHTTP', { + this.internalHttpListener = this.internalLoadBalancer!.addListener('InternalHTTP', { port: 80, protocol: elbv2.ApplicationProtocol.HTTP, defaultAction: elbv2.ListenerAction.fixedResponse(404, { @@ -796,8 +783,7 @@ def lambda_handler(event, context): // HTTPS Listener (if certificate provided) if (config.certificateArn) { - (this as { internalHttpsListener: elbv2.ApplicationListener }).internalHttpsListener = - this.internalLoadBalancer!.addListener('InternalHTTPS', { + this.internalHttpsListener = this.internalLoadBalancer!.addListener('InternalHTTPS', { port: 443, protocol: elbv2.ApplicationProtocol.HTTPS, certificates: [elbv2.ListenerCertificate.fromArn(config.certificateArn)], diff --git a/resources/lib/constructs/spicy-ecs-service.ts b/resources/lib/constructs/spicy-ecs-service.ts index 1a0fcc4..f6b6e68 100644 --- a/resources/lib/constructs/spicy-ecs-service.ts +++ b/resources/lib/constructs/spicy-ecs-service.ts @@ -365,7 +365,7 @@ export class SpicyEcsService extends Construct { }; const healthCheck = { - path: props.healthCheck?.path ?? '/health', + path: props.healthCheck?.path ?? '/', interval: props.healthCheck?.interval ?? 15, timeout: props.healthCheck?.timeout ?? 14, healthyThresholdCount: props.healthCheck?.healthyThresholdCount ?? 2, @@ -835,33 +835,14 @@ export class SpicyEcsService extends Construct { * The isActive flag in BlueGreenDnsConfig is informational only - both records are always created. * The actual routing is determined by which service stack has the lower priority listener rule. */ - private configureBlueGreenDns(dns: BlueGreenDnsConfig, loadBalancer: elbv2.IApplicationLoadBalancer): void { - // Extract domain from hostname (e.g., "api.example.com" -> "example.com") - const domainParts = dns.activeHostname.split('.'); - const zoneName = domainParts.slice(-2).join('.'); - const activeRecordName = domainParts.slice(0, -2).join('.') || '@'; - const inactiveRecordName = dns.inactiveHostname.split('.').slice(0, -2).join('.') || '@'; - - const hostedZone = route53.HostedZone.fromHostedZoneAttributes(this, 'HostedZone', { - hostedZoneId: dns.hostedZoneId, - zoneName: zoneName, - }); - - // Active hostname DNS record - always points to the ALB - // Note: This record is created by both blue and green stacks, but routing is via listener priorities - new route53.ARecord(this, 'ActiveDnsRecord', { - zone: hostedZone, - recordName: activeRecordName === '@' ? undefined : activeRecordName, - target: route53.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(loadBalancer)), - }); - - // Inactive hostname DNS record - always points to the ALB - // Note: This record is created by both blue and green stacks, but routing is via listener priorities - new route53.ARecord(this, 'InactiveDnsRecord', { - zone: hostedZone, - recordName: inactiveRecordName === '@' ? undefined : inactiveRecordName, - target: route53.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(loadBalancer)), - }); + private configureBlueGreenDns(_dns: BlueGreenDnsConfig, _loadBalancer: elbv2.IApplicationLoadBalancer): void { + // DNS records are NOT created here. They belong to the persistent ALB stack (spicy-alb.ts), + // which owns the Route53 A records pointing to the load balancer. Creating them here would + // cause CloudFormation conflicts when both blue and green service stacks attempt to manage + // the same DNS records. + // + // Traffic routing between blue/green services is controlled entirely by ALB listener rule + // priorities — not by DNS changes. Both hostnames always resolve to the same ALB. } /**