В Kinsta у нас есть проекты всех размеров для хостинга приложений, хостинга баз данных и управляемого хостинга WordPress.

С помощью решений облачного хостинга Kinsta вы можете развертывать приложения на нескольких языках и платформах, таких как NodeJS, PHP, Ruby, Go, Scala и Python. С помощью Dockerfile вы можете развернуть любое приложение. Вы можете подключить свой репозиторий Git (размещенный на GitHub, GitLab или Bitbucket), чтобы развернуть свой код непосредственно в Kinsta.

Вы можете разместить базы данных MariaDB, Redis, MySQL и PostgreSQL «из коробки», что сэкономит вам время, чтобы сосредоточиться на разработке приложений, а не мучиться с конфигурациями хостинга.

А если вы выберете наш управляемый хостинг WordPress, вы ощутите мощь компьютеров Google Cloud C2 в сети уровня Premium и безопасность, интегрированную с Cloudflare, что сделает ваши веб-сайты WordPress самыми быстрыми и безопасными на рынке.

Преодоление проблемы разработки облачных приложений в распределенной команде

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

  • Разработчики и группы обеспечения качества, независимо от своих операционных систем, создают простую и минимальную настройку для разработки и тестирования функций.
  • Команды DevOps, SysOps и Infra для настройки и обслуживания промежуточных и производственных сред.

В Kinsta мы во многом полагаемся на Docker для обеспечения стабильного опыта на каждом этапе, от разработки до производства. В этом посте мы познакомим вас с:

  • Как использовать Docker Desktop для повышения продуктивности разработчиков.
  • Как мы создаем образы Docker и передаем их в реестр контейнеров Google через конвейеры CI с помощью CircleCI и GitHub Actions.
  • Как мы используем конвейеры компакт-дисков для продвижения дополнительных изменений в производство с помощью образов Docker, Google Kubernetes Engine и Cloud Deploy.
  • Как команда контроля качества эффективно использует готовые образы Docker в различных средах.

Использование Docker Desktop для улучшения опыта разработчиков

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

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

Объявите свою конфигурацию с помощью Docker Compose

Для начала создайте файл Docker Compose, docker-compose.yml. Это декларативный файл конфигурации, написанный в формате YAML, который сообщает Docker, какое желаемое состояние вашего приложения. Docker использует эту информацию для настройки среды для вашего приложения.

Файлы Docker Compose очень удобны, когда у вас работает более одного контейнера и между контейнерами существуют зависимости.

Чтобы создать свой docker-compose.yml файл:

  1. Начните с выбора image в качестве основы для нашего приложения. Выполните поиск в Docker Hub и попытайтесь найти образ Docker, который уже содержит зависимости вашего приложения. Обязательно используйте определенный тег изображения, чтобы избежать ошибок. Используя latest тег может вызвать непредвиденные ошибки в вашем приложении. Вы можете использовать несколько базовых образов для нескольких зависимостей. Например, один для PostgreSQL и один для Redis.
  2. Использовать volumes чтобы сохранить данные на вашем хосте, если вам нужно. Сохранение данных на хост-компьютере помогает избежать потери данных в случае удаления контейнеров докеров или необходимости их воссоздания.
  3. Использовать networks чтобы изолировать вашу установку, чтобы избежать сетевых конфликтов с хостом и другими контейнерами. Это также помогает вашим контейнерам легко находить и взаимодействовать друг с другом.

Объединив все вместе, мы имеем docker-compose.yml это выглядит так:

version: '3.8'services:
  db:
    image: postgres:14.7-alpine3.17
    hostname: mk_db
    restart: on-failure
    ports:
      - ${DB_PORT:-5432}:5432
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: ${DB_USER:-user}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
      POSTGRES_DB: ${DB_NAME:-main}
    networks:
      - mk_network
  redis:
    image: redis:6.2.11-alpine3.17
    hostname: mk_redis
    restart: on-failure
    ports:
      - ${REDIS_PORT:-6379}:6379
    networks:
      - mk_network
      
