Single-primary: separate HTTP from SSH traffic

Using a Network Load Balancer forces all the traffic to be handled at
the transport level (Level 4). Whilst this is fine for SSH traffic, it
is not ideal for HTTP traffic, which should be handled at application
level (Level 7).

Split HTTP and SSH traffic and handle HTTP traffic via an Application
Load Balancer.

This unlocks many benefits, among which:
- Advanced routing: based on path, host, headers, request method,
  source IP, etc
- Application-based sticky sessions
- Seamless integration with X-Ray
- Detailed access logs

Separating HTTP and SSH traffic requires to make two endpoints
available at DNS level, respectively:

- <HttpSubDomain>.<HostedZoneName>:443
- <SshSubDomain>.<HostedZoneName>:29418

Bug: Issue 15087
Change-Id: I30b55c3718d2b52d1651a8e541411dc29946cc40
diff --git a/gerrit/etc/gerrit.config.template b/gerrit/etc/gerrit.config.template
index 7488c31..0656386 100644
--- a/gerrit/etc/gerrit.config.template
+++ b/gerrit/etc/gerrit.config.template
@@ -51,6 +51,9 @@
 
 [sshd]
 	listenAddress = *:29418
+{% if SSHD_ADVERTISED_ADDRESS %}
+    advertisedAddress = {{ SSHD_ADVERTISED_ADDRESS }}
+{% endif %}
 [httpd]
 	listenUrl = http://*:8080/
 	requestLog = true
diff --git a/gerrit/setup_gerrit.py b/gerrit/setup_gerrit.py
index 74ecdba..d18d95d 100755
--- a/gerrit/setup_gerrit.py
+++ b/gerrit/setup_gerrit.py
@@ -173,6 +173,7 @@
         'REFS_DB_ENABLED': os.getenv('REFS_DB_ENABLED'),
         'DYNAMODB_LOCKS_TABLE_NAME': os.getenv('DYNAMODB_LOCKS_TABLE_NAME'),
         'DYNAMODB_REFS_TABLE_NAME': os.getenv('DYNAMODB_REFS_TABLE_NAME'),
+        'SSHD_ADVERTISED_ADDRESS': os.getenv('SSHD_ADVERTISED_ADDRESS'),
     })
     f.write(template.render(config_for_template))
 
diff --git a/single-primary/Makefile b/single-primary/Makefile
index 067c9e8..4c62493 100644
--- a/single-primary/Makefile
+++ b/single-primary/Makefile
@@ -63,7 +63,8 @@
 		ParameterKey=ClusterStackName,ParameterValue=$(CLUSTER_STACK_NAME) \
 		ParameterKey=TemplateBucketName,ParameterValue=$(TEMPLATE_BUCKET_NAME) \
 		ParameterKey=HostedZoneName,ParameterValue=$(HOSTED_ZONE_NAME) \
-		ParameterKey=Subdomain,ParameterValue=$(SUBDOMAIN) \
+		ParameterKey=HttpSubdomain,ParameterValue=$(HTTP_SUBDOMAIN) \
+		ParameterKey=SshSubdomain,ParameterValue=$(SSH_SUBDOMAIN) \
 		ParameterKey=DockerRegistryUrl,ParameterValue=$(DOCKER_REGISTRY_URI) \
 		ParameterKey=CertificateArn,ParameterValue=$(SSL_CERTIFICATE_ARN) \
 		ParameterKey=GerritKeyPrefix,ParameterValue=$(GERRIT_KEY_PREFIX) \
diff --git a/single-primary/README.md b/single-primary/README.md
index 05bf3db..2aaa012 100644
--- a/single-primary/README.md
+++ b/single-primary/README.md
@@ -103,9 +103,11 @@
 
 ### Access your Gerrit
 
-You Gerrit instance will be available at this URL: `http://<HOSTED_ZONE_NAME>.<SUBDOMAIN>`.
+The Gerrit instance will be available at two different URLs, to handle HTTP and
+SSH traffic respectively:
 
-The available ports are `8080` for HTTP and `29418` for SSH.
+* HTTP traffic: `https://<HTTP_SUBDOMAIN>.<HOSTED_ZONE_NAME>:443`
+* SSH traffic: `ssh://<SSH_SUBDOMAIN>.<HOSTED_ZONE_NAME>:29418`
 
 ### External Services
 
diff --git a/single-primary/cf-dns-route.yml b/single-primary/cf-dns-route.yml
index bf0d474..9a25016 100644
--- a/single-primary/cf-dns-route.yml
+++ b/single-primary/cf-dns-route.yml
@@ -7,26 +7,50 @@
       Default: gerrit-service
 
 Resources:
-  DnsRecord:
+  GerritSSHDnsRecord:
       Type: AWS::Route53::RecordSet
       Properties:
         Name:
           !Join
             - '.'
-            - - Fn::ImportValue: !Join [':', [!Ref 'ServiceStackName', 'Subdomain']]
+            - - Fn::ImportValue: !Join [':', [!Ref 'ServiceStackName', 'SshSubdomain']]
               - Fn::ImportValue: !Join [':', [!Ref 'ServiceStackName', 'HostedZoneName']]
         HostedZoneName:
           !Join
             - ''
             - - Fn::ImportValue: !Join [':', [!Ref 'ServiceStackName', 'HostedZoneName']]
               - '.'
-        Comment: DNS name for Gerrit Primary.
+        Comment: DNS name for Load Balancer serving SSH requests to primary gerrit
         Type: A
         AliasTarget:
           DNSName:
             Fn::ImportValue:
-              !Join [':', [!Ref 'ServiceStackName', 'PublicLoadBalancerDNSName']]
+              !Join [':', [!Ref 'ServiceStackName', 'GerritSSHLoadBalancerDNSName']]
           HostedZoneId:
             Fn::ImportValue:
-              !Join [':', [!Ref 'ServiceStackName', 'CanonicalHostedZoneID']]
+              !Join [':', [!Ref 'ServiceStackName', 'GerritSSHCanonicalHostedZoneID']]
           EvaluateTargetHealth: False
+
+  GerritHTTPDnsRecord:
+    Type: AWS::Route53::RecordSet
+    Properties:
+      Name:
+        !Join
+          - '.'
+          - - Fn::ImportValue: !Join [':', [!Ref 'ServiceStackName', 'HttpSubdomain']]
+            - Fn::ImportValue: !Join [':', [!Ref 'ServiceStackName', 'HostedZoneName']]
+      HostedZoneName:
+        !Join
+        - ''
+        - - Fn::ImportValue: !Join [':', [!Ref 'ServiceStackName', 'HostedZoneName']]
+          - '.'
+      Comment: DNS name for Load Balancer serving HTTP requests to primary gerrit
+      Type: A
+      AliasTarget:
+        DNSName:
+          Fn::ImportValue:
+            !Join [':', [!Ref 'ServiceStackName', 'GerritHTTPLoadBalancerDNSName']]
+        HostedZoneId:
+          Fn::ImportValue:
+            !Join [':', [!Ref 'ServiceStackName', 'GerritHTTPCanonicalHostedZoneID']]
+        EvaluateTargetHealth: False
\ No newline at end of file
diff --git a/single-primary/cf-service.yml b/single-primary/cf-service.yml
index c961006..7b9fb06 100644
--- a/single-primary/cf-service.yml
+++ b/single-primary/cf-service.yml
@@ -44,10 +44,14 @@
   HostedZoneName:
         Description: The route53 HostedZoneName.
         Type: String
-  Subdomain:
-        Description: The subdomain of the Gerrit cluster
+  HttpSubdomain:
+        Description: The HTTP subdomain of the Gerrit cluster
         Type: String
-        Default: gerrit-primary-demo
+        Default: gerrit-http-primary-demo
+  SshSubdomain:
+    Description: The SSH subdomain of the Gerrit cluster
+    Type: String
+    Default: gerrit-ssh-primary-demo
   LoadBalancerScheme:
         Description: Load Balancer schema, The nodes of an Internet-facing load balancer have public IP addresses.
         Type: String
@@ -207,7 +211,9 @@
                   Image: !Sub '${DockerRegistryUrl}/${DockerImage}'
                   Environment:
                     - Name: CANONICAL_WEB_URL
-                      Value: !Sub 'https://${Subdomain}.${HostedZoneName}'
+                      Value: !Sub 'https://${HttpSubdomain}.${HostedZoneName}'
+                    - Name: SSHD_ADVERTISED_ADDRESS
+                      Value: !Sub '${SshSubdomain}.${HostedZoneName}:${SSHPort}'
                     - Name: HTTPD_LISTEN_URL
                       Value: !Sub 'proxy-https://*:${HTTPPort}/'
                     - Name: AWS_REGION
@@ -312,9 +318,48 @@
                 Host:
                   SourcePath: !Join ['/', ["/gerrit-mount-point", !FindInMap ['Gerrit', 'Volume', 'Logs']]]
 
