Эта статья была впервые опубликована на bahr.dev. Подпишитесь на рассылку и получайте новые статьи прямо на почту!

Еще в 2019 году я создал онлайн-кассу для спортивных клубов. По своей сути магазин представлял собой веб-приложение, которое обрабатывает платежи и отправляет PDF-файлы по электронной почте. Когда дело дошло до настройки, все стало сложнее: у каждого клуба было свое название, разные картинки, а иногда даже разные вопросы, которые они хотели задать своим клиентам. Чтобы дать каждому из клубов индивидуальный опыт, мы предоставили каждому из них собственный субдомен. В конце концов было шесть различных развертываний внешнего интерфейса, несколько веток, и кодовые базы начали расходиться. Недавно я узнал, что вы можете использовать DNS ARecords для маршрутизации всех запросов в определенном домене на один и тот же интерфейс. Спасибо ДонгГюн!

В этой статье объясняется, как можно указать несколько поддоменов на одно и то же развертывание внешнего интерфейса, создав записи DNS и статический веб-сайт с помощью AWS Cloud Development Kit (CDK). Это позволит вам предоставить каждому из ваших клиентов индивидуальный опыт, имея только одно развертывание внешнего интерфейса.

Ярлык. Если вам не нужна инфраструктура как код (IaC), то запись ARecord в маршруте 53 с *.yourdomain.com, которая указывает на существующую базу раздачи CloudFront, даст вам тот же результат.

Магия находится в главе Wildcard Routing. Проверьте полный исходный код на GitHub.

Предпосылки

Для развертывания решения из этой статьи у вас должна быть учетная запись AWS и некоторый опыт работы с AWS CDK. Также хорошо иметь неиспользуемый домен, зарегистрированный в Amazon Route 53, но мы также узнаем, как использовать других провайдеров и используемые домены.

В этой статье используется CDK версии 1.60.0. Дайте мне знать, если что-то сломается в новых версиях!

Пожалуйста, загрузите свою учетную запись для CDK, запустив cdk bootstrap. Нам это понадобится для DnsValidatedCertificate.

Необязательно: Понимание того, как работают DNS и особенно серверы имен, очень поможет вам в устранении потенциальных проблем с маршрутизацией.

Решение

Давайте найдем решение, поставив себя на место клиентов. Как покупатель я хочу зайти на bear.picture.bahr.dev или forest.picture.bahr.dev или любой другой адрес в формате *.picture.bahr.dev и потом увидеть картинку к слову в начале. Как разработчик я хочу как можно меньше сложности. Развертывание нескольких интерфейсов увеличивает сложность.

Поток запросов будет выглядеть следующим образом:

Выше видно, что меняется только домен, но больше ничего. В основе решения лежат записи ARecords с подстановочными знаками, которые позволяют нам направлять трафик для любого субдомена в конкретную цель. Затем веб-сайт может взять URL-адрес, извлечь поддомен и запросить правильное изображение. В следующей главе мы подробно рассмотрим каждую часть.

1. Создайте размещенную зону

Чтобы зарегистрировать записи DNS в AWS, нам нужно создать Hosted Zone в Route 53. Каждая размещенная зона стоит 0,50 доллара США в месяц.

Размещенную зону проще всего настроить, если у вас есть домен, которым управляет Route 53 и который вы пока не используете ни для чего другого.

Мы также рассмотрим, как вы можете настроить свою размещенную зону, если вы уже используете свой домен Route 53 для другой цели (например, для своего блога) или если этот домен управляется другим провайдером, чем Route 53.

В зависимости от того, кто управляет вашим доменом (например, Route 53 или GoDaddy) и если вы уже используете свой домен apex для других веб-сайтов, вам придется немного изменить решение. В моем примере я уже использую домен вершины bahr.dev для своего блога, и домен управляется GoDaddy. В следующих главах мы увидим, как указать там правильные записи.

