AWS Gerrit master-slave recipe

CF templates and scripts to set up a Gerrit master
instance replicating to a slave

Feature: Issue 12516
Change-Id: I46aa690e9fc2435d29259003eed47d672bb73d29
diff --git a/.gitignore b/.gitignore
index 28ed166..00e1907 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,8 @@
 setup.env
 gerrit.setup
+replication.setup
 gerrit/plugins/*.jar
 gerrit/etc/*key*
 gerrit.config
 secure.config
+replication.config
diff --git a/README.md b/README.md
index 48e96ea..9c1a3ee 100644
--- a/README.md
+++ b/README.md
@@ -31,3 +31,4 @@
 ## Templates
 
 * [Standalone Gerrit master sandbox with LDAP authentication](/single-master/README.md)
+* [Gerrit master and slave sandbox with LDAP authentication](/master-slave/README.md)
diff --git a/gerrit/Dockerfile b/gerrit/Dockerfile
index f00271a..93051f6 100644
--- a/gerrit/Dockerfile
+++ b/gerrit/Dockerfile
@@ -1,4 +1,4 @@
-FROM gerritcodereview/gerrit:latest
+FROM gerritcodereview/gerrit:3.1.4-centos7
 
 USER root
 
@@ -6,9 +6,12 @@
     && yum update -y \
     && yum install -y python36u python36u-libs python36u-devel python36u-pip
 
+COPY --chown=gerrit:gerrit ssh-config /var/gerrit/.ssh/config
+
 # Installing scripts to get SSH Keys from Secret Manager
 COPY --chown=gerrit:gerrit requirements.txt /tmp
 COPY --chown=gerrit:gerrit gerrit.setup /tmp
+COPY --chown=gerrit:gerrit replication.setup /tmp
 COPY --chown=gerrit:gerrit setup_gerrit.py /tmp
 RUN chmod +x /tmp/setup_gerrit.py \
     && pip3 install -r /tmp/requirements.txt
diff --git a/gerrit/add_secrets_aws_secrets_manager.sh b/gerrit/add_secrets_aws_secrets_manager.sh
new file mode 100755
index 0000000..c5b7a00
--- /dev/null
+++ b/gerrit/add_secrets_aws_secrets_manager.sh
@@ -0,0 +1,62 @@
+#!/bin/bash -e
+
+SECRETS_DIRECTORY=$1
+if [ -z "$SECRETS_DIRECTORY" ];
+then
+  echo "Secrets directory must be specified"
+  exit 1
+fi
+
+# Avoid to open output in less for each AWS command
+export AWS_PAGER=;
+KEY_PREFIX=gerrit_secret
+
+echo "Adding SSH Keys..."
+
+keys=(
+  "ssh_host_ecdsa_384_key"
+  "ssh_host_ecdsa_384_key.pub"
+  "ssh_host_ecdsa_521_key"
+  "ssh_host_ecdsa_521_key.pub"
+  "ssh_host_ecdsa_key"
+  "ssh_host_ecdsa_key.pub"
+  "ssh_host_ed25519_key"
+  "ssh_host_ed25519_key.pub"
+  "ssh_host_rsa_key"
+  "ssh_host_rsa_key.pub"
+)
+
+for key_name in "${keys[@]}"
+do
+  echo aws secretsmanager create-secret --name ${KEY_PREFIX}_${key_name} \
+      --description "Gerrit ${key_name}" \
+      --secret-string file://$SECRETS_DIRECTORY/${key_name}
+done
+
+if [ -f "$SECRETS_DIRECTORY/replication_user_id_rsa.pub" ]; then
+  echo "Adding Replication SSH Keys..."
+  aws secretsmanager create-secret --name ${KEY_PREFIX}_replication_user_id_rsa.pub \
+      --description "Gerrit replication_user_id_rsa.pub" \
+      --secret-string file://$SECRETS_DIRECTORY/replication_user_id_rsa.pub
+  aws secretsmanager create-secret --name ${KEY_PREFIX}_replication_user_id_rsa \
+      --description "Gerrit replication_user_id_rsa" \
+      --secret-string file://$SECRETS_DIRECTORY/replication_user_id_rsa
+fi
+
+echo "Adding Register Email Private Key..."
+
+aws secretsmanager create-secret --name ${KEY_PREFIX}_registerEmailPrivateKey \
+    --description "Gerrit Register Email Private Key" \
+    --secret-string file://$SECRETS_DIRECTORY/registerEmailPrivateKey
+
+echo "Adding LDAP password..."
+
+aws secretsmanager create-secret --name ${KEY_PREFIX}_ldapPassword \
+    --description "LDAP password" \
+    --secret-string file://$SECRETS_DIRECTORY/ldapPassword
+
+echo "Adding SMTP password..."
+
+aws secretsmanager create-secret --name ${KEY_PREFIX}_smtpPassword \
+    --description "SMTP password" \
+    --secret-string file://$SECRETS_DIRECTORY/smtpPassword
diff --git a/gerrit/entrypoint.sh b/gerrit/entrypoint.sh
index ac397ed..0c50ef3 100755
--- a/gerrit/entrypoint.sh
+++ b/gerrit/entrypoint.sh
@@ -3,12 +3,14 @@
 /tmp/setup_gerrit.py
 git config -f /var/gerrit/etc/gerrit.config gerrit.canonicalWebUrl "${CANONICAL_WEB_URL:-http://$HOSTNAME}"
 git config -f /var/gerrit/etc/gerrit.config httpd.listenUrl "${HTTPD_LISTEN_URL:-http://*:8080/}"
+git config -f /var/gerrit/etc/gerrit.config container.slave "${CONTAINER_SLAVE:-false}"
 
-if [ ! -d /var/gerrit/git/All-Projects.git ] || [ "$1" == "init" ]
+if [ $CONTAINER_SLAVE ]
 then
-  rm -fr /var/gerrit/plugins/*
-  echo "Initializing Gerrit site ..."
-  java -jar /var/gerrit/bin/gerrit.war init --batch --no-auto-start --install-all-plugins -d /var/gerrit
+  rm -fr /var/gerrit/plugins/replication.jar
+  java -jar /var/gerrit/bin/gerrit.war reindex --index groups
+else
+  java -jar /var/gerrit/bin/gerrit.war init --no-auto-start --batch --install-all-plugins -d /var/gerrit
 fi
 
 echo "Running Gerrit ..."
diff --git a/gerrit/etc/replication.config.template b/gerrit/etc/replication.config.template
new file mode 100644
index 0000000..c03a807
--- /dev/null
+++ b/gerrit/etc/replication.config.template
@@ -0,0 +1,18 @@
+[replication]
+  lockErrorMaxRetries = 30
+  maxRetries = 30
+
+[gerrit]
+  autoReload = true
+  replicateOnStartup = true
+
+[remote "slave-1"]
+  url = {{ SLAVE_1_URL }}
+  adminUrl = {{ SLAVE_1_AMDIN_URL }}
+  mirror = true
+  push = +refs/*:refs/*
+  threads = 10
+  createMissingRepositories = true
+  replicateProjectDeletions = true
+  replicateHiddenProjects = true
+  timeout = 60
diff --git a/gerrit/replication.setup.template b/gerrit/replication.setup.template
new file mode 100644
index 0000000..e4b7372
--- /dev/null
+++ b/gerrit/replication.setup.template
@@ -0,0 +1,3 @@
+[remote-slave]
+  url = git://replication.internal:29500/${name}.git
+  adminUrl = ssh://gerrit@replication.internal:1022/var/gerrit/git/${name}.git
diff --git a/gerrit/setup_gerrit.py b/gerrit/setup_gerrit.py
index 0925804..c9282dd 100755
--- a/gerrit/setup_gerrit.py
+++ b/gerrit/setup_gerrit.py
@@ -53,6 +53,16 @@
             return base64.b64decode(get_secret_value_response['SecretBinary'])
 
 
+"""
+This script setup Gerrit configuration and its plugins when the container spins up.
+
+It reads from:
+ - AWS Secret Manager: Statically defined.
+ - gerrit.setup: Statically defined.
+ - environment variables: Dinamycally defined.
+
+"""
+
 secretIds = [
     "ssh_host_ecdsa_384_key",
     "ssh_host_ecdsa_384_key.pub",
@@ -76,6 +86,20 @@
     with open(GERRIT_CONFIG_DIRECTORY + secretId, 'w', encoding='utf-8') as f:
         f.write(get_secret(GERRIT_KEY_PREFIX + secretId))
 
+GERRIT_SSH_DIRECTORY = "/var/gerrit/.ssh"
+GERRIT_REPLICATION_SSH_KEYS = GERRIT_SSH_DIRECTORY + "/id_rsa"
+
+print("Installing Replication SSH Keys from Secret Manager in: " +
+      GERRIT_REPLICATION_SSH_KEYS)
+
+if not os.path.exists(GERRIT_SSH_DIRECTORY):
+    os.mkdir(GERRIT_SSH_DIRECTORY)
+    os.chmod(GERRIT_SSH_DIRECTORY, 0o700)
+
+with open(GERRIT_REPLICATION_SSH_KEYS, 'w', encoding='utf-8') as f:
+    f.write(get_secret(GERRIT_KEY_PREFIX + 'replication_user_id_rsa'))
+os.chmod(GERRIT_REPLICATION_SSH_KEYS, 0o400)
+
 file_loader = FileSystemLoader(GERRIT_CONFIG_DIRECTORY)
 env = Environment(loader=file_loader)
 
@@ -91,10 +115,10 @@
         SMTP_PASSWORD=get_secret(GERRIT_KEY_PREFIX + "smtpPassword"))
     )
 
+BASE_CONFIG_DIR = "/tmp"
 config = configparser.ConfigParser()
-config.read('/tmp/gerrit.setup')
-print("Setting Gerrit config in '" + GERRIT_CONFIG_DIRECTORY +
-      "gerrit.config'")
+config.read(BASE_CONFIG_DIR + '/gerrit.setup')
+print("Setting Gerrit config in '" + GERRIT_CONFIG_DIRECTORY + "gerrit.config'")
 template = env.get_template("gerrit.config.template")
 with open(GERRIT_CONFIG_DIRECTORY + "gerrit.config", 'w',
           encoding='utf-8') as f:
@@ -107,3 +131,15 @@
         SMTP_USER=config['smtp']["user"],
         SMTP_DOMAIN=config['smtp']["domain"])
     )
+
+containerSlave = os.getenv('CONTAINER_SLAVE')
+if (not containerSlave):
+    print("Setting Replication config in '" +
+          GERRIT_CONFIG_DIRECTORY + "replication.config'")
+    config.read(BASE_CONFIG_DIR + '/replication.setup')
+    template = env.get_template("replication.config.template")
+    with open(GERRIT_CONFIG_DIRECTORY + "replication.config", 'w', encoding='utf-8') as f:
+        f.write(template.render(
+                SLAVE_1_URL=config['remote-slave']['url'],
+                SLAVE_1_AMDIN_URL=config['remote-slave']['adminUrl']
+                ))
diff --git a/gerrit/ssh-config b/gerrit/ssh-config
new file mode 100644
index 0000000..5e2a4c1
--- /dev/null
+++ b/gerrit/ssh-config
@@ -0,0 +1,3 @@
+Host *
+    StrictHostKeyChecking no
+    IdentityFile /var/gerrit/.ssh/id_rsa
diff --git a/master-slave/Makefile b/master-slave/Makefile
new file mode 100644
index 0000000..4afbc7f
--- /dev/null
+++ b/master-slave/Makefile
@@ -0,0 +1,152 @@
+include setup.env
+
+CLUSTER_TEMPLATE:=cf-cluster.yml
+SERVICE_MASTER_TEMPLATE:=cf-service-master.yml
+SERVICE_SLAVE_TEMPLATE:=cf-service-slave.yml
+DNS_ROUTING_TEMPLATE:=cf-dns-route.yml
+AWS_FC_COMMAND=export AWS_PAGER=;aws cloudformation
+
+.PHONY: create-all delete-all \
+				cluster service-master dns-routing \
+				wait-for-cluster-creation wait-for-service-master-creation wait-for-dns-routing-creation \
+				wait-for-cluster-deletion wait-for-service-master-deletion wait-for-dns-routing-deletion \
+				gerrit-build gerrit-publish
+
+create-all: cluster wait-for-cluster-creation \
+						service-master service-slave \
+						wait-for-service-master-creation wait-for-service-slave-creation \
+						dns-routing wait-for-dns-routing-creation
+
+cluster:
+	$(AWS_FC_COMMAND) create-stack \
+		--stack-name $(CLUSTER_STACK_NAME) \
+		--capabilities CAPABILITY_IAM  \
+		--template-body file://`pwd`/$(CLUSTER_TEMPLATE) \
+		--region $(AWS_REGION)
+
+service-master:
+	$(AWS_FC_COMMAND) create-stack \
+		--stack-name $(SERVICE_MASTER_STACK_NAME) \
+		--capabilities CAPABILITY_IAM  \
+		--template-body file://`pwd`/$(SERVICE_MASTER_TEMPLATE) \
+		--region $(AWS_REGION) \
+		--parameters \
+		ParameterKey=ClusterStackName,ParameterValue=$(CLUSTER_STACK_NAME) \
+		ParameterKey=HostedZoneName,ParameterValue=$(HOSTED_ZONE_NAME) \
+		ParameterKey=Subdomain,ParameterValue=$(MASTER_SUBDOMAIN) \
+		ParameterKey=DockerRegistryUrl,ParameterValue=$(DOCKER_REGISTRY_URI) \
+		ParameterKey=CertificateArn,ParameterValue=$(SSL_CERTIFICATE_ARN) \
+		ParameterKey=SlaveServiceStackName,ParameterValue=$(SERVICE_SLAVE_STACK_NAME)
+
+service-slave:
+	$(AWS_FC_COMMAND) create-stack \
+		--stack-name $(SERVICE_SLAVE_STACK_NAME) \
+		--capabilities CAPABILITY_IAM  \
+		--template-body file://`pwd`/$(SERVICE_SLAVE_TEMPLATE) \
+		--region $(AWS_REGION) \
+		--parameters \
+		ParameterKey=ClusterStackName,ParameterValue=$(CLUSTER_STACK_NAME) \
+		ParameterKey=HostedZoneName,ParameterValue=$(HOSTED_ZONE_NAME) \
+		ParameterKey=Subdomain,ParameterValue=$(SLAVE_SUBDOMAIN) \
+		ParameterKey=DockerRegistryUrl,ParameterValue=$(DOCKER_REGISTRY_URI) \
+		ParameterKey=CertificateArn,ParameterValue=$(SSL_CERTIFICATE_ARN)
+
+dns-routing:
+	$(AWS_FC_COMMAND) create-stack \
+		--stack-name $(DNS_ROUTING_STACK_NAME) \
+		--capabilities CAPABILITY_IAM  \
+		--template-body file://`pwd`/$(DNS_ROUTING_TEMPLATE) \
+		--region $(AWS_REGION) \
+		--parameters \
+		ParameterKey=MasterServiceStackName,ParameterValue=$(SERVICE_MASTER_STACK_NAME) \
+		ParameterKey=SlaveServiceStackName,ParameterValue=$(SERVICE_SLAVE_STACK_NAME)
+
+wait-for-cluster-creation:
+	@echo "*** Wait for cluster stack '$(CLUSTER_STACK_NAME)' creation"
+	$(AWS_FC_COMMAND) wait stack-create-complete \
+	--stack-name $(CLUSTER_STACK_NAME) \
+	--region $(AWS_REGION)
+	@echo "*** Cluster stack '$(CLUSTER_STACK_NAME)' created"
+
+wait-for-service-master-creation:
+	@echo "*** Wait for service master stack '$(SERVICE_MASTER_STACK_NAME)' creation"
+	$(AWS_FC_COMMAND) wait stack-create-complete \
+	--stack-name $(SERVICE_MASTER_STACK_NAME) \
+	--region $(AWS_REGION)
+	@echo "*** Service stack '$(SERVICE_MASTER_STACK_NAME)' created"
+
+wait-for-service-slave-creation:
+	@echo "*** Wait for service slave stack '$(SERVICE_SLAVE_STACK_NAME)' creation"
+	$(AWS_FC_COMMAND) wait stack-create-complete \
+	--stack-name $(SERVICE_SLAVE_STACK_NAME) \
+	--region $(AWS_REGION)
+	@echo "*** Service stack '$(SERVICE_SLAVE_STACK_NAME)' created"
+
+wait-for-dns-routing-creation:
+	@echo "*** Wait for DNS routing stack '$(DNS_ROUTING_STACK_NAME)' creation"
+	$(AWS_FC_COMMAND) wait stack-create-complete \
+	--stack-name $(DNS_ROUTING_STACK_NAME) \
+	--region $(AWS_REGION)
+	@echo "*** DNS Routing stack '$(DNS_ROUTING_STACK_NAME)' created"
+
+wait-for-cluster-deletion:
+	@echo "*** Wait for cluster stack '$(CLUSTER_STACK_NAME)' deletion"
+	$(AWS_FC_COMMAND) wait stack-delete-complete \
+	--stack-name $(CLUSTER_STACK_NAME) \
+	--region $(AWS_REGION)
+	@echo "*** Cluster stack '$(CLUSTER_STACK_NAME)' deleted"
+
+wait-for-service-master-deletion:
+	@echo "*** Wait for service master stack '$(SERVICE_MASTER_STACK_NAME)' deletion"
+	$(AWS_FC_COMMAND) wait stack-delete-complete \
+	--stack-name $(SERVICE_MASTER_STACK_NAME) \
+	--region $(AWS_REGION)
+	@echo "*** Service stack master '$(SERVICE_MASTER_STACK_NAME)' deleted"
+
+wait-for-service-slave-deletion:
+	@echo "*** Wait for service master stack '$(SERVICE_SLAVE_STACK_NAME)' deletion"
+	$(AWS_FC_COMMAND) wait stack-delete-complete \
+	--stack-name $(SERVICE_SLAVE_STACK_NAME) \
+	--region $(AWS_REGION)
+	@echo "*** Service stack master '$(SERVICE_SLAVE_STACK_NAME)' deleted"
+
+wait-for-dns-routing-deletion:
+	@echo "*** Wait for DNS routing stack '$(DNS_ROUTING_STACK_NAME)' deletion"
+	$(AWS_FC_COMMAND) wait stack-delete-complete \
+	--stack-name $(DNS_ROUTING_STACK_NAME) \
+	--region $(AWS_REGION)
+	@echo "*** DNS routing stack '$(DNS_ROUTING_STACK_NAME)' deleted"
+
+delete-cluster:
+	$(AWS_FC_COMMAND) delete-stack \
+	--stack-name $(CLUSTER_STACK_NAME) \
+	--region $(AWS_REGION)
+
+delete-service-master:
+	$(AWS_FC_COMMAND) delete-stack \
+	--stack-name $(SERVICE_MASTER_STACK_NAME) \
+	--region $(AWS_REGION)
+
+delete-service-slave:
+	$(AWS_FC_COMMAND) delete-stack \
+	--stack-name $(SERVICE_SLAVE_STACK_NAME) \
+	--region $(AWS_REGION)
+
+delete-dns-routing:
+	$(AWS_FC_COMMAND) delete-stack \
+	--stack-name $(DNS_ROUTING_STACK_NAME) \
+	--region $(AWS_REGION)
+
+delete-all: delete-dns-routing wait-for-dns-routing-deletion \
+						delete-service-slave wait-for-service-slave-deletion \
+						delete-service-master wait-for-service-master-deletion \
+						delete-cluster wait-for-cluster-deletion
+
+gerrit-publish:
+	$(MAKE) -C ../gerrit gerrit-publish RECIPE=master-slave
+
+git-daemon-publish:
+	$(MAKE) -C git-daemon git-daemon-publish
+
+git-ssh-publish:
+	$(MAKE) -C git-ssh git-ssh-publish
diff --git a/master-slave/README.md b/master-slave/README.md
new file mode 100644
index 0000000..1b96f32
--- /dev/null
+++ b/master-slave/README.md
@@ -0,0 +1,240 @@
+# Gerrit Master-Slave
+
+This set of Templates provide all the components to deploy a single Gerrit master
+and a single Gerrit slave in ECS
+
+## Architecture
+
+Four templates are provided in this example:
+* `cf-cluster`: define the ECS cluster and the networking stack
+* `cf-service-master`: define the service stack running Gerrit master
+* `cf-service-slave`: define the service stack running Gerrit slave
+* `cf-dns-route`: define the DNS routing for the service
+
+### Networking
+
+* Single VPC:
+ * CIDR: 10.0.0.0/16
+* Single Availability Zone
+* 1 public Subnets:
+ * CIDR: 10.0.0.0/24
+* 1 public NLB exposing:
+ * Gerrit master HTTP on port 8080
+ * Gerrit master SSH on port 29418
+* 1 public NLB exposing:
+ * Gerrit slave HTTP on port 8081
+ * Gerrit slave SSH on port 39418
+ * SSH agent on port 1022
+ * Git daemon on port 9418
+* 1 Internet Gateway
+* 2 type A alias DNS entry, for Gerrit master and slave
+* A SSL certificate available in [AWS Certificate Manager](https://aws.amazon.com/certificate-manager/)
+
+### Data persistency
+
+* EBS volumes for:
+  * Indexes
+  * Caches
+  * Data
+  * Git repositories
+
+### Deployment type
+
+* Latest Gerrit version deployed using the official [Docker image](https://hub.docker.com/r/gerritcodereview/gerrit)
+* Application deployed in ECS on a single EC2 instance
+
+### Logging
+
+* Gerrit `error_log` is exported in a Log Group in CloudWatch
+* Other Gerrit logs still need to be exported
+
+### Monitoring
+
+* Standard CloudWatch monitoring metrics for each component
+
+## How to run it
+
+### Setup
+
+The `setup.env.template` is an example of setup file for the creation of the stacks.
+
+Before creating the stacks, create a `setup.env` in the `Makefile` directory and
+correctly set the value of the environment variables.
+
+This is the list of available parameters:
+
+* `DOCKER_REGISTRY_URI`: Mandatory. URI of the Docker registry. See the
+  [prerequisites](#prerequisites) section for more details.
+* `SSL_CERTIFICATE_ARN`: Mandatory. ARN of the SSL Certificate.
+* `CLUSTER_STACK_NAME`: Optional. Name of the cluster stack. `gerrit-cluster` by default.
+* `SERVICE_MASTER_STACK_NAME`: Optional. Name of the master service stack. `gerrit-service-master` by default.
+* `SERVICE_SLAVE_STACK_NAME`: Optional. Name of the slave service stack. `gerrit-service-slave` by default.
+* `DNS_ROUTING_STACK_NAME`: Optional. Name of the DNS routing stack. `gerrit-dns-routing` by default.
+* `HOSTED_ZONE_NAME`: Optional. Name of the hosted zone. `mycompany.com` by default.
+* `MASTER_SUBDOMAIN`: Optional. Name of the master sub domain. `gerrit-master-demo` by default.
+* `SLAVE_SUBDOMAIN`: Optional. Name of the slave sub domain. `gerrit-slave-demo` by default.
+
+### Prerequisites
+
+As a prerequisite to run this stack, you will need:
+* a registered and correctly configured domain in
+[Route53](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/getting-started.html)
+* to [publish the Docker image](#publish-custom-gerrit-docker-image) with your
+Gerrit configuration in AWS ECR
+* to [publish the SSH Agent Docker image](#publish-ssh-agent) in AWS ECR
+* to [publish the Git Daemon Docker image](#publish-git-daemon) in AWS ECR
+* to [add Gerrit secrets](#add-gerrit-secrets-in-aws-secret-manager) in AWS Secret
+Manager
+* an SSL Certificate in AWS Certificate Manager (you can find more information on
+  how to create and handle certificates in AWS [here](https://aws.amazon.com/certificate-manager/getting-started/)
+
+### Add Gerrit Secrets in AWS Secret Manager
+
+[AWS Secret Manager](https://aws.amazon.com/secrets-manager/) is a secure way of
+storing and managing secrets of any type.
+
+The secrets you will have to add are the Gerrit SSH keys and the Register Email
+Private Key set in `secure.config`.
+
+#### SSH Keys
+
+The SSH keys you will need to add are the one usually created and used by Gerrit:
+* ssh_host_ecdsa_384_key
+* ssh_host_ecdsa_384_key.pub
+* ssh_host_ecdsa_521_key
+* ssh_host_ecdsa_521_key.pub
+* ssh_host_ecdsa_key
+* ssh_host_ecdsa_key.pub
+* ssh_host_ed25519_key
+* ssh_host_ed25519_key.pub
+* ssh_host_rsa_key
+* ssh_host_rsa_key.pub
+
+Plus a key used by the replication plugin:
+* replication_user_id_rsa
+* replication_user_id_rsa.pub
+
+You will have to create the keys and place them in a directory.
+
+#### Register Email Private Key
+
+You will need to create a secret and put it in a file called `registerEmailPrivateKey`
+in the same directory of the SSH keys.
+
+#### LDAP Password
+
+You will need to put the admin LDAP password in a file called `ldapPassword`
+in the same directory of the SSH keys.
+
+#### SMTP Password
+
+You will need to put the SMTP password in a file called `smtpPassword`
+in the same directory of the SSH keys.
+
+#### Import into AWS Secret Manager
+
+You can now run the [script](../gerrit/add_secrets_aws_secrets_manager.sh) to
+upload them to AWS Secret Manager:
+`add_secrets_aws_secrets_manager.sh /path/to/your/keys/directory`
+
+### Publish custom Gerrit Docker image
+
+* Create the repository in the Docker registry:
+  `aws ecr create-repository --repository-name aws-gerrit/gerrit`
+* Set the Docker registry URI in `DOCKER_REGISTRY_URI`
+* Create a `gerrit.setup` and set the correct parameters
+ * An example of the possible setting are in `gerrit.setup.template`
+ * The structure and parameters of `gerrit.setup` are the same as a normal `gerrit.config`
+ * Refer to the [Gerrit Configuration Documentation](https://gerrit-review.googlesource.com/Documentation/config-gerrit.html)
+   for the meaning of the parameters
+* Add the plugins you want to install in `./gerrit/plugins`
+* Publish the image: `make gerrit-publish`
+
+### Publish SSH Agent
+
+* Create the repository in the Docker registry:
+  `aws ecr create-repository --repository-name aws-gerrit/git-ssh`
+* Publish the image: `make git-ssh-publish`
+
+### Publish Git Daemon
+
+* Create the repository in the Docker registry:
+  `aws ecr create-repository --repository-name aws-gerrit/git-daemon`
+* Publish the image: `make git-daemon-publish`
+
+### Getting Started
+
+* Create a key pair to access the EC2 instances in the cluster:
+
+```
+aws ec2 create-key-pair --key-name gerrit-cluster-keys \
+  --query 'KeyMaterial' --output text > gerrit-cluster.pem
+```
+
+*NOTE: the EC2 key pair are useful when you need to connect to the EC2 instances
+for troubleshooting purposes. Store them in a `pem` file to use when ssh-ing into your
+instances as follow: `ssh -i yourKeyPairs.pem <ec2_instance_ip>`*
+
+* Create the cluster, services and DNS routing stacks:
+
+```
+make create-all
+```
+
+The slave will start with 5 min delay to allow the replication from master of `All-Users`
+and `All-Projects` to happen.
+You can now check in the slave logs to see when the slave is up and running.
+
+### Cleaning up
+
+```
+make delete-all
+```
+
+### Access your Gerrit instances
+
+Get the URL of your Gerrit master instance this way:
+
+```
+aws cloudformation describe-stacks \
+  --stack-name <SERVICE_MASTER_STACK_NAME> \
+  | grep -A1 '"OutputKey": "CanonicalWebUrl"' \
+  | grep OutputValue \
+  | cut -d'"' -f 4
+```
+
+Similarly for the slave:
+```
+aws cloudformation describe-stacks \
+  --stack-name <SERVICE_SLAVE_STACK_NAME> \
+  | grep -A1 '"OutputKey": "CanonicalWebUrl"' \
+  | grep OutputValue \
+  | cut -d'"' -f 4
+```
+
+Gerrit master instance ports:
+* HTTP `8080`
+* SSH `29418`
+
+Gerrit slave instance ports:
+* HTTP `9080`
+* SSH `39418`
+
+# External services
+
+This is a list of external services that you might need to setup your stack and some suggestions
+on how to easily create them.
+
+## SMTP Server
+
+If you need to setup a SMTP service Amazon Simple Email Service can be used.
+Details how setup Amazon SES can be found [here](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-set-up.html).
+
+To correctly setup email notifications Gerrit requires ssl protocol on default port 465 to
+be enabled on SMTP Server. It is possible to setup Gerrit to talk to standard SMTP port 25
+but by default all EC2 instances are blocking it. To enable port 25 please follow [this](https://aws.amazon.com/premiumsupport/knowledge-center/ec2-port-25-throttle/) link.
+
+## LDAP Server
+
+If you need a testing LDAP server you can find details on how to easily
+create one in the [LDAP folder](../ldap/README.md).
diff --git a/master-slave/cf-cluster.yml b/master-slave/cf-cluster.yml
new file mode 100644
index 0000000..4f914e5
--- /dev/null
+++ b/master-slave/cf-cluster.yml
@@ -0,0 +1,287 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Description: A stack for deploying containerized applications onto a cluster of EC2
+             hosts using Elastic Container Service. This stack runs containers on
+             hosts that are in a public VPC subnet.
+Parameters:
+  DesiredCapacity:
+    Type: Number
+    Default: '1'
+    Description: Number of EC2 instances to launch in your ECS cluster.
+  MaxSize:
+    Type: Number
+    Default: '6'
+    Description: Maximum number of EC2 instances that can be launched in your ECS cluster.
+  ECSAMI:
+    Description: AMI ID
+    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
+    Default: /aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id
+  InstanceType:
+    Description: EC2 instance type
+    Type: String
+    Default: c4.xlarge
+    AllowedValues: [t2.micro, t2.small, t2.medium, t2.large, m3.medium, m3.large,
+      m3.xlarge, m3.2xlarge, m4.large, m4.xlarge, m4.2xlarge, m4.4xlarge, m4.10xlarge,
+      c4.large, c4.xlarge, c4.2xlarge, c4.4xlarge, c4.8xlarge, c3.large, c3.xlarge,
+      c3.2xlarge, c3.4xlarge, c3.8xlarge, r3.large, r3.xlarge, r3.2xlarge, r3.4xlarge,
+      r3.8xlarge, i2.xlarge, i2.2xlarge, i2.4xlarge, i2.8xlarge]
+    ConstraintDescription: Please choose a valid instance type.
+  ECSKeyName:
+    Type: String
+    Default: gerrit-cluster-keys
+    Description: EC2 key pair name the cluter's instances
+  EnvironmentName:
+      Description: An environment name used to build the log stream names
+      Type: String
+      Default: test
+Mappings:
+  # Hard values for the subnet masks. These masks define
+  # the range of internal IP addresses that can be assigned.
+  # The VPC can have all IP's from 10.0.0.0 to 10.0.255.255
+  # There is the subnet which cover the ranges:
+  #
+  # 10.0.0.0 - 10.0.0.255
+  SubnetConfig:
+    VPC:
+      CIDR: '10.0.0.0/16'
+    PublicOne:
+      CIDR: '10.0.0.0/24'
+Resources:
+  VPC:
+    Type: AWS::EC2::VPC
+    Properties:
+      EnableDnsSupport: true
+      EnableDnsHostnames: true
+      CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR']
+
+  # Public subnets, where containers can have public IP addresses
+  PublicSubnetOne:
+    Type: AWS::EC2::Subnet
+    Properties:
+      AvailabilityZone:
+         Fn::Select:
+         - 0
+         - Fn::GetAZs: {Ref: 'AWS::Region'}
+      VpcId: !Ref 'VPC'
+      CidrBlock: !FindInMap ['SubnetConfig', 'PublicOne', 'CIDR']
+      MapPublicIpOnLaunch: true
+
+  # Setup networking resources for the public subnets. Containers
+  # in the public subnets have public IP addresses and the routing table
+  # sends network traffic via the internet gateway.
+  InternetGateway:
+    Type: AWS::EC2::InternetGateway
+  GatewayAttachement:
+    Type: AWS::EC2::VPCGatewayAttachment
+    Properties:
+      VpcId: !Ref 'VPC'
+      InternetGatewayId: !Ref 'InternetGateway'
+  PublicRouteTable:
+    Type: AWS::EC2::RouteTable
+    Properties:
+      VpcId: !Ref 'VPC'
+  PublicRoute:
+    Type: AWS::EC2::Route
+    DependsOn: GatewayAttachement
+    Properties:
+      RouteTableId: !Ref 'PublicRouteTable'
+      DestinationCidrBlock: '0.0.0.0/0'
+      GatewayId: !Ref 'InternetGateway'
+  PublicSubnetOneRouteTableAssociation:
+    Type: AWS::EC2::SubnetRouteTableAssociation
+    Properties:
+      SubnetId: !Ref PublicSubnetOne
+      RouteTableId: !Ref PublicRouteTable
+
+  # ECS Resources
+  ECSCluster:
+    Type: AWS::ECS::Cluster
+
+  EcsHostSecurityGroup:
+    Type: AWS::EC2::SecurityGroup
+    Properties:
+      GroupDescription: Access to the ECS hosts that run containers
+      VpcId: !Ref 'VPC'
+      SecurityGroupIngress:
+          # Allow access to NLB from anywhere on the internet
+          - CidrIp: 0.0.0.0/0
+            IpProtocol: -1
+
+  CloudWatchLogsGroup:
+      Type: AWS::Logs::LogGroup
+      Properties:
+          LogGroupName: !Ref AWS::StackName
+          RetentionInDays: 14
+
+  # Autoscaling group. This launches the actual EC2 instances that will register
+  # themselves as members of the cluster, and run the docker containers.
+  ECSAutoScalingGroup:
+    Type: AWS::AutoScaling::AutoScalingGroup
+    Properties:
+      VPCZoneIdentifier:
+        - !Ref PublicSubnetOne
+      LaunchConfigurationName: !Ref 'ContainerInstances'
+      MinSize: '1'
+      MaxSize: !Ref 'MaxSize'
+      DesiredCapacity: !Ref 'DesiredCapacity'
+    CreationPolicy:
+      ResourceSignal:
+        Timeout: PT15M
+    UpdatePolicy:
+      AutoScalingReplacingUpdate:
+        WillReplace: 'true'
+  ContainerInstances:
+    Type: AWS::AutoScaling::LaunchConfiguration
+    Properties:
+      ImageId: !Ref 'ECSAMI'
+      SecurityGroups: [!Ref 'EcsHostSecurityGroup']
+      InstanceType: !Ref 'InstanceType'
+      IamInstanceProfile: !Ref 'EC2InstanceProfile'
+      KeyName: !Ref ECSKeyName
+      UserData:
+        Fn::Base64: !Sub |
+          #!/bin/bash -xe
+          echo ECS_CLUSTER=${ECSCluster} >> /etc/ecs/ecs.config
+          # Make sure latest version of the helper scripts are installed as per recommendation:
+          # https://github.com/awsdocs/aws-cloudformation-user-guide/blob/master/doc_source/cfn-helper-scripts-reference.md#using-the-latest-version
+          yum install -y aws-cfn-bootstrap
+          # Get the CloudWatch Logs agent
+          echo -e "
+            {\"logs\":
+              {\"logs_collected\":
+                {\"files\":
+                  {\"collect_list\":
+                    [
+                      {\"file_path\": \"/var/lib/docker/volumes/gerrit-logs-slave/_data/httpd_log\",
+                      \"log_group_name\": \"${AWS::StackName}\",
+                      \"log_stream_name\": \"${EnvironmentName}/{instance_id}/slave/httpd_log\",
+                      \"timezone\": \"UTC\"
+                      },
+                      {\"file_path\": \"/var/lib/docker/volumes/gerrit-logs-slave/_data/sshd_log\",
+                      \"log_group_name\": \"${AWS::StackName}\",
+                      \"log_stream_name\": \"${EnvironmentName}/{instance_id}/slave/sshd_log\",
+                      \"timezone\": \"UTC\"
+                      },
+                      {\"file_path\": \"/var/lib/docker/volumes/gerrit-logs-slave/_data/gc_log\",
+                      \"log_group_name\": \"${AWS::StackName}\",
+                      \"log_stream_name\": \"${EnvironmentName}/{instance_id}/slave/gc_log\",
+                      \"timezone\": \"UTC\"
+                      },
+                      {\"file_path\": \"/var/lib/docker/volumes/gerrit-logs-slave/_data/audit_log\",
+                      \"log_group_name\": \"${AWS::StackName}\",
+                      \"log_stream_name\": \"${EnvironmentName}/{instance_id}/slave/audit_log\",
+                      \"timezone\": \"UTC\"
+                      },
+                      {\"file_path\": \"/var/lib/docker/volumes/gerrit-logs-master/_data/replication_log\",
+                      \"log_group_name\": \"${AWS::StackName}\",
+                      \"log_stream_name\": \"${EnvironmentName}/{instance_id}/master/replication_log\",
+                      \"timezone\": \"UTC\"
+                      },
+                      {\"file_path\": \"/var/lib/docker/volumes/gerrit-logs-master/_data/httpd_log\",
+                      \"log_group_name\": \"${AWS::StackName}\",
+                      \"log_stream_name\": \"${EnvironmentName}/{instance_id}/master/httpd_log\",
+                      \"timezone\": \"UTC\"
+                      },
+                      {\"file_path\": \"/var/lib/docker/volumes/gerrit-logs-master/_data/sshd_log\",
+                      \"log_group_name\": \"${AWS::StackName}\",
+                      \"log_stream_name\": \"${EnvironmentName}/{instance_id}/master/sshd_log\",
+                      \"timezone\": \"UTC\"
+                      },
+                      {\"file_path\": \"/var/lib/docker/volumes/gerrit-logs-master/_data/gc_log\",
+                      \"log_group_name\": \"${AWS::StackName}\",
+                      \"log_stream_name\": \"${EnvironmentName}/{instance_id}/master/gc_log\",
+                      \"timezone\": \"UTC\"
+                      },
+                      {\"file_path\": \"/var/lib/docker/volumes/gerrit-logs-master/_data/audit_log\",
+                      \"log_group_name\": \"${AWS::StackName}\",
+                      \"log_stream_name\": \"${EnvironmentName}/{instance_id}/master/audit_log\",
+                      \"timezone\": \"UTC\"
+                      }
+                    ]
+                  }
+                }
+              }
+            }" >> /home/ec2-user/gerritlogsaccess.json
+          # Install the CloudWatch Logs agent
+          yum install -y wget
+          wget https://s3.amazonaws.com/amazoncloudwatch-agent/centos/amd64/latest/amazon-cloudwatch-agent.rpm
+          rpm -U ./amazon-cloudwatch-agent.rpm
+          /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c file:/home/ec2-user/gerritlogsaccess.json -s
+          # Signal to CloudFormation aws-cfn-bootstrap has been correctly updated
+          /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource ECSAutoScalingGroup --region ${AWS::Region}
+  AutoscalingRole:
+    Type: AWS::IAM::Role
+    Properties:
+      AssumeRolePolicyDocument:
+        Statement:
+        - Effect: Allow
+          Principal:
+            Service: [application-autoscaling.amazonaws.com]
+          Action: ['sts:AssumeRole']
+      Path: /
+      Policies:
+      - PolicyName: service-autoscaling
+        PolicyDocument:
+          Statement:
+          - Effect: Allow
+            Action:
+              - 'application-autoscaling:*'
+              - 'cloudwatch:DescribeAlarms'
+              - 'cloudwatch:PutMetricAlarm'
+              - 'ecs:DescribeServices'
+              - 'ecs:UpdateService'
+            Resource: '*'
+  EC2InstanceProfile:
+    Type: AWS::IAM::InstanceProfile
+    Properties:
+      Path: /
+      Roles: [!Ref 'EC2Role']
+
+  # Role for the EC2 hosts. This allows the ECS agent on the EC2 hosts
+  # to communciate with the ECS control plane, as well as download the docker
+  # images from ECR to run on your host.
+  EC2Role:
+    Type: AWS::IAM::Role
+    Properties:
+      AssumeRolePolicyDocument:
+        Statement:
+        - Effect: Allow
+          Principal:
+            Service: [ec2.amazonaws.com]
+          Action: ['sts:AssumeRole']
+      Path: /
+      Policies:
+      - PolicyName: ecs-service
+        PolicyDocument:
+          Statement:
+          - Effect: Allow
+            Action:
+              - 'ecs:CreateCluster'
+              - 'ecs:DeregisterContainerInstance'
+              - 'ecs:DiscoverPollEndpoint'
+              - 'ecs:Poll'
+              - 'ecs:RegisterContainerInstance'
+              - 'ecs:StartTelemetrySession'
+              - 'ecs:Submit*'
+              - 'logs:CreateLogStream'
+              - 'logs:PutLogEvents'
+              - 'ecr:GetAuthorizationToken'
+              - 'ecr:BatchGetImage'
+              - 'ecr:GetDownloadUrlForLayer'
+            Resource: '*'
+
+Outputs:
+  ClusterName:
+    Description: The name of the ECS cluster
+    Value: !Ref 'ECSCluster'
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ClusterName' ] ]
+  VPCId:
+    Description: The ID of the VPC that this stack is deployed in
+    Value: !Ref 'VPC'
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'VPCId' ] ]
+  PublicSubnetOne:
+    Description: Public subnet one
+    Value: !Ref 'PublicSubnetOne'
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicSubnetOne' ] ]
diff --git a/master-slave/cf-dns-route.yml b/master-slave/cf-dns-route.yml
new file mode 100644
index 0000000..9f6234b
--- /dev/null
+++ b/master-slave/cf-dns-route.yml
@@ -0,0 +1,36 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Description: A stack for the Gerrit service Route53 routing.
+Parameters:
+  MasterServiceStackName:
+      Description: Stack name of the ECS Master Gerrit service
+      Type: String
+      Default: gerrit-service-master
+  SlaveServiceStackName:
+      Description: Stack name of the ECS Slave Gerrit service
+      Type: String
+      Default: gerrit-service-slave
+
+Resources:
+  MasterDnsRecord:
+      Type: AWS::Route53::RecordSet
+      Properties:
+        Name:
+          !Join
+            - '.'
+            - - Fn::ImportValue: !Join [':', [!Ref 'MasterServiceStackName', 'Subdomain']]
+              - Fn::ImportValue: !Join [':', [!Ref 'MasterServiceStackName', 'HostedZoneName']]
+        HostedZoneName:
+          !Join
+            - ''
+            - - Fn::ImportValue: !Join [':', [!Ref 'MasterServiceStackName', 'HostedZoneName']]
+              - '.'
+        Comment: DNS name for Gerrit Master.
+        Type: A
+        AliasTarget:
+          DNSName:
+            Fn::ImportValue:
+              !Join [':', [!Ref 'MasterServiceStackName', 'PublicLoadBalancerDNSName']]
+          HostedZoneId:
+            Fn::ImportValue:
+              !Join [':', [!Ref 'MasterServiceStackName', 'CanonicalHostedZoneID']]
+          EvaluateTargetHealth: False
diff --git a/master-slave/cf-service-master.yml b/master-slave/cf-service-master.yml
new file mode 100644
index 0000000..fd46039
--- /dev/null
+++ b/master-slave/cf-service-master.yml
@@ -0,0 +1,312 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Description: Deploy a service into an ECS cluster behind a public load balancer.
+Parameters:
+  GerritServiceName:
+    Type: String
+    Default: gerrit-master
+  SlaveServiceStackName:
+    Type: String
+    Default: gerrit-slave
+  ClusterStackName:
+      Description: Stack name of the ECS cluster to deply the serivces
+      Type: String
+      Default: gerrit-cluster
+  EnvironmentName:
+      Description: An environment name used to build the log stream names
+      Type: String
+      Default: test
+  DockerImage:
+        Description: Gerrit official Docker image
+        Type: String
+        Default: aws-gerrit/gerrit:latest
+  GerritLBDockerImage:
+        Description: Load Balancer official Docker image
+        Type: String
+        Default: haproxy:1.7
+  DockerRegistryUrl:
+        Description: Docker registry URL
+        Type: String
+  DesiredCount:
+        Description: How many instances of this task should we run across our cluster?
+        Type: Number
+        Default: 1
+  HTTPPort:
+        Description: Gerrit HTTP port
+        Type: Number
+        Default: 8080
+  HTTPSPort:
+        Description: Gerrit HTTPS port
+        Type: Number
+        Default: 443
+  SSHPort:
+        Description: Gerrit SSH port
+        Type: Number
+        Default: 29418
+  CertificateArn:
+        Description: SSL Certificates ARN
+        Type: String
+  HostedZoneName:
+        Description: The route53 HostedZoneName.
+        Type: String
+  Subdomain:
+        Description: The subdomain of the Gerrit cluster
+        Type: String
+        Default: gerrit-master-demo
+  GerritGitVolume:
+      Description: Gerrit git volume name
+      Type: String
+      Default: gerrit-git-master
+  GerritDataVolume:
+      Description: Gerrit data volume name
+      Type: String
+      Default: gerrit-data-master
+  GerritIndexVolume:
+      Description: Gerrit index volume name
+      Type: String
+      Default: gerrit-index-master
+  GerritCacheVolume:
+      Description: Gerrit cache volume name
+      Type: String
+      Default: gerrit-cache-master
+  GerritDbVolume:
+      Description: Gerrit db volume name
+      Type: String
+      Default: gerrit-db-master
+  GerritLogsVolume:
+      Description: Gerrit logs volume name
+      Type: String
+      Default: gerrit-logs-master
+
+Resources:
+    Service:
+        Type: AWS::ECS::Service
+        DependsOn:
+          - HTTPListener
+          - SSHListener
+        Properties:
+            Cluster:
+              Fn::ImportValue:
+                  !Join [':', [!Ref 'ClusterStackName', 'ClusterName']]
+            DesiredCount: !Ref DesiredCount
+            TaskDefinition: !Ref TaskDefinition
+            LoadBalancers:
+                - ContainerName: !Ref GerritServiceName
+                  ContainerPort: !Ref HTTPPort
+                  TargetGroupArn: !Ref HTTPTargetGroup
+                - ContainerName: !Ref GerritServiceName
+                  ContainerPort: !Ref SSHPort
+                  TargetGroupArn: !Ref SSHTargetGroup
+
+    TaskDefinition:
+        Type: AWS::ECS::TaskDefinition
+        Properties:
+            Family: !Join ['', [!Ref GerritServiceName, TaskDefinition]]
+            TaskRoleArn: !Ref ECSTaskExecutionRole
+            ExecutionRoleArn: !Ref ECSTaskExecutionRole
+            NetworkMode: bridge
+            ContainerDefinitions:
+                - Name: !Ref GerritServiceName
+                  Essential: true
+                  Image: !Sub '${DockerRegistryUrl}/${DockerImage}'
+                  Environment:
+                    - Name: CANONICAL_WEB_URL
+                      Value: !Sub 'https://${Subdomain}.${HostedZoneName}'
+                    - Name: HTTPD_LISTEN_URL
+                      Value: !Sub 'proxy-https://*:${HTTPPort}/'
+                    - Name: AWS_REGION
+                      Value: !Ref AWS::Region
+                  MountPoints:
+                    - SourceVolume: !Ref GerritGitVolume
+                      ContainerPath: /var/gerrit/git
+                    - SourceVolume: !Ref GerritDataVolume
+                      ContainerPath: /var/gerrit/data
+                    - SourceVolume: !Ref GerritIndexVolume
+                      ContainerPath: /var/gerrit/index
+                    - SourceVolume: !Ref GerritCacheVolume
+                      ContainerPath: /var/gerrit/cache
+                    - SourceVolume: !Ref GerritDbVolume
+                      ContainerPath: /var/gerrit/db
+                    - SourceVolume: !Ref GerritLogsVolume
+                      ContainerPath: /var/gerrit/logs
+                  Cpu: 1024
+                  Memory: 2048
+                  PortMappings:
+                    - ContainerPort: !Ref HTTPPort
+                      HostPort: !Ref HTTPPort
+                      Protocol: tcp
+                    - ContainerPort: !Ref SSHPort
+                      HostPort: !Ref SSHPort
+                      Protocol: tcp
+                  LogConfiguration:
+                    LogDriver: awslogs
+                    Options:
+                        awslogs-group: !Ref ClusterStackName
+                        awslogs-region: !Ref AWS::Region
+                        awslogs-stream-prefix: !Ref EnvironmentName
+            Volumes:
+              - Name: !Ref 'GerritDbVolume'
+                DockerVolumeConfiguration:
+                  Scope: shared
+                  Autoprovision: true
+                  Driver: local
+                  Labels:
+                    gerrit-db: !Join ['-', [!Ref EnvironmentName, !Ref GerritDbVolume]]
+              - Name: !Ref 'GerritGitVolume'
+                DockerVolumeConfiguration:
+                  Scope: shared
+                  Autoprovision: true
+                  Driver: local
+                  Labels:
+                    gerrit-git: !Join ['-', [!Ref EnvironmentName, !Ref GerritGitVolume]]
+              - Name: !Ref 'GerritDataVolume'
+                DockerVolumeConfiguration:
+                  Scope: shared
+                  Autoprovision: true
+                  Driver: local
+                  Labels:
+                    gerrit-data: !Join ['-', [!Ref EnvironmentName, !Ref GerritDataVolume]]
+              - Name: !Ref 'GerritCacheVolume'
+                DockerVolumeConfiguration:
+                  Scope: shared
+                  Autoprovision: true
+                  Driver: local
+                  Labels:
+                    gerrit-cache: !Join ['-', [!Ref EnvironmentName, !Ref GerritCacheVolume]]
+              - Name: !Ref 'GerritIndexVolume'
+                DockerVolumeConfiguration:
+                  Scope: shared
+                  Autoprovision: true
+                  Driver: local
+                  Labels:
+                    gerrit-index: !Join ['-', [!Ref EnvironmentName, !Ref GerritIndexVolume]]
+              - Name: !Ref 'GerritLogsVolume'
+                DockerVolumeConfiguration:
+                  Scope: shared
+                  Autoprovision: true
+                  Driver: local
+                  Labels:
+                    gerrit-logs: !Join ['-', [!Ref EnvironmentName, !Ref GerritLogsVolume]]
+
+
+    LoadBalancer:
+        Type: AWS::ElasticLoadBalancingV2::LoadBalancer
+        Properties:
+            Type: network
+            Scheme: internet-facing
+            Subnets:
+              - Fn::ImportValue:
+                  !Join [':', [!Ref 'ClusterStackName', 'PublicSubnetOne']]
+            Tags:
+                - Key: Name
+                  Value: !Join ['-', [!Ref 'EnvironmentName', !Ref 'GerritServiceName', 'nlb']]
+
+    HTTPTargetGroup:
+        Type: AWS::ElasticLoadBalancingV2::TargetGroup
+        DependsOn: LoadBalancer
+        Properties:
+            VpcId:
+              Fn::ImportValue:
+                  !Join [':', [!Ref 'ClusterStackName', 'VPCId']]
+            Port: !Ref HTTPPort
+            Protocol: TCP
+
+    HTTPListener:
+        Type: AWS::ElasticLoadBalancingV2::Listener
+        DependsOn: LoadBalancer
+        Properties:
+            Certificates:
+              - CertificateArn: !Ref CertificateArn
+            DefaultActions:
+            - Type: forward
+              TargetGroupArn: !Ref HTTPTargetGroup
+            LoadBalancerArn: !Ref LoadBalancer
+            Port: !Ref HTTPSPort
+            Protocol: TLS
+
+    SSHTargetGroup:
+        Type: AWS::ElasticLoadBalancingV2::TargetGroup
+        DependsOn: LoadBalancer
+        Properties:
+            VpcId:
+              Fn::ImportValue:
+                  !Join [':', [!Ref 'ClusterStackName', 'VPCId']]
+            Port: !Ref SSHPort
+            Protocol: TCP
+
+    SSHListener:
+        Type: AWS::ElasticLoadBalancingV2::Listener
+        DependsOn: LoadBalancer
+        Properties:
+            DefaultActions:
+            - Type: forward
+              TargetGroupArn: !Ref SSHTargetGroup
+            LoadBalancerArn: !Ref LoadBalancer
+            Port: !Ref SSHPort
+            Protocol: TCP
+
+    # This is a role which is used by the ECS tasks themselves.
+    ECSTaskExecutionRole:
+      Type: AWS::IAM::Role
+      Properties:
+        AssumeRolePolicyDocument:
+          Statement:
+          - Effect: Allow
+            Principal:
+              Service: [ecs-tasks.amazonaws.com]
+            Action: ['sts:AssumeRole']
+        Path: /
+        Policies:
+          - PolicyName: AmazonECSTaskExecutionRolePolicy
+            PolicyDocument:
+              Statement:
+              - Effect: Allow
+                Action:
+                  # Allow the ECS Tasks to download images from ECR
+                  - 'ecr:GetAuthorizationToken'
+                  - 'ecr:BatchCheckLayerAvailability'
+                  - 'ecr:GetDownloadUrlForLayer'
+                  - 'ecr:BatchGetImage'
+                  # Allow the ECS tasks to upload logs to CloudWatch
+                  - 'logs:CreateLogStream'
+                  - 'logs:PutLogEvents'
+                Resource: '*'
+          - PolicyName: AmazonECSTaskSecretManagerRolePolicy
+            PolicyDocument:
+              Statement:
+              - Effect: Allow
+                Action:
+                  # Allow the ECS Tasks to get SSH Keys
+                  - 'secretsmanager:GetSecretValue'
+                  - 'kms:Decrypt'
+                Resource: '*'
+Outputs:
+  PublicLoadBalancerDNSName:
+    Description: The DNS name of the external load balancer
+    Value: !GetAtt 'LoadBalancer.DNSName'
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicLoadBalancerDNSName' ] ]
+  CanonicalHostedZoneID:
+    Description: Canonical Hosted Zone ID
+    Value: !GetAtt 'LoadBalancer.CanonicalHostedZoneID'
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'CanonicalHostedZoneID' ] ]
+  PublicLoadBalancerUrl:
+    Description: The url of the external load balancer
+    Value: !Join ['', ['http://', !GetAtt 'LoadBalancer.DNSName']]
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicLoadBalancerUrl' ] ]
+  HostedZoneName:
+    Description: Route53 Hosted Zone name
+    Value: !Ref HostedZoneName
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'HostedZoneName' ] ]
+  Subdomain:
+    Description: Service DNS subdomain
+    Value: !Ref Subdomain
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'Subdomain' ] ]
+  CanonicalWebUrl:
+    Description: Canonical Web URL
+    Value: !Sub 'https://${Subdomain}.${HostedZoneName}'
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'CanonicalWebUrl' ] ]
diff --git a/master-slave/cf-service-slave.yml b/master-slave/cf-service-slave.yml
new file mode 100644
index 0000000..5284a75
--- /dev/null
+++ b/master-slave/cf-service-slave.yml
@@ -0,0 +1,462 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Description: Deploy a service into an ECS cluster behind a public load balancer.
+Parameters:
+  GerritServiceName:
+      Type: String
+      Default: gerrit-slave
+  GitDaemonServiceName:
+      Type: String
+      Default: gerrit-git-daemon
+  GitSSHServiceName:
+      Type: String
+      Default: gerrit-git-ssh
+  ClusterStackName:
+      Description: Stack name of the ECS cluster to deply the serivces
+      Type: String
+      Default: gerrit-cluster
+  EnvironmentName:
+      Description: An environment name used to build the log stream names
+      Type: String
+      Default: test
+  GerritDockerImage:
+      Description: Gerrit official Docker image
+      Type: String
+      Default: aws-gerrit/gerrit:latest
+  GitDaemonDockerImage:
+      Description: Git daemon Docker image
+      Type: String
+      Default: aws-gerrit/git-daemon:latest
+  GitSSHDockerImage:
+      Description: Git SSH Docker image
+      Type: String
+      Default: aws-gerrit/git-ssh:latest
+  DockerRegistryUrl:
+      Description: Docker registry URL
+      Type: String
+  DesiredCount:
+      Description: How many instances of this task should we run across our cluster?
+      Type: Number
+      Default: 1
+  HTTPHostPort:
+      Description: Gerrit Host HTTP port
+      Type: Number
+      Default: 9080
+  HTTPContainePort:
+      Description: Gerrit Container HTTP port
+      Type: Number
+      Default: 8080
+  HTTPSPort:
+      Description: Gerrit HTTPS port
+      Type: Number
+      Default: 443
+  SSHHostPort:
+      Description: Gerrit SSH port
+      Type: Number
+      Default: 39418
+  SSHContainerPort:
+      Description: Gerrit SSH port
+      Type: Number
+      Default: 29418
+  GitPort:
+      Description: Git daemon port
+      Type: Number
+      Default: 9418
+  GitSSHPort:
+      Description: Git ssh port
+      Type: Number
+      Default: 1022
+  GitSSHContainerPort:
+      Description: Git ssh port
+      Type: Number
+      Default: 22
+  CertificateArn:
+      Description: SSL Certificates ARN
+      Type: String
+  HostedZoneName:
+      Description: The route53 HostedZoneName.
+      Type: String
+  Subdomain:
+      Description: The subdomain of the Gerrit cluster
+      Type: String
+      Default: gerrit-slave-demo
+  GerritGitVolume:
+      Description: Gerrit git volume name
+      Type: String
+      Default: gerrit-git-slave
+  GerritDataVolume:
+      Description: Gerrit data volume name
+      Type: String
+      Default: gerrit-data-slave
+  GerritCacheVolume:
+      Description: Gerrit cache volume name
+      Type: String
+      Default: gerrit-cache-slave
+  GerritDbVolume:
+      Description: Gerrit db volume name
+      Type: String
+      Default: gerrit-db-slave
+  GerritLogsVolume:
+      Description: Gerrit logs volume name
+      Type: String
+      Default: gerrit-logs-slave
+
+Resources:
+    GerritService:
+        Type: AWS::ECS::Service
+        DependsOn:
+          - HTTPListener
+          - SSHListener
+        Properties:
+            Cluster:
+              Fn::ImportValue:
+                  !Join [':', [!Ref 'ClusterStackName', 'ClusterName']]
+            DesiredCount: !Ref DesiredCount
+            TaskDefinition: !Ref GerritTaskDefinition
+            LoadBalancers:
+                - ContainerName: !Ref GerritServiceName
+                  ContainerPort: !Ref HTTPContainePort
+                  TargetGroupArn: !Ref HTTPTargetGroup
+                - ContainerName: !Ref GerritServiceName
+                  ContainerPort: !Ref SSHContainerPort
+                  TargetGroupArn: !Ref SSHTargetGroup
+
+    GerritReplicationService:
+        Type: AWS::ECS::Service
+        DependsOn:
+          - GitListener
+          - GitSSHListener
+        Properties:
+            Cluster:
+              Fn::ImportValue:
+                  !Join [':', [!Ref 'ClusterStackName', 'ClusterName']]
+            DesiredCount: !Ref DesiredCount
+            TaskDefinition: !Ref GerritReplicationTaskDefinition
+            LoadBalancers:
+                - ContainerName: !Ref GitDaemonServiceName
+                  ContainerPort: !Ref GitPort
+                  TargetGroupArn: !Ref GitTargetGroup
+                - ContainerName: !Ref GitSSHServiceName
+                  ContainerPort: !Ref GitSSHContainerPort
+                  TargetGroupArn: !Ref GitSSHTargetGroup
+
+    GerritTaskDefinition:
+        Type: AWS::ECS::TaskDefinition
+        Properties:
+            Family: !Join ['', [!Ref GerritServiceName, TaskDefinition]]
+            TaskRoleArn: !Ref ECSTaskExecutionRole
+            ExecutionRoleArn: !Ref ECSTaskExecutionRole
+            NetworkMode: bridge
+            ContainerDefinitions:
+                - Name: !Ref GerritServiceName
+                  Essential: true
+                  Image: !Sub '${DockerRegistryUrl}/${GerritDockerImage}'
+                  Environment:
+                    - Name: CANONICAL_WEB_URL
+                      Value: !Sub 'https://${Subdomain}.${HostedZoneName}'
+                    - Name: HTTPD_LISTEN_URL
+                      Value: !Sub 'proxy-https://*:${HTTPHostPort}/'
+                    - Name: CONTAINER_SLAVE
+                      Value: true
+                    - Name: AWS_REGION
+                      Value: !Ref AWS::Region
+                  MountPoints:
+                    - SourceVolume: !Ref GerritGitVolume
+                      ContainerPath: /var/gerrit/git
+                    - SourceVolume: !Ref GerritDataVolume
+                      ContainerPath: /var/gerrit/data
+                    - SourceVolume: !Ref GerritCacheVolume
+                      ContainerPath: /var/gerrit/cache
+                    - SourceVolume: !Ref GerritDbVolume
+                      ContainerPath: /var/gerrit/db
+                    - SourceVolume: !Ref GerritLogsVolume
+                      ContainerPath: /var/gerrit/logs
+                  Cpu: 1024
+                  Memory: 2048
+                  PortMappings:
+                    - ContainerPort: !Ref HTTPContainePort
+                      HostPort: !Ref HTTPHostPort
+                      Protocol: tcp
+                    - ContainerPort: !Ref SSHContainerPort
+                      HostPort: !Ref SSHHostPort
+                      Protocol: tcp
+                  LogConfiguration:
+                    LogDriver: awslogs
+                    Options:
+                        awslogs-group: !Ref ClusterStackName
+                        awslogs-region: !Ref AWS::Region
+                        awslogs-stream-prefix: !Ref EnvironmentName
+            Volumes:
+              - Name: !Ref 'GerritDbVolume'
+                DockerVolumeConfiguration:
+                  Scope: shared
+                  Autoprovision: true
+                  Driver: local
+                  Labels:
+                    gerrit-db: !Join ['-', [!Ref EnvironmentName, !Ref GerritDbVolume]]
+              - Name: !Ref 'GerritGitVolume'
+                DockerVolumeConfiguration:
+                  Scope: shared
+                  Autoprovision: true
+                  Driver: local
+                  Labels:
+                    gerrit-git: !Join ['-', [!Ref EnvironmentName, !Ref GerritGitVolume]]
+              - Name: !Ref 'GerritDataVolume'
+                DockerVolumeConfiguration:
+                  Scope: shared
+                  Autoprovision: true
+                  Driver: local
+                  Labels:
+                    gerrit-data: !Join ['-', [!Ref EnvironmentName, !Ref GerritDataVolume]]
+              - Name: !Ref 'GerritCacheVolume'
+                DockerVolumeConfiguration:
+                  Scope: shared
+                  Autoprovision: true
+                  Driver: local
+                  Labels:
+                    gerrit-cache: !Join ['-', [!Ref EnvironmentName, !Ref GerritCacheVolume]]
+              - Name: !Ref 'GerritLogsVolume'
+                DockerVolumeConfiguration:
+                  Scope: shared
+                  Autoprovision: true
+                  Driver: local
+                  Labels:
+                    gerrit-logs: !Join ['-', [!Ref EnvironmentName, !Ref GerritLogsVolume]]
+
+    GerritReplicationTaskDefinition:
+        Type: AWS::ECS::TaskDefinition
+        Properties:
+            Family: !Join ['', [!Ref GerritServiceName, TaskDefinition]]
+            TaskRoleArn: !Ref ECSTaskExecutionRole
+            ExecutionRoleArn: !Ref ECSTaskExecutionRole
+            NetworkMode: bridge
+            ContainerDefinitions:
+                - Name: !Ref GitDaemonServiceName
+                  Essential: true
+                  Image: !Sub '${DockerRegistryUrl}/${GitDaemonDockerImage}'
+                  MountPoints:
+                    - SourceVolume: !Ref GerritGitVolume
+                      ContainerPath: /var/gerrit/git
+                  Cpu: 256
+                  Memory: 512
+                  PortMappings:
+                    - ContainerPort: !Ref GitPort
+                      HostPort: !Ref GitPort
+                      Protocol: tcp
+                  LogConfiguration:
+                    LogDriver: awslogs
+                    Options:
+                        awslogs-group: !Ref ClusterStackName
+                        awslogs-region: !Ref AWS::Region
+                        awslogs-stream-prefix: !Ref EnvironmentName
+                - Name: !Ref GitSSHServiceName
+                  Essential: true
+                  Image: !Sub '${DockerRegistryUrl}/${GitSSHDockerImage}'
+                  Environment:
+                    - Name: TZ
+                      Value: US/Pacific
+                    - Name: SSH_USERS
+                      Value: gerrit:1000:1000
+                    - Name: AWS_REGION
+                      Value: !Ref AWS::Region
+                  MountPoints:
+                    - SourceVolume: !Ref GerritGitVolume
+                      ContainerPath: /var/gerrit/git
+                  Cpu: 1024
+                  Memory: 2048
+                  PortMappings:
+                    - ContainerPort: !Ref GitSSHContainerPort
+                      HostPort: !Ref GitSSHPort
+                      Protocol: tcp
+                  LogConfiguration:
+                    LogDriver: awslogs
+                    Options:
+                        awslogs-group: !Ref ClusterStackName
+                        awslogs-region: !Ref AWS::Region
+                        awslogs-stream-prefix: !Ref EnvironmentName
+            Volumes:
+              - Name: !Ref 'GerritGitVolume'
+                DockerVolumeConfiguration:
+                  Scope: shared
+                  Autoprovision: true
+                  Driver: local
+                  Labels:
+                    gerrit-git: !Join ['-', [!Ref EnvironmentName, !Ref GerritGitVolume]]
+
+    LoadBalancer:
+        Type: AWS::ElasticLoadBalancingV2::LoadBalancer
+        Properties:
+            Type: network
+            Scheme: internet-facing
+            Subnets:
+              - Fn::ImportValue:
+                  !Join [':', [!Ref 'ClusterStackName', 'PublicSubnetOne']]
+            Tags:
+                - Key: Name
+                  Value: !Join ['-', [!Ref 'EnvironmentName', !Ref 'GerritServiceName', 'nlb']]
+
+    HTTPTargetGroup:
+        Type: AWS::ElasticLoadBalancingV2::TargetGroup
+        DependsOn: LoadBalancer
+        Properties:
+            VpcId:
+              Fn::ImportValue:
+                  !Join [':', [!Ref 'ClusterStackName', 'VPCId']]
+            Port: !Ref HTTPHostPort
+            Protocol: TCP
+            HealthCheckPort: !Ref HTTPContainePort
+
+    HTTPListener:
+        Type: AWS::ElasticLoadBalancingV2::Listener
+        DependsOn: LoadBalancer
+        Properties:
+            Certificates:
+              - CertificateArn: !Ref CertificateArn
+            DefaultActions:
+            - Type: forward
+              TargetGroupArn: !Ref HTTPTargetGroup
+            LoadBalancerArn: !Ref LoadBalancer
+            Port: !Ref HTTPSPort
+            Protocol: TLS
+
+    SSHTargetGroup:
+        Type: AWS::ElasticLoadBalancingV2::TargetGroup
+        DependsOn: LoadBalancer
+        Properties:
+            VpcId:
+              Fn::ImportValue:
+                  !Join [':', [!Ref 'ClusterStackName', 'VPCId']]
+            Port: !Ref SSHHostPort
+            Protocol: TCP
+            HealthCheckPort: !Ref SSHContainerPort
+
+    SSHListener:
+        Type: AWS::ElasticLoadBalancingV2::Listener
+        DependsOn: LoadBalancer
+        Properties:
+            DefaultActions:
+            - Type: forward
+              TargetGroupArn: !Ref SSHTargetGroup
+            LoadBalancerArn: !Ref LoadBalancer
+            Port: !Ref SSHHostPort
+            Protocol: TCP
+
+    GitTargetGroup:
+        Type: AWS::ElasticLoadBalancingV2::TargetGroup
+        DependsOn: LoadBalancer
+        Properties:
+            VpcId:
+              Fn::ImportValue:
+                  !Join [':', [!Ref 'ClusterStackName', 'VPCId']]
+            Port: !Ref GitPort
+            Protocol: TCP
+
+    GitListener:
+        Type: AWS::ElasticLoadBalancingV2::Listener
+        DependsOn: LoadBalancer
+        Properties:
+            DefaultActions:
+            - Type: forward
+              TargetGroupArn: !Ref GitTargetGroup
+            LoadBalancerArn: !Ref LoadBalancer
+            Port: !Ref GitPort
+            Protocol: TCP
+
+    GitSSHTargetGroup:
+        Type: AWS::ElasticLoadBalancingV2::TargetGroup
+        DependsOn: LoadBalancer
+        Properties:
+            VpcId:
+              Fn::ImportValue:
+                  !Join [':', [!Ref 'ClusterStackName', 'VPCId']]
+            Port: !Ref GitSSHPort
+            Protocol: TCP
+
+    GitSSHListener:
+        Type: AWS::ElasticLoadBalancingV2::Listener
+        DependsOn: LoadBalancer
+        Properties:
+            DefaultActions:
+            - Type: forward
+              TargetGroupArn: !Ref GitSSHTargetGroup
+            LoadBalancerArn: !Ref LoadBalancer
+            Port: !Ref GitSSHPort
+            Protocol: TCP
+
+    SlaveDnsRecord:
+        Type: AWS::Route53::RecordSet
+        Properties:
+          Name: !Sub '${Subdomain}.${HostedZoneName}'
+          HostedZoneName: !Sub '${HostedZoneName}.'
+          Comment: DNS name for Gerrit Slave.
+          Type: A
+          AliasTarget:
+            DNSName: !GetAtt 'LoadBalancer.DNSName'
+            HostedZoneId: !GetAtt 'LoadBalancer.CanonicalHostedZoneID'
+            EvaluateTargetHealth: False
+
+    # This is a role which is used by the ECS tasks themselves.
+    ECSTaskExecutionRole:
+      Type: AWS::IAM::Role
+      Properties:
+        AssumeRolePolicyDocument:
+          Statement:
+          - Effect: Allow
+            Principal:
+              Service: [ecs-tasks.amazonaws.com]
+            Action: ['sts:AssumeRole']
+        Path: /
+        Policies:
+          - PolicyName: AmazonECSTaskExecutionRolePolicy
+            PolicyDocument:
+              Statement:
+              - Effect: Allow
+                Action:
+                  # Allow the ECS Tasks to download images from ECR
+                  - 'ecr:GetAuthorizationToken'
+                  - 'ecr:BatchCheckLayerAvailability'
+                  - 'ecr:GetDownloadUrlForLayer'
+                  - 'ecr:BatchGetImage'
+                  # Allow the ECS tasks to upload logs to CloudWatch
+                  - 'logs:CreateLogStream'
+                  - 'logs:PutLogEvents'
+                Resource: '*'
+          - PolicyName: AmazonECSTaskSecretManagerRolePolicy
+            PolicyDocument:
+              Statement:
+              - Effect: Allow
+                Action:
+                  # Allow the ECS Tasks to get SSH Keys
+                  - 'secretsmanager:GetSecretValue'
+                  - 'kms:Decrypt'
+                Resource: '*'
+Outputs:
+  PublicLoadBalancerDNSName:
+    Description: The DNS name of the external load balancer
+    Value: !GetAtt 'LoadBalancer.DNSName'
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicLoadBalancerDNSName' ] ]
+  CanonicalHostedZoneID:
+    Description: Canonical Hosted Zone ID
+    Value: !GetAtt 'LoadBalancer.CanonicalHostedZoneID'
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'CanonicalHostedZoneID' ] ]
+  PublicLoadBalancerUrl:
+    Description: The url of the external load balancer
+    Value: !Join ['', ['http://', !GetAtt 'LoadBalancer.DNSName']]
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicLoadBalancerUrl' ] ]
+  HostedZoneName:
+    Description: Route53 Hosted Zone name
+    Value: !Ref HostedZoneName
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'HostedZoneName' ] ]
+  Subdomain:
+    Description: Service DNS subdomain
+    Value: !Ref Subdomain
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'Subdomain' ] ]
+  CanonicalWebUrl:
+    Description: Canonical Web URL
+    Value: !Sub 'https://${Subdomain}.${HostedZoneName}'
+    Export:
+      Name: !Join [ ':', [ !Ref 'AWS::StackName', 'CanonicalWebUrl' ] ]
diff --git a/master-slave/git-daemon/Dockerfile b/master-slave/git-daemon/Dockerfile
new file mode 100644
index 0000000..5147a60
--- /dev/null
+++ b/master-slave/git-daemon/Dockerfile
@@ -0,0 +1,11 @@
+FROM alpine:3.11
+
+RUN apk add git git-daemon
+
+RUN adduser --disabled-password gerrit
+
+USER gerrit
+
+ENTRYPOINT git daemon --enable=receive-pack --reuseaddr --verbose \
+                      --export-all --port=9418 --listen=0.0.0.0 \
+                      --base-path=/var/gerrit/git /var/gerrit/git
diff --git a/master-slave/git-daemon/Makefile b/master-slave/git-daemon/Makefile
new file mode 100644
index 0000000..d9fe694
--- /dev/null
+++ b/master-slave/git-daemon/Makefile
@@ -0,0 +1,14 @@
+include ../setup.env
+
+IMAGE_NAME:=git-daemon
+
+docker-registry-login:
+	aws ecr get-login-password --region $(AWS_REGION) \
+		| docker login --username AWS --password-stdin $(DOCKER_REGISTRY_URI)/aws-gerrit/$(IMAGE_NAME)
+
+git-daemon-build:
+	docker build -t aws-gerrit/$(IMAGE_NAME) .
+		docker tag aws-gerrit/$(IMAGE_NAME):latest $(DOCKER_REGISTRY_URI)/aws-gerrit/$(IMAGE_NAME):latest
+
+git-daemon-publish: docker-registry-login git-daemon-build
+	docker push $(DOCKER_REGISTRY_URI)/aws-gerrit/$(IMAGE_NAME):latest
diff --git a/master-slave/git-ssh/Dockerfile b/master-slave/git-ssh/Dockerfile
new file mode 100644
index 0000000..09e42b4
--- /dev/null
+++ b/master-slave/git-ssh/Dockerfile
@@ -0,0 +1,29 @@
+FROM panubo/sshd:1.2.0
+
+RUN apk add git
+
+# This hack is widely applied to avoid python printing issues in docker containers.
+# See: https://github.com/Docker-Hub-frolvlad/docker-alpine-python3/pull/13
+ENV PYTHONUNBUFFERED=1
+
+RUN echo "**** install Python ****" && \
+    apk add --no-cache python3 && \
+    if [ ! -e /usr/bin/python ]; then ln -sf python3 /usr/bin/python ; fi && \
+    \
+    echo "**** install pip ****" && \
+    python3 -m ensurepip && \
+    rm -r /usr/lib/python*/ensurepip && \
+    pip3 install --no-cache --upgrade pip setuptools wheel && \
+    if [ ! -e /usr/bin/pip ]; then ln -s pip3 /usr/bin/pip ; fi
+
+RUN adduser --h /home/gerrit -D gerrit && \
+    mkdir -p /home/gerrit/.ssh && \
+    chown -R gerrit:gerrit /home/gerrit/.ssh
+COPY --chown=gerrit:gerrit requirements.txt /tmp
+COPY --chown=gerrit:gerrit setup_ssh.py /tmp
+RUN chmod +x /tmp/setup_ssh.py \
+    && pip3 install -r /tmp/requirements.txt
+
+COPY ./entrypoint.sh /bin
+
+CMD /bin/entrypoint.sh
diff --git a/master-slave/git-ssh/Makefile b/master-slave/git-ssh/Makefile
new file mode 100644
index 0000000..88db26b
--- /dev/null
+++ b/master-slave/git-ssh/Makefile
@@ -0,0 +1,14 @@
+include ../setup.env
+
+IMAGE_NAME:=git-ssh
+
+docker-registry-login:
+	aws ecr get-login-password --region $(AWS_REGION) \
+		| docker login --username AWS --password-stdin $(DOCKER_REGISTRY_URI)/aws-gerrit/$(IMAGE_NAME)
+
+git-ssh-build:
+	docker build -t aws-gerrit/$(IMAGE_NAME) .
+		docker tag aws-gerrit/$(IMAGE_NAME):latest $(DOCKER_REGISTRY_URI)/aws-gerrit/$(IMAGE_NAME):latest
+
+git-ssh-publish: docker-registry-login git-ssh-build
+	docker push $(DOCKER_REGISTRY_URI)/aws-gerrit/$(IMAGE_NAME):latest
diff --git a/master-slave/git-ssh/entrypoint.sh b/master-slave/git-ssh/entrypoint.sh
new file mode 100755
index 0000000..c67dd7a
--- /dev/null
+++ b/master-slave/git-ssh/entrypoint.sh
@@ -0,0 +1,10 @@
+#!/bin/bash -e
+
+/tmp/setup_ssh.py
+chown -R gerrit:gerrit /home/gerrit/.ssh
+
+chown -R gerrit:gerrit /var/gerrit/git/
+
+sed -i s/"gerrit:!"/"gerrit:*"/g /etc/shadow
+
+/usr/sbin/sshd -D -e -f /etc/ssh/sshd_config
diff --git a/master-slave/git-ssh/requirements.txt b/master-slave/git-ssh/requirements.txt
new file mode 100644
index 0000000..5223aff
--- /dev/null
+++ b/master-slave/git-ssh/requirements.txt
@@ -0,0 +1 @@
+boto3==1.12.34
diff --git a/master-slave/git-ssh/setup_ssh.py b/master-slave/git-ssh/setup_ssh.py
new file mode 100644
index 0000000..fc228d0
--- /dev/null
+++ b/master-slave/git-ssh/setup_ssh.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+
+import boto3
+import base64
+import os
+from botocore.exceptions import ClientError
+
+
+def get_secret(secret_name):
+    # Create a Secrets Manager client
+    session = boto3.session.Session()
+    client = session.client(
+        service_name='secretsmanager',
+        region_name=os.getenv('AWS_REGION')
+    )
+
+    try:
+        get_secret_value_response = client.get_secret_value(
+            SecretId=secret_name
+        )
+    except ClientError as e:
+        if e.response['Error']['Code'] == 'DecryptionFailureException':
+            # Secrets Manager can't decrypt the protected secret text using the provided KMS key.
+            # Deal with the exception here, and/or rethrow at your discretion.
+            raise e
+        elif e.response['Error']['Code'] == 'InternalServiceErrorException':
+            # An error occurred on the server side.
+            # Deal with the exception here, and/or rethrow at your discretion.
+            raise e
+        elif e.response['Error']['Code'] == 'InvalidParameterException':
+            # You provided an invalid value for a parameter.
+            # Deal with the exception here, and/or rethrow at your discretion.
+            raise e
+        elif e.response['Error']['Code'] == 'InvalidRequestException':
+            # You provided a parameter value that is not valid for the current state of the resource.
+            # Deal with the exception here, and/or rethrow at your discretion.
+            raise e
+        elif e.response['Error']['Code'] == 'ResourceNotFoundException':
+            # We can't find the resource that you asked for.
+            # Deal with the exception here, and/or rethrow at your discretion.
+            raise e
+    else:
+        # Decrypts secret using the associated KMS CMK.
+        # Depending on whether the secret is a string or binary, one of these fields will be populated.
+        if 'SecretString' in get_secret_value_response:
+            return get_secret_value_response['SecretString']
+        else:
+            return base64.b64decode(get_secret_value_response['SecretBinary'])
+
+
+"""
+This script setup Gerrit configuration and its plugins when the container spins up.
+
+It reads from:
+ - AWS Secret Manager: Statically defined.
+ - gerrit.setup: Statically defined.
+ - environment variables: Dinamycally defined.
+
+"""
+
+GERRIT_KEY_PREFIX = "gerrit_secret_"
+SSH_KEYS_DIRECTORY = "/home/gerrit/.ssh"
+
+print("Installing SSH Keys from Secret Manager in directory: " + SSH_KEYS_DIRECTORY)
+
+with open(SSH_KEYS_DIRECTORY + '/authorized_keys', 'w', encoding='utf-8') as f:
+    f.write(get_secret(GERRIT_KEY_PREFIX + 'replication_user_id_rsa.pub'))
+os.chmod(SSH_KEYS_DIRECTORY, 0o700)
+os.chmod(SSH_KEYS_DIRECTORY + '/authorized_keys', 0o600)
+
+print("Finished installation...")
diff --git a/master-slave/setup.env.template b/master-slave/setup.env.template
new file mode 100644
index 0000000..7dfcc0f
--- /dev/null
+++ b/master-slave/setup.env.template
@@ -0,0 +1,10 @@
+CLUSTER_STACK_NAME:=gerrit-cluster
+SERVICE_MASTER_STACK_NAME:=gerrit-service-master
+SERVICE_SLAVE_STACK_NAME:=gerrit-service-slave
+DNS_ROUTING_STACK_NAME:=gerrit-dns-routing
+HOSTED_ZONE_NAME:=gerritforgeaws.com
+MASTER_SUBDOMAIN:=gerrit-master.gerrit-demo
+SLAVE_SUBDOMAIN:=gerrit-slave.gerrit-demo
+AWS_REGION:=us-east-1
+DOCKER_REGISTRY_URI:=<yourAccountId>.dkr.ecr.us-east-1.amazonaws.com
+SSL_CERTIFICATE_ARN=arn:aws:acm:us-east-1:<yourAccountId>:certificate/33e2c235-a4d1-42b7-b866-18d8d744975c
diff --git a/single-master/README.md b/single-master/README.md
index 478ffa1..f330239 100644
--- a/single-master/README.md
+++ b/single-master/README.md
@@ -124,7 +124,8 @@
 
 #### Import into AWS Secret Manager
 
