1: Using a custom domain


 

Now that your own application is deployed, let's reroute the Elastic Beanstalk domain to your own custom domain.

 

Purchase a domain from AWS Route 53

Buy domain from AWSGIF

Login into AWS and go to Route 53. You can purchase a new domain or transfer an existing domain on Route 53 with the "Domain Registration" option. For this tutorial, we are going to keep everything in AWS.

 

 

Add the EB domain as an alias to your www custom domain

AWS www rerouteGIF

Let's connect your EB domain to the custom domain. In Route 52, click "Hosted zones" and the domain you purchased through AWS should be listed. Two records have been created automatically but you need to create two new alias records to your custom domain, one with www and one without it.

Click the button "Create Record Set" at the top of the page. There should now be some blank fields on the right side of the page for you to add:

Name - type www without the period after

Type - select "A - IPv4 address"

Alias - select "Yes" and the "Alias Target" will appear

Alias Target - select the EB application name that hosts your Django project

Routing Policy - select "Simple" (the default)

Evaluate Target Health - select "No" (the default)

Click the button "Create" to save the new route.

 

Add the second EB domain as an alias to your custom domain

AWS rerouteGIF

Click the button "Create Record Set" at the top of the page. There should now be some blank fields on the right side of the page for you to add:

Name - leave this blank

Type - select "A - IPv4 address"

Alias - select "Yes" and the "Alias Target" will appear

Alias Target - select the EB application name that hosts your Django project

Routing Policy - select "Simple" (the default)

Evaluate Target Health - select "No" (the default)

Click the button "Create" to save the new route.

 

Add the new domains to the settings.py

env > mysite > mysite > settings.py

ALLOWED_HOSTS = [
'....elasticbeanstalk.com',  #keep your EB domain
'exampledomain.com',  #new
'www.exampledomain.com', #new
]

Go back to Sublime and open your Django settings.py file. Under ALLOWED_HOSTS add the two new domains. One will have a www and the other will not. Make sure all of the domains listed are surrounded by quotation marks and have commas between them. Save the changes made to the file.

 

Redeploy to push the settings update

macOS Terminal

User-Macbook:mysite user$ eb deploy

Windows Command Prompt

C:\Users\Owner\Desktop\Code\env\mysite> eb deploy

Check one last time that the settings.py file has the correct domain names listed under ALLOWED HOSTS then run eb deploy to push the domain name update.  Wait a few seconds and then enter your custom domain name in the browser. The same website that was on the Elastic Beanstalk domain should display.








2: Adding a SSL certificate


Next we need to add a SSL or Secure Sockets Layer to the custom domain so sensitive information will be encrypted from attackers. You can create a public certificate for free on AWS Certificate Manager.

 

Create a SSL certificate using AWS Certificate Manager

AWS certificateGIF

Login into AWS and go to Certificate Manager and select "Get started" under the "Provision certificates".

Choose "Request a public certificate" then click "Request a certificate".  Enter the names of the custom domains, both the domain with and without the www then click nextSelect "DNS validation" and click next.  You don't have to add any tags, just click review. Review the information entered and then click "Confirm and request".

On the validation page click the dropdown arrow next to each domain.  Click the button "Create record in Route 53", if available. A pop-up menu will say the CNAME assigned to the certificate. Click the confirmation button in the pop-up so AWS automatically adds all of the DNS records to the domain. Do this for all of the domains listed. If the button is not available, export the two CNAMEs and add them by hand to the Route 53.

Now you have to wait. The certificate will take a few minutes to be successfully validated and issued. The status will read as Pending validation until completion. The status will then be in green text and read Issued.

 

Assign the certificate to the EB environment

Add load balancer to EBGIF

Now we need to assign the certificate to the proper Elasticbeanstalk load balancer. Go to the AWS Elasticbeanstalk menu and find the environment you are using (change the region if needed).

Click on the application name, then on "Configuration" in the left side menu. Scroll down to "Load balancer" and click Modify.

Under Classic Load Balancer click Add listener:

Listener port - type 443

Listener protocol - choose HTTPS

Instance port - type 80

Instance protocol - choose HTTP

Select the SSL certificate created for the particular domain

Click Add in the menu.