Предупреждение. Прежде чем удалять размещенные зоны, убедитесь, что вы удалили все связанные записи в корневой размещенной зоне или у стороннего поставщика. Висячие записи CNAME и NS могут позволить злоумышленнику предоставлять контент от вашего имени.

1.1 Свежий домен, которым управляет Route 53

Это самый легкий путь. Все, что нам нужно, это зона хостинга для нашего домена.

import { HostedZone } from '@aws-cdk/aws-route53';
...
const domain = `bahr.dev`;
const hostedZone = new HostedZone(this, "HostedZone", {
    zoneName: domain
});

Маршрут 53 теперь может обслуживать записи DNS для этого домена.

1.2 Используемый домен, которым управляет Route 53

Это предполагает, что у вас уже есть размещенная зона для вашего домена вершины, вы используете домен вершины для чего-то другого и вместо этого хотите использовать субдомен. Верхний домен — это ваш домен верхнего уровня, например. bahr.dev или google.com.

Нам нужно сообщить DNS-серверам, что информация о поддомене находится в другой размещенной зоне, и сделать это, создав файл ZoneDelegationRecord.

import { HostedZone } from '@aws-cdk/aws-route53';
...
// bahr.dev is already in use, so we'll start 
// at the subdomain picture.bahr.dev
const apexDomain = 'bahr.dev';
const domain = `picture.${apexDomain}`;
// as above we create a hostedzone for the subdomain
const hostedZone = new HostedZone(this, "HostedZone", {
    zoneName: domain
});
// add a ZoneDelegationRecord so that requests for *.picture.bahr.dev 
// and picture.bahr.dev are handled by our newly created HostedZone
const nameServers: string[] = hostedZone.hostedZoneNameServers!;    
const rootZone = HostedZone.fromLookup(this, 'Zone', { 
  domainName: apexDomain 
});
new ZoneDelegationRecord(this, "Delegation", {
    recordName: domain,
    nameServers,
    zone: rootZone,
    ttl: Duration.minutes(1)
});

Низкое время жизни (TTL) позволяет быстрее работать методом проб и ошибок, поскольку срок действия кеша DNS истекает быстрее. Вы должны увеличить это, когда будете готовиться к производству.

Позже мы добавим ARecords, чтобы запросы к picture.bahr.dev и *.picture.bahr.dev направлялись в одну и ту же базу раздачи CloudFront. bahr.dev не пострадает.

1.3 Домен управляется поставщиком, отличным от AWS

Мы снова создадим размещенную зону на маршруте 53, но на этот раз нам потребуется ручная работа, чтобы зарегистрировать серверы имен нашей размещенной зоны у нашего поставщика DNS. Для начала сначала создайте Hosted Zone через консоль AWS.

Это даст нам размещенную зону с двумя записями для серверов имен (NS) и начала полномочий (SOA). Мы скопируем авторитетный сервер имен и скажем нашему провайдеру DNS делегировать запросы в нашу размещенную зону в AWS.

Скопируйте полномочный сервер имен из записи SOA, перейдите к своему поставщику DNS и создайте запись сервера имен, где вы замените значения для Name и Value:

Type: NS
Name: picture
Value: ns-1332.awsdns-38.org

Используйте конкретное значение, например picture, если вы хотите начать с поддомена, такого как *.picture.bahr.dev, или используйте @, если вы хотите использовать свой домен вершины, например *.bahr.dev.

Затем используйте следующий фрагмент CDK, чтобы импортировать размещенную зону, которую вы создали вручную.

import { HostedZone } from '@aws-cdk/aws-route53';
...
const domain = `picture.bahr.dev`;
const hostedZone = HostedZone.fromLookup(this, 'HostedZone', { 
  domainName: domain 
});

2 Сертификат

Теперь, когда у нас настроена маршрутизация DNS, мы можем запросить и проверить сертификат. Нам нужен этот сертификат для обслуживания нашего веб-сайта с https.

С помощью CDK мы можем создать и проверить сертификат одной командой:

import { DnsValidatedCertificate, ValidationMethod } from "@aws-cdk/aws-certificatemanager";
...
const certificate = new DnsValidatedCertificate(this, "Certificate", {
    region: 'us-east-1',
    hostedZone: hostedZone,
    domainName: this.domain,
    subjectAlternativeNames: [`*.${this.domain}`],
    validationDomains: {
        [this.domain]: this.domain,
        [`*.${this.domain}`]: this.domain
    },
    validationMethod: ValidationMethod.DNS,
});

Здесь много всего происходит, так что давайте разберемся.

Сначала устанавливаем регион us-east-1, потому что CloudFront требует, чтобы сертификаты были в us-east-1.

Затем мы используем конструкцию CDK DnsValidatedCertificate, которая порождает запрос сертификата и лямбда-функцию для регистрации записи CNAME в Route 53. Эта запись используется для проверки того, что мы действительно являемся владельцем домена.

Параметр hostedZone указывает, к какой размещенной зоне должен подключаться сертификат. Это размещенная зона, которую мы создали ранее.

domainName и subjectAlternativeNames указывают, для каких доменов должен быть действителен сертификат. Остальные параметры настраивают процесс проверки.

3 Развертывание внешнего интерфейса

Имея сертификат, мы можем создать развертывание одностраничного приложения (SPA) через S3 и CloudFront. Мы используем пакет npm cdk-spa-deploy, чтобы сократить объем кода, необходимого для настройки корзины S3 и подключения дистрибутива CloudFront.

import { SPADeploy } from 'cdk-spa-deploy';
...
const deployment = new SPADeploy(this, 'spaDeployment')
    .createSiteWithCloudfront({
        indexDoc: 'index.html', 
        websiteFolder: './website', 
        certificateARN: certificate.certificateArn, 
        cfAliases: [this.domain, `*.${this.domain}`]
    });

index.html может быть HTML-файлом размером <p>Hello world!</p> и должен храниться в папке ./website.

В браузере мы можем использовать JavaScript для получения поддомена. Строка кода ниже разбивает URL-адрес ice.picture.bahr.dev на массив ['ice', 'picture', 'bahr', 'dev'], а затем выбирает первый элемент 'ice'.

const subdomain = window.location.host.split('.')[0];

С помощью этой информации веб-сайт может затем связаться с CMS, чтобы получить нужные активы для вашего клиента.

4 Маршрутизация с подстановочными знаками

И, наконец, пришло время маршрутизации по подстановочным знакам. С приведенным ниже кодом CDK все запросы к *.picture.bahr.dev и picture.bahr.dev будут перенаправляться на развертывание внешнего интерфейса, которое мы настроили выше.

import { CloudFrontTarget } from "@aws-cdk/aws-route53-targets";
import { ARecord, RecordTarget } from '@aws-cdk/aws-route53';
...
const cloudfrontTarget = RecordTarget.fromAlias(new CloudFrontTarget(deployment.distribution));
new ARecord(this, "ARecord", {
    zone: hostedZone,
    recordName: `${this.domain}`,
    target: cloudfrontTarget
});
new ARecord(this, "WildCardARecord", {
    zone: hostedZone,
    recordName: `*.${this.domain}`,
    target: cloudfrontTarget
});

Как только все записи DNS будут распространены, мы можем протестировать нашу настройку. Обратите внимание, что развертывание всего решения иногда занимает от 10 до 15 минут.

Попробуй сам

Вот полный код CDK, который вы можете скопировать в существующую кодовую базу CDK.

Я предлагаю вам начать с проверки исходного кода и настроить домен и зону хостинга под свои нужды. Добавьте ZoneDelegationRecord, если вам это нужно. Обязательно запустите cdk bootstrap, если вы еще этого не сделали.

