Enable HTTPS and HTTP-Redirect on AWS Elastic Beanstalk


AWS Elastic Beanstalk is powerful deployment tools on AWS. It allows users to create applications and push them to a definable set of AWS services, including Amazon EC2, Amazon RDS, Amazon Simple Notification Service (SNS), Amazon CloudWatch, Auto Scaling Group, and Elastic Load Balancer (ELB). The problem is: Elastic Beanstalk Web Console is not so powerful. But, it can be extended using script (.ebextensions) and CLI.

Problem: HTTP and HTTPS Configuration on Elastic Beanstalk (Web Console)

AWS Elastic Beanstalk Configuration: Load Balancer

From the Elastic Beanstalk Web Console, you can configure a web application to listen HTTP and HTTPS port using Elastic Load Balancer (ELB). But, the ELB will forward/proxy the request into a single HTTP port. It means that HTTP and HTTPS will serve same response from user point-of-view. HTTPS connection is terminated (HTTPS-termination) in ELB.

Some users (at least me and some users in stackoverflow.com/serverfault.com) want:

  • HTTP request is replied with HTTP redirection (3xx status code) to HTTPS.
  • HTTPS request is replied by actual web app.
  • No HTTPS-termination in ELB.

Solution: Configure Elastic Load Balancer using .ebextentions

Some of Elastic Beanstalk resources can be customized using .ebextenstions script (see: Customize Containers and Environment Resources). Now, we will configure the ELB to proxy HTTP and HTTPS request to different EC2 instance’s ports.

  1. Create .ebextensions directory inside your app root path.
  2. Create a file (e.g: 00-load-balancer.config) inside .ebextensions directory.
  3. Write the following configuration into the file (.ebextensions/00-load-balancer.config).
{
  "Resources": {
    "AWSEBSecurityGroup": {
      "Type": "AWS::EC2::SecurityGroup",
      "Properties": {
        "GroupDescription": "Allow HTTP and HTTPS",
        "SecurityGroupIngress": [
          {
            "IpProtocol": "tcp",
            "FromPort": 80,
            "ToPort": 80,
            "CidrIp": "0.0.0.0/0"
          },
          {
            "IpProtocol": "tcp",
            "FromPort": 443,
            "ToPort": 443,
            "CidrIp": "0.0.0.0/0"
          }
        ]
      }
    },
    "AWSEBLoadBalancerSecurityGroup": {
      "Type": "AWS::EC2::SecurityGroup",
      "Properties": {
        "GroupDescription": "Allow HTTP and HTTPS",
        "SecurityGroupIngress": [
          {
            "IpProtocol": "tcp",
            "FromPort": 80,
            "ToPort": 80,
            "CidrIp": "0.0.0.0/0"
          },
          {
            "IpProtocol": "tcp",
            "FromPort": 443,
            "ToPort": 443,
            "CidrIp": "0.0.0.0/0"
          }
        ],
        "SecurityGroupEgress": [
          {
            "IpProtocol": "tcp",
            "FromPort": 80,
            "ToPort": 80,
            "CidrIp": "0.0.0.0/0"
          },
          {
            "IpProtocol": "tcp",
            "FromPort": 443,
            "ToPort": 443,
            "CidrIp": "0.0.0.0/0"
          }
        ]
      }
    },
    "AWSEBLoadBalancer": {
      "Type": "AWS::ElasticLoadBalancing::LoadBalancer",
      "Properties": {
        "HealthCheck": {
          "HealthyThreshold": "3",
          "Interval": "30",
          "Target": "HTTP:80/status.html",
          "Timeout": "5",
          "UnhealthyThreshold": "5"
        },
        "Listeners": [
          {
            "LoadBalancerPort": 80,
            "Protocol": "HTTP",
            "InstancePort": 80,
            "InstanceProtocol": "HTTP"
          },
          {
            "LoadBalancerPort": 443,
            "Protocol": "HTTPS",
            "InstancePort": 443,
            "InstanceProtocol": "HTTPS",
            "SSLCertificateId": "arn:aws:iam::123456789012:server-certificate/YourSSLCertificate"
          }
        ],
        "SecurityGroups": [
          { "Fn::GetAtt": [ "AWSEBLoadBalancerSecurityGroup", "GroupId" ] }
        ]
      }
    }
  }
}