There should now be two load balancers listed.  The first is Port 80 and the other is Port 443 (Pending create). Click Apply at the very bottom of the page to save the changes and update the application.

 

Look for the SSL security in the browser

The application will then take a few minutes to update its configuration. When it is done, you can go to your website domain and to the left of the domain name/URL there should be a little lock that indicates the connection is secure.

SSL lock








3: Adding the HTTPS redirct


Now let's add the HTTPS or Hypertext Transfer Protocol Secure redirect so users can send data between the browser and the server securely by always being on the HTTPS site.

 

Add an HTTP to HTTPS redirect

env > mysite > .ebextensions > (New File) https-redirect-python.config

Create a new file called https-redirect-python.configGIF

Go to the .ebextensions folder we added to the Django project. Create a new file in this folder called https-redirect-python.config.

 

Add an HTTP to HTTPS redirect

env > mysite > .ebextensions > https-redirect-python.config

###################################################################################################
#### Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
####
#### Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file
#### except in compliance with the License. A copy of the License is located at
####
####     http://aws.amazon.com/apache2.0/
####
#### or in the "license" file accompanying this file. This file is distributed on an "AS IS"
#### BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#### License for the specific language governing permissions and limitations under the License.
###################################################################################################

###################################################################################################
#### This configuration file configures Apache for Python environments to redirect HTTP requests on
#### port 80 to HTTPS on port 443 after you have configured your environment to support HTTPS
#### connections:
####
#### Configuring Your Elastic Beanstalk Environment's Load Balancer to Terminate HTTPS:
####  http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/configuring-https-elb.html
####
#### Terminating HTTPS on EC2 Instances Running Python:
####  http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/https-singleinstance-python.html
####
#### Note:
#### This example isn't designed to work with a Django solution.
###################################################################################################