volumes:
  db_data:

networks:
  mk_network:
    name: mk_network

Контейнеризация приложения

Создайте образ Docker для вашего приложения

Сначала нам нужно создать образ Docker, используя Dockerfileа затем вызовите это из docker-compose.yml.

Чтобы создать свой Dockerfile файл:

  1. Начните с выбора изображения в качестве основы. Используйте наименьшее базовое изображение, подходящее для приложения. Обычно образы Alpine очень минимальны, при этом почти не установлено никаких дополнительных пакетов. Вы можете начать с образа Alpine и опираться на него:
    FROM node:18.15.0-alpine3.17
    
  2. Иногда вам нужно использовать определенную архитектуру ЦП, чтобы избежать конфликтов. Например, предположим, что вы используете arm64-based процессор, но вам нужно построить amd64 изображение. Вы можете сделать это, указав -- platform в Dockerfile:
    FROM --platform=amd64 node:18.15.0-alpine3.17
    
  3. Определите каталог приложения, установите зависимости и скопируйте выходные данные в свой корневой каталог:
    WORKDIR /opt/app 
    COPY package.json yarn.lock ./ 
    RUN yarn install 
    COPY . .
  4. Позвоните в Dockerfile от docker-compose.yml:
    services:
      ...redis
      ...db
      
      app:
        build:
          context: .
          dockerfile: Dockerfile
        platforms:
          - "linux/amd64"
        command: yarn dev
        restart: on-failure
        ports:
          - ${PORT:-4000}:${PORT:-4000}
        networks:
          - mk_network
        depends_on:
          - redis
          - db
  5. Внедрите автоматическую перезагрузку, чтобы при изменении чего-либо в исходном коде вы могли немедленно просмотреть свои изменения без необходимости пересобирать приложение вручную. Для этого сначала создайте образ, а затем запустите его в отдельном сервисе:
    services:
      ... redis
      ... db
      
      build-docker:
        image: myapp
        build:
          context: .
          dockerfile: Dockerfile
      app:
        image: myapp
        platforms:
          - "linux/amd64"
        command: yarn dev
        restart: on-failure
        ports:
          - ${PORT:-4000}:${PORT:-4000}
        volumes:
          - .:/opt/app
          - node_modules:/opt/app/node_modules
        networks:
          - mk_network
        depends_on:
          - redis
          - db
          - build-docker
          
    volumes:
      node_modules:

Совет для профессионалов: обратите внимание, что node_modules также монтируется явно, чтобы избежать проблем с пакетами, специфичных для платформы. Это означает, что вместо использования node_modules на хосте Docker-контейнер использует свой собственный, но отображает его на хосте в отдельном томе.

Постепенно создавайте рабочие образы с помощью непрерывной интеграции

Большинство наших приложений и сервисов используют для развертывания CI/CD. Docker играет важную роль в этом процессе. Каждое изменение в основной ветке немедленно запускает конвейер сборки через GitHub Actions или CircleCI. Общий рабочий процесс очень прост: он устанавливает зависимости, запускает тесты, создает образ докера и отправляет его в реестр контейнеров Google (или реестр артефактов). Часть, которую мы обсуждаем в этой статье, — это этап сборки.

Создание образов Docker

Мы используем многоэтапные сборки из соображений безопасности и производительности.

Этап 1: Строитель

На этом этапе мы копируем всю базу кода со всем исходным кодом и конфигурацией, устанавливаем все зависимости, включая зависимости от разработчиков, и собираем приложение. Это создает dist/ папку и копирует туда собранную версию кода. Но это изображение слишком велико и имеет огромный набор следов, чтобы его можно было использовать в производстве. Кроме того, поскольку мы используем частные реестры NPM, мы используем наши частные NPM_TOKEN и на этом этапе. Итак, мы определенно не хотим, чтобы эта сцена была открыта внешнему миру. Единственное, что нам нужно на этом этапе, это dist/ папка.

Этап 2: Производство