-You can now run the script to upload them to AWS Secret Manager:
+You can now run the [script](../gerrit/add_secrets_aws_secrets_manager.sh) to
+upload them to AWS Secret Manager:
 `add_secrets_aws_secrets_manager.sh /path/to/your/keys/directory`
 
 ### Publish custom Gerrit Docker image
diff --git a/single-master/add_secrets_aws_secrets_manager.sh b/single-master/add_secrets_aws_secrets_manager.sh
deleted file mode 100755
index 0e7e1da..0000000
--- a/single-master/add_secrets_aws_secrets_manager.sh
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/bin/bash -e
-
-SECRETS_DIRECTORY=$1
-if [ -z "$SECRETS_DIRECTORY" ];
-then
-  echo "Secrets directory must be specified"
-  exit 1
-fi
-
-# Avoid to open output in less for each AWS command
-export AWS_PAGER=;
-KEY_PREFIX=gerrit_secret
-
-echo "Adding SSH Keys..."
-
-aws secretsmanager create-secret --name ${KEY_PREFIX}_ssh_host_ecdsa_384_key \
-    --description "Gerrit ssh_host_ecdsa_384_key" \
-    --secret-string file://$SECRETS_DIRECTORY/ssh_host_ecdsa_384_key
-aws secretsmanager create-secret --name ${KEY_PREFIX}_ssh_host_ecdsa_384_key.pub \
-    --description "Gerrit ssh_host_ecdsa_384_key.pub" \
-    --secret-string file://$SECRETS_DIRECTORY/ssh_host_ecdsa_384_key.pub
-aws secretsmanager create-secret --name ${KEY_PREFIX}_ssh_host_ecdsa_521_key \
-    --description "Gerrit ssh_host_ecdsa_521_key" \
-    --secret-string file://$SECRETS_DIRECTORY/ssh_host_ecdsa_521_key
-aws secretsmanager create-secret --name ${KEY_PREFIX}_ssh_host_ecdsa_521_key.pub \
-    --description "Gerrit ssh_host_ecdsa_521_key.pub" \
-    --secret-string file://$SECRETS_DIRECTORY/ssh_host_ecdsa_521_key.pub
-aws secretsmanager create-secret --name ${KEY_PREFIX}_ssh_host_ecdsa_key \
-    --description "Gerrit ssh_host_ecdsa_key" \
-    --secret-string file://$SECRETS_DIRECTORY/ssh_host_ecdsa_key
-aws secretsmanager create-secret --name ${KEY_PREFIX}_ssh_host_ecdsa_key.pub \
-    --description "Gerrit ssh_host_ecdsa_key.pub" \
-    --secret-string file://$SECRETS_DIRECTORY/ssh_host_ecdsa_key.pub
-aws secretsmanager create-secret --name ${KEY_PREFIX}_ssh_host_ed25519_key \
-    --description "Gerrit ssh_host_ed25519_key" \
-    --secret-string file://$SECRETS_DIRECTORY/ssh_host_ed25519_key
-aws secretsmanager create-secret --name ${KEY_PREFIX}_ssh_host_ed25519_key.pub \
-    --description "Gerrit ssh_host_ed25519_key.pub" \
-    --secret-string file://$SECRETS_DIRECTORY/ssh_host_ed25519_key.pub
-aws secretsmanager create-secret --name ${KEY_PREFIX}_ssh_host_rsa_key \
-    --description "Gerrit ssh_host_rsa_key" \
-    --secret-string file://$SECRETS_DIRECTORY/ssh_host_rsa_key
-aws secretsmanager create-secret --name ${KEY_PREFIX}_ssh_host_rsa_key.pub \
-    --description "Gerrit ssh_host_rsa_key.pub" \
-    --secret-string file://$SECRETS_DIRECTORY/ssh_host_rsa_key.pub
-
-echo "Adding Register Email Private Key..."
-
-aws secretsmanager create-secret --name ${KEY_PREFIX}_registerEmailPrivateKey \
-    --description "Gerrit Register Email Private Key" \
-    --secret-string file://$SECRETS_DIRECTORY/registerEmailPrivateKey
-
-echo "Adding LDAP password..."
-
-aws secretsmanager create-secret --name ${KEY_PREFIX}_ldapPassword \
-    --description "LDAP password" \
-    --secret-string file://$SECRETS_DIRECTORY/ldapPassword
-
-echo "Adding SMTP password..."
-
-aws secretsmanager create-secret --name ${KEY_PREFIX}_smtpPassword \
-    --description "SMTP password" \
-    --secret-string file://$SECRETS_DIRECTORY/smtpPassword