files:
  "/etc/httpd/conf/http-redirect.conf":
    mode: "000644"
    owner: root
    group: root
    content: |
      RewriteEngine On
      RewriteCond %{HTTP:X-Forwarded-Proto} !https
      RewriteCond %{HTTP_USER_AGENT} !ELB-HealthChecker
      RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}

  "/opt/elasticbeanstalk/hooks/config.py":
    mode: "000644"
    owner: root
    group: root
    content: |
      import json
      import sys
      import os
      import time
      import shutil
      import urllib2
      import logging
      import ast
      import subprocess
      from subprocess import check_call, call
      from subprocess import Popen, PIPE

      APACHE_TEMPLATE = r'''
      LoadModule wsgi_module modules/mod_wsgi.so
      WSGIPythonHome /opt/python/run/baselinenv
      WSGISocketPrefix run/wsgi
      WSGIRestrictEmbedded On

      <VirtualHost *:%s>
      # Adding an "Include" below rather than the content of "/etc/httpd/conf/http-redirect.conf" as percent characters are intepreted and replaced inside this "/opt/elasticbeanstalk/hooks/config.py" file.
      Include /etc/httpd/conf/http-redirect.conf
      
      %s

      WSGIScriptAlias / %s


      <Directory %s/>
        Require all granted
      </Directory>

      WSGIDaemonProcess wsgi processes=%s threads=%s display-name=%%{GROUP} \
        python-home=/opt/python/run/venv/ \
        python-path=%s user=%s group=%s \
        home=%s
      WSGIProcessGroup wsgi
      </VirtualHost>

      LogFormat "%%h (%%{X-Forwarded-For}i) %%l %%u %%t \"%%r\" %%>s %%b \"%%{Referer}i\" \"%%{User-Agent}i\"" combined

      '''

      ENV_VAR_NAMESPACE = 'aws:elasticbeanstalk:container:python:environment'
      PYCONFIG_NAMESPACE = 'aws:elasticbeanstalk:container:python'
      STATICFILES_NAMESPACE = 'aws:elasticbeanstalk:container:python:staticfiles'

      DEFAULT_ROOTPAGE_CHECK_TIMEOUT = 3 #used for apache root page check timeout
      DEFAULT_RETRY_SLEEP_IN_SECOND = 1 #used for apache root page check sleep

      # Vetted user visible error messages.
      USER_ERROR_MESSAGES = {
          'deployfail': 'Application version failed to deploy.',
          'badappconfig': ("Your application configuration file is invalid. "
                           "Snapshot your logs for details."),
          'badrequirements': ("Your requirements.txt is invalid. Snapshot "
                              "your logs for details."),
          'failedcommands': ("Error running commands specified in your "
                             "application configuration file. Snapshot your "
                             "logs for details."),
          'badoptions': ("The option settings in your application "
                         "configuration file are invalid. Snapshot "
                         "your logs for details."),
          'badwsgipath': 'Your WSGIPath refers to a file that does not exist.',
      }


      log = logging.getLogger('hooks')


      class PythonHooksError(Exception):
          pass


      class ContainerConfigLoader(object):
          def load_config(self):
              pass


      class SimplifiedConfigLoader(ContainerConfigLoader):
          """Converts the configuration into a simpler format."""
          def load_config(self):
              configs = {}
        
              configs['environment'] = get_user_env()
        
              configs['http_port'] = get_container_config('instance_port')
              configs['wsgi_path'] = get_optionsetting('aws:elasticbeanstalk:container:python', 'WSGIPath')
              configs['num_threads'] = int(get_optionsetting('aws:elasticbeanstalk:container:python', 'NumThreads'))
              configs['num_processes'] = int(get_optionsetting('aws:elasticbeanstalk:container:python', 'NumProcesses'))
        
              static_files_optionsetting = ast.literal_eval(get_optionsetting('aws:elasticbeanstalk:container:python:staticfiles', 'PythonStaticFiles'))
              static_files = {}
              for keyval in static_files_optionsetting:
                  key, value = keyval.split('=', 1)
                  static_files[key] = value
              configs['static_files'] = static_files
        
              return configs


      def get_user_env():
          return json.loads(execute(['/opt/elasticbeanstalk/bin/get-config', 'environment']))


      def get_optionsetting(option_namespace, option_name):
          return execute(['/opt/elasticbeanstalk/bin/get-config', 'optionsettings', '-n', option_namespace, '-o', option_name])


      def get_container_config(config_name):
          return execute(['/opt/elasticbeanstalk/bin/get-config', 'container', '-k', config_name])


      def execute(command_arr):
          return Popen(command_arr, stdout = PIPE).communicate()[0]


      def configure_stdout_logger():
          logging.basicConfig(
              level=logging.DEBUG,
              format="%(asctime)-15s %(levelname)-8s %(message)s",
              stream=sys.stdout)


      def get_python_version():
          return get_container_config('python_version')


      def generate_apache_config(params, filename):
          APP_DEPLOY_DIR = get_container_config('app_deploy_dir')
          APP_USER = get_container_config('app_user')
    
          static_file_content = _generate_static_file_config(params.get('static_files', {}))

          contents = APACHE_TEMPLATE % (
              params['http_port'], static_file_content,
              os.path.normpath(os.path.join(APP_DEPLOY_DIR, params['wsgi_path'])),
              APP_DEPLOY_DIR, params['num_processes'], params['num_threads'],
              APP_DEPLOY_DIR,
              APP_USER, APP_USER, APP_DEPLOY_DIR)
          open(filename, 'w').write(contents)


      def _generate_static_file_config(mapping):
          APP_DEPLOY_DIR = get_container_config('app_deploy_dir')
    
          contents = []
          for key, value in mapping.items():
              contents.append('Alias %s %s' % (key, os.path.join(APP_DEPLOY_DIR, value)))
              contents.append('<Directory %s>' % os.path.join(APP_DEPLOY_DIR, value))
              contents.append('Order allow,deny')
              contents.append('Allow from all')
              contents.append('</Directory>')
              contents.append('')
          return '\n'.join(contents)


      def validate_wsgi_path_param(params, base_dir):
          if not os.path.exists(base_dir):
              log.error("The base dir used for validating wsgi path params does not exist: %s", base_dir)
          if not os.path.isfile(os.path.join(base_dir, params['wsgi_path'])):
              emit_error_event(USER_ERROR_MESSAGES['badwsgipath'])
              log.error('The specified WSGIPath of "%s" was not found in the source bundle', params['wsgi_path'])


      def generate_env_var_config(params, filename):
          # Create an environment variable that can be sourced
          # in shell scripts:
          # . <envfile>
          # echo $VAR_FROM_APP_PARAMS
          # In addition to the existing vars, there's two more
          # env vars that are useful, the PYTHONPATH and the PATH.
          # However, with these vars we want to prepend/append values,
          # not completely replace, which is why they're done
          # separately.
          APP_DEPLOY_DIR = get_container_config('app_deploy_dir')
    
          env_var_config = [
              'export PYTHONPATH="%s/:$PYTHONPATH"' % APP_DEPLOY_DIR,
              'export PATH="/opt/python/run/venv/bin/:$PATH"',
          ]
          for env_var in params['environment']:
              value = params['environment'][env_var]
              value = value.replace("\\", "\\\\")
              value = value.replace('"', '\\"')
              env_var_config.append('export %s="%s"' % (env_var, value))

          env_var_config.append('')
          contents = '\n'.join(env_var_config)
          open(filename, 'w').write(contents)


      def inject_python_path(environment):
          APP_STAGING_DIR = get_container_config('app_staging_dir')
          if 'PYTHONPATH' not in environment:
              environment['PYTHONPATH'] = APP_STAGING_DIR
          else:
              environment['PYTHONPATH'] = '%s:%s' % (APP_STAGING_DIR, environment['PYTHONPATH'])


      def inject_path(environment):
          BASE_PATH_DIRS = get_container_config('base_path_dirs')
          if 'PATH' not in environment:
              environment['PATH'] = '/opt/python/run/venv/bin:' + BASE_PATH_DIRS
          else:
              environment['PATH'] = '/opt/python/run/venv/bin/:%s:%s' % (environment['PATH'], BASE_PATH_DIRS)


      def create_new_on_deck_dir():
          APP_STAGING_BASE = get_container_config('app_staging_base')
          BUNDLE_DIR = get_container_config('bundle_dir')
          cleanup_previous_deploy()
          latest_dir = sorted([int(i) for i in os.listdir(BUNDLE_DIR)])[-1]
          new_dir = os.path.join(BUNDLE_DIR, str(latest_dir + 1))
          os.mkdir(new_dir)
          os.symlink(new_dir, APP_STAGING_BASE)


      def cleanup_previous_deploy():
          # If the previous deploy fails, this is our chance to clean it up
          # before we start with our current app deploy.
          APP_STAGING_BASE = get_container_config('app_staging_base')
          if os.path.exists(APP_STAGING_BASE):
              actual_dir = os.readlink(APP_STAGING_BASE)
              os.remove(APP_STAGING_BASE)
              shutil.rmtree(actual_dir)


      def copy_apache_config():
          WSGI_STAGING_CONFIG = get_container_config('wsgi_staging_config')
          WSGI_DEPLOY_CONFIG = get_container_config('wsgi_deploy_config')
    
          if not os.path.exists(WSGI_STAGING_CONFIG):
              raise PythonHooksError("Config file does not exist: %s" % WSGI_STAGING_CONFIG)
          os.rename(WSGI_STAGING_CONFIG, WSGI_DEPLOY_CONFIG)


      def start_supervisord():
          if call('pgrep supervisord', shell=True) != 0:
              check_call('start supervisord', shell=True)

              # The supervisor config specifies that httpd must stay running
              # for 1 second so we have to give it time to startup and settle before
              # verifying that it's running.
              time.sleep(3)
              ensure_apache_is_running()


      def start_apache():
          apache_cmd('start', should_be_running=True)


      def restart_apache():
          apache_cmd('restart', should_be_running=True)


      def apache_cmd(command, should_be_running=True):
          check_call('/usr/local/bin/supervisorctl -c /opt/python/etc/supervisord.conf %s httpd' % command, shell=True)
          time.sleep(1.5)
          if should_be_running:
              ensure_apache_is_running()
              prime_apache(DEFAULT_ROOTPAGE_CHECK_TIMEOUT)


      def apache_is_running():
          rc = call('/usr/local/bin/supervisorctl -c /opt/python/etc/supervisord.conf status httpd | grep RUNNING', shell=True)
          return rc == 0


      def ensure_apache_is_running():
          if not apache_is_running():
              # try killing any other httpd processes and try again
              print "apache failed to start... killing all existing httpd processes and trying again"
              sys.stdout.flush()
              call('killall httpd', shell=True)
              time.sleep(1.5)
              call("echo Semaphores owned by apache:;ipcs -s | grep apache", stderr=subprocess.STDOUT, shell=True)
              check_call("echo Deleting apache semaphores:;ipcs -s | grep apache | awk '{print $2;}' | while read -r line; do ipcrm sem \"$line\"; done", stderr=subprocess.STDOUT, shell=True)
              check_call('/usr/local/bin/supervisorctl -c /opt/python/etc/supervisord.conf start httpd', shell=True)
              time.sleep(1.5)

              if not apache_is_running():
                  raise PythonHooksError("Apache is not running, but it's supposed to be.")


      def prime_apache(max_timeout):
          if (max_timeout is None or type(max_timeout) != int):
              max_timeout = DEFAULT_ROOTPAGE_CHECK_TIMEOUT
          remain_timeout = max_timeout
          while 0 <= remain_timeout:
              try:
                  urllib2.urlopen('http://localhost/', timeout=3).read()
                  return
              except Exception as err:
                  time.sleep(DEFAULT_RETRY_SLEEP_IN_SECOND)
                  remain_timeout = remain_timeout - DEFAULT_RETRY_SLEEP_IN_SECOND
          log.info('Apache is running, but root page is not responding in %s seconds.' % max_timeout)


      def emit_error_event(msg):
          check_call('eventHelper.py --msg "%s" --severity ERROR' % msg, shell=True)


      def diagnostic(msg):
          # Only call from an except block.
          check_call('eventHelper.py --msg "%s" --severity SYSTEM' % msg, shell=True)
          log.error(msg, exc_info=True)

