primary-replica: 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

Bug: Issue 15087
Change-Id: I588b776f08583b46c7325432bbb39a3457c4ca39
diff --git a/primary-replica/Makefile b/primary-replica/Makefile
index 0afea75..7bffb65 100644
--- a/primary-replica/Makefile
+++ b/primary-replica/Makefile
@@ -82,8 +82,9 @@
 		ParameterKey=ClusterStackName,ParameterValue=$(CLUSTER_STACK_NAME) \
 		ParameterKey=TemplateBucketName,ParameterValue=$(TEMPLATE_BUCKET_NAME) \
 		ParameterKey=HostedZoneName,ParameterValue=$(HOSTED_ZONE_NAME) \
-		ParameterKey=Subdomain,ParameterValue=$(PRIMARY_SUBDOMAIN) \
-		ParameterKey=ReplicaSubdomain,ParameterValue=$(REPLICA_SUBDOMAIN) \
+		ParameterKey=HttpSubdomain,ParameterValue=$(HTTP_PRIMARY_SUBDOMAIN) \
+		ParameterKey=SshSubdomain,ParameterValue=$(SSH_PRIMARY_SUBDOMAIN) \
+		ParameterKey=SshReplicaSubdomain,ParameterValue=$(SSH_REPLICA_SUBDOMAIN) \
 		ParameterKey=DockerRegistryUrl,ParameterValue=$(DOCKER_REGISTRY_URI) \
 		ParameterKey=CertificateArn,ParameterValue=$(SSL_CERTIFICATE_ARN) \
 		ParameterKey=ReplicaServiceStackName,ParameterValue=$(SERVICE_REPLICA_STACK_NAME) \
@@ -127,7 +128,8 @@
 		ParameterKey=ClusterStackName,ParameterValue=$(CLUSTER_STACK_NAME) \
 		ParameterKey=TemplateBucketName,ParameterValue=$(TEMPLATE_BUCKET_NAME) \
 		ParameterKey=HostedZoneName,ParameterValue=$(HOSTED_ZONE_NAME) \
-		ParameterKey=Subdomain,ParameterValue=$(REPLICA_SUBDOMAIN) \
+		ParameterKey=HttpSubdomain,ParameterValue=$(HTTP_REPLICA_SUBDOMAIN) \
+		ParameterKey=SshSubdomain,ParameterValue=$(SSH_REPLICA_SUBDOMAIN) \
 		ParameterKey=DockerRegistryUrl,ParameterValue=$(DOCKER_REGISTRY_URI) \
 		ParameterKey=CertificateArn,ParameterValue=$(SSL_CERTIFICATE_ARN) \
 		ParameterKey=GerritKeyPrefix,ParameterValue=$(GERRIT_KEY_PREFIX)\
diff --git a/primary-replica/README.md b/primary-replica/README.md
index d710050..9cff49c 100644
--- a/primary-replica/README.md
+++ b/primary-replica/README.md
@@ -56,8 +56,10 @@
 * `SERVICE_PRIMARY_STACK_NAME`: Optional. Name of the primary service stack. `gerrit-service-primary` by default.
 * `SERVICE_REPLICA_STACK_NAME`: Optional. Name of the replica service stack. `gerrit-service-replica` by default.
 * `DASHBOARD_STACK_NAME` : Optional. Name of the dashboard stack. `gerrit-dashboard` by default.
-* `PRIMARY_SUBDOMAIN`: Optional. Name of the primary sub domain. `gerrit-primary-demo` by default.
-* `REPLICA_SUBDOMAIN`: Optional. Name of the replica sub domain. `gerrit-replica-demo` by default.
+* `HTTP_PRIMARY_SUBDOMAIN`: Optional. Name of the primary sub domain for HTTP traffic. `gerrit-http-primary-demo` by default.
+* `SSH_PRIMARY_SUBDOMAIN`: Optional. Name of the primary sub domain for SSH traffic. `gerrit-ssh-primary-demo` by default.
+* `HTTP_REPLICA_SUBDOMAIN`: Optional. Name of the replica sub domain for HTTP traffic. `gerrit-http-replica-demo` by default.
+* `SSH_REPLICA_SUBDOMAIN`: Optional. Name of the replica sub domain for SSH traffic. `gerrit-ssh-replica-demo` by default.
 * `GERRIT_PRIMARY_INSTANCE_ID`: Optional. Identifier for the Gerrit primary instance.
 "gerrit-primary-replica-PRIMARY" by default.
 * `GERRIT_REPLICA_INSTANCE_ID`: Optional. Identifier for the Gerrit replica instance.
