Целью большинства команд веб-разработки является переход к непрерывному развёртыванию, для которого одним из существенных факторов может послужить наличие у каждой ветви функции собственного URL развёртывания (например, my-feature.example.com).

Любой, кто знаком с Vercel, знает, насколько полезны эти области предпросмотра для команд разработчиков. Vercel, AWS Amplify Console и другие играют важную роль в большинстве проектов, хотя если у вас есть требования к построению вроде использования нескольких источников, разных режимов кэширования и т.д., то вам может потребоваться управлять инстансом CloudFront самостоятельно. 

Для крупных проектов, в которых есть отдел контроля качества, наличие выделенного URL для каждой функции означает, что вы не ограничены малым количеством статических сред вроде “тестирования” и “обкатки”. Вы можете масштабировать вашу команду разработки, не нарушая строгих требований контроля качества. 

Использование Terraform, который я вам покажу, на мой взгляд является отличным способом предоставления предпросмотра URL при помощи AWS без задействования дополнительных ресурсов. Т.е. вам не потребуется разворачивать новые дистрибуции CloudFront или заморачиваться созданием новых DNS-записей для каждой ветви функционала. 

Вот что мы будем создавать — простые области предпросмотра посредством AWS.

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

Подход

Я буду использовать подстановочные домены. Сначала мы создадим SSL-сертификат при помощи ACM, а затем прикрепим подстановочный домен к CloudFront и создадим псевдоним записи в Route 53, после чего в завершении добавим функции Lambda@Edge для перенаправления наших динамических поддоменов по верному пути на S3.

Для простоты на всех этапах и во всех средах мы будем использовать один аккаунт AWS.

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

Подстановочный SSL-сертификат

Для его создания я буду использовать Terraform.

resource "aws_acm_certificate" "main" {
  domain_name               = local.cdn_domain_name
  subject_alternative_names = [local.wildcard_domain]
  validation_method         = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

Этот код создаёт сертификат, тем не менее Amazon нужно проверить, что мы действительно владеем этим доменным именем. Мы можем сделать эту проверку автоматической, добавив DNS-запись при помощи Route 53.

resource "aws_route53_record" "validation" {
  name    = aws_acm_certificate.main.domain_validation_options[0].resource_record_name
  type    = aws_acm_certificate.main.domain_validation_options[0].resource_record_type
  records = [aws_acm_certificate.main.domain_validation_options[0].resource_record_value]

  zone_id         = data.aws_route53_zone.external.zone_id
  ttl             = 60
  allow_overwrite = true
}

И в завершении добавляем шаг проверки.

resource "aws_acm_certificate_validation" "main" {
  certificate_arn = aws_acm_certificate.main.arn
  validation_record_fqdns = [
    aws_route53_record.validation.fqdn
  ]

  timeouts {
    create = "10m"
  }
}

Amazon S3

Amazon S3 — это идеальное место для хранения статических объектов вроде HTML/CSS/JS. Нам нужна лишь корзина с включенным хостингом веб-сайта. Вы можете предпочесть использовать идентификаторы доступа к источнику для дальнейшей защиты файлов, хотя в большинстве случаев хостинга веб-сайта на S3 вполне достаточно. 

resource "aws_s3_bucket" "bucket" {
  bucket = local.bucket_name
  acl    = "public-read"
  policy = data.aws_iam_policy_document.bucket.json

  website {
    index_document = "index.html"
    error_document = "error.html"
  }
}
data "aws_iam_policy_document" "bucket" {
  statement {
    actions = ["s3:GetObject"]
    resources = [
      "arn:aws:s3:::${local.bucket_name}",
      "arn:aws:s3:::${local.bucket_name}/*"
    ]
    principals {
      identifiers = ["*"]
      type        = "*"
    }
  }
}

Конвейер развёртывания

Что здесь не рассмотрено, так это конвейер непрерывной интеграции (CI). Ваш процесс сборки должен выполняться как обычно, однако при развёртывании в качестве префикса-ключа должен использоваться транслитерированный url (slug) ветки Git.

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

deploy:
  stage: deploy
  script:
    - S3="s3://example-bucket/$CI_COMMIT_REF_SLUG"
    - aws s3 rm $S3 --recursive
    - aws s3 cp $S3 --recursive
  environment:
    name: $CI_COMMIT_REF_SLUG
    url: https://$CI_COMMIT_REF_SLUG.example.com

Обратите внимание, как мы динамически установили url среды. Это отличная возможность для пул-реквестов, поскольку вы можете просмотреть историю развёртывания и использовать удобную вложенную ссылку. 

В этом примере я определил среду как число PR

Lambda@Edge

А вот и важнейшая часть. Если вы не знакомы с Lambda@Edge, то, по сути, это лямбда-функция, которая дублируется глобально автоматически и может быть прикреплена к четырём разным событиям CloudFront.

Для того, чтобы перенаправить запрос верной функциональности, нам нужно его проинспектировать и определить, в какой каталог S3 отослать.

Предположим, нам нужно, чтобы все области функциональностей имели префикс preview. Тогда для нашей директории develop адрес будет выглядеть так: preview-develop.example.com. При этом запросы для всех поддоменов, не начинающихся с preview (например, www.example.com), нам следует направлять в корневую директорию.

Чтобы это осуществить, нам нужно создать лямбду и прикрепить её к событию запроса источника (origin request) CloudFront. В моём случае для этого я переписал путь к источнику, добавив к нему имя ветки в виде префикса.

exports.handler = (event, context, callback) => {
  const { request } = event.Records[0].cf;

  try {
    const host = request.headers['x-forwarded-host'][0].value;
    const branch = host.match(/^preview-([^\.]+)/)[1];
    request.origin.custom.path = `/${branch}`;
  } catch (e) {
    request.origin.custom.path = `/master`;
  }

  return callback(null, request);
};

Пока что это не сработает, поскольку объект запроса ожидает заголовок x-forwarded-host. По умолчанию он не будет доступен в запросе к источнику, но можно переписать этот заголовок в лямбде, обрабатывающей событие запроса зрителя (viewer request). Следовательно, на данном этапе нам нужно создать эту лямбду.

exports.handler = (event, context, callback) => {
  const { request } = event.Records[0].cf;

  request.headers['x-forwarded-host'] = [
    {
      key: 'X-Forwarded-Host',
      value: request.headers.host[0].value
    }
  ];

  return callback(null, request);
};

Для краткости я не стал включать сюда код Terraform, но вы можете посетить созданный мной репозиторий Git и самостоятельно в нём покопаться. Ознакомьтесь с моим лямбда-модулем, где вы сможете увидеть, какие разрешения ему требуются, чтобы CloudFront запускал лямбды.eknowles/aws-cloudfront-preview-domains
This repo contains the Terraform code to build a CDN with feature branch subdomains. Article =>…github.com

Amazon CloudFront

Amazon CloudFront — это быстрая сеть доставки контента (CDN). Она идеально подходит для кэширования веб-приложений в точках присутствия. В нашем текущем примере мы создадим дистрибуцию CloudFront с одним источником S3.

resource "aws_cloudfront_distribution" "cdn" {
  enabled             = true
  default_root_object = "index.html"
  price_class         = "PriceClass_100"
  aliases             = [local.cdn_domain_name, local.wildcard_domain]

  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate.main.arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.1_2016"
  }

  origin {
    domain_name = aws_s3_bucket.bucket.website_endpoint
    origin_id   = "app"

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "http-only"
      origin_ssl_protocols   = ["TLSv1", "TLSv1.1", "TLSv1.2"]
    }
  }

  default_cache_behavior {
    target_origin_id       = "app"
    allowed_methods        = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
    cached_methods         = ["GET", "HEAD"]
    min_ttl                = 0
    default_ttl            = 0 # 3600
    max_ttl                = 0 # 86400
    viewer_protocol_policy = "redirect-to-https"

    lambda_function_association {
      event_type   = "origin-request"
      lambda_arn   = module.origin_request_lambda.qualified_arn
      include_body = false
    }

    lambda_function_association {
      event_type   = "viewer-request"
      lambda_arn   = module.viewer_request_lambda.qualified_arn
      include_body = false
    }

    forwarded_values {
      query_string = false
      headers      = ["x-forwarded-host"]

      cookies {
        forward = "none"
      }
    }
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }
}