In the above config, we modified 3 resources:

  • EC2 Instance Security Group, allow to listen on port HTTP (80) and HTTPS (443).
  • ELB Security Group, allow to listen on port HTTP (80) and HTTPS (443).
  • ELB, we modified ELB to:
    • Do health check EC2 instances on port 80 by HTTP request to /status.html. So, we need to create a hole in port HTTP to allow access the page (will be described later). Elastic Beanstalk doesn’t allow us to do health check using HTTPS request. If you want to do health check by checking TCP port 80, just remove this config section.
    • Make ELB listen to port 80 and forward it to EC2 instance’s port 80.
    • Make ELB listen to port 443 and forward it to EC2 instance’s port 443.

Now the ELB configuration is ready. But, we need to configure web server inside EC2 instances.

Elastic Beanstalk provides some different type of environment (e.g: Java, Python, Ruby, Docker, etc.). Each environment might have different configuration. You can check it on Supported Platforms. At the time of writing this post, they use some web proxy/server (i.e. Apache 2.2, Apache 2.4, Nginx 1.6.2 and IIS 8.5) to listen at port 80 (HTTP).

In this post, I only tell you how to configure Single Docker Container Elastic Beanstalk, which is using Nginx 1.6.2 as proxy. Basically, Single Docker Container Elastic Beanstalk use Nginx to proxy the request to a Docker container. Each time you deploy a new update, Elastic Beanstalk agent inside EC2 instance will update Docker upstream in /etc/nginx/conf.d/elasticbeanstalk-nginx-docker-upstream.conf. Another environment can be configured slightly same.

  1. Create a file (e.g: 01-nginx-proxy.config) inside .ebextensions directory.
  2. Write the following configuration into the file (.ebextensions/01-nginx-proxy.config). Don’t forget to adjust some config (e.g: domain name, SSL certificate, etc.).
files:
  "/etc/nginx/sites-available/000-default.conf":
    mode: "000644"
    owner: root
    group: root
    content: |
      map $http_upgrade $connection_upgrade {
        default   "upgrade";
        ""        "";
      }
 
      server {
        listen         80;
        server_name    your-domain.com;
 
        location = /status.html {
          proxy_pass          http://docker;
          proxy_http_version  1.1;
 
          proxy_set_header    Connection          $connection_upgrade;
          proxy_set_header    Upgrade             $http_upgrade;
          proxy_set_header    Host                $host;
          proxy_set_header    X-Real-IP           $remote_addr;
          proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
          proxy_set_header    X-Forwarded-Host    $host;
          proxy_set_header    X-Forwarded-Server  $host;
        }
 
        location / {
          return        301 https://$host$request_uri;
        }
      }
 
      server {
        listen 443;
 
        ssl                  on;
        ssl_session_timeout  5m;
        ssl_protocols        TLSv1 TLSv1.1 TLSv1.2;
        ssl_certificate      /opt/ssl/default-ssl.crt;
        ssl_certificate_key  /opt/ssl/default-ssl.pem;
        ssl_session_cache    shared:SSL:10m;
 
        location / {
          proxy_pass          http://docker;
          proxy_http_version  1.1;
 
          proxy_set_header    Connection          $connection_upgrade;
          proxy_set_header    Upgrade             $http_upgrade;
          proxy_set_header    Host                $host;
          proxy_set_header    X-Real-IP           $remote_addr;
          proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
          proxy_set_header    X-Forwarded-Host    $host;
          proxy_set_header    X-Forwarded-Server  $host;
        }
      }
 
  "/opt/ssl/default-ssl.crt":
    mode: "000400"
    owner: root
    group: root
    content: |
      -----BEGIN CERTIFICATE-----
      *
      * YOUR-CHAINED-SSL-CERTIFICATE-HERE
      *
      -----END CERTIFICATE-----
 
 
  "/opt/ssl/default-ssl.pem":
    mode: "000400"
    owner: root
    group: root
    content: |
      -----BEGIN RSA PRIVATE KEY-----
      *
      * YOUR-SSL-PRIVATE-KEY-HERE
      *
      -----END RSA PRIVATE KEY-----
 
commands:
   00_enable_site:
    command: 'rm -f /etc/nginx/sites-enabled/* && ln -s /etc/nginx/sites-available/000-default.conf /etc/nginx/sites-enabled/000-default.conf'