diff --git a/primary-replica/cf-dns-route.yml b/primary-replica/cf-dns-route.yml
index ec9b369..ee2bea9 100644
--- a/primary-replica/cf-dns-route.yml
+++ b/primary-replica/cf-dns-route.yml
@@ -11,26 +11,50 @@
       Default: gerrit-service-replica
 
 Resources:
-  PrimaryDnsRecord:
+  PrimaryHTTPDnsRecord:
       Type: AWS::Route53::RecordSet
       Properties:
         Name:
           !Join
             - '.'
-            - - Fn::ImportValue: !Join [':', [!Ref 'PrimaryServiceStackName', 'Subdomain']]
+            - - Fn::ImportValue: !Join [':', [!Ref 'PrimaryServiceStackName', 'HttpSubdomain']]
               - Fn::ImportValue: !Join [':', [!Ref 'PrimaryServiceStackName', 'HostedZoneName']]
         HostedZoneName:
           !Join
             - ''
             - - Fn::ImportValue: !Join [':', [!Ref 'PrimaryServiceStackName', 'HostedZoneName']]
               - '.'
-        Comment: DNS name for Gerrit Primary.
+        Comment: DNS name for Load Balancer serving HTTP requests to primary gerrit
         Type: A
         AliasTarget:
           DNSName:
             Fn::ImportValue:
-              !Join [':', [!Ref 'PrimaryServiceStackName', 'PublicLoadBalancerDNSName']]
+              !Join [':', [!Ref 'PrimaryServiceStackName', 'GerritHTTPLoadBalancerDNSName']]
           HostedZoneId:
             Fn::ImportValue:
-              !Join [':', [!Ref 'PrimaryServiceStackName', 'CanonicalHostedZoneID']]
+              !Join [':', [!Ref 'PrimaryServiceStackName', 'GerritHTTPCanonicalHostedZoneID']]
           EvaluateTargetHealth: False
+
+  PrimarySSHDnsRecord:
+    Type: AWS::Route53::RecordSet
+    Properties:
+      Name:
+        !Join
+        - '.'
+        - - Fn::ImportValue: !Join [':', [!Ref 'PrimaryServiceStackName', 'SshSubdomain']]
+          - Fn::ImportValue: !Join [':', [!Ref 'PrimaryServiceStackName', 'HostedZoneName']]
+      HostedZoneName:
+        !Join
+        - ''
+        - - Fn::ImportValue: !Join [':', [!Ref 'PrimaryServiceStackName', 'HostedZoneName']]
+          - '.'
+      Comment: DNS name for Load Balancer serving SSH requests to primary gerrit
+      Type: A
+      AliasTarget:
+        DNSName:
+          Fn::ImportValue:
+            !Join [':', [!Ref 'PrimaryServiceStackName', 'GerritSSHLoadBalancerDNSName']]
+        HostedZoneId:
+          Fn::ImportValue:
+            !Join [':', [!Ref 'PrimaryServiceStackName', 'GerritSSHCanonicalHostedZoneID']]
+        EvaluateTargetHealth: False
\ No newline at end of file
diff --git a/primary-replica/cf-service-primary.yml b/primary-replica/cf-service-primary.yml
index 3fe939f..10a0642 100644
--- a/primary-replica/cf-service-primary.yml
+++ b/primary-replica/cf-service-primary.yml
@@ -55,18 +55,22 @@
   HostedZoneName:
         Description: The route53 HostedZoneName.
         Type: String