Сюда нужно поместить очень многое, поскольку CloudFront — это огромный ресурс, который может требовать вплоть до 10 минут на своё обновление. Я же включил лишь необходимые компоненты. В зависимости от приложения, вы можете настроить вашу CDN по-своему, определив разные режимы кэширования для разных маршрутов, но стоит обратить внимание на раздел forwarded_values. Важно убедиться, что вы внесли в белый список заголовок x-forwarded-host, чтобы лямбда, обрабатывающая запросы, могла видеть значения. 

Что касается псевдонимов, то я включил “голый” домен example.com, а также подстановочный *.example.com. Это важно для того, чтобы ваша CDN разрешала эти запросы. 

Я предположил, что вы будете выполнять это не в продакшен аккаунте AWS, поэтому установил TTL как 0.

Добавление DNS-записей при помощи Route53

В завершении всего этого процесса вам потребуется убедиться, что пользователи могут направляться в вашу CDN. Здесь мы просто добавим две A-записи, выступающие псевдонимами вашей дистрибуции CloudFront: одну для “голого” домена и вторую для подстановочного.  

resource "aws_route53_record" "wildcard_cdn" {
  zone_id = data.aws_route53_zone.external.zone_id
  name    = local.wildcard_domain
  type    = "A"

  alias {
    name                   = aws_cloudfront_distribution.cdn.domain_name
    zone_id                = aws_cloudfront_distribution.cdn.hosted_zone_id
    evaluate_target_health = false
  }
}
resource "aws_route53_record" "naked_cdn" {
  zone_id = data.aws_route53_zone.external.zone_id
  name    = local.cdn_domain_name
  type    = "A"

  alias {
    name                   = aws_cloudfront_distribution.cdn.domain_name
    zone_id                = aws_cloudfront_distribution.cdn.hosted_zone_id
    evaluate_target_health = false
  }
}

Итог

Мы это сделали! Теперь у вас должно сложиться хорошее понимание того, как можно использовать подстановочные домены, чтобы создавать бесконечные области предпросмотра для ваших веб-приложений. Что же дальше? Вы можете использовать лямбду для запросов зрителя, чтобы обезопасить эти области предпросмотра проверкой присутствия IP запроса в белом списке. Подробнее о Lambda@Edge вы можете узнать в блоге AWS.

Надеюсь, что для вас эта тема оказалась полезной. Есть много способов организации продакшен-сред, и это лишь один из них.

 Я же ещё раз напоминаю, что исходный код Terraform и тестовые данные доступны в репозитории на GitHub:eknowles/aws-cloudfront-preview-domains
This repo contains the Terraform code to build a CDN with feature branch subdomains. Article =>…github.com

Читайте также:


Перевод статьи Ed Knowles: Create Infinite Preview Environments in AWS with Lambda@Edge

Предыдущая статьяУведомления о контактах
Следующая статьяГениально или глупо? Самая неоднозначная нейросеть