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