-  Subdomain:
-        Description: The subdomain of the Gerrit cluster
+  HttpSubdomain:
+        Description: The subdomain of the loadbalancer serving HTTP traffic for the primary Gerrit
         Type: String
-        Default: gerrit-primary-demo
+        Default: gerrit-primary-http-demo
+  SshSubdomain:
+    Description: The subdomain of the loadbalancer serving SSH traffic for the primary Gerrit
+    Type: String
+    Default: gerrit-primary-ssh-demo
   LoadBalancerScheme:
         Description: Load Balancer schema, The nodes of an Internet-facing load balancer have public IP addresses.
         Type: String
         Default: internet-facing
         AllowedValues: [internal, internet-facing]
-  ReplicaSubdomain:
-        Description: The subdomain of the Gerrit replica
-        Type: String
+  SshReplicaSubdomain:
+    Description: The subdomain of the loadbalancer serving SSH traffic for the replicas
+    Type: String
   GerritKeyPrefix:
         Description: Gerrit credentials keys prefix
         Type: String
@@ -219,7 +223,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
@@ -263,7 +269,7 @@
                     - Name: GIT_SSH_PORT
                       Value: !Ref GitSSHPort
                     - Name: REPLICA_SUBDOMAIN
-                      Value: !Ref ReplicaSubdomain
+                      Value: !Ref SshReplicaSubdomain
                     - Name: HOSTED_ZONE_NAME
                       Value: !Ref HostedZoneName
                     - Name: REINDEX_AT_STARTUP
@@ -334,9 +340,47 @@
                 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 to primary Gerrit"
+        GroupName: !Sub '${ClusterStackName}-gerrit-http-sg'
+        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:
@@ -353,13 +397,13 @@
 
     HTTPTargetGroup:
         Type: AWS::ElasticLoadBalancingV2::TargetGroup
-        DependsOn: LoadBalancer
+        DependsOn: GerritHTTPLoadBalancer
         Properties:
             VpcId:
               Fn::ImportValue:
                   !Join [':', [!Ref 'ClusterStackName', 'VPCId']]
             Port: !Ref HTTPPort
-            Protocol: TCP
+            Protocol: HTTP
             HealthCheckProtocol: HTTP
             HealthCheckPort: !Ref HTTPPort
             HealthCheckPath: '/config/server/healthcheck~status'
@@ -367,20 +411,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:
@@ -394,12 +437,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
 
@@ -410,33 +452,62 @@
         TimeoutInMinutes: '5'
 
 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 gerrit HTTP traffic
+    Value: !Ref HttpSubdomain
     Export:
-      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'Subdomain' ] ]
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'HttpSubdomain' ] ]
+  SshSubdomain:
+    Description: Service DNS subdomain for gerrit 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/primary-replica/cf-service-replica.yml b/primary-replica/cf-service-replica.yml
index 4d355f5..e7a75df 100644
--- a/primary-replica/cf-service-replica.yml
+++ b/primary-replica/cf-service-replica.yml
@@ -74,10 +74,14 @@
   HostedZoneName:
       Description: The route53 HostedZoneName.
       Type: String
-  Subdomain:
-      Description: The subdomain of the Gerrit cluster
+  HttpSubdomain:
+      Description: The subdomain of the Gerrit loadbalancer serving HTTP traffic for replicas
       Type: String
-      Default: gerrit-replica-demo
+      Default: gerrit-http-replica-demo
+  SshSubdomain:
+    Description: The subdomain of the Gerrit loadbalancer serving SSH traffic for replicas
+    Type: String
+    Default: gerrit-ssh-replica-demo
   LoadBalancerScheme:
       Description: Load Balancer schema, The nodes of an Internet-facing load balancer have public IP addresses.
       Type: String
@@ -257,7 +261,9 @@
                   Image: !Sub '${DockerRegistryUrl}/${GerritDockerImage}'
                   Environment:
                     - Name: CANONICAL_WEB_URL
-                      Value: !Sub 'https://${Subdomain}.${HostedZoneName}'
+                      Value: !Sub 'https://${HttpSubdomain}.${HostedZoneName}'
+                    - Name: SSHD_ADVERTISED_ADDRESS
+                      Value: !Sub '${SshSubdomain}.${HostedZoneName}:${SSHHostPort}'
                     - Name: HTTPD_LISTEN_URL
                       Value: !Sub 'proxy-https://*:${HTTPContainePort}/'
                     - Name: CONTAINER_REPLICA
@@ -428,9 +434,47 @@
               - '/'
               - !GetAtt GerritService.Name
 
-    LoadBalancer:
+    ReplicaHTTPLoadBalancerSG:
+      Type: AWS::EC2::SecurityGroup
+      Properties:
+        VpcId:
+          Fn::ImportValue:
+            !Join [':', [!Ref 'ClusterStackName', 'VPCId']]
+        GroupDescription: "Allow public HTTPS traffic to replicas"
+        GroupName: !Sub '${ClusterStackName}-replica-http-sg'
+        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)"
+
+    ReplicaHTTPLoadBalancer:
+      Type: AWS::ElasticLoadBalancingV2::LoadBalancer
+      Properties:
+        Name: !Sub '${ClusterStackName}-replica-http'
+        Type: application
+        Scheme: !Ref 'LoadBalancerScheme'
+        SecurityGroups:
+          - !Ref ReplicaHTTPLoadBalancerSG
+        Subnets:
+          - Fn::ImportValue:
+              !Join [':', [!Ref 'ClusterStackName', 'PublicSubnetOne']]
+          - Fn::ImportValue:
+              !Join [':', [!Ref 'ClusterStackName', 'PublicSubnetTwo']]
+        Tags:
+          - Key: Name
+            Value: !Join ['-', [!Ref 'EnvironmentName', !Ref 'GerritServiceName', 'alb']]
+
+    ReplicaSSHLoadBalancer:
         Type: AWS::ElasticLoadBalancingV2::LoadBalancer
         Properties:
+            Name: !Sub '${ClusterStackName}-replica-ssh'
             Type: network
             Scheme: !Ref 'LoadBalancerScheme'
             LoadBalancerAttributes:
@@ -447,13 +491,13 @@
 
     HTTPTargetGroup:
         Type: AWS::ElasticLoadBalancingV2::TargetGroup
-        DependsOn: LoadBalancer
+        DependsOn: ReplicaHTTPLoadBalancer
         Properties:
             VpcId:
               Fn::ImportValue:
                   !Join [':', [!Ref 'ClusterStackName', 'VPCId']]
             Port: !Ref HTTPHostPort
-            Protocol: TCP
+            Protocol: HTTP
             HealthCheckPort: !Ref HTTPHostPort
             HealthCheckProtocol: HTTP
             HealthCheckPath: '/config/server/healthcheck~status'
@@ -461,27 +505,24 @@
             TargetGroupAttributes:
               - Key: 'stickiness.enabled'
                 Value: true
-              # NLB only supports source_ip. Move this to `lb_cookie` or `app_cookie`
-              # when this target group is moved behind an ALB
               - Key: 'stickiness.type'
-                Value: 'source_ip'
+                Value: 'lb_cookie'
 
     HTTPListener:
         Type: AWS::ElasticLoadBalancingV2::Listener
-        DependsOn: LoadBalancer
         Properties:
             Certificates:
               - CertificateArn: !Ref CertificateArn
             DefaultActions:
             - Type: forward
               TargetGroupArn: !Ref HTTPTargetGroup
-            LoadBalancerArn: !Ref LoadBalancer
+            LoadBalancerArn: !Ref ReplicaHTTPLoadBalancer
             Port: !Ref HTTPSPort
-            Protocol: TLS
+            Protocol: HTTPS
 
     SSHTargetGroup:
         Type: AWS::ElasticLoadBalancingV2::TargetGroup
-        DependsOn: LoadBalancer
+        DependsOn: ReplicaSSHLoadBalancer
         Properties:
             VpcId:
               Fn::ImportValue:
@@ -495,18 +536,17 @@
 
     SSHListener:
         Type: AWS::ElasticLoadBalancingV2::Listener
-        DependsOn: LoadBalancer
         Properties:
             DefaultActions:
             - Type: forward
               TargetGroupArn: !Ref SSHTargetGroup
-            LoadBalancerArn: !Ref LoadBalancer
+            LoadBalancerArn: !Ref ReplicaSSHLoadBalancer
             Port: !Ref SSHHostPort
             Protocol: TCP
 
     GitTargetGroup:
         Type: AWS::ElasticLoadBalancingV2::TargetGroup
-        DependsOn: LoadBalancer
+        DependsOn: ReplicaSSHLoadBalancer
         Properties:
             VpcId:
               Fn::ImportValue:
@@ -516,18 +556,17 @@
 
     GitListener:
         Type: AWS::ElasticLoadBalancingV2::Listener
-        DependsOn: LoadBalancer
         Properties:
             DefaultActions:
             - Type: forward
               TargetGroupArn: !Ref GitTargetGroup
-            LoadBalancerArn: !Ref LoadBalancer
+            LoadBalancerArn: !Ref ReplicaSSHLoadBalancer
             Port: !Ref GitPort
             Protocol: TCP
 
     GitSSHTargetGroup:
         Type: AWS::ElasticLoadBalancingV2::TargetGroup
-        DependsOn: LoadBalancer
+        DependsOn: ReplicaSSHLoadBalancer
         Properties:
             VpcId:
               Fn::ImportValue:
@@ -537,27 +576,38 @@
 
     GitSSHListener:
         Type: AWS::ElasticLoadBalancingV2::Listener
-        DependsOn: LoadBalancer
         Properties:
             DefaultActions:
             - Type: forward
               TargetGroupArn: !Ref GitSSHTargetGroup
-            LoadBalancerArn: !Ref LoadBalancer
+            LoadBalancerArn: !Ref ReplicaSSHLoadBalancer
             Port: !Ref GitSSHPort
             Protocol: TCP
 
-    ReplicaDnsRecord:
+    ReplicaSSHDnsRecord:
         Type: AWS::Route53::RecordSet
         Properties:
-          Name: !Sub '${Subdomain}.${HostedZoneName}'
+          Name: !Sub '${SshSubdomain}.${HostedZoneName}'
           HostedZoneName: !Sub '${HostedZoneName}.'
-          Comment: DNS name for Gerrit Replica.
+          Comment: DNS name for Load Balancer serving SSH requests to gerrit replicas
           Type: A
           AliasTarget:
-            DNSName: !GetAtt 'LoadBalancer.DNSName'
-            HostedZoneId: !GetAtt 'LoadBalancer.CanonicalHostedZoneID'
+            DNSName: !GetAtt 'ReplicaSSHLoadBalancer.DNSName'
+            HostedZoneId: !GetAtt 'ReplicaSSHLoadBalancer.CanonicalHostedZoneID'
             EvaluateTargetHealth: False
 
+    ReplicaHTTPDnsRecord:
+      Type: AWS::Route53::RecordSet
+      Properties:
+        Name: !Sub '${HttpSubdomain}.${HostedZoneName}'
+        HostedZoneName: !Sub '${HostedZoneName}.'
+        Comment: DNS name for Load Balancer serving HTTP requests to gerrit replicas
+        Type: A
+        AliasTarget:
+          DNSName: !GetAtt 'ReplicaHTTPLoadBalancer.DNSName'
+          HostedZoneId: !GetAtt 'ReplicaHTTPLoadBalancer.CanonicalHostedZoneID'
+          EvaluateTargetHealth: False
+
     ECSTaskExecutionRoleStack:
       Type: AWS::CloudFormation::Stack
       Properties:
@@ -565,33 +615,62 @@
         TimeoutInMinutes: '5'
 
 Outputs:
-  PublicLoadBalancerDNSName:
-    Description: The DNS name of the external load balancer
-    Value: !GetAtt 'LoadBalancer.DNSName'
+  ########
+  # HTTP #
+  ########
+  ReplicaHTTPLoadBalancerDNSName:
+    Description: The DNS name of the replica HTTP load balancer
+    Value: !GetAtt 'ReplicaHTTPLoadBalancer.DNSName'
     Export:
-      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicLoadBalancerDNSName' ] ]
-  CanonicalHostedZoneID:
-    Description: Canonical Hosted Zone ID
-    Value: !GetAtt 'LoadBalancer.CanonicalHostedZoneID'
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ReplicaHTTPLoadBalancerDNSName' ] ]
+  ReplicaHTTPCanonicalHostedZoneID:
+    Description: Canonical Hosted Zone ID of the replica HTTP load balancer
+    Value: !GetAtt 'ReplicaHTTPLoadBalancer.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', 'ReplicaHTTPCanonicalHostedZoneID' ] ]
+  ReplicaHTTPLoadBalancerUrl:
+    Description: The url of the replica HTTP load balancer
+    Value: !Join ['', ['http://', !GetAtt 'ReplicaHTTPLoadBalancer.DNSName']]
     Export:
-      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicLoadBalancerUrl' ] ]
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ReplicaHTTPLoadBalancerUrl' ] ]
+  #######
+  # SSH #
+  #######
+  ReplicaSSHLoadBalancerDNSName:
+    Description: The DNS name of the replica SSH load balancer
+    Value: !GetAtt 'ReplicaSSHLoadBalancer.DNSName'
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ReplicaSSHLoadBalancerDNSName' ] ]
+  ReplicaSSHCanonicalHostedZoneID:
+    Description: Canonical Hosted Zone ID of the replica SSH load balancer
+    Value: !GetAtt 'ReplicaSSHLoadBalancer.CanonicalHostedZoneID'
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ReplicaSSHCanonicalHostedZoneID' ] ]
+  ReplicaSSHLoadBalancerUrl:
+    Description: The url of the replica SSH load balancer
+    Value: !Join ['', ['http://', !GetAtt 'ReplicaSSHLoadBalancer.DNSName']]
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ReplicaSSHLoadBalancerUrl' ] ]
+  #######
+  # 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 replicas HTTP traffic
+    Value: !Ref HttpSubdomain
     Export:
-      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'Subdomain' ] ]
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'HttpSubdomain' ] ]
+  SshSubdomain:
+    Description: Service DNS subdomain for replicas 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/primary-replica/setup.env.template b/primary-replica/setup.env.template
index 8186090..4aade05 100644
--- a/primary-replica/setup.env.template
+++ b/primary-replica/setup.env.template
@@ -8,8 +8,10 @@
 DNS_ROUTING_MONITORING_STACK_NAME:=$(AWS_PREFIX)-monitoring-dns-routing
 DASHBOARD_STACK_NAME:=$(AWS_PREFIX)-dashboard
 HOSTED_ZONE_NAME:=yourcompany.com
-PRIMARY_SUBDOMAIN:=$(AWS_PREFIX)-primary.gerrit-demo
-REPLICA_SUBDOMAIN:=$(AWS_PREFIX)-replica.gerrit-demo
+HTTP_PRIMARY_SUBDOMAIN:=$(AWS_PREFIX)-http-primary.gerrit-demo
+SSH_PRIMARY_SUBDOMAIN:=$(AWS_PREFIX)-ssh-primary.gerrit-demo
+HTTP_REPLICA_SUBDOMAIN:=$(AWS_PREFIX)-http-replica.gerrit-demo
+SSH_REPLICA_SUBDOMAIN:=$(AWS_PREFIX)-ssh-replica.gerrit-demo
 PROMETHEUS_SUBDOMAIN:=$(AWS_PREFIX)-prometheus.gerrit-demo
 GRAFANA_SUBDOMAIN:=$(AWS_PREFIX)-grafana.gerrit-demo
 DOCKER_REGISTRY_URI:=<yourAccountId>.dkr.ecr.us-east-1.amazonaws.com