Недавно мы запустили наш новый веб-сайт 🍻, и во время разработки этого нового веб-сайта я многому научился. Одним из них является то, как CDN помогает нам предотвратить поломку на этапе развертывания нашей непрерывной интеграции (CI). В качестве фона мы только что реализовали CI в наших сервисах, и у меня есть план использовать CDN для нашего веб-сайта, но у меня никогда не было времени реализовать это, пока эта проблема не возникла.

Мы используем комбинацию Gitlab CI и AWS Elastic Container Service (ECS) для всех наших контейнеров докеров. В настоящее время у нас работает около 10 сервисов для всей нашей системной среды. Одна из таких услуг - наш новый интерфейсный веб-сайт, созданный с использованием Express.js и Nuxt.js. Этот новый интерфейсный веб-сайт в значительной степени использует javascript и имеет функцию рендеринга на стороне сервера (SSR). Первоначально мы хотим иметь полностью одностраничное приложение (SPA), но этот вид приложения затруднит сканирование нашего веб-сайта роботами поисковых систем (кроме бота Google), и это причина того, что у нас есть функция SSR на нашем новом веб-сайте.

Во время процесса развертывания ECS запускает новый контейнер (или они называются Task) в зависимости от желаемого числа, установленного в конфигурации службы. Если желаемый номер задачи равен 2, то ECS запустит 2 новые задачи на основе нового определения задачи. Эти две новые задачи будут добавлены к Сервису вместе с двумя старыми задачами, которые были там из предыдущего развертывания.

Затем, если вся новая задача перешла в состояние RUNNING, ECS будет истощать соединение с каждой старой задачей перед ее завершением. Этот процесс обычно занимает несколько минут, прежде чем все старые Задачи будут удалены. Во время этого перехода, если служба является внешней службой веб-сайта, которая обслуживает HTTP-соединение с браузером пользователя, существует вероятность того, что этот переход сделает веб-сайт недоступным для пользователя.

Почему?

Прежде чем мы перейдем к процессу развертывания, у нас есть процесс «сборки» для нашего CI. В процессе сборки мы встраиваем все шаблоны Vue (поскольку мы используем Nuxt.js) в пакеты javascript и компилируем файлы Typescripts в простой javascript (мы используем Typescript для нашего приложения Express.js). Этот процесс сгенерирует новый готовый к производству пакет javascript, который будет содержать хеш-версию в имени файла. Пример: app.072413282a677463d5a9.js. Затем эти файлы регистрируются во внутреннем манифесте приложения, поэтому позже, когда пользователь посещает веб-сайт, он может предоставить пользователю правильную версию файла. Все скомпилированные файлы сохраняются в контейнере локально, и здесь возникает проблема.

Начнем с иллюстрации. Как я объяснил ранее, во время развертывания ECS запустит новую задачу перед удалением старой задачи. Итак, есть момент, когда и старая, и новая задача обслуживаются балансировщиком нагрузки.

Затем пользователь A посещает сайт во время процесса развертывания, и балансировщик нагрузки перенаправляет HTTP-соединение в контейнер (давайте проигнорируем алгоритм, который используется балансировщиком нагрузки для определения целевого контейнера на данный момент). В этом случае контейнер, который получает HTTP-запрос, - это задача A, которая является старым контейнером. Задача A: сгенерировать ответ HTML браузеру пользователя, и ответ HTML будет содержать тег скрипта или стиля, который загружает старую версию, которая хранится локально в файле манифеста задачи A, то есть app.old.js.

После загрузки HTML-ответа в пользовательский браузер браузер затем пытается загрузить скрипт, который был определен в HTML, который равен app.old.js. Затем этот запрос передается в балансировщик нагрузки, и балансировщик нагрузки решил отправить трафик в Задачу B, которая является новым контейнером. Поскольку все статические ресурсы хранятся локально, поэтому в Задаче B нет app.old.js. На нем есть только app.new.js. Затем браузер не может загрузить ресурсы, и, наконец, веб-сайт сломан.

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

Конечно, есть полезные пользователи, которые пытаются связаться со службой поддержки клиентов, чтобы убедиться, что они могут произвести оплату, но такой тип пользователей довольно редок. Чаще всего, если пользователь видит, что страница не работает, он просто покидает сайт и больше не вернется. Эта проблема плохо сказывается на бизнесе, и нам нужно ее решить.

Как?

Решение простое. Все, что нам нужно сделать, это переместить все статические ресурсы в стороннее хранилище. Поскольку мы используем Amazon Web Service, мы можем использовать их сервис S3 для хранения файлов. Затем я подумал, почему просто использовать S3 и почему бы нам не использовать CloudFront (CDN), чтобы сидеть перед корзиной S3.

