AWS: Application Load Balancer – HTTP => HTTPS и host-header редиректы

By | 08/30/2018
 

Имеется Application Load Balancer, за которым находятся три хоста: два фронтенда, и один master.

Задачи следующие:

  1. добавить редиректы HTTP => HTTPS
  2. добавить редиректы доменов dev.admin.dme.example.com на master-хост, а остальных – на frontend-хосты
  3. добавить редиректы www <=> non-www

Редирект HTTP на HTTPS

Тут столкнулся с очередным “подарком” от AWS CloudFormation:

After checking our internal information, the newly released action “Redirects” and “Fixed Responses” [1] does not have support with the CloudFormation yet. We have received similar cases from other customers, and our internal team is working towards making this feature available to the customers as soon as possible. Unfortunately, as a support engineer, I do not have an ETA for this feature to be available for use to the customers.

При том, что в Terraform уже неделю как “завезли” эту возможность (такая же история была с cross-region peering для VPC – в Terraform опция region есть, в CloudFormation – нет).

В общем – пока делаем руками, потом обновим CloudFormation шаблон.

У ресурса ALB есть два Listener-а – HTTP, и HTTPS.

HTTP Listener в CloudFormation сейчас выглядит так:

...
    "AlbHTTPListener": {
      "Type" : "AWS::ElasticLoadBalancingV2::Listener",
      "Properties" : {
        "DefaultActions": [{
          "Type": "forward",
          "TargetGroupArn": { "Ref": "AppFrontTargetGroup" }
        }],
        "LoadBalancerArn" : { "Ref": "LoadBalancer" },
        "Port" : 80,
        "Protocol" : "HTTP"
      }
    },
...

Переходим в Console, и для HTTP-листенера редактируем Default action – удаляем правило Forward to dme-dev-front-target, и задаём Redirect to HTTPS:

Сохраняем:

Проверяем:

curl -LvI dev.dme.example.com
...
* Connected to dev.dme.example.com (18.218.148.182) port 80 (#0)
...
> Host: dev.dme.example.com
...
< HTTP/1.1 301 Moved Permanently
HTTP/1.1 301 Moved Permanently
< Server: awselb/2.0
Server: awselb/2.0
...
< Location: https://dev.dme.example.com:443/
Location: https://dev.dme.example.com:443/
...
* Issue another request to this URL: 'https://dev.dme.example.com:443/'
...
* Connected to dev.dme.example.com (52.14.15.235) port 443 (#1)
...
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
...
< HTTP/2 200
HTTP/2 200

Запрос приходит по HTTP, попадает на HTTP Listener Load Balancer-а, который возвращает 301-й редирект на HTTPS, который перенаправляет запрос на HTTPS Listener этого же балансировщика.

Увы, т.к. автоматизация для этого решения не работает, а добавлять AWS Lambda только ради редиректов желания нет – то HTTP => HTTPS редирект в продакшене пока буду делать по старинке – на стороне NGINX.

Master и Front редиректы

Следующей задачей будет перенаправление трафика в зависимости от имени хоста.

Т.е. запросы типа dev.admin.dme.example.com – надо направлять на один хост за балансировщиком, а все остальные запросы – на другие хосты, фронтеды.

Для настройки редиректов у ALB есть два фильтра – host-header и path-based. Используем host-header, и две отдельные target-группы – одна будет для master хоста, вторая – для фронтендов.

Таргет-группа в шаблоне CloudFormation для фронтов выглядит так:

"AppFrontTargetGroup" : {
  "Type" : "AWS::ElasticLoadBalancingV2::TargetGroup",
  "Properties" : {
    "HealthCheckIntervalSeconds": 30,
    "HealthCheckProtocol": "HTTP",
    "HealthCheckTimeoutSeconds": 10,
    "HealthyThresholdCount": 4,
    "Matcher" : {
      "HttpCode" : "200"
    },
    "Name": { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "front-target"] ] },
    "Port": 80,
    "Protocol": "HTTP",
    "TargetGroupAttributes": [
      {
        "Key": "deregistration_delay.timeout_seconds",
        "Value": "20"
      },
      {
        "Key": "stickiness.enabled",
        "Value": true
      }
    ],
    "Targets": [
      { "Id": {"Ref" : "Front1EC2Instance"}, "Port": 80 },
      { "Id": {"Ref" : "Front2EC2Instance"}, "Port": 80 }
    ],
    "UnhealthyThresholdCount": 3,
    "VpcId": {"Ref" : "VPC"},
    "Tags" : [
      {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "dme-front-targets"] ] } },
      {"Key" : "Env", "Value" : {"Ref" : "ENV"} }
    ]
  }
},

