From f7308e4ec92ac8b88a2da4813161053478a1396b Mon Sep 17 00:00:00 2001 From: Heron Rossi Date: Sun, 27 Nov 2022 10:22:29 -0300 Subject: [PATCH] [#3543] Classic to Application Load Balancer migration Implementation for migrating the rack API Classic Load Balancer to an Application Load Balancer. - cloudformation changes - new cli command `convox rack sync` to retrieve the new rack API URL It's important to note that with the ELB migration, the rack API hostname will change. The switch between hostname will be transparent for console managed racks. For users running racks outside of the console, we provide the `convox rack sync --name rack-name` command that should be executed after the update. This will print the new hostname to stdout. --- .gitignore | 3 +- pkg/cli/rack.go | 4 + pkg/structs/provider.go | 2 - provider/aws/formation/rack.json | 229 +++++++++++++++++-------------- 4 files changed, 133 insertions(+), 105 deletions(-) diff --git a/.gitignore b/.gitignore index cb90949c84..068d75deea 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,4 @@ profile.out cmd/convox/pkg provider/aws/lambda/autoscale/handler provider/aws/lambda/autoscale/lambda.zip -provider/aws/lambda/autoscale/handler -provider/aws/lambda/autoscale/lambda.zip +.vscode/ diff --git a/pkg/cli/rack.go b/pkg/cli/rack.go index 459b8693c4..11aa09fda5 100644 --- a/pkg/cli/rack.go +++ b/pkg/cli/rack.go @@ -77,6 +77,10 @@ func init() { Validate: stdcli.Args(2), }) + register("rack sync", "sync v2 rack API url", RackSync, stdcli.CommandOptions{ + Flags: []stdcli.Flag{flagRack, stdcli.StringFlag("name", "n", "rack name. Use it for non console managed racks")}, + }) + register("rack update", "update the rack", RackUpdate, stdcli.CommandOptions{ Flags: []stdcli.Flag{flagRack, flagWait}, Validate: stdcli.ArgsMax(1), diff --git a/pkg/structs/provider.go b/pkg/structs/provider.go index ba3d17b20a..c260054641 100644 --- a/pkg/structs/provider.go +++ b/pkg/structs/provider.go @@ -93,9 +93,7 @@ type Provider interface { SystemUninstall(name string, w io.Writer, opts SystemUninstallOptions) error SystemUpdate(opts SystemUpdateOptions) error Sync(string) error - WithContext(ctx context.Context) Provider - Workers() error } diff --git a/provider/aws/formation/rack.json b/provider/aws/formation/rack.json index 87734040b1..e5d01a0d15 100644 --- a/provider/aws/formation/rack.json +++ b/provider/aws/formation/rack.json @@ -1,8 +1,6 @@ { "AWSTemplateFormatVersion": "2010-09-09", "Conditions": { - "ApiRouterALB": { "Fn::Equals": [ { "Ref": "ApiRouter" }, "ALB" ] }, - "ApiRouterELB": { "Fn::Equals": [ { "Ref": "ApiRouter" }, "ELB" ] }, "Autoscale": { "Fn::Equals": [ { "Ref": "Autoscale" }, "Yes" ] }, "BlankAmi": { "Fn::Equals": [ { "Ref": "Ami" }, "" ] }, "BlankAvailabilityZones": { "Fn::Equals": [ { "Fn::Join": [ "", { "Ref": "AvailabilityZones" } ] }, "" ] }, @@ -25,6 +23,10 @@ "BlankRouterSecurityGroup": { "Fn::Equals": [ { "Fn::Join": [ ",", { "Ref": "RouterSecurityGroup" } ] }, "" ] }, "BlankRouterInternalSecurityGroup": { "Fn::Equals": [ { "Fn::Join": [ ",", { "Ref": "RouterInternalSecurityGroup" } ] }, "" ] }, "BlankSslPolicy": { "Fn::Equals": [ { "Ref": "SslPolicy" }, "" ] }, + "CreateThirdPrivateSubnet": {"Fn::Or": [ + {"Condition": "PrivateApiAndThirdAvailabilityZoneAndHighAvailability"}, + {"Condition": "PrivateAndThirdAvailabilityZoneAndHighAvailability"} + ]}, "DedicatedBuilder": { "Fn::Not": [ { "Fn::Equals": [ { "Ref": "BuildInstance" }, "" ] } ] }, "Development": { "Fn::Equals": [ { "Ref": "Development" }, "Yes" ] }, "EnableCloudWatch": { "Fn::Equals": [ { "Ref": "LogDriver" }, "CloudWatch" ] }, @@ -117,6 +119,9 @@ "Fn::And": [ { "Condition": "Private" }, { "Condition": "ThirdAvailabilityZone" }, { "Condition": "HighAvailability" } ] }, "PrivateApi": { "Fn::Equals": [ { "Ref": "PrivateApi" }, "Yes" ] }, + "PrivateApiAndThirdAvailabilityZoneAndHighAvailability": { + "Fn::And": [ { "Condition": "PrivateApi" }, { "Condition": "ThirdAvailabilityZone" }, { "Condition": "HighAvailability" } ] + }, "PrivateBuild": { "Fn::Or": [ { "Fn::Equals": [ { "Ref": "Private" }, "Yes" ] }, { "Fn::Equals": [ { "Ref": "PrivateBuild" }, "Yes" ] } ] }, "PrivateInstances": { "Fn::Equals": [ { "Ref": "Private" }, "Yes" ] }, "PublicRouter": { "Fn::Equals": [ { "Ref": "InternalOnly" }, "No" ] }, @@ -229,15 +234,12 @@ "Value": { "Ref": "CMKPolicy" } }, "Dashboard": { - "Value": { "Fn::If": [ "ApiRouterALB", - { "Fn::Join": [ ".", [ - "rack", - { "Fn::Select": [ 0, { "Fn::Split": [ ".", { "Fn::GetAtt": [ "Router", "DNSName" ] } ] } ] }, - { "Fn::Select": [ 1, { "Fn::Split": [ ".", { "Fn::GetAtt": [ "Router", "DNSName" ] } ] } ] }, - "convox.site" - ] ] }, - { "Fn::GetAtt": [ "Balancer", "DNSName" ] } - ] } + "Value": { "Fn::Join": [ ".", [ + "rack", + { "Fn::Select": [ 0, { "Fn::Split": [ ".", { "Fn::GetAtt": [ "ApiBalancer", "DNSName" ] } ] } ] }, + { "Fn::Select": [ 1, { "Fn::Split": [ ".", { "Fn::GetAtt": [ "ApiBalancer", "DNSName" ] } ] } ] }, + "convox.site" + ] ] } }, "Domain": { "Condition": "PublicRouter", @@ -493,7 +495,7 @@ "Value": { "Ref": "SubnetPrivate1" } }, "SubnetPrivate2": { - "Condition": "PrivateAndThirdAvailabilityZoneAndHighAvailability", + "Condition": "CreateThirdPrivateSubnet", "Export": { "Name": { "Fn::Sub": "${AWS::StackName}:SubnetPrivate2" } }, "Value": { "Ref": "SubnetPrivate2" } }, @@ -1223,7 +1225,7 @@ } }, "SubnetPrivate2": { - "Condition": "PrivateAndThirdAvailabilityZoneAndHighAvailability", + "Condition": "CreateThirdPrivateSubnet", "Type": "AWS::EC2::Subnet", "Properties": { "Tags": [ { "Key": "Name", "Value": { "Fn::Join": [ " ", [ { "Ref": "AWS::StackName" }, "private", "2" ] ] } } ], @@ -1403,15 +1405,12 @@ "Type": "CNAME", "TTL": "3600", "ResourceRecords": [ - { "Fn::If": [ "ApiRouterALB", - { "Fn::Join": [ ".", [ - "rack", - { "Fn::Select": [ 0, { "Fn::Split": [ ".", { "Fn::GetAtt": [ "Router", "DNSName" ] } ] } ] }, - { "Fn::Select": [ 1, { "Fn::Split": [ ".", { "Fn::GetAtt": [ "Router", "DNSName" ] } ] } ] }, - "convox.site" - ] ] }, - { "Fn::GetAtt": [ "Balancer", "DNSName" ] } - ] } + { "Fn::Join": [ ".", [ + "rack", + { "Fn::Select": [ 0, { "Fn::Split": [ ".", { "Fn::GetAtt": [ "ApiBalancer", "DNSName" ] } ] } ] }, + { "Fn::Select": [ 1, { "Fn::Split": [ ".", { "Fn::GetAtt": [ "ApiBalancer", "DNSName" ] } ] } ] }, + "convox.site" + ] ] } ] } }, @@ -2620,42 +2619,6 @@ ] } }, - "RouterApiTargetGroup": { - "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", - "Condition": "ApiRouterALB", - "Properties": { - "HealthCheckIntervalSeconds": "5", - "HealthCheckTimeoutSeconds": "3", - "HealthyThresholdCount": "2", - "UnhealthyThresholdCount": "2", - "HealthCheckPath": "/check", - "Matcher": { "HttpCode": "200" }, - "Port": "5443", - "Protocol": "HTTPS", - "TargetGroupAttributes": [ - { "Key": "deregistration_delay.timeout_seconds", "Value": "30" } - ], - "TargetType": "instance", - "VpcId": { "Fn::If": [ "BlankExistingVpc", { "Ref": "Vpc" }, { "Ref": "ExistingVpc" } ] } - } - }, - "RouterApiListenerRule": { - "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", - "Condition": "ApiRouterALB", - "Properties": { - "Actions": [ { "Type": "forward", "TargetGroupArn": { "Ref": "RouterApiTargetGroup" } } ], - "Conditions": [ - { "Field": "host-header", "Values": [ { "Fn::Join": [ ".", [ - "rack", - { "Fn::Select": [ 0, { "Fn::Split": [ ".", { "Fn::GetAtt": [ "Router", "DNSName" ] } ] } ] }, - { "Fn::Select": [ 1, { "Fn::Split": [ ".", { "Fn::GetAtt": [ "Router", "DNSName" ] } ] } ] }, - "convox.site" - ] ] } ] } - ], - "ListenerArn": { "Ref": "RouterListener443" }, - "Priority": "1" - } - }, "RouterInternal": { "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", "Condition": "Internal", @@ -2746,47 +2709,117 @@ ] } }, - "Balancer": { - "Type": "AWS::ElasticLoadBalancing::LoadBalancer", - "Condition": "ApiRouterELB", + "ApiBalancer": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "DependsOn": [ "ApiRole", "LogsPolicy" ], "Properties": { - "ConnectionDrainingPolicy": { "Enabled": true, "Timeout": 60 }, - "ConnectionSettings": { "IdleTimeout": 3600 }, - "CrossZone": true, - "HealthCheck": { - "HealthyThreshold": "2", - "Interval": 5, - "Target": { "Fn::If": [ "PrivateApi", "HTTPS:3100/check", "HTTPS:3000/check" ] }, - "Timeout": 3, - "UnhealthyThreshold": "2" - }, - "LBCookieStickinessPolicy": [ { "PolicyName": "affinity" } ], - "Listeners": { "Fn::If": [ "PrivateApi", - [ - { "Protocol": "TCP", "LoadBalancerPort": "443", "InstanceProtocol": "TCP", "InstancePort": "3100" } - ], - [ - { "Protocol": "TCP", "LoadBalancerPort": "443", "InstanceProtocol": "TCP", "InstancePort": "3000" } - ] - ] }, - "LoadBalancerName": { "Fn::If": [ "PrivateApi", { "Fn::Join": [ "-", [ { "Ref": "AWS::StackName" }, "i" ] ] }, { "Ref": "AWS::StackName" } ] }, - "Scheme": { "Fn::If": [ "PrivateApi", "internal", { "Ref": "AWS::NoValue" } ] }, - "SecurityGroups": [ { "Ref": "BalancerSecurity" } ], + "Name": { "Fn::If": [ "PrivateApi", { "Fn::Join": [ "-", [ { "Ref": "AWS::StackName" }, "i" ] ] }, { "Ref": "AWS::StackName" } ] }, + "LoadBalancerAttributes": [ + { "Key": "access_logs.s3.enabled", "Value": "true" }, + { "Key": "access_logs.s3.bucket", "Value": { "Fn::If": [ "BlankLogBucket", { "Ref": "ELBLogs" }, { "Ref": "LogBucket" } ] } }, + { "Key": "access_logs.s3.prefix", "Value": { "Fn::Sub": "convox/logs/${AWS::StackName}/alb" } }, + { "Key": "idle_timeout.timeout_seconds", "Value": { "Ref": "LoadBalancerIdleTimeout" } }, + { "Key": "routing.http.desync_mitigation_mode", "Value": { "Ref": "RouterMitigationMode" } }, + { "Key": "routing.http.drop_invalid_header_fields.enabled", "Value": "true" } + ], + "Scheme": { "Fn::If": [ "PrivateApi", "internal", "internet-facing" ] }, "Subnets": { "Fn::If": [ "PrivateApi", - [ { "Ref": "SubnetPrivate0" }, { "Ref": "SubnetPrivate1" }, { "Fn::If": [ "ThirdAvailabilityZoneAndHighAvailability", { "Ref": "SubnetPrivate2" }, { "Ref": "AWS::NoValue" } ] } ], + [ { "Ref": "SubnetPrivate0" }, { "Ref": "SubnetPrivate1" }, { "Fn::If": [ "CreateThirdPrivateSubnet", { "Ref": "SubnetPrivate2" }, { "Ref": "AWS::NoValue" } ] } ], [ { "Ref": "Subnet0" }, { "Ref": "Subnet1" }, { "Fn::If": [ "ThirdAvailabilityZoneAndHighAvailability", { "Ref": "Subnet2" }, { "Ref": "AWS::NoValue" } ] } ] ] }, + "SecurityGroups": [ { "Ref": "ApiBalancerSecurity" } ], "Tags": [ - { "Key": "GatewayAttachment", "Value": { "Fn::If": [ "ExistingVpc", "existing", { "Ref": "GatewayAttachment" } ] } }, - { "Key": "Name", "Value": { "Ref": "AWS::StackName" } } + { "Key": "Rack", "Value": { "Ref": "AWS::StackName" } } ] } }, - "BalancerSecurity": { + "ApiBalancerCertificate": { + "Type": "AWS::CertificateManager::Certificate", + "Properties": { + "DomainName": { "Fn::Join": [ ".", [ + "*", + { "Fn::Select": [ 0, { "Fn::Split": [ ".", { "Fn::GetAtt": [ "ApiBalancer", "DNSName" ] } ] } ] }, + { "Fn::Select": [ 1, { "Fn::Split": [ ".", { "Fn::GetAtt": [ "ApiBalancer", "DNSName" ] } ] } ] }, + "convox.site" + ] ] }, + "DomainValidationOptions": [ + { + "DomainName": { "Fn::Join": [ ".", [ + "*", + { "Fn::Select": [ 0, { "Fn::Split": [ ".", { "Fn::GetAtt": [ "ApiBalancer", "DNSName" ] } ] } ] }, + { "Fn::Select": [ 1, { "Fn::Split": [ ".", { "Fn::GetAtt": [ "ApiBalancer", "DNSName" ] } ] } ] }, + "convox.site" + ] ] }, + "ValidationDomain": "convox.site" + } + ] + } + }, + "ApiBalancerListener443": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "Certificates": [ { "CertificateArn": { "Ref": "ApiBalancerCertificate" } } ], + "DefaultActions": [ { "Type": "fixed-response", "FixedResponseConfig": { "StatusCode": "404", "MessageBody": "unknown host" } } ], + "LoadBalancerArn": { "Ref" : "ApiBalancer" }, + "Port": "443", + "Protocol": "HTTPS", + "SslPolicy": { "Fn::If": [ "BlankSslPolicy", { "Ref": "AWS::NoValue" }, { "Ref": "SslPolicy" } ] } + } + }, + "ApiBalancerTargetGroup": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "DependsOn": "ApiBalancerListener443", + "Properties": { + "HealthCheckIntervalSeconds": "5", + "HealthCheckTimeoutSeconds": "3", + "HealthyThresholdCount": "2", + "UnhealthyThresholdCount": "2", + "HealthCheckPath": "/check", + "Matcher": { "HttpCode": "200" }, + "Port": { "Fn::If": ["PrivateApi", "4100", "4000"] }, + "Protocol": "HTTPS", + "TargetGroupAttributes": [ + { "Key": "deregistration_delay.timeout_seconds", "Value": "30" } + ], + "TargetType": "instance", + "VpcId": { "Fn::If": [ "BlankExistingVpc", { "Ref": "Vpc" }, { "Ref": "ExistingVpc" } ] } + } + }, + "ApiBalancerListenerRule": { + "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", + "Properties": { + "Actions": [ { "Type": "forward", "TargetGroupArn": { "Ref": "ApiBalancerTargetGroup" } } ], + "Conditions": [ + { "Field": "host-header", "Values": [ { "Fn::Join": [ ".", [ + "rack", + { "Fn::Select": [ 0, { "Fn::Split": [ ".", { "Fn::GetAtt": [ "ApiBalancer", "DNSName" ] } ] } ] }, + { "Fn::Select": [ 1, { "Fn::Split": [ ".", { "Fn::GetAtt": [ "ApiBalancer", "DNSName" ] } ] } ] }, + "convox.site" + ] ] } ] } + ], + "ListenerArn": { "Ref": "ApiBalancerListener443" }, + "Priority": "1" + } + }, + "ApiBalancerListenerRuleInternal": { + "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", + "Properties": { + "Actions": [ { "Type": "forward", "TargetGroupArn": { "Ref": "ApiBalancerTargetGroup" } } ], + "Conditions": [ + { "Field": "host-header", "Values": [ { "Fn::Join": [ ".", [ + "rack", + {"Ref": "AWS::StackName"}, + "convox" + ] ] } ] } + ], + "ListenerArn": { "Ref": "ApiBalancerListener443" }, + "Priority": "2" + } + }, + "ApiBalancerSecurity": { "Type": "AWS::EC2::SecurityGroup", - "Condition": "ApiRouterELB", "Properties": { "GroupDescription": { "Fn::Sub": "${AWS::StackName} balancer" }, "SecurityGroupIngress": [ @@ -2906,7 +2939,7 @@ ] } }, - "ApiMonitorService": { + "ServiceMonitorApi": { "Type": "AWS::ECS::Service", "DependsOn": [ "Instances" ], "Properties": { @@ -2917,17 +2950,14 @@ "TaskDefinition": { "Ref": "ApiMonitorTasks" } } }, - "ApiWebService": { + "ServiceWebApi": { "Type": "AWS::ECS::Service", - "DependsOn": [ "Instances" ], + "DependsOn": [ "Instances", "ApiBalancerListenerRule" ], "Properties": { "Cluster": { "Ref": "Cluster" }, "DeploymentConfiguration": { "MinimumHealthyPercent": { "Fn::If": [ "HighAvailability", "50", "0" ] }, "MaximumPercent": "200" }, "DesiredCount": { "Fn::If": [ "HighAvailability", { "Ref": "ApiCount" }, 1 ] }, - "LoadBalancers": [ { "Fn::If": [ "ApiRouterALB", - { "ContainerName": "web", "ContainerPort": "5443", "TargetGroupArn": { "Ref": "RouterApiTargetGroup" } }, - { "ContainerName": "web", "ContainerPort": "5443", "LoadBalancerName": { "Ref": "Balancer" } } - ] } ], + "LoadBalancers": [ { "ContainerName": "web", "ContainerPort": "5443", "TargetGroupArn": { "Ref": "ApiBalancerTargetGroup" } }], "Role": { "Fn::GetAtt": [ "ServiceRole", "Arn" ] }, "TaskDefinition": { "Ref": "ApiWebTasks" } } @@ -3263,12 +3293,9 @@ "MemoryReservation": { "Ref": "ApiWebMemory" }, "MountPoints": [ { "SourceVolume": "docker", "ContainerPath": "/var/run/docker.sock" } ], "Name": "web", - "PortMappings": { "Fn::If": [ "ApiRouterALB", - [ { "ContainerPort": "5443", "Protocol": "tcp" } ], - { "Fn::If": [ "PrivateApi", - [ { "HostPort": "3100", "ContainerPort": "5443", "Protocol": "tcp" } ], - [ { "HostPort": "3000", "ContainerPort": "5443", "Protocol": "tcp" } ] - ] } + "PortMappings": { "Fn::If": [ "PrivateApi", + [ { "HostPort": "4100", "ContainerPort": "5443", "Protocol": "tcp" } ], + [ { "HostPort": "4000", "ContainerPort": "5443", "Protocol": "tcp" } ] ] } } ],