← Back to team overview

launchpad-reviewers team mailing list archive

[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>
+