launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #30323
[Merge] ~cjwatson/launchpad:charm-appserver-frontend-relations into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:charm-appserver-frontend-relations into launchpad:master with ~cjwatson/launchpad:charm-librarian-frontend-relations as a prerequisite.
Commit message:
charm: Set up frontend relations for the appserver
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/447792
The Apache configuration files here are derived from those on production, with added templating and some minor tidying-up. The `service_name` settings are passed through the stack of Juju relations (with Squid and Apache) and can be used in Apache `balancer://` URLs with no extra configuration required.
The balancer arrangements do present a slight problem with appserver requests that explicitly need to be uncached, since the `squid-reverseproxy` charm passes through the services it receives from its `website` relation with the same names. We work around this by adding a `launchpad-appserver-main-cached` service that explicitly goes through the cache, and once https://code.launchpad.net/~cjwatson/squid-reverseproxy-charm/+git/squid-reverseproxy-charm/+merge/447787 lands we'll configure the `squid-reverseproxy` charm to only re-export selected services from its `website` relation. A downside of this is that we need an extra haproxy frontend with its own port, but this is only really visible within the frontend stack and I don't think it should pose an operational problem.
The "api" virtual host is published on a separate relation because it currently needs to be on a separate IP address in order to support old Ubuntu versions whose Python versions didn't support SNI. On production we currently have manually-configured additional IP addresses on the frontends to achieve this; in a charmed deployment, the simplest approach is just to deploy a separate Apache frontend application for each set of public IP addresses we want to have.
There are a number of other virtual hosts on production that don't proxy to the appserver, because they only serve redirections or static files; there are also a number of references in this merge proposal to `DocumentRoot`s that don't exist yet. My plan is to handle those with a separate charm that can be a subordinate to the relevant frontend and can ship those simple virtual hosts and additional files.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:charm-appserver-frontend-relations into launchpad:master.
diff --git a/charm/launchpad-appserver/charmcraft.yaml b/charm/launchpad-appserver/charmcraft.yaml
index 7c06e0a..0f0f6d7 100644
--- a/charm/launchpad-appserver/charmcraft.yaml
+++ b/charm/launchpad-appserver/charmcraft.yaml
@@ -40,6 +40,7 @@ parts:
source-type: git
plugin: dump
organize:
+ apache-vhost-config: layers/interface/apache-vhost-config
launchpad-base: layers/layer/launchpad-base
launchpad-db: layers/layer/launchpad-db
launchpad-payload: layers/layer/launchpad-payload
diff --git a/charm/launchpad-appserver/config.yaml b/charm/launchpad-appserver/config.yaml
index 4f8cf12..75fdd79 100644
--- a/charm/launchpad-appserver/config.yaml
+++ b/charm/launchpad-appserver/config.yaml
@@ -50,6 +50,10 @@ options:
- option forwardfor
- http-check disable-on-404
- balance roundrobin
+ internal_bzr_codebrowse_endpoint:
+ type: string
+ description: The URL of the internal Bazaar code browsing endpoint.
+ default: ""
internal_macaroon_secret_key:
type: string
description: >
@@ -73,6 +77,22 @@ options:
type: int
description: Port for the main application server.
default: 8085
+ port_main_cached:
+ type: int
+ description: Port for the main application server via Squid.
+ default: 8086
+ ssl_chain_required:
+ type: boolean
+ description: >
+ Whether an intermediate certificate chain is needed for this service.
+ In development, we use self-signed certificates which don't have a
+ certificate chain; but in real deployments, we need to send a
+ certificate chain.
+ default: false
+ webmaster_email:
+ type: string
+ description: Email address to include in Apache virtual host configuration.
+ default: "webmaster@xxxxxxxxxxxxxx"
wsgi_worker_max_requests:
type: int
description: >
diff --git a/charm/launchpad-appserver/layer.yaml b/charm/launchpad-appserver/layer.yaml
index 921b8a8..591f8b1 100644
--- a/charm/launchpad-appserver/layer.yaml
+++ b/charm/launchpad-appserver/layer.yaml
@@ -1,6 +1,7 @@
includes:
- layer:launchpad-db
- layer:coordinator
+ - interface:apache-vhost-config
- interface:http
- interface:memcache
repo: https://git.launchpad.net/launchpad
diff --git a/charm/launchpad-appserver/metadata.yaml b/charm/launchpad-appserver/metadata.yaml
index 8fcad01..ac30c5f 100644
--- a/charm/launchpad-appserver/metadata.yaml
+++ b/charm/launchpad-appserver/metadata.yaml
@@ -19,5 +19,9 @@ requires:
memcache:
interface: memcache
provides:
+ api-vhost-config:
+ interface: apache-vhost-config
loadbalancer:
interface: http
+ vhost-config:
+ interface: apache-vhost-config
diff --git a/charm/launchpad-appserver/reactive/launchpad-appserver.py b/charm/launchpad-appserver/reactive/launchpad-appserver.py
index de4d1d0..4173c4e 100644
--- a/charm/launchpad-appserver/reactive/launchpad-appserver.py
+++ b/charm/launchpad-appserver/reactive/launchpad-appserver.py
@@ -234,36 +234,44 @@ def configure_loadbalancer():
unit_name = hookenv.local_unit().replace("/", "-")
unit_ip = hookenv.unit_private_ip()
- services = [
- {
- "service_name": "appserver-main",
- "service_port": config["port_main"],
- "service_host": "0.0.0.0",
- "service_options": list(service_options_main),
- "servers": [
- [
- f"main_{unit_name}",
- unit_ip,
- config["port_main"],
- server_options,
- ]
- ],
- },
- {
- "service_name": "appserver-xmlrpc",
- "service_port": config["port_xmlrpc"],
- "service_host": "0.0.0.0",
- "service_options": list(service_options_xmlrpc),
- "servers": [
- [
- f"xmlrpc_{unit_name}",
- unit_ip,
- config["port_xmlrpc"],
- server_options,
- ]
- ],
- },
- ]
+ main_service = {
+ "service_name": "launchpad-appserver-main",
+ "service_port": config["port_main"],
+ "service_host": "0.0.0.0",
+ "service_options": list(service_options_main),
+ "servers": [
+ [
+ f"main_{unit_name}",
+ unit_ip,
+ config["port_main"],
+ server_options,
+ ]
+ ],
+ }
+ # Add a copy of launchpad-appserver-main which should be explicitly
+ # cached. This cooperates with configuration in launchpad-mojo-specs
+ # which arranges for Squid to only re-export some of the services it
+ # receives: as a result of this, balancer://launchpad-appserver-main/ in
+ # Apache configuration goes directly to haproxy, while
+ # balancer://launchpad-appserver-main-cached/ goes via Squid to haproxy.
+ main_cached_service = main_service.copy()
+ main_cached_service["service_name"] = "launchpad-appserver-main-cached"
+ main_cached_service["service_port"] = config["port_main_cached"]
+ xmlrpc_service = {
+ "service_name": "launchpad-appserver-xmlrpc",
+ "service_port": config["port_xmlrpc"],
+ "service_host": "0.0.0.0",
+ "service_options": list(service_options_xmlrpc),
+ "servers": [
+ [
+ f"xmlrpc_{unit_name}",
+ unit_ip,
+ config["port_xmlrpc"],
+ server_options,
+ ]
+ ],
+ }
+ services = [main_service, main_cached_service, xmlrpc_service]
services_yaml = yaml.dump(services)
for rel in hookenv.relations_of_type("loadbalancer"):
@@ -276,3 +284,67 @@ def configure_loadbalancer():
@when_not_all("loadbalancer.available", "service.configured")
def deconfigure_loadbalancer():
remove_state("launchpad.loadbalancer.configured")
+
+
+@when("vhost-config.available", "service.configured")
+@when_not("launchpad.vhost.configured")
+def configure_vhost():
+ vhost_config = endpoint_from_flag("vhost-config.available")
+ config = dict(hookenv.config())
+ config["base_dir"] = base.base_dir()
+ vhost_names = ("mainsite", "feeds", "xmlrpc")
+ vhost_config.publish_vhosts(
+ [
+ vhost_config.make_vhost(
+ 80,
+ "\n".join(
+ templating.render(
+ f"vhosts/{vhost_name}-http.conf.j2", None, config
+ )
+ for vhost_name in vhost_names
+ ),
+ ),
+ vhost_config.make_vhost(
+ 443,
+ "\n".join(
+ templating.render(
+ f"vhosts/{vhost_name}-https.conf.j2", None, config
+ )
+ for vhost_name in vhost_names
+ ),
+ ),
+ ]
+ )
+ set_state("launchpad.vhost.configured")
+
+
+@when("launchpad.vhost.configured")
+@when_not_all("vhost-config.available", "service.configured")
+def deconfigure_vhost():
+ remove_state("launchpad.vhost.configured")
+
+
+@when("api-vhost-config.available", "service.configured")
+@when_not("launchpad.api-vhost.configured")
+def configure_api_vhost():
+ vhost_config = endpoint_from_flag("api-vhost-config.available")
+ config = dict(hookenv.config())
+ config["base_dir"] = base.base_dir()
+ vhost_config.publish_vhosts(
+ [
+ vhost_config.make_vhost(
+ 80, templating.render("vhosts/api-http.conf.j2", None, config)
+ ),
+ vhost_config.make_vhost(
+ 443,
+ templating.render("vhosts/api-https.conf.j2", None, config),
+ ),
+ ]
+ )
+ set_state("launchpad.api-vhost.configured")
+
+
+@when("launchpad.api-vhost.configured")
+@when_not_all("api-vhost-config.available", "service.configured")
+def deconfigure_api_vhost():
+ remove_state("launchpad.api-vhost.configured")
diff --git a/charm/launchpad-appserver/templates/vhosts/api-http.conf.j2 b/charm/launchpad-appserver/templates/vhosts/api-http.conf.j2
new file mode 100644
index 0000000..392e495
--- /dev/null
+++ b/charm/launchpad-appserver/templates/vhosts/api-http.conf.j2
@@ -0,0 +1,21 @@
+<VirtualHost *:80>
+ ServerName api.{{ domain }}
+ ServerAdmin {{ webmaster_email }}
+
+ CustomLog /var/log/apache2/api.{{ domain }}-access.log combined
+ ErrorLog /var/log/apache2/api.{{ domain }}-error.log
+
+ <Directory {{ base_dir }}/www>
+ Require all granted
+ </Directory>
+
+ Alias /robots.txt {{ base_dir }}/www/robots.txt
+ Alias /offline.html {{ base_dir }}/www/offline.html
+
+ RewriteEngine On
+ RewriteCond %{HTTPS} off
+ RewriteCond %{REQUEST_URI} !/server-status
+ RewriteCond %{REQUEST_URI} !/robots.txt
+ RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}
+</VirtualHost>
+
diff --git a/charm/launchpad-appserver/templates/vhosts/api-https.conf.j2 b/charm/launchpad-appserver/templates/vhosts/api-https.conf.j2
new file mode 100644
index 0000000..21bbdf6
--- /dev/null
+++ b/charm/launchpad-appserver/templates/vhosts/api-https.conf.j2
@@ -0,0 +1,67 @@
+<VirtualHost *:443>
+ ServerName api.{{ domain }}
+ ServerAdmin {{ webmaster_email }}
+
+ SSLEngine on
+ SSLCertificateFile /etc/ssl/certs/{{ domain }}.crt
+ SSLCertificateKeyFile /etc/ssl/private/{{ domain }}.key
+{%- if ssl_chain_required %}
+ SSLCertificateChainFile /etc/ssl/private/{{ domain }}_chain.crt
+{%- endif %}
+
+ CustomLog /var/log/apache2/api.{{ domain }}-access.log combined
+ ErrorLog /var/log/apache2/api.{{ domain }}-error.log
+ LogLevel warn
+
+ Alias /robots.txt {{ base_dir }}/www/robots.txt
+ Alias /offline.html {{ base_dir }}/www/offline.html
+
+ ProxyRequests off
+ <Proxy *>
+ Require all granted
+ ErrorDocument 403 /403.html
+ ErrorDocument 500 /offline.html
+ ErrorDocument 502 /offline.html
+ ErrorDocument 503 /offline.html
+ </Proxy>
+
+ ProxyPassReverse / balancer://launchpad-appserver-main/
+ ProxyPassReverse / balancer://launchpad-appserver-main-cached/
+ ProxyPreserveHost on
+
+ RewriteEngine on
+
+ RewriteRule ^/offline\.html$ - [PT]
+ RewriteRule ^/robots\.txt$ - [PT]
+ RewriteRule ^/\+apidoc/(.*) /$1 [PT]
+ RewriteRule ^/favicon\.(ico|gif|png)$ - [PT]
+
+ # API documentation.
+ RewriteCond %{REQUEST_URI} ^/([^/]*/?|[^/]+/index(\.\w+)?)$
+ RewriteRule ^/(.*)$ balancer://launchpad-assets/+apidoc/$1 [P,L]
+
+ # Other cacheable URLs.
+ RewriteCond %{HTTP_COOKIE} ^$
+ RewriteCond %{HTTP:Authorization} ^$
+ RewriteCond %{REQUEST_URI} !/\+login
+ RewriteRule ^/(.*)$ balancer://launchpad-appserver-main-cached/$1 [P,L]
+
+ # Non-cacheable API requests, passed on to the appserver.
+ RewriteRule ^/(.*)$ balancer://launchpad-appserver-main/$1 [P,L]
+
+ <Location />
+ # Insert filter.
+ SetOutputFilter DEFLATE
+
+ # Don't compress images.
+ SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
+
+ # Don't gzip anything that starts /@@/ and doesn't end .js
+ # (i.e. images).
+ SetEnvIfNoCase Request_URI ^/@@/ no-gzip dont-vary
+ SetEnvIfNoCase Request_URI ^/@@/.*\.js$ !no-gzip !dont-vary
+
+ Require all granted
+ </Location>
+</VirtualHost>
+
diff --git a/charm/launchpad-appserver/templates/vhosts/feeds-http.conf.j2 b/charm/launchpad-appserver/templates/vhosts/feeds-http.conf.j2
new file mode 100644
index 0000000..fe62ead
--- /dev/null
+++ b/charm/launchpad-appserver/templates/vhosts/feeds-http.conf.j2
@@ -0,0 +1,29 @@
+<VirtualHost *:80>
+ ServerName feeds.{{ domain }}
+ ServerAdmin {{ webmaster_email }}
+
+ ErrorLog /var/log/apache2/feeds.{{ domain }}-error.log
+ CustomLog /var/log/apache2/feeds.{{ domain }}-access.log combined
+ LogLevel warn
+
+ DocumentRoot {{ base_dir }}/www
+
+ <Directory {{ base_dir }}/www/>
+ Require all granted
+ </Directory>
+
+ ProxyRequests off
+ <Proxy *>
+ Require all granted
+ ErrorDocument 500 /offline.html
+ ErrorDocument 502 /offline.html
+ ErrorDocument 503 /offline.html
+ </Proxy>
+
+ ProxyPass /robots.txt !
+ ProxyPass /offline.html !
+ ProxyPass / balancer://launchpad-appserver-main-cached/
+ ProxyPassReverse / balancer://launchpad-appserver-main-cached/
+ ProxyPreserveHost on
+</VirtualHost>
+
diff --git a/charm/launchpad-appserver/templates/vhosts/feeds-https.conf.j2 b/charm/launchpad-appserver/templates/vhosts/feeds-https.conf.j2
new file mode 100644
index 0000000..3ea0d1c
--- /dev/null
+++ b/charm/launchpad-appserver/templates/vhosts/feeds-https.conf.j2
@@ -0,0 +1,18 @@
+<VirtualHost *:443>
+ ServerName feeds.{{ domain }}
+ ServerAdmin {{ webmaster_email }}
+
+ SSLEngine on
+ SSLCertificateFile /etc/ssl/certs/{{ domain }}.crt
+ SSLCertificateKeyFile /etc/ssl/private/{{ domain }}.key
+{%- if ssl_chain_required %}
+ SSLCertificateChainFile /etc/ssl/private/{{ domain }}_chain.crt
+{%- endif %}
+
+ CustomLog /var/log/apache2/feeds.{{ domain }}-access.log combined
+ ErrorLog /var/log/apache2/feeds.{{ domain }}-error.log
+ LogLevel warn
+
+ Redirect permanent / http://feeds.{{ domain }}/
+</VirtualHost>
+
diff --git a/charm/launchpad-appserver/templates/vhosts/mainsite-http.conf.j2 b/charm/launchpad-appserver/templates/vhosts/mainsite-http.conf.j2
new file mode 100644
index 0000000..85b3eff
--- /dev/null
+++ b/charm/launchpad-appserver/templates/vhosts/mainsite-http.conf.j2
@@ -0,0 +1,51 @@
+<VirtualHost *:80>
+ ServerName {{ domain }}
+ ServerAlias answers.{{ domain }}
+ ServerAlias blueprints.{{ domain }}
+ ServerAlias bugs.{{ domain }}
+ ServerAlias code.{{ domain }}
+ ServerAlias translations.{{ domain }}
+ ServerAdmin {{ webmaster_email }}
+
+ # Similar to the default Apache log format, but abuses the ident field
+ # to store the HTTP request's Host header (used for web stats).
+ LogFormat "%h %{Host}i %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined-vhost
+
+ CustomLog /var/log/apache2/{{ domain }}-access.log combined-vhost
+ ErrorLog /var/log/apache2/{{ domain }}-error.log
+
+ DocumentRoot {{ base_dir }}/www
+ <Directory {{ base_dir }}/www>
+ Require all granted
+ </Directory>
+
+ ProxyRequests off
+ <Proxy *>
+ Require all granted
+ ErrorDocument 403 /403.html
+ ErrorDocument 500 /offline.html
+ ErrorDocument 502 /offline.html
+ ErrorDocument 503 /offline.html
+ </Proxy>
+
+ RewriteEngine On
+
+ # Any URL ending in "+login" must be handled over HTTPS.
+ RewriteRule ^/(.*)/\+login https://{{ domain }}/$1/+login [L,R=301]
+ # /server-status and /robots.txt are served over HTTP.
+ RewriteRule ^/server-status - [L,R]
+ RewriteRule ^/robots.txt - [L,R]
+ # We serve a geolocated list of Ubuntu mirrors over HTTP.
+ RewriteRule ^/ubuntu/\+countrymirrors-archive - [L,R]
+ # All other URLs are redirected permanently to HTTPS.
+ RewriteCond "%{HTTP_HOST}" "^((?:[^.]+)\.)?{{ domain }}" [NC]
+ RewriteRule ^/(.*)$ "https://%1{{ domain }}/$1" [L,R=301]
+
+ ProxyPass /server-status !
+ ProxyPass /robots.txt !
+
+ ProxyPreserveHost on
+ ProxyPass / balancer://launchpad-appserver-main/
+ ProxyPassReverse / balancer://launchpad-appserver-main/
+</VirtualHost>
+
diff --git a/charm/launchpad-appserver/templates/vhosts/mainsite-https.conf.j2 b/charm/launchpad-appserver/templates/vhosts/mainsite-https.conf.j2
new file mode 100644
index 0000000..6ada8d3
--- /dev/null
+++ b/charm/launchpad-appserver/templates/vhosts/mainsite-https.conf.j2
@@ -0,0 +1,88 @@
+<VirtualHost *:443>
+ ServerName {{ domain }}
+ ServerAlias answers.{{ domain }}
+ ServerAlias blueprints.{{ domain }}
+ ServerAlias bugs.{{ domain }}
+ ServerAlias code.{{ domain }}
+ ServerAlias translations.{{ domain }}
+ ServerAdmin {{ webmaster_email }}
+
+ SSLEngine on
+ SSLCertificateFile /etc/ssl/certs/{{ domain }}.crt
+ SSLCertificateKeyFile /etc/ssl/private/{{ domain }}.key
+{%- if ssl_chain_required %}
+ SSLCertificateChainFile /etc/ssl/private/{{ domain }}_chain.crt
+{%- endif %}
+
+ # Similar to the default Apache log format, but abuses the ident field
+ # to store the HTTP request's Host header (used for web stats).
+ LogFormat "%h %{Host}i %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined-vhost
+
+ CustomLog /var/log/apache2/{{ domain }}-access.log combined-vhost
+ ErrorLog /var/log/apache2/{{ domain }}-error.log
+ LogLevel warn
+
+ DocumentRoot {{ base_dir }}/www
+
+ ProxyRequests off
+ <Proxy *>
+ Require all granted
+ ErrorDocument 403 /403.html
+ ErrorDocument 500 /offline.html
+ ErrorDocument 502 /offline.html
+ ErrorDocument 503 /offline.html
+ </Proxy>
+
+ ProxyPassReverse / balancer://launchpad-appserver-main/
+ ProxyPassReverse / balancer://launchpad-appserver-main-cached/
+ ProxyPreserveHost on
+
+ RewriteEngine on
+
+{% if google_site_verification %}
+ # https://portal.admin.canonical.com/C49078: File needed for Google to
+ # verify domain control.
+ RewriteRule ^/google{{ google_site_verification }}$ - [PT]
+{%- endif %}
+ RewriteRule ^/offline\.html$ - [PT]
+ RewriteRule ^/robots\.txt$ - [PT]
+
+ RewriteRule ^/(\+apidoc.*)$ balancer://launchpad-assets/$1 [P,L]
+ RewriteRule ^/(\+combo/.*)$ balancer://launchpad-assets/$1 [P,L]
+ RewriteRule ^/(\+icing/.*)$ balancer://launchpad-assets/$1 [P,L]
+ RewriteRule ^/(\+tour.*)$ balancer://launchpad-assets/$1 [P,L]
+ RewriteRule ^/(@@/.*)$ balancer://launchpad-assets/$1 [P,L]
+ RewriteRule ^/(favicon\.(?:ico|gif|png))$ balancer://launchpad-assets/$1 [P,L]
+
+{% if internal_bzr_codebrowse_endpoint %}
+ # https://portal.admin.canonical.com/C46608: Proxy requests to Loggerhead.
+ RewriteRule ^/\+loggerhead/(.*)$ {{ internal_bzr_codebrowse_endpoint }}$1 [P,L,NE]
+{%- endif %}
+
+ # Most anonymous requests are cacheable.
+ RewriteCond %{HTTP_COOKIE} ^$
+ RewriteCond %{HTTP:Authorization} ^$
+ RewriteCond %{REQUEST_URI} !/\+login
+ RewriteCond %{REQUEST_URI} !/\+openid
+ RewriteCond %{REQUEST_METHOD} !POST
+ RewriteRule ^/(.*)$ balancer://launchpad-appserver-main-cached/$1 [P,L]
+
+ # Everything else goes to the appserver, bypassing the cache.
+ RewriteRule ^/(.*)$ balancer://launchpad-appserver-main/$1 [P,L]
+
+ <Location />
+ # Insert filter.
+ SetOutputFilter DEFLATE
+
+ # Don't compress images.
+ SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
+
+ # Don't gzip anything that starts /@@/ and doesn't end .js
+ # (i.e. images).
+ SetEnvIfNoCase Request_URI ^/@@/ no-gzip dont-vary
+ SetEnvIfNoCase Request_URI ^/@@/.*\.js$ !no-gzip !dont-vary
+
+ Require all granted
+ </Location>
+</VirtualHost>
+
diff --git a/charm/launchpad-appserver/templates/vhosts/xmlrpc-http.conf.j2 b/charm/launchpad-appserver/templates/vhosts/xmlrpc-http.conf.j2
new file mode 100644
index 0000000..f373249
--- /dev/null
+++ b/charm/launchpad-appserver/templates/vhosts/xmlrpc-http.conf.j2
@@ -0,0 +1,10 @@
+<VirtualHost *:80>
+ ServerName xmlrpc.{{ domain }}
+ ServerAdmin {{ webmaster_email }}
+
+ CustomLog /var/log/apache2/xmlrpc.{{ domain }}-access.log combined
+ ErrorLog /var/log/apache2/xmlrpc.{{ domain }}-error.log
+
+ Redirect permanent / https://xmlrpc.{{ domain }}/
+</VirtualHost>
+
diff --git a/charm/launchpad-appserver/templates/vhosts/xmlrpc-https.conf.j2 b/charm/launchpad-appserver/templates/vhosts/xmlrpc-https.conf.j2
new file mode 100644
index 0000000..4ef607d
--- /dev/null
+++ b/charm/launchpad-appserver/templates/vhosts/xmlrpc-https.conf.j2
@@ -0,0 +1,30 @@
+<VirtualHost *:443>
+ ServerName xmlrpc.launchpad.net
+ ServerAdmin {{ webmaster_email }}
+
+ SSLEngine on
+ SSLCertificateFile /etc/ssl/certs/{{ domain }}.crt
+ SSLCertificateKeyFile /etc/ssl/private/{{ domain }}.key
+{%- if ssl_chain_required %}
+ SSLCertificateChainFile /etc/ssl/private/{{ domain }}_chain.crt
+{%- endif %}
+
+ CustomLog /var/log/apache2/xmlrpc.{{ domain }}-access.log combined
+ ErrorLog /var/log/apache2/xmlrpc.{{ domain }}-error.log
+ LogLevel warn
+
+ DocumentRoot {{ base_dir }}/www
+
+ ProxyRequests off
+ <Proxy *>
+ #Order deny,allow
+ #Allow from all
+ Require all granted
+ </Proxy>
+
+ ProxyPass /robots.txt !
+ ProxyPass / balancer://launchpad-appserver-xmlrpc/
+ ProxyPassReverse / balancer://launchpad-appserver-xmlrpc/
+ ProxyPreserveHost on
+</VirtualHost>
+