import * as cdk from '@aws-cdk/core';
import { SPADeploy } from 'cdk-spa-deploy';
import { DnsValidatedCertificate, ValidationMethod } from "@aws-cdk/aws-certificatemanager";
import { CloudFrontTarget } from "@aws-cdk/aws-route53-targets";
import { HostedZone, ARecord, RecordTarget } from '@aws-cdk/aws-route53';
export class WildcardSubdomainsStack extends cdk.Stack {
  private readonly domain: string;
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    
    const domain = `picture.bahr.dev`;
    const hostedZone = new HostedZone(this, "HostedZone", {
      zoneName: domain
    });
    const certificate = new DnsValidatedCertificate(this, "Certificate", {
      hostedZone,
      domainName: this.domain,
      subjectAlternativeNames: [`*.${this.domain}`],
      validationDomains: {
        [this.domain]: this.domain,
        [`*.${this.domain}`]: this.domain
      },
      validationMethod: ValidationMethod.DNS
    });
    const deployment = new SPADeploy(this, 'spaDeployment')
        .createSiteWithCloudfront({
            indexDoc: 'index.html', 
            websiteFolder: './website', 
            certificateARN: certificate.certificateArn, 
            cfAliases: [this.domain, `*.${this.domain}`]
        });
    const cloudfrontTarget = RecordTarget
        .fromAlias(new CloudFrontTarget(deployment.distribution));
  
    new ARecord(this, "ARecord", {
      zone: hostedZone,
      recordName: `${this.domain}`,
      target: cloudfrontTarget
    });
    new ARecord(this, "WildCardARecord", {
      zone: hostedZone,
      recordName: `*.${this.domain}`,
      target: cloudfrontTarget
    });
  }
}

Теперь запустите AWS_PROFILE=myProfile npm run deploy, чтобы развернуть решение. Замените myProfile любым профилем, который вы используете для AWS. Подробнее о профилях AWS.

Развертывание может занять от 10 до 15 минут. Возьмите кофе и позвольте CDK делать свое дело. Если вы столкнулись с проблемами, проверьте раздел устранения неполадок ниже.

После завершения развертывания вы сможете посетить любой поддомен указанного вами домена (например, bear.picture.bahr.dev для домена picture.bahr.dev) и увидеть свой веб-сайт.

Исправление проблем

Маршрутизация DNS не работает.

Большое время жизни (TTL) в записях DNS может затруднить тестирование изменений. Постарайтесь максимально снизить TTL.

Если ваш домен не управляется Route 53, убедитесь, что DNS-маршрутизация от вашего DNS-провайдера настроена правильно.

Если вы используете свой домен apex для чего-то другого, обязательно настройте ZoneDelegationRecord, который перенаправляет трафик для вашего субдомена в вашу новую размещенную зону.

Не удалось очистить развертывание.

В зависимости от того, на каком этапе развертывания произошел сбой, не все ресурсы могут быть очищены. Скорее всего, это связано с записью CNAME, созданной лямбда-функцией DnsValidatedCertificate. Перейдите в Hosted Zone, удалите запись CNAME и удалите стек, запустив cdk destroy или удалив его через сервис CloudFormation консоли AWS.

Не удалось создать ресурс. Не удается прочитать свойство «Имя» неопределенного

Очистите стек, удалите и повторно разверните его. Я не уверен, откуда взялась эта ошибка, но повторная попытка исправила ее для меня.

Время проверки сертификата истекло.

Убедитесь, что вы используете правильный подход, чтобы необходимая запись CNAME была видна DNS-серверам. Если вы уже использовали свой домен раньше, настройте правильный ZoneDelegationRecord. Это может быть немного сложно, поэтому не стесняйтесь связаться со мной в Твиттере.

Следующие шаги

Проверьте полный исходный код и попробуйте сами! Если вы хотите внести свой вклад, PR для шаблонов cdk, вероятно, будет хорошей идеей.

Дальнейшее чтение

Понравилась эта статья? Я публикую новую статью каждый месяц. Свяжитесь со мной в Twitter и подпишитесь на новые статьи на свой почтовый ящик!