diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml new file mode 100644 index 0000000..e444040 --- /dev/null +++ b/.github/workflows/publish-image.yml @@ -0,0 +1,36 @@ +name: Publish Docker image + +on: + push: + branches: + - main + +permissions: + contents: read + packages: write + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Prepare image name + id: image + run: echo "name=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/deploy-app:${GITHUB_SHA}" >> "$GITHUB_OUTPUT" + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.image.outputs.name }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4279c07 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM node:20-alpine + +COPY deploy-app /app +WORKDIR /app + +RUN npm ci + +ENV PORT=3000 +EXPOSE 3000 + +CMD ["npm", "start"] \ No newline at end of file diff --git a/README.md b/README.md index 63a9889..6dff659 100644 --- a/README.md +++ b/README.md @@ -1,135 +1,277 @@ +# Деплой приложения через Docker-образ + +В этом упражнении мы потренируемся доставлять приложение без реального сервера и платного облака. + +Идея: один участник пары собирает Docker-образ и публикует его в Docker Registry, а второй участник скачивает этот образ на своём ноутбуке и запускает приложение. Второй участник не использует исходный код приложения: для него Docker-образ становится готовым артефактом деплоя. + # 0. Подготовка к занятию -Установи себе Docker (можно с помощью Docker Desktop). Для Windows ещё понадобится настроить WSL. +Установи Docker. На Windows можно использовать Docker Desktop, для него также понадобится WSL. + +Проверь, что Docker работает: + +```bash +docker --version +docker run hello-world +``` + +Для основного задания понадобится аккаунт в Docker Hub: https://hub.docker.com/ + +Если Docker Hub недоступен или не получается войти, используй запасной вариант из раздела `7. Передача образа без Docker Registry`. + +# 1. Роли в паре + +Разделитесь на роли: + +- Участник A: собирает и публикует Docker-образ. +- Участник B: скачивает опубликованный образ и запускает приложение на своём ноутбуке. -# 1. Напиши Dockerfile +После прохождения упражнения поменяйтесь ролями. -Основа сборки образа - Dockerfile. В нём описываются шаги по настройке того, что в итоге будет запщуено в Docker. Там можно описывать сборку приложения, установку вспомогательных программ и пакетов, запуск каких-либо команд. Подробнее можно изучить в [документации](https://docs.docker.com/reference/dockerfile/). +Важное ограничение: участник B не должен запускать `npm install`, открывать папку `deploy-app` или собирать приложение из исходного кода. Он получает только имя Docker-образа и команду запуска. -Мы же напишем небольшой Dockerfile, который будет подготавливать окружение для запуска приложения на Express. +# 2. Напиши Dockerfile -Создай в корне репозитория файл под названием `Dockerfile` и вставь в него следующее содержимое: +Основа сборки образа - `Dockerfile`. В нём описываются шаги подготовки окружения для запуска приложения. Подробнее можно изучить в [документации Dockerfile](https://docs.docker.com/reference/dockerfile/). + +Создай в корне репозитория файл `Dockerfile` и вставь в него: + +```dockerfile +FROM node:20-alpine -```docker -FROM node:latest COPY deploy-app /app WORKDIR /app -RUN npm i -ENTRYPOINT [ "npm", "start" ] +RUN npm ci + +ENV PORT=3000 +EXPOSE 3000 + +CMD ["npm", "start"] +``` + +Что здесь происходит: + +- `FROM` задаёт базовый образ. Здесь используется Node.js 20 на Alpine Linux. +- `COPY` копирует приложение из папки `deploy-app` внутрь образа. +- `WORKDIR` задаёт рабочую директорию для следующих команд. +- `RUN npm ci` устанавливает зависимости по `package-lock.json`. +- `ENV PORT=3000` задаёт порт приложения по умолчанию. +- `EXPOSE 3000` документирует, какой порт слушает контейнер. +- `CMD` задаёт команду запуска приложения. + +# 3. Собери и проверь образ локально + +Команды выполняет участник A. + +Собери образ: + +```bash +docker build -t deploy-app:1.0.0 . +``` + +Запусти контейнер: + +```bash +docker run --rm -p 3000:3000 deploy-app:1.0.0 +``` + +Открой в браузере: + +```text +http://localhost:3000 +``` + +Если приложение открылось, останови контейнер через `Ctrl+C`. + +# 4. Опубликуй образ в Docker Hub + +Команды выполняет участник A. + +Войди в Docker Hub: + +```bash +docker login +``` + +Затегируй образ. Вместо `dockerhub_username` подставь свой логин в Docker Hub: + +```bash +docker tag deploy-app:1.0.0 dockerhub_username/deploy-app:1.0.0 +``` + +Опубликуй образ: + +```bash +docker push dockerhub_username/deploy-app:1.0.0 +``` + +После успешной публикации передай участнику B только имя образа: + +```text +dockerhub_username/deploy-app:1.0.0 ``` -Разберёмся, что здесь написано: +# 5. Скачай и запусти образ на втором ноутбуке + +Команды выполняет участник B. -- `FROM` задаёт базовый образ для нашего образа. Базовых образов может быть несколько, следовательно, будет несколько инструкций `FROM`. -- `COPY` копирует файлы и директории с хоста в образ. Первый параметр - путь на хосте, он всегда относительный. Второй - путь в образе, может быть как относительным, так и абсолютным. -- `WORKDIR` задаёт рабочую директорию для выполнения следующих команд. -- `RUN` выполняет описанную далее команду (обычно в shell). В данном случае мы устанавливаем пакеты. -- `ENTRYPOINT` задаёт команду/приложение, которая будет выполнена/которое будет запущено при старте контейнера из нашего образа. В нашем случае мы запускаем приложение с помощью npm start. +Скачай образ. Вместо `dockerhub_username` используй логин участника A: -# 2. Собери образ и запусти контейнер +```bash +docker pull dockerhub_username/deploy-app:1.0.0 +``` -Попробуем теперь собрать образ. В директории с Dockerfile напиши команду +Запусти контейнер: ```bash -docker build -t my-app . +docker run --rm -p 3000:3000 dockerhub_username/deploy-app:1.0.0 +``` + +Открой в браузере: + +```text +http://localhost:3000 ``` -`-t` - так называемый "тег" образа, метка, по которому образ проще будет идентифицировать. Без него будет образ будет идентифицироваться по SHA-хэшу. Обычно в тег пишут ещё и версию образа. -`.` - контекст для билда, в нашем случае он равен текущей директории. +Если приложение открылось, деплой через Docker-образ выполнен успешно. + +# 6. Обнови приложение и выпусти новую версию -После сборки образа скопируй название образа из вывода команды `docker build` и запусти контейнер с помощью +Теперь участник A должен изменить приложение и выпустить новую версию образа. + +Например, можно изменить текст на главной странице в файле `deploy-app/routes/index.js`. + +После изменения собери новую версию: ```bash -docker run название_образа +docker build -t deploy-app:1.0.1 . ``` -Должны появиться логи запуска Express-приложения. +Затегируй и опубликуй её: -# 3. Yandex Cloud +```bash +docker tag deploy-app:1.0.1 dockerhub_username/deploy-app:1.0.1 +docker push dockerhub_username/deploy-app:1.0.1 +``` -Поскольку нам доступен этот провайдер, научимся деплоить в него. -Для этого открой https://console.yandex.cloud/ и в облаке `fiit-urfu-cloud` (если оно доступно) создай каталог с любым названием: -![1.png](screenshots/1.png) +Участник B скачивает и запускает новую версию: -После этого перейди в созданный каталог, потом во вкладку Сервисные аккаунт, перейди оттуда в `Identity and Access Management`. Создай там сервисный аккаунт. Можно сразу навесить на него роль `admin`. -![2.png](screenshots/2.png) +```bash +docker pull dockerhub_username/deploy-app:1.0.1 +docker run --rm -p 3000:3000 dockerhub_username/deploy-app:1.0.1 +``` -Нажми на него, затем на "Создать новый ключ" -> "Создать авторизованный ключ". -![3.png](screenshots/3.png) -Сохрани себе json-файл с ключами, он пригодится для настройки GitHub Actions. +Проверьте, что в браузере видна обновлённая версия приложения. -Теперь перейди обратно в каталог ресурсов, нажми на "Создать ресурс". -![4.png](screenshots/4.png) +# 7. Передача образа без Docker Registry -Выбери `Реестр Docker-образов`, `Создать реестр`, назови как угодно. Галочки со сканирования можно снять, если не хочешь тратить дополнительные средства на них, но рекомендуется оставить, как есть. -Сохрани себе идентификатор реестра. +Этот вариант нужен, если Docker Hub недоступен или нет аккаунта. -# 4. Настройка секретов в GitHub +Участник A сохраняет образ в файл: -В своём репозитории выбери вкладку `Settings` и найди пункт `Secrets and variables`: -![5.png](screenshots/5.png) +```bash +docker build -t deploy-app:1.0.0 . +docker save deploy-app:1.0.0 -o deploy-app.tar +``` -Выбери `Actions` и нажми на `New repository secret`: -![6.png](screenshots/6.png) +После этого файл `deploy-app.tar` нужно передать участнику B любым удобным способом. -Назови секрет YC_SA_JSON_CREDENTIALS и вставь содержимое JSON-файла с ключами с прошлого шага. +Участник B загружает образ из файла: -# 5. Деплой через GitHub Actions +```bash +docker load -i deploy-app.tar +``` -Все пайплайны в GitHub Actions должны лежать в папке `.github/workflows` в корне репозитория. +Проверяет, что образ появился локально: -Создай эту директорию, создай в ней файл с произвольным именем и расширением `.yml`. Вставь в него следующее содержимое: +```bash +docker images +``` + +Запускает приложение: + +```bash +docker run --rm -p 3000:3000 deploy-app:1.0.0 +``` + +Этот способ хуже похож на реальный деплой, потому что образ передаётся файлом. Но он всё равно показывает главный принцип: приложение можно доставить и запустить без установки зависимостей вручную. + +# 8. Дополнительное задание: публикация образа через GitHub Actions + +В реальных проектах образ обычно собирается не на ноутбуке разработчика, а в CI/CD. + +В этом задании GitHub Actions будет автоматически собирать Docker-образ и публиковать его в GitHub Container Registry. + +Создай файл `.github/workflows/publish-image.yml`: ```yaml -on: push +name: Publish Docker image + +on: + push: + branches: + - main + permissions: contents: read - pages: write - id-token: write + packages: write + jobs: - yc_deploy_job: + publish: runs-on: ubuntu-latest - name: A job to deploy to Yandex Cloud + steps: - - name: checkout + - name: Checkout repository uses: actions/checkout@v4 - - name: Login to Yandex Cloud Container Registry - id: login-cr - uses: yc-actions/yc-cr-login@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 with: - yc-sa-json-credentials: ${{ secrets.YC_SA_JSON_CREDENTIALS }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Build, tag, and push image to Yandex Cloud Container Registry - env: - CR_REGISTRY: aaaaaaaaa - CR_REPOSITORY: my-cr-repo - IMAGE_TAG: ${{ github.sha }} - run: | - docker build -t cr.yandex/$CR_REGISTRY/$CR_REPOSITORY:$IMAGE_TAG . - docker push cr.yandex/$CR_REGISTRY/$CR_REPOSITORY:$IMAGE_TAG + - name: Prepare image name + id: image + run: echo "name=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/deploy-app:${GITHUB_SHA}" >> "$GITHUB_OUTPUT" + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.image.outputs.name }} ``` -Что здесь происходит: +Сделай commit и push в GitHub. После успешного выполнения workflow образ появится в Packages. + +Чтобы второй участник смог скачать образ без авторизации, package нужно сделать публичным в настройках GitHub Packages. + +После этого участник B сможет выполнить: + +```bash +docker pull ghcr.io/github_username/deploy-app:commit_sha +docker run --rm -p 3000:3000 ghcr.io/github_username/deploy-app:commit_sha +``` -- workflow будет запускаться на каждый пуш; -- задаются разрешения на некоторые действия, нас интересует `id-token` (чтобы можно было его использовать для обращения к Yandex Cloud); -- создаётся job с заданными именем и описанием и набором шагов: - - склонировать репозиторий на сборочный агент; - - аутентифицироваться в Yandex Cloud с помощью yc-sa-json-credentials (это тот самый JSON с ключами, который получили на третьем шаге); - - собрать и запушить образ в заданный с помощью переменной CR_REGISTRY реестр Docker-образов. - Обрати внимание: в CR_REGISTRY нужно записать идентификатор созданного на третьем шаге реестра. +# 9. Контрольные вопросы -Сделай commit и push в свой репозиторий. Проверь, что пайплайн прошёл успешно (вкладка Actions). +Ответь на вопросы после выполнения задания: -# 6. Создание serverless-контейнера в Yandex Cloud +- Чем Docker-образ отличается от контейнера? +- Почему участнику B не нужно устанавливать Node.js и зависимости проекта? +- Зачем образу нужен тег? +- Что изменится, если опубликовать новую версию с тем же тегом `latest`? +- Чем передача через Docker Registry лучше передачи файла `deploy-app.tar`? +- Что в этом упражнении является аналогом деплоя на сервер? -Теперь создадим serverless-контейнер (чтобы не возиться самим с настройкой VM, Docker, DNS и прочего). -Перейди в консоль Yandex Cloud, выбери свой каталог и создай ресурс типа `Serverless контейнер`. Задай ему имя и описание. +# 10. Что должно получиться -Оставь параметры по умолчанию и выбери в параметрах образа сначала свой реестр, затем - запушенный образ. -![7.png](screenshots/7.png) -![8.png](screenshots/8.png) +В конце упражнения у пары должно быть: -Нажми на `Создать ревизию`. После этого дождись создания контейнера. Если в нём не окажется ревизий - нажми на `Создать ревизию` и выполни настройку ещё раз. После создания ревизии нажми на ссылку, которую даёт Yandex Cloud и проверь, что открывается приложение на Express. +- `Dockerfile` в корне репозитория; +- локально собранный Docker-образ; +- опубликованный образ в Docker Hub или GHCR; +- приложение, запущенное на втором ноутбуке без исходного кода; +- понимание, что Docker-образ можно использовать как переносимый артефакт деплоя. diff --git a/deploy-app.tar b/deploy-app.tar new file mode 100644 index 0000000..d30544f Binary files /dev/null and b/deploy-app.tar differ