In the above config, we:

  • Create SSL certificate and key file.
  • Create Nginx site config:
    • Listen port 80 (HTTP) and redirect all request to HTTPS, except for /status.html. We create a hole here to allow load balancer do health check.
    • Listen port 443 (HTTPS) and proxy the request to actual web server (in this case, Docker container upstream, http://docker).
  • Remove all enabled-sites config and create symlink for the new Nginx config.

After that, you can zip your app directory and deploy it to Elastic Beanstalk via Web Console or CLI.

AWS CloudFormation: VPC with Public and Private Subnets


This is an AWS CloudFormation template to create a VPC environment with public and private subnets. The subnets will be located at two different availability zones:

  • Availability Zone 1
    • Public Subnet 1
    • Private Subnet 1
  • Availability Zone 2
    • Public Subnet 2
    • Private Subnet 2

This template also create an NAT instance inside public subnet 1.

{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "A VPC environment in two availability zones with an NAT instance.",
"Parameters": {
"envPrefix": {
"Description": "Environment name prefix.",
"Type": "String",
"Default": "Test"
},
"vpcCidr": {
"Description": "VPC CIDR block.",
"Type": "String",
"Default": "10.4.0.0/16",
"AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})",
"ConstraintDescription": "Must be a valid IP CIDR range of the form x.x.x.x/x."
},
"publicSubnet1Cidr": {
"Description": "Public subnet 1 CIDR block.",
"Type": "String",
"Default": "10.4.0.0/24",
"AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})",
"ConstraintDescription": "Must be a valid IP CIDR range of the form x.x.x.x/x and subnet of VPC."
},
"privateSubnet1Cidr": {
"Description": "Private subnet 1 CIDR block.",
"Type": "String",
"Default": "10.4.1.0/24",
"AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})",
"ConstraintDescription": "Must be a valid IP CIDR range of the form x.x.x.x/x and subnet of VPC."
},
"publicSubnet2Cidr": {
"Description": "Public subnet 2 CIDR block.",
"Type": "String",
"Default": "10.4.10.0/24",
"AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})",
"ConstraintDescription": "Must be a valid IP CIDR range of the form x.x.x.x/x and subnet of VPC."
},
"privateSubnet2Cidr": {
"Description": "Private subnet 2 CIDR block.",
"Type": "String",
"Default": "10.4.11.0/24",
"AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})",
"ConstraintDescription": "Must be a valid IP CIDR range of the form x.x.x.x/x and subnet of VPC."
},
"subnet1AZ": {
"Description": "Subnet 1 availability zone.",
"Type": "AWS::EC2::AvailabilityZone::Name"
},
"subnet2AZ": {
"Description": "Subnet 2 availability zone.",
"Type": "AWS::EC2::AvailabilityZone::Name"
},
"natInstanceType": {
"Description": "Amazon EC2 instance type for the NAT instance. This instance will be put on public subnet 1.",
"Type": "String",
"Default": "t2.small",
"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"
]
},
"natSshAccessCidr": {
"Description": "IP CIDR from where you could SSH into NAT instance",
"Type": "String",
"MinLength": "9",
"MaxLength": "18",
"Default": "0.0.0.0/0",
"AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})",
"ConstraintDescription": "must be a valid CIDR range of the form x.x.x.x/x."
},
"natKeyName": {
"Description" : "Name of an existing EC2 KeyPair to enable SSH access to NAT instances.",
"Type": "AWS::EC2::KeyPair::KeyName",
"ConstraintDescription" : "Must be the name of an existing EC2 KeyPair."
}
},
"Mappings": {
"AWSNATAMI": {
"eu-central-1" : {"AMI": "ami-46073a5b"},
"sa-east-1" : {"AMI": "ami-fbfa41e6"},
"ap-northeast-1" : {"AMI": "ami-03cf3903"},
"eu-west-1" : {"AMI": "ami-6975eb1e"},
"us-east-1" : {"AMI": "ami-303b1458"},
"us-west-1" : {"AMI": "ami-7da94839"},
"us-west-2" : {"AMI": "ami-69ae8259"},
"ap-southeast-2" : {"AMI": "ami-e7ee9edd"},
"ap-southeast-1" : {"AMI": "ami-b49dace6"}
}
},
"Resources": {
"vpc": {
"Type": "AWS::EC2::VPC",
"Properties": {
"CidrBlock": {"Ref": "vpcCidr"},
"InstanceTenancy": "default",
"EnableDnsSupport": "true",
"EnableDnsHostnames": "true",
"Tags": [
{
"Key": "Name",
"Value": {"Fn::Join" : ["-", [{"Ref" : "envPrefix"}, "VPC"]]}
}
]
}
},
"publicSubnet1": {
"Type": "AWS::EC2::Subnet",
"DependsOn": ["vpc", "attachGateway"],
"Properties": {
"CidrBlock": {"Ref": "publicSubnet1Cidr"},
"AvailabilityZone": {"Ref" : "subnet1AZ"},
"VpcId": {"Ref": "vpc"},
"Tags": [
{
"Key": "Name",
"Value": {"Fn::Join" : ["-", [{"Ref" : "envPrefix"}, "Subnet-Public-1"]]}
}
]
}
},
"privateSubnet1": {
"Type": "AWS::EC2::Subnet",
"DependsOn": ["vpc", "attachGateway"],
"Properties": {
"CidrBlock": {"Ref": "privateSubnet1Cidr"},
"AvailabilityZone": {"Ref" : "subnet1AZ"},
"VpcId": {"Ref": "vpc"},
"Tags": [
{
"Key": "Name",
"Value": {"Fn::Join" : ["-", [{"Ref" : "envPrefix"}, "Subnet-Private-1"]]}
}
]
}
},
"publicSubnet2": {
"Type": "AWS::EC2::Subnet",
"DependsOn": ["vpc", "attachGateway"],
"Properties": {
"CidrBlock": {"Ref": "publicSubnet2Cidr"},
"AvailabilityZone": {"Ref" : "subnet2AZ"},
"VpcId": {"Ref": "vpc"},
"Tags": [
{
"Key": "Name",
"Value": {"Fn::Join" : ["-", [{"Ref" : "envPrefix"}, "Subnet-Public-2"]]}
}
]
}
},
"privateSubnet2": {
"Type": "AWS::EC2::Subnet",
"DependsOn": ["vpc", "attachGateway"],
"Properties": {
"CidrBlock": {"Ref": "privateSubnet2Cidr"},
"AvailabilityZone": {"Ref" : "subnet2AZ"},
"VpcId": {"Ref": "vpc"},
"Tags": [
{
"Key": "Name",
"Value": {"Fn::Join" : ["-", [{"Ref" : "envPrefix"}, "Subnet-Private-2"]]}
}
]
}
},
"inetGateway": {
"Type": "AWS::EC2::InternetGateway",
"DependsOn": ["vpc"],
"Properties": {
"Tags": [
{
"Key": "Name",
"Value": {"Fn::Join" : ["-", [{"Ref" : "envPrefix"}, "InternetGateway"]]}
}
]
}
},
"attachGateway": {
"Type": "AWS::EC2::VPCGatewayAttachment",
"DependsOn": ["vpc", "inetGateway"],
"Properties": {
"VpcId": {"Ref": "vpc"},
"InternetGatewayId": {"Ref": "inetGateway"}
}
},
"rtbPublic": {
"Type": "AWS::EC2::RouteTable",
"DependsOn": ["vpc", "attachGateway"],
"Properties": {
"VpcId": {"Ref": "vpc"},
"Tags": [
{
"Key": "Name",
"Value": {"Fn::Join" : ["-", [{"Ref" : "envPrefix"}, "RTB-Public"]]}
}
]
}
},
"routePublic": {
"Type": "AWS::EC2::Route",
"DependsOn": ["rtbPublic"],
"Properties": {
"DestinationCidrBlock": "0.0.0.0/0",
"RouteTableId": {"Ref": "rtbPublic"},
"GatewayId": {"Ref": "inetGateway"}
},
"DependsOn": "attachGateway"
},
"subnetRouteTableAssociationPublic1": {
"Type": "AWS::EC2::SubnetRouteTableAssociation",
"DependsOn": ["rtbPublic", "publicSubnet1"],
"Properties": {
"RouteTableId": {"Ref": "rtbPublic"},
"SubnetId": {"Ref": "publicSubnet1"}
}
},
"subnetRouteTableAssociationPublic2": {
"Type": "AWS::EC2::SubnetRouteTableAssociation",
"DependsOn": ["rtbPublic", "publicSubnet2"],
"Properties": {
"RouteTableId": {"Ref": "rtbPublic"},
"SubnetId": {"Ref": "publicSubnet2"}
}
},
"rtbPrivate": {
"Type": "AWS::EC2::RouteTable",
"DependsOn": ["vpc"],
"Properties": {
"VpcId": {"Ref": "vpc"},
"Tags": [
{
"Key": "Name",
"Value": {"Fn::Join" : ["-", [{"Ref" : "envPrefix"}, "RTB-Private"]]}
}
]
}
},
"subnetRouteTableAssociationPrivate1": {
"Type": "AWS::EC2::SubnetRouteTableAssociation",
"DependsOn": ["rtbPublic", "privateSubnet1"],
"Properties": {
"RouteTableId": {"Ref": "rtbPrivate"},
"SubnetId": {"Ref": "privateSubnet1"}
}
},
"subnetRouteTableAssociationPrivate2": {
"Type": "AWS::EC2::SubnetRouteTableAssociation",
"DependsOn": ["rtbPublic", "privateSubnet2"],
"Properties": {
"RouteTableId": {"Ref": "rtbPrivate"},
"SubnetId": {"Ref": "privateSubnet2"}
}
},
"natEc2Instance": {
"Type": "AWS::EC2::Instance",
"DependsOn": ["vpc", "attachGateway", "publicSubnet1", "sgNAT"],
"Properties": {
"DisableApiTermination": "false",
"InstanceInitiatedShutdownBehavior": "stop",
"InstanceType": {"Ref": "natInstanceType"},
"ImageId": {"Fn::FindInMap": ["AWSNATAMI", {"Ref": "AWS::Region"}, "AMI"]},
"KeyName": {"Ref": "natKeyName"},
"Monitoring": "false",
"SourceDestCheck": "false",
"Tags": [
{
"Key": "Name",
"Value": {"Fn::Join" : ["-", [{"Ref" : "envPrefix"}, "NAT"]]}
}
],
"NetworkInterfaces": [
{
"DeleteOnTermination": "true",
"Description": "Primary network interface",
"DeviceIndex": 0,
"SubnetId": {"Ref": "publicSubnet1"},
"GroupSet": [
{"Ref": "sgNAT"}
],
"AssociatePublicIpAddress": "true"
}
]
}
},
"sgNAT": {
"Type": "AWS::EC2::SecurityGroup",
"DependsOn": ["vpc", "attachGateway"],
"Properties": {
"GroupDescription": "Security group for NAT instances",
"VpcId": {"Ref": "vpc"},
"SecurityGroupIngress": [
{
"IpProtocol": "tcp",
"FromPort": "0",
"ToPort": "1024",
"CidrIp": {"Ref": "privateSubnet1Cidr"}
},
{
"IpProtocol": "udp",
"FromPort": "0",
"ToPort": "1024",
"CidrIp": {"Ref": "privateSubnet1Cidr"}
},
{
"IpProtocol": "tcp",
"FromPort": "0",
"ToPort": "1024",
"CidrIp": {"Ref": "privateSubnet2Cidr"}
},
{
"IpProtocol": "udp",
"FromPort": "0",
"ToPort": "1024",
"CidrIp": {"Ref": "privateSubnet2Cidr"}
},
{
"IpProtocol": "tcp",
"FromPort": "22",
"ToPort": "22",
"CidrIp": {"Ref": "natSshAccessCidr"}
}
],
"SecurityGroupEgress": [
{
"IpProtocol": "-1",
"CidrIp": "0.0.0.0/0"
}
],
"Tags": [
{
"Key": "Name",
"Value": {"Fn::Join" : ["-", [{"Ref" : "envPrefix"}, "SG-NAT"]]}
}
]
}
},
"routePrivate": {
"Type": "AWS::EC2::Route",
"Properties": {
"DestinationCidrBlock": "0.0.0.0/0",
"RouteTableId": {"Ref": "rtbPrivate"},
"InstanceId": {"Ref": "natEc2Instance"}
}
}
}
}