Большинство людей используют этот этап во время выполнения, поскольку он очень близок к тому, что нам нужно для запуска приложения. Однако нам все равно необходимо установить производственные зависимости, а это означает, что мы оставляем следы и нуждаемся в NPM_TOKEN. Так что этот этап еще не готов к раскрытию. Также обратите внимание на yarn cache clean в строке 19. Эта крошечная команда сокращает размер нашего изображения до 60%.

Этап 3: Время выполнения

Последняя ступень должна быть как можно более тонкой и занимать минимум места. Поэтому мы просто копируем готовое приложение из производства и идем дальше. Вкладываем всю эту пряжу и NPM_TOKEN все позади и запускайте только приложение.

Это финал Dockerfile.production:

# Stage 1: build the source code 
FROM node:18.15.0-alpine3.17 as builder 
WORKDIR /opt/app 
COPY package.json yarn.lock ./ 
RUN yarn install 
COPY . . 
RUN yarn build 

# Stage 2: copy the built version and build the production dependencies FROM node:18.15.0-alpine3.17 as production 
WORKDIR /opt/app 
COPY package.json yarn.lock ./ 
RUN yarn install --production && yarn cache clean 
COPY --from=builder /opt/app/dist/ ./dist/ 

# Stage 3: copy the production ready app to runtime 
FROM node:18.15.0-alpine3.17 as runtime 
WORKDIR /opt/app 
COPY --from=production /opt/app/ . 
CMD ["yarn", "start"]

Обратите внимание, что на всех этапах мы начинаем копирование package.json и yarn.lock сначала файлы, устанавливая зависимости, а затем копируя остальную часть базы кода. Причина в том, что Docker строит каждую команду как слой поверх предыдущей. И каждая сборка может использовать предыдущие уровни, если они доступны, и создавать новые уровни только в целях повышения производительности.

Допустим, вы что-то изменили в src/services/service1.ts не прикасаясь к пакетам. Это означает, что первые четыре слоя этапа построения нетронуты и могут быть использованы повторно. Это значительно ускоряет процесс сборки.

Передача приложения в реестр контейнеров Google через конвейеры CircleCI

Существует несколько способов создания образа Docker в конвейерах CircleCI. В нашем случае мы решили использовать circleci/gcp-gcr orbs:

executors:
  docker-executor:
    docker:
      - image: cimg/base:2023.03
orbs:
  gcp-gcr: circleci/[email protected]
jobs:
  ...
  deploy:
    description: Build & push image to Google Artifact Registry
    executor: docker-executor
    steps:
      ...
      - gcp-gcr/build-image:
          image: my-app
          dockerfile: Dockerfile.production
          tag: ${CIRCLE_SHA1:0:7},latest
      - gcp-gcr/push-image:
          image: my-app
          tag: ${CIRCLE_SHA1:0:7},latest

Для создания и распространения нашего приложения требуется минимальная конфигурация благодаря Docker.

Публикация приложения в реестре контейнеров Google с помощью действий GitHub

В качестве альтернативы CircleCI мы можем использовать GitHub Actions для непрерывного развертывания приложения. Мы создали gcloud а также создайте и отправьте образ Docker в gcr.io:

jobs:
  setup-build:
    name: Setup, Build
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v3

    - name: Get Image Tag
      run: |
        echo "TAG=$(git rev-parse --short HEAD)" >> $GITHUB_ENV

    - uses: google-github-actions/setup-gcloud@master
      with:
        service_account_key: ${{ secrets.GCP_SA_KEY }}
        project_id: ${{ secrets.GCP_PROJECT_ID }}

    - run: |-
        gcloud --quiet auth configure-docker

    - name: Build
      run: |-
        docker build \
          --tag "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:$TAG" \
          --tag "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:latest" \
          .

    - name: Push
      run: |-
        docker push "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:$TAG"
        docker push "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:latest"

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

Развертывание изменений в Google Kubernetes Engine с помощью конвейеров доставки Google

Наличие готовых к использованию образов Docker для каждого изменения также упрощает развертывание в рабочей среде или откат в случае, если что-то пойдет не так. Мы используем Google Kubernetes Engine для управления и обслуживания наших приложений, а также используем Google Cloud Deploy и Delivery Pipelines для процесса непрерывного развертывания.

Когда образ Docker создается после каждого небольшого изменения (с показанным выше конвейером CI), мы делаем еще один шаг вперед и развертываем изменение в нашем кластере разработки, используя gcloud. Давайте посмотрим на этот шаг в конвейере CircleCI:

- run:
    name: Create new release
    command: gcloud deploy releases create release-${CIRCLE_SHA1:0:7} --delivery-pipeline my-del-pipeline --region $REGION --annotations commitId=$CIRCLE_SHA1 --images my-app=gcr.io/${PROJECT_ID}/my-app:${CIRCLE_SHA1:0:7}

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

Какую пользу от этого процесса получит команда обеспечения качества

Группе контроля качества в основном нужны предварительные облачные версии приложений для тестирования. Однако иногда им необходимо локально запустить предварительно созданное приложение (со всеми зависимостями), чтобы протестировать определенную функцию. В этих случаях они не хотят или не должны проходить через все трудности клонирования всего проекта, установки пакетов npm, сборки приложения, сталкиваться с ошибками разработчика и проходить весь процесс разработки, чтобы запустить приложение. Теперь, когда все уже доступно в виде образа Docker в реестре контейнеров Google, все, что им нужно, — это сервис в файле компоновки Docker:

services:
  ...redis
  ...db
  
  app:
    image: gcr.io/${PROJECT_ID}/my-app:latest
    restart: on-failure
    ports:
      - ${PORT:-4000}:${PORT:-4000}
    environment:
      - NODE_ENV=production
      - REDIS_URL=redis://redis:6379
      - DATABASE_URL=postgresql://${DB_USER:-user}:${DB_PASSWORD:-password}@db:5432/main
    networks:
      - mk_network
    depends_on:
      - redis
      - db

С помощью этой службы они могут запускать приложение на своих локальных компьютерах с помощью контейнеров Docker, запустив:

docker compose up

Это огромный шаг к упрощению процессов тестирования. Даже если отдел контроля качества решит протестировать определенный тег приложения, он сможет легко изменить тег изображения в строке 6 и повторно запустить команду Docker Compose. Даже если они решат сравнить разные версии приложения одновременно, они легко смогут добиться этого с помощью нескольких настроек. Самое большое преимущество — это избавление нашей команды контроля качества от проблем, связанных с разработчиками.

Преимущества использования Docker

  • Почти нулевой след для зависимостей: Если вы когда-нибудь решите обновить версию Redis или Postgres, вы можете просто изменить одну строку и перезапустить приложение. Не нужно ничего менять в вашей системе. Кроме того, если у вас есть два приложения, которым нужен Redis (возможно, даже с разными версиями), вы можете оба работать в своей изолированной среде без каких-либо конфликтов друг с другом.
  • Несколько экземпляров приложения: Во многих случаях нам нужно запустить одно и то же приложение с другой командой. Например, инициализация БД, запуск тестов, наблюдение за изменениями БД или прослушивание сообщений. В каждом из этих случаев, поскольку у нас уже есть готовый собранный образ, мы просто добавляем еще один сервис в файл компоновки Docker с помощью другой команды, и все готово.
  • Упрощенная среда тестирования: Чаще всего вам просто нужно запустить приложение. Вам не нужен код, пакеты или какие-либо подключения к локальной базе данных. Вам нужно только убедиться, что приложение работает правильно, или вам нужен работающий экземпляр в качестве серверной службы, пока вы работаете над собственным проектом. Это также может относиться к специалистам по обеспечению качества, рецензентам запросов на включение или даже специалистам по UX, которые хотят убедиться, что их дизайн реализован правильно. Наша настройка докера позволяет всем им легко работать без необходимости решать слишком много технических проблем.

Эта статья была первоначально опубликована на Docker.