И для мастера – так:

"AppMasterTargetGroup" : {
  "Type" : "AWS::ElasticLoadBalancingV2::TargetGroup",
  "Properties" : {
    "HealthCheckIntervalSeconds": 30,
    "HealthCheckProtocol": "HTTP",
    "HealthCheckTimeoutSeconds": 10,
    "HealthyThresholdCount": 4,
    "Matcher" : {
      "HttpCode" : "200"
    },
    "Name": { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "master-target"] ] },
    "Port": 80,
    "Protocol": "HTTP",
    "TargetGroupAttributes": [
      {
        "Key": "deregistration_delay.timeout_seconds",
        "Value": "20"
      },
      {
        "Key": "stickiness.enabled",
        "Value": true
      }
    ],
    "Targets": [
      { "Id": {"Ref" : "MasterEC2Instance"}, "Port": 80 }
    ],
    "UnhealthyThresholdCount": 3,
    "VpcId": {"Ref" : "VPC"},
    "Tags" : [
      {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "dme-master-targets"] ] } },
      {"Key" : "Env", "Value" : {"Ref" : "ENV"} }
    ]
  }
},

В блоке Targets:

...
    "Targets": [
      { "Id": {"Ref" : "MasterEC2Instance"}, "Port": 80 }
    ],
...

Перечисляем инстансы, которые будут подключены этой таргет-группе.

Далее – настраиваем фильтр на ALB Listener, используя AWS::ElasticLoadBalancingV2::ListenerRule:

"HTTPSListenerRule": {
  "Type": "AWS::ElasticLoadBalancingV2::ListenerRule",
  "Properties": {
    "Actions": [{
      "Type": "forward",
      "TargetGroupArn": { "Ref": "AppMasterTargetGroup" }
    }],
    "Conditions": [{
      "Field": "host-header",
      "Values": [ "dev.admin.dme.example.com" ]
    }],
    "ListenerArn": { "Ref": "AlbHTTPSListener" },
    "Priority": 1
  }
},

Тут в Actions указываем редирект на AppMasterTargetGroup, в которую включён Master-хост, а в Conditions используем host-header, в котором проверяем значение, и если оно == dev.admin.dme.example.com – то выполняем Actions:Type:forward на мастер-таргет.

Обновляем стек:

Проверяем – добавляем на Мастер хосте:

root@ip-10-0-7-43:/home/admin# echo "This is Master" > /var/www/html/index.nginx-debian.html

И на обоих фронтах:

root@ip-10-0-7-12:/home/admin# echo "This is Front1" > /var/www/html/index.nginx-debian.html
root@ip-10-0-7-28:/home/admin# echo "This is Front2" > /var/www/html/index.nginx-debian.html

Выполняем запрос к dev.admin.dme.example.com:

curl -L dev.admin.dme.example.com
This is Master

И просто к Dev:

curl -L dev.dme.example.com
This is Front2
curl -L dev.dme.example.com
This is Front1

ОК – всё работает.

Редиректы www <=> non-www

Тут делаем аналогично редиректу на HTTPS – в настройках HTTPS Listener добавляем условие редиректа по host-header: только выбираем Custom host, path, query:

Опять-таки, т.к. CloudFormation такие редиректы пока не поддерживает – то в продакшен будем выходить с редиректами на стороне NGINX, но сама по себе опция полезна, и потом перенесу редиректы на ALB.

UPD: зачем, если есть CNAME на DNS/Route53?

Проверяем:

curl -LvI https://www.dev.dme.example.com
...
< HTTP/2 301
HTTP/2 301
< server: awselb/2.0
server: awselb/2.0
< date: Thu, 30 Aug 2018 10:01:51 GMT
date: Thu, 30 Aug 2018 10:01:51 GMT
< content-type: text/html
content-type: text/html
< content-length: 150
content-length: 150
< location: https://dev.dme.example.com:443/
location: https://dev.dme.example.com:443/
...
< HTTP/2 200
HTTP/2 200

Готово.