-    LoadBalancer:
+    GerritHTTPLoadBalancerSG:
+      Type: AWS::EC2::SecurityGroup
+      Properties:
+        VpcId:
+          Fn::ImportValue:
+            !Join [':', [!Ref 'ClusterStackName', 'VPCId']]
+        GroupDescription: "Allow public HTTPS traffic"
+        GroupName: !Sub '${ClusterStackName}-primary-http'
+        SecurityGroupIngress:
+          - IpProtocol: 'tcp'
+            FromPort: !Ref HTTPSPort
+            ToPort: !Ref HTTPSPort
+            CidrIp: '0.0.0.0/0'
+            Description: "HTTPS connections from everywhere (IPv4)"
+          - IpProtocol: 'tcp'
+            FromPort: !Ref HTTPSPort
+            ToPort: !Ref HTTPSPort
+            CidrIpv6: '::/0'
+            Description: "HTTPS connections from everywhere (IPv6)"
+
+
+    GerritHTTPLoadBalancer:
+      Type: AWS::ElasticLoadBalancingV2::LoadBalancer
+      Properties:
+        Name: !Sub '${ClusterStackName}-primary-http'
+        Type: application
+        Scheme: !Ref 'LoadBalancerScheme'
+        SecurityGroups:
+          - !Ref GerritHTTPLoadBalancerSG
+        Subnets:
+          - Fn::ImportValue:
+              !Join [':', [!Ref 'ClusterStackName', 'PublicSubnetOne']]
+          - Fn::ImportValue:
+              !Join [':', [!Ref 'ClusterStackName', 'PublicSubnetTwo']]
+        Tags:
+          - Key: Name
+            Value: !Join ['-', [!Ref 'EnvironmentName', !Ref 'GerritServiceName', 'alb']]
+
+    GerritSSHLoadBalancer:
         Type: AWS::ElasticLoadBalancingV2::LoadBalancer
         Properties:
+            Name: !Sub '${ClusterStackName}-primary-ssh'
             Type: network
             Scheme: !Ref 'LoadBalancerScheme'
             LoadBalancerAttributes:
@@ -331,13 +376,13 @@
 
     HTTPTargetGroup:
         Type: AWS::ElasticLoadBalancingV2::TargetGroup
-        DependsOn: LoadBalancer
+        DependsOn: GerritHTTPLoadBalancer
         Properties:
             VpcId:
               Fn::ImportValue:
-                  !Join [':', [!Ref 'ClusterStackName', 'VPCId']]
+                !Join [':', [!Ref 'ClusterStackName', 'VPCId']]
             Port: !Ref HTTPPort
-            Protocol: TCP
+            Protocol: HTTP
             HealthCheckProtocol: HTTP
             HealthCheckPort: !Ref HTTPPort
             HealthCheckPath: '/config/server/healthcheck~status'
@@ -345,20 +390,19 @@
 
     HTTPListener:
         Type: AWS::ElasticLoadBalancingV2::Listener
-        DependsOn: LoadBalancer
         Properties:
             Certificates:
               - CertificateArn: !Ref CertificateArn
             DefaultActions:
             - Type: forward
               TargetGroupArn: !Ref HTTPTargetGroup
-            LoadBalancerArn: !Ref LoadBalancer
+            LoadBalancerArn: !Ref GerritHTTPLoadBalancer
             Port: !Ref HTTPSPort
-            Protocol: TLS
+            Protocol: HTTPS
 
     SSHTargetGroup:
         Type: AWS::ElasticLoadBalancingV2::TargetGroup
-        DependsOn: LoadBalancer
+        DependsOn: GerritSSHLoadBalancer
         Properties:
             VpcId:
               Fn::ImportValue:
@@ -372,12 +416,11 @@
 
     SSHListener:
         Type: AWS::ElasticLoadBalancingV2::Listener
-        DependsOn: LoadBalancer
         Properties:
             DefaultActions:
             - Type: forward
               TargetGroupArn: !Ref SSHTargetGroup
-            LoadBalancerArn: !Ref LoadBalancer
+            LoadBalancerArn: !Ref GerritSSHLoadBalancer
             Port: !Ref SSHPort
             Protocol: TCP
 
@@ -757,33 +800,62 @@
                 }
 
 Outputs:
-  PublicLoadBalancerDNSName:
-    Description: The DNS name of the external load balancer
-    Value: !GetAtt 'LoadBalancer.DNSName'
+  ########
+  # HTTP #
+  ########
+  GerritHTTPLoadBalancerDNSName:
+    Description: The DNS name of the Gerrit HTTP load balancer
+    Value: !GetAtt 'GerritHTTPLoadBalancer.DNSName'
     Export:
-      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicLoadBalancerDNSName' ] ]
-  CanonicalHostedZoneID:
-    Description: Canonical Hosted Zone ID
-    Value: !GetAtt 'LoadBalancer.CanonicalHostedZoneID'
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'GerritHTTPLoadBalancerDNSName' ] ]
+  GerritHTTPCanonicalHostedZoneID:
+    Description: Canonical Hosted Zone ID of the Gerrit HTTP load balancer
+    Value: !GetAtt 'GerritHTTPLoadBalancer.CanonicalHostedZoneID'
     Export:
-      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'CanonicalHostedZoneID' ] ]
-  PublicLoadBalancerUrl:
-    Description: The url of the external load balancer
-    Value: !Join ['', ['http://', !GetAtt 'LoadBalancer.DNSName']]
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'GerritHTTPCanonicalHostedZoneID' ] ]
+  GerritHTTPLoadBalancerUrl:
+    Description: The url of the Gerrit HTTP load balancer
+    Value: !Join ['', ['http://', !GetAtt 'GerritHTTPLoadBalancer.DNSName']]
     Export:
-      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicLoadBalancerUrl' ] ]
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'GerritHTTPLoadBalancerUrl' ] ]
+  #######
+  # SSH #
+  #######
+  GerritSSHLoadBalancerDNSName:
+    Description: The DNS name of the Gerrit SSH load balancer
+    Value: !GetAtt 'GerritSSHLoadBalancer.DNSName'
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'GerritSSHLoadBalancerDNSName' ] ]
+  GerritSSHCanonicalHostedZoneID:
+    Description: Canonical Hosted Zone ID of the Gerrit SSH load balancer
+    Value: !GetAtt 'GerritSSHLoadBalancer.CanonicalHostedZoneID'
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'GerritSSHCanonicalHostedZoneID' ] ]
+  GerritSSHLoadBalancerUrl:
+    Description: The url of the Gerrit SSH load balancer
+    Value: !Join ['', ['http://', !GetAtt 'GerritSSHLoadBalancer.DNSName']]
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'GerritSSHLoadBalancerUrl' ] ]
+  #######
+  # DNS #
+  #######
   HostedZoneName:
     Description: Route53 Hosted Zone name
     Value: !Ref HostedZoneName
     Export:
       Name: !Join [ ':', [ !Ref 'AWS::StackName', 'HostedZoneName' ] ]
-  Subdomain:
-    Description: Service DNS subdomain
-    Value: !Ref Subdomain
+  HttpSubdomain:
+    Description: Service DNS subdomain for HTTP traffic
+    Value: !Ref HttpSubdomain
     Export:
-      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'Subdomain' ] ]
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'HttpSubdomain' ] ]
+  SshSubdomain:
+    Description: Service DNS subdomain for SSH traffic
+    Value: !Ref SshSubdomain
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'SshSubdomain' ] ]
   CanonicalWebUrl:
     Description: Canonical Web URL
-    Value: !Sub 'https://${Subdomain}.${HostedZoneName}'
+    Value: !Sub 'https://${HttpSubdomain}.${HostedZoneName}'
     Export:
       Name: !Join [ ':', [ !Ref 'AWS::StackName', 'CanonicalWebUrl' ] ]
diff --git a/single-primary/setup.env.template b/single-primary/setup.env.template
index b3689d9..f7398dc 100644
--- a/single-primary/setup.env.template
+++ b/single-primary/setup.env.template
@@ -2,7 +2,8 @@
 CLUSTER_INSTANCE_TYPE:=m4.large
 DNS_ROUTING_STACK_NAME:=$(AWS_PREFIX)-dns-routing
 HOSTED_ZONE_NAME:=mycompany.com
-SUBDOMAIN:=$(AWS_PREFIX)-primary-demo
+HTTP_SUBDOMAIN:=$(AWS_PREFIX)-http-primary-demo
+SSH_SUBDOMAIN:=$(AWS_PREFIX)-ssh-primary-demo
 DOCKER_REGISTRY_URI:=<your_aws_account_number>.dkr.ecr.us-east-2.amazonaws.com
 SSL_CERTIFICATE_ARN=arn:aws:acm:us-east-2:<your_aws_account_number>:certificate/41eb8e52-c82b-420e-a5b2-d79107f3e5e1
 GERRIT_RAM=6000