Честно говоря, у меня вообще никогда не было опыта настройки CDN. Так что для меня это довольно интересно. Поскольку технология очень специфична, я объясню технические детали, как нам удается создать автоматическое развертывание CDN в процессе сборки на CI. В качестве фона мы используем Nuxt.js в качестве оболочки Vue.js, чтобы позволить нам легко настраивать рендеринг на стороне сервера, затем мы снова оборачиваем Nuxt.js внутри Express.js, чтобы мы могли обслуживать как страницу Nuxt.js SSR, так и API веб-сайта в том же приложении.

Прежде чем мы сможем обслуживать статический файл на CloudFront, нам нужно загрузить все файлы в S3 в качестве ресурсов Origin. Вы можете найти множество ресурсов, как сделать корзину S3 в качестве ресурсов Origin на CloudFront. На нашем новом сайте есть 2 типа ресурсов: статические и динамические.

Динамические ресурсы - это такие файлы, как фотографии или видео, которые загружает наш пользователь. Эти ресурсы уже хранятся на S3, поэтому мы особо не делаем ничего для того, чтобы эти ресурсы можно было обслуживать через CloudFront. Просто измените URL-адрес с URL-адреса S3 на URL-адрес CloudFront. Например, мы устанавливаем DYNAMIC_PUBLIC_RESOURCE_PREFIX с https://s3-us-west-2.amazonaws.com/bucket-name/static на https://random-subdomain.cloudfront.net/static.

Статические ресурсы - это файл, который создается в процессе сборки, обычно это форма файлов javascript, файла CSS и небольших файлов изображений, таких как значки или фоновое изображение. Когда статический ресурс построен, все URL-адреса, такие как изображения или шрифт, будут ссылаться на локальный, поэтому нам нужно настроить дополнительную конфигурацию на nuxt.config.js, чтобы убедиться, что URL-адрес преобразован в конечную точку CloudFront, когда ресурс компилируется во время процесса сборки CI.

После того, как я проверил базу кода, есть 3 случая, когда в приложении используется статический URL-адрес ресурса, свойство background-image CSS и ресурс шрифтов в нашем файле SCSS (мы используем SCSS в качестве препроцессора CSS), свойство background-image CSS на Vue templates, и Image src атрибут HTML в шаблоне Vue.

URL ресурса в файлах SCSS

Изменить URL-адрес ресурса в файлах SCSS очень просто. Все, что вам нужно сделать, это указать URL-адрес CloudFront CDN в publicPath nuxt.config.js конфигурации сборки. Поскольку мы сохраняем URL-адрес в переменной среды во время сборки, мы можем просто использовать переменную среды CLOUDFRONT_ENDPOINT для получения URL-адреса. Также мы должны убедиться, что это применяется только к производственной или промежуточной сборке, поэтому мы зависим от NODE_ENV, чтобы убедиться, что мы используем каталог по умолчанию /build/ только при разработке. Но мы должны убедиться, что он уже импортирован в переменную среды Nuxt.js, потому что в противном случае вы не можете использовать CLOUDFRONT_ENDPOINT и NODE_ENV в конфигурации.

Если суммировать всю эту конфигурацию, результат будет примерно таким:

module.exports = {
  env: {
    NODE_ENV: process.env.CLOUDFRONT_ENDPOINT,
    CLOUDFRONT_ENDPOINT: process.env.CLOUDFRONT_ENDPOINT
  },
  build: {
    publicPath:
      process.env.NODE_ENV !== "development"
        ? process.env.CLOUDFRONT_ENDPOINT
        : "/build/",
  }
}

CSS-свойство background-image в шаблонах Vue

Далее описано, как преобразовать свойство background-image CSS, содержащее url(), чтобы использовать конечную точку CloudFront вместо локальной. В шаблонах Vue у нас есть <style> разделов, которые содержат свойство CSS для компонента. На нашем веб-сайте у нас есть только один общий файл CSS, отформатированный как файл SCSS, а затем весь стиль должен быть помещен в компонент Vue, поскольку это упрощает организацию кода. Настройка общедоступного пути не приводит к автоматическому изменению URL-адреса этого свойства background-image. Найти решение этой проблемы довольно сложно, поскольку мы не нашли подходящего примера в Интернете.

Мы обнаружили, что Nuxt.js по умолчанию использует Post CSS, и мы можем изменить конфигурацию Post CSS в файле nuxt.config.js. Итак, все, что нам нужно сделать, это изменить конфигурацию плагина postcss-url, чтобы преобразовать URL-адрес ресурса с локального в конечную точку CloudFront. В итоге мы получили такую ​​конфигурацию.