In that file, copy and paste the all of the code above. This code was taken directly from the AWS documentation GitHub Repository at https://github.com/awsdocs/elastic-beanstalk-samples/tree/master/configuration-files/aws-provided/security-configuration/https-redirect. Although it seems daunting, this code handles the HTTP to HTTPS redirect, configuring the proxy server in front of the application to redirect insecure HTTP requests automatically to secure HTTPS connections. Make sure to save this file before pushing the update.

 

Redeploy to push the settings update

macOS Terminal

User-Macbook:mysite user$ eb deploy

Windows Command Prompt

C:\Users\Owner\Desktop\Code\env\mysite> eb deploy

Check to make sure the new file is saved correctly then run eb deploy to push the HTTPS redirect update.  Wait a few seconds and then enter your custom domain name in the browser. Enter your http://customdomain.com/ url and you should be brought to https://customdomain.com/ automatically. 








4: Pushing Django project updates


We have pushed updates for the custom domain and the HTTPS redirect but you can update your project any time by redeploying it on the AWS CLI. Just follow these procedures to make sure everything is deployed properly.

 

Deactivate virtual environment

macOS Terminal

(env)User-Macbook:mysite user$ deactivate

Windows Command Prompt

(env) C:\Users\Owner\Desktop\Code\env\mysite> deactivate

Deactivate your virtual environment and stay in the mysite folder.

 

macOS Terminal

User-Macbook:mysite user$ eb status
Environment details for: django-env
  Application name: django-webapp
  ...
  Status: Ready
  Health: Green

Windows Command Prompt

C:\Users\Owner\Desktop\Code\env\mysite> eb status
Environment details for: django-env
  Application name: django-webapp
  ...
  Status: Ready
  Health: Green

Check to make sure you are still connected to the right AWS environment and application by running eb status.

 

macOS Terminal

User-Macbook:mysite user$ eb deploy
...

User-Macbook:mysite user$ eb open

Windows Command Prompt

C:\Users\Owner\Desktop\Code\env\mysite> eb deploy
...

C:\Users\Owner\Desktop\Code\env\mysite> eb open

Make sure all of the updated files are saved and then run eb deploy. Wait a few seconds and then enter your custom domain name in the browser or run eb open.






Quiz Questions


1. Which AWS service allows users to buy domains?


Next lesson


Check out the comments and debug buttons if you get stuck.