module.exports = {
  env: {
    NODE_ENV: process.env.CLOUDFRONT_ENDPOINT,
    CLOUDFRONT_ENDPOINT: process.env.CLOUDFRONT_ENDPOINT
  },
  build: {
    publicPath:
      process.env.NODE_ENV !== "development"
        ? process.env.CLOUDFRONT_ENDPOINT
        : "/build/",
    postcss: {
      plugins: {
        "postcss-import": {},
        "postcss-url": {
          url: asset => {
            // Exclude development mode
            if (process.env.NODE_ENV === "development") {
              return asset.url;
            }

            // Exclude data-url resources
            if (asset.url.substr(0, 4) === "data") {
              return asset.url;
            }

            // Exclude non image asset
            if (asset.url.substr(0, 8) !== "/img/") {
              return asset.url;
            }

            // Return cdn url
            return process.env.CLOUDFRONT_ENDPOINT + asset.url;
          }
        }
      }
  }
}

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

Изображение src атрибут HTML в Vue шаблоне

Последний - это изменение URL статического изображения в шаблоне Vue. В файлах Vue есть раздел под названием <template>, куда мы помещаем наш HTML-код, и всегда есть вероятность, что мы используем в нем статическое изображение файла. Нравится:

<template>
  <div id="header">
    <img src="/img/icon.png">
  </div>
</template>

Как мы изменим URL-адрес этого кода с локального URL-адреса на конечную точку CloudFront? Попробовав различные методы, мы закончили тем, что использовали настраиваемый плагин в нашем приложении Nuxt.js для замены этого URL-адреса с помощью внедренного модуля.

export default ({ store }, inject) => {
  inject("image", path => {
    if (process.env.NODE_ENV === "development") {
      return `/img/${path}`;
    }

    return `${process.env.CLOUDFRONT_ENDPOINT}/img/${path}`;
  });
};

Затем мы прикрепляем этот плагин к nuxt.config.js.

module.exports = {
  plugins: [
    { src: "~/plugins/image.js" }
  ]
}

После этого мы можем использовать модуль $image в компоненте Vue следующим образом:

<template>
  <div id="header">
    <img :src="$image('/icon.png')">
  </div>
</template>

Используя этот простой плагин, мы можем поменять местами URL-адреса статического изображения в зависимости от среды, в которой находится машина. Конечно, мне нужно найти и заменить все URL-адреса, что привело к огромным изменениям в нашем git-коммите.

Итак, мы можем изменить URL-адрес статических и динамических ресурсов на основе кода. Затем нам также нужно изменить нашу конфигурацию CI, чтобы убедиться, что все эти файлы. К счастью, мы можем найти эту конфигурацию в документации Nuxt.js. В этой документации мы можем загрузить весь скомпилированный скрипт в папку dist, и нам нужно изменить эту конфигурацию, чтобы все наши статические изображения также загружались в AWS S3. Это довольно просто изменить, нам просто нужно изменить gulp.src('./' + config.distDir + '/**'); на gulp.src(['./' + config.distDir + '/**', './static']);.

Нам также необходимо включить эту задачу gulp в наш Dockerfile через скрипт npm, чтобы она запускалась, когда CI достигает процесса сборки. В наш package.json мы добавляем этот скрипт gulp.

{
  ...
  "scripts": {
    "cdn:deploy": "gulp deploy"
  },
  ...
}

Затем мы добавляем задачу npm в Dockerfile.

# Install application
RUN npm install \
    && npm run build \
    && npm run cdn:deploy

После всех этих изменений теперь мы можем расслабиться, потому что пользователь больше не увидит, что наш веб-сайт сломан во время развертывания CI. Несмотря на то, что процесс кажется завершенным, мы еще не реализовали функцию очистки старых файлов из корзины S3. Так что нам все еще нужно время от времени очищать старые файлы из корзины вручную. Если я каким-то образом найду, как сделать это автоматически, я обновлю этот пост, чтобы отразить эту функциональность.

Вот и все, я надеюсь, что этот пост поможет вам, если в настоящее время у вас есть проблемы с развертыванием CDN с использованием Nuxt.js. Увидимся в моем следующем посте.

ОБНОВИТЬ:

Привет, я нашел способ удалить старые файлы, созданные Nuxt.js. Вы можете увидеть в этом сообщении: https://medium.com/@alfianeffendy/how-we-manage-to-remove-unused-compiled-nuxt-js-files-on-aws-s3-ab3b6cfe2993