Заголовок намагається підсумувати безліч дрібних деталей всього кількома словами.
Повний код можна побачити тут, і, коротко кажучи, ідея цієї статті — показати спосіб створення:
- докеризованої повноцінної стекової програми
- використання робочих простірів для спільного використання коду між
api
таui
- доступність для всіх за допомогою Elastic Beanstalk
- використання docker-compose з nginx для зворотного проксі та запуску лише одного екземпляра
- автоматизація за допомогою GitHub Actions
Ідея цієї повноцінної стекової програми:
- пакет для express
api
з простим кореневим маршрутом, що повертає об'єкт типу Pong - пакет для програми Remix, що споживає
api
просто отримуючи даніapi
і відображаючи його текст - пакет для моделі pong
Стаття буде поділена на кілька розділів:
- базова конфігурація робочих просторів
- докеризація
api
таui
(створення Dockerfile для кожного) - створення docker-compose yaml для локального тестування та майбутнього розгортання в Elastic Beanstalk
- створення конфігурації nginx для зворотного проксі, яку буде використовувати Elastic Beanstalk
- створення GitHub Action для завантаження образів до ECR
- налаштування
ebcli
(Elastic Beanstalk CLI) - налаштування облікових даних AWS для використання в GitHub Actions
- автоматизація процесу в GitHub Actions
- налаштування healthd для роботи в новій структурі
Перший важливий крок
Одне важливе, що потрібно зробити перед усім іншим — ваш репозиторій на GitHub повинен мати налаштовану організацію. Ви завжди можете імпортувати будь-який репозиторій до вашої організації, але якщо ви починаєте з нуля, набагато зручніше створити її одразу.
Базова конфігурація робочих просторів
Загальна ідея використання робочих просторів полягає в тому, щоб спростити спільне використання коду між модулями, створити чіткіше розмежування між пакетами, кодом та доменом і повторно використовувати моделі. Ідея моделей полягає в тому, щоб мати структуру об'єкта, що використовується, отже, в нашому випадку ми створимо просту модель, що визначає, що тип Pong має строкове поле pong.
export type Pong = {
pong: string;
};
У нашому дуже простому випадку ми будемо ділитися пакетом models
між api
та ui
, що дасть нам впевненість у правильності результату з типами в api
, що буде вірно типізовано вui
.
Оскільки в цьому проекті є багато різних понять, я надам посилання для їх правильної конфігурації. Наприклад, перше, що потрібно зробити, — це використати функціонал робочих просторів yarn, що є таким простим, як визначення наступного в packages.json
:
{
"workspaces": [
"packages/*"
]
}
І ваша структура проекту має виглядати ось так:
packages
> api
> package.json // посилається на models
> ui
> package.json // посилається на models
> models
packages.json
yarn.lock
Щоб використовувати models
у пакетах api
або ui
, ви просто посилаєтеся на нього в їхніх файлах package.json
.
Ідея полягає в тому, що при запуску yarn
у кореневій папці, він шукатиме спочатку бібліотеки, на які є посилання в папці пакунків. Таким чином, ви зможете використовувати моделі як нижче, у проектах api
та ui
.
import { Pong } from 'models'
Докеризація пакунків
При створенні Dockerfile важливо знати, що його контекст базується на його розташуванні, тому щоб спростити процес, ми розмістимо Docker-файли поруч з папками пакунків:
packages
> api
> ui
> models
api.Dockerfile
ui.Dockerfile
package.json
Таким чином, ми зможемо отримати доступ до всього необхідного у наших Dockerfile. Docker-файли в цьому проекті використовують багатошарові зображення alpine та інші аспекти, що виходять за межі цієї статті, але ідея полягає в тому, щоб мати мінімальні вимоги для роботи продакшн-образу, тому можна побачити поділ між етапами створення та фінальним образом.
Ви можете подивитися Docker-файли як приклад, але ідея в тому, щоб запустити ваш додаток у найпростішому можливому середовищі.
Для створення образів достатньо виконати команду:
docker build -f ./packages/ui.Dockerfile . -t this-is-the-ui-image-name
Після того, як обидва образи будуть створені, ми можемо вказати їх у нашому docker.compose.yml
.
Docker Compose
Ідея Docker Compose на даний момент полягає в налаштуванні комунікації між усіма частинами програми. Нам знадобиться Nginx, щоб мати зворотний проксі, створивши для нього окрему конфігурацію.
Зворотний проксі — це сервер, який стоїть перед іншими серверами та перенаправляє запити клієнтів на ці інші сервери. У нашому випадку нижче ми налаштуємо наш сервер так, щоб він приймав запити та, залежно від шляху, перенаправляв їх на конкретні сервіси.
upstream ui {
server this-is-the-ui-service-name:3000;
}
upstream api {
server this-is-the-api-service-name:5000;
}
server {
listen 80;
server_name _;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
location / {
proxy_pass http://ui/;
}
location /api/ {
proxy_pass http://api/;
}
}
Як видно з конфігураційного файлу, ми вказуємо:
- кореневий шлях
/
для перенаправлення на сервісui
- а
/api
— для перенаправлення на сервісapi
З'єднання між сервісами здійснюється за допомогою змінної середовища API_URL
в назві сервісу docker для ui
, це використовується в методі fetch.
Створення GitHub Action для завантаження образів до ECR
Після того, як Docker-образи були створені та протестовані локально, настав час завантажити їх до сховища образів.
У цій статті ми будемо використовувати все на AWS, тому будемо використовувати Elastic Container Registry (ECR).
Для цього ми використаємо офіційну дію від AWS, яка вимагає, щоб AWS довіряв OpenID Connect (OIDC) від GitHub для доступу до своїх ресурсів.
Найкоротший спосіб пояснити ці кроки та весь процес полягає в аутентифікації в GitHub Workflows, щоб мати можливість керувати та працювати з сервісами AWS, наприклад, для відправлення образу до ECR. Для цього має існувати довірчі відносини між GitHub (які налаштовуються навіть шляхом визначення того, який репозиторій відповідає за це) і вашим обліковим записом AWS.
- відкрийте консоль IAM.
- у лівому меню навігації виберіть Identity providers
- У панелі Identity providers виберіть Add Provider
- для Provider type виберіть OpenID Connect
- для Provider URL введіть URL GitHub OIDC IDP https://token.actions.githubusercontent.com
- виберіть Get thumbprint, щоб перевірити сертифікат вашого IDP. Щоб дізнатися більше про OIDC thumbprints, перегляньте Отримання відбитка для OpenID Connect Identity Provider.
- для Audience введіть sts.amazonaws.com
Для цілей цієї статті ми створимо політику, яка буде використовуватися в ролі, створеній нижче, з повним доступом до Elastic Container, але настійно рекомендується прикріплювати лише необхідні політики.
Після створення постачальника ідентичності, час створювати роль:
- перейдіть у roles > create role > виберіть постачальника ідентичності, якого ви щойно створили для GitHub > виберіть sts Amazon audience
- ваш проєкт має бути налаштований для роботи з організацією
- виберіть ваш репозиторій
- прикріпіть створену політику вище та заверште створення ролі
Перед створенням Workflow вам слід також створити ваш репозиторій у ECR, що є дуже простим процесом: просто зайдіть туди і створіть ваш репозиторій. Його назва буде використовуватися в Workflow.
Важливі частини Workflow, що стосуються процесу OIDC — це вхідний параметр role-to-assume
:
- name: Configure AWS credentials from Test account
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: THE_ARN_OF_THE_ROLE_YOU_CREATED_ABOVE
aws-region: us-east-1
Логін до ECR, який використовує вище згадану дію для входу до ECR:
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
Після виконання цих двох кроків можна буде побудувати та відправити ваші образи до ECR. У репозиторії цієї статті використовується скрипт, який використовує REPOSITORY
та PACKAGE_NAME
для створення тегу образу і його відправлення в ECR.
- name: Sending UI Docker image
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
# ці змінні використовуються для репозиторію цієї статті
# ви можете мати власний спосіб, навіть вручну написаний скрипт для цього
REPOSITORY: THE_NAME_OF_YOUR_ECR_REGISTRY
PACKAGE_NAME: ui
run: yarn push-docker
set -e
TAG=$PACKAGE_NAME-$GITHUB_REF_NAME
build_docker_image() {
echo "Building docker image $REGISTRY/$REPOSITORY:$TAG"
docker build -t $REGISTRY/$REPOSITORY:$TAG -f packages/$PACKAGE_NAME.Dockerfile .
}
push_docker_image_to_ecr() {
echo "Pushing Docker image - $REGISTRY/$REPOSITORY:$TAG"
docker push $REGISTRY/$REPOSITORY:$TAG
}
Налаштування ebcli
Цей крок не є обов'язковим, все можна робити через консоль Amazon, але для тестування він досить зручний.
Найпростіший спосіб зробити це — створити користувача з правильними правами для маніпулювання Elastic Beanstalk.
- перейдіть до AWS Console > перейдіть до IAM > перейдіть до Users > Create User
- назвіть його будь-як, наприклад, eb-cli
- прикріпіть політику AdministratorAccess-AWSElasticBeanstalk (це просто для зручності, ви можете вибрати лише конкретні політики AWSElasticBeanstalk, які вам потрібні)
- після створення виберіть користувача та створіть ключ доступу
- виберіть Command Line Interface (CLI)
- скопіюйте значення та додайте їх у ваш
~/.aws/config
[profile eb-cli]
aws_access_key_id = YOUR_ACCESS_KEY_ID
aws_secret_access_key = YOUR_AWS_SECRET_ACCESS_KEY
Після налаштування вашого профілю eb-cli
ви тепер можете виконувати команди ebcli.
- перейдіть до вашої папки проєкту та виконайте
eb init
- він запитає вас деякі питання, такі як регіон, застосунок для використання та ім'я застосунку, яке ви можете вибрати за своїми потребами. Важливою частиною є те, що коли він запитає про платформу, він може визначити, що ви використовуєте Node або інше (залежно від структури вашого проєкту), оскільки ми будемо використовувати Docker, просто скажіть "No" і виберіть опцію
Docker
Це створить папку .elasticbeastalk
та файл config.yml
у вашому проєкті і додасть деякі рядки до вашого gitignore
. Ідея цього файлу в тому, що він буде використовувати ці налаштування для взаємодії з сервісом Elastic Beanstalk. Після цього кроку навіть буде створено застосунок на AWS, якщо ви перейдете до консолі AWS.
Просунуте використання ebcli через його налаштування
Ось де починається цікава частина: існує безліч способів створення середовища, яке в кінцевому підсумку є вашим екземпляром EC2 для застосунку, який працює і доступний для всього світу. Ми будемо використовувати глобальні налаштування з папки .elasticbeanstalk
.
Після виконання команди eb init
вже буде створено конфігураційний файл.
Ми збираємось перейменувати або створити файл config.global.yml у папці .elasticbeanstalk
. Ми назвали його "global", оскільки після виконання попередніх команд деякі рядки були додані до .gitignore
, і як ви можете побачити, шаблон *.global.yml
не ігнорується.
# Elastic Beanstalk Files
.elasticbeanstalk/*
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml
Тепер ми додаємо атрибут deploy, що вказує на zip-файл, який буде створений пізніше шляхом додавання файлів docker-compose.yml
та конфігураційного файлу для nginx.
Іншим важливим моментом є атрибут branch-defaults
, що важливий, тому що коли ми виконуємо команду eb deploy
, нам не потрібно вказувати середовище, оскільки це робиться за допомогою гілки. Коли це виконується в гілці main
, система точно знає, яке середовище потрібно використовувати. Однак, ви завжди можете вказати середовище у вашій команді розгортання. Така конфігурація з гілками та середовищами є корисною, особливо коли у вас є, скажімо, гілка для розробки, яка розгортається в одне середовище, а інша — в продукційне.
deploy:
artifact: the_name_of_your_deploy.zip
branch-defaults:
main:
environment: docker-elastic-beanstalk-up-dev
global:
application_name: docker-elastic-beanstalk-up
branch: null
default_ec2_keyname: null
default_platform: Docker running on 64bit Amazon Linux 2023
default_region: us-east-1
include_git_submodules: true
instance_profile: null
platform_name: null
platform_version: null
profile: eb-cli
repository: null
sc: git
workspace_type: Application
Оскільки ми будемо розгортати за допомогою docker-compose
, змінимо зображення в файлі docker-compose.yml
, щоб вони вказували на відповідні URI зображень ECR, створених на попередньому етапі цієї статті, а потім створимо zip-файл для розгортання через EB.
zip deploy.zip docker-compose.yml default.conf -r
Якщо ви слідуєте за цією статтею, у вас є лише застосунок у EB без середовищ, тому після того, як zip-файл буде створено, ми повинні мати змогу виконати команду eb create
. Він запитає деякі питання, такі як ім'я середовища, і створить середовище, використовуючи файл розгортання, що ми налаштували в .elasticbeanstalk/config.global.yml
. Це займе деякий час, і якщо все пройде добре, ви побачите таке повідомлення:
INFO Successfully launched environment: docker-elastic-beanstalk-up-dev
Після цього ви можете виконати eb open
, і це відкриє URL вашого застосунку.
Автоматизація процесу в GitHub actions
Коли все працює, і ви можете виконати eb open
, наступним кроком буде автоматизація цього процесу за допомогою CI/CD, щоб все працювало автоматично. Ми частково завершили це завдання, оскільки вже налаштували частину з ECR. Тепер нам потрібно додати етап з zip-файлом та команду eb deploy
в GitHub workflow.
Так само, як і локально для eb cli
, для роботи в workflow потрібно налаштувати AWS credentials. На сьогодні немає офіційного способу зробити це в поточних actions, тому ми можемо використати такий етап після кроку aws-actions/amazon-ecr-login@v2:
- name: Add profile credentials to ~/.aws/credentials
run: |
aws configure set aws_access_key_id ${{ env.AWS_ACCESS_KEY_ID }} --profile eb-cli
aws configure set aws_secret_access_key ${{ env.AWS_SECRET_ACCESS_KEY }} --profile eb-cli
aws configure set aws_session_token ${{ env.AWS_SESSION_TOKEN }} --profile eb-cli
Але для того, щоб виконати вищезгадане, нам також потрібно додати політику до ролі, яку ми створили раніше, щоб вона могла працювати з Elastic Beanstalk. Хоча це не найкраща практика, оскільки рекомендується створити окрему роль для цього, для цієї статті та для простоти ми просто прикріпимо політику AdministratorAccess-AWSElasticBeanstalk до нашої ролі.
Ідея полягає в тому, щоб встановити той самий профіль, у цьому випадку --profile eb-cli
, який використовується в .elasticbeanstalk/config.global.yml
, оскільки саме цей профіль буде використовуватися на етапі розгортання.
Після додавання цього кроку, вам потрібно додати ще один, який викликає скрипт для розгортання вашого застосунку, в основному так, як ми це робили локально. Ми просто додаємо крок, що викликає наш скрипт для розгортання, який у цьому випадку встановлює ebcli, упаковує потрібні файли в zip і розгортає їх.
- name: Deploy🤞
run: yarn deploy
deploy_to_elastic() {
pip install awsebcli
zip deploy.zip docker-compose.yml default.conf -r
eb deploy
}
deploy_to_elastic
Теоретично, цього буде достатньо, щоб ваш застосунок був розгорнутий у вашому CI/CD.
Налаштування Healthd для роботи в новій структурі
Якщо ви слідували всім крокам до цього моменту, ви помітите, що хоча ваш застосунок показує здоров'я як "ок" (health ok):
всі люблять здоров'я ok
Загальний стан здоров'я показує порожні дані:
ми хочемо більше інформації, будь ласка
Elastic Beanstalk припускає, що ви використовуєте проксі веб-сервера як контейнер. Внаслідок цього, проксі-сервер NGINX відключений для середовищ Docker, що використовують Docker Compose.
Це означає, що коли ми використовуємо docker-compose.yml
для розгортання нашого застосунку, нам потрібно зробити додаткові кроки, щоб загальний стан здоров'я працював, як очікується. По-перше, нам потрібно слідувати інструкціям AWS.
services:
nginx-proxy:
image: "nginx"
volumes:
- "${EB_LOG_BASE_DIR}/nginx-proxy:/var/log/nginx"
Директорія
var/log/nginx
містить логи для сервісу nginx-proxy в контейнері, і вони будуть змонтовані в директорію/var/log/eb-docker/containers/nginx-proxy
на хості.
Нам потрібно зробити логи доступними для хоста, щоб healthd
міг зібрати відповідні дані. Після цього ми повинні використовувати інший ресурс EB, який є папку .ebextensions
тут, яка є способом налаштування ресурсів AWS за допомогою конфігураційних файлів, використовуючи деякі з його конвенцій. Для додаткової інформації дивіться посилання.
Звичайний файл access.log
від nginx
використовує формат:
172.31.6.131 - - [24/Jan/2024:20:48:57 +0000] "GET / HTTP/1.1" 200 790 "-" "ELB-HealthChecker/2.0" "-"
Але healthd
потребує іншого формату:
1437609879.311"/"200"0.083"0.083"177.72.242.17
Примітка від AWS пояснює і дає приклад, де можна завантажити і перевірити .ebextensions
, що використовуються. У нашому репозиторії цей файл можна побачити тут. Файл слідує деяким з конвенцій ebextensions
, але важливі частини виглядають так:
sed -i 's|appstat_log_path: .*|appstat_log_path: /var/log/eb-docker/containers/nginx-proxy/application.log|' /etc/healthd/config.yaml
systemctl restart healthd
Ця команда замінює стандартний appstatlogpath для healthd
, що вказує на /var/logs/nginx/healthd/application.log, на наш змонтований об'єм /var/log/eb-docker/containers/nginx-proxy/application.log, а потім перезапускає сервіс healthd
.
Вам не потрібно турбуватися про те, як виконати цю команду, оскільки ідея полягає в тому, щоб використовувати конфігураційні файли в межах ebextensions
, і за умовчанням EB буде їх виконувати за вас.
Цей крок необхідний, щоб зробити healthd
(healthd) обізнаним про файли, але тепер нам потрібно зробити так, щоб nginx зберігав їх для нашого змонтованого об'єму, а також включити формат healthd
, як зазначено вище. Для цього нам потрібно змінити наш файл nginx default.conf
, коментарі пояснюють, що важливо 😀
log_format healthd '$msec"$uri"'
'$status"$request_time"$upstream_response_time"'
'$http_x_forwarded_for';
# ... yadda yadda yadda
server {
# ... yadda yadda yadda
root /var/log; # це важливо, щоб nginx не зламався через відсутність налаштування root
if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})T(\d{2})") {
set $year $1;
set $month $2;
set $day $3;
set $hour $4;
}
access_log /var/log/nginx/access.log main;
# наступний рядок найважливіший, оскільки ми говоримо
# nginx зберігати access_log в шлях, який ми змонтували раніше в
# нашому `docker-compose.yml`, використовуючи формат healthd і використовуючи
# рік, місяць, день і годину у файлі, що є вимогою
# від healthd
access_log /var/log/nginx/application.log.$year-$month-$day-$hour healthd;
# ... yadda yadda yadda
}
Підсумовуючи, нам потрібно переконатися, що ми включили папку .ebextensions
, коли пакуємо наш застосунок у нашому скрипті для розгортання.
zip deploy.zip docker-compose.yml default.conf .ebextensions -r
Як додаткову допомогу, ось PR, що був зроблений, щоб загальний стан здоров'я почав працювати.
ми любимо інформацію
Бонус 1 — Композитні дії GitHub
Як ви можете побачити в репозиторії, ми маємо однакові кроки як для api
, так і для ui
, ми можемо використовувати композитні дії GitHub. Тому ми можемо замінити це:
- name: Configure AWS credentials from Test account
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.ROLE_TO_ASSUME }}
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Add profile credentials to ~/.aws/credentials
run: |
aws configure set aws_access_key_id ${{ env.AWS_ACCESS_KEY_ID }} --profile eb-cli
aws configure set aws_secret_access_key ${{ env.AWS_SECRET_ACCESS_KEY }} --profile eb-cli
aws configure set aws_session_token ${{ env.AWS_SESSION_TOKEN }} --profile eb-cli
На це:
- name: Configure AWS
id: configure-aws
uses: ./.github/actions/configure-aws
Для цього необхідно створити папку з назвою дії з файлом action.yml
всередині, тому в цьому випадку ми маємо таку повну структуру:
.github
> actions
> configure-aws
> action.yml // використовує ./.github/actions/add-profile-credentials
> add-profile-credentials
> action.yml
> workflows
> api.yml // використовує ./.github/actions/configure-aws
> ui.yml // використовує ./.github/actions/configure-aws
Де action.yml
— це місце, де існує композитна дія. Подивіться в репозиторії, щоб надихнутися.
Бонус 2 — Якщо у вас простіша налаштування, скажімо, лише API
В такому випадку ви могли б використовувати інший спосіб розгортання в Elastic Beanstalk, використовуючи файл Dockerrun.aws.json.
Знову ж таки, існує кілька способів вирішення цього, і є кілька додаткових налаштувань, але ми скористаємося найпростішим способом і, залишаючись повністю в межах AWS, можемо отримати переваги від вже створеної ролі та використовувати образи, надіслані в ECR.
Для цього змініть атрибут deploy artifact в .elasticbeanstalk/config.global.yml
:
deploy:
artifact: Dockerrun.aws.json
Не забувайте, що роль, яку ви використовуєте в GitHub actions, повинна мати дозволи для ECR. Зміст файлу Dockerrun.aws.json
буде таким простим:
{
"AWSEBDockerrunVersion": "1",
"Image": {
"Name": "THE_NAME_OF_YOUR_IMAGE",
"Update": "true"
},
"Ports": [
{
"ContainerPort": 5000
}
]
}
Як додаткову підказку, ви можете зробити образ динамічним, використовуючи скрипт і команду sed. В цьому випадку замініть назву вашого образу на якусь змінну, яку можна буде змінити:
{
"AWSEBDockerrunVersion": "1",
"Image": {
"Name": "${IMAGE}",
"Update": "true"
},
"Ports": [
{
"ContainerPort": 5000
}
]
}
Змініть GitHub action, щоб реєстр був введенням і містив додаткові змінні, які можуть вам знадобитися:
- name: Deploy🤞
env:
REGISTRY: ${{ steps.configure-aws.outputs.ecr-registry }}
PACKAGE_NAME: api
REPOSITORY: your-ecr-repository-name
run: yarn deploy
А в вашому скрипті можна зробити щось таке:
#!/bin/bash
set -e
deploy_to_elastic() {
pip install awsebcli
TAG=$PACKAGE_NAME-$GITHUB_REF_NAME
IMAGE=$REGISTRY/$REPOSITORY:$TAG
# це в результаті буде виглядати так
# 721840483.dkr.ecr.us-east-1.amazonaws.com/your-ecr-repository-name:api-your-current-branch-name
# потрібно заекранувати, інакше слеш викличе помилку в sed
&/g')
sed -e "s/\${IMAGE}/$ESCAPED_API_IMAGE/g" Dockerrun.template.aws.json > Dockerrun.aws.json
eb deploy --staged
}
deploy_to_elastic
Це дозволить вашому скрипту завжди розгортати поточну гілку у вашому середовищі Elastic Beanstalk. Це просто ідея для динамічності, і використовуючи такий підхід, ви завжди будете мати тільки один екземпляр, але в подальшому ви можете створювати різні середовища в EB, наприклад, для QA, Staging і Production.
Якщо ви хочете переглянути повний код, то в репозиторії є гілка dockerrun.aws.json.
Бонус 3 — Використання Lerna для повного використання можливостей GitHub Actions та робочих простору
Це довга тема, для якої було створено окрему статтю, яку можна переглянути тут.
Перекладено з: Automate (CI/CD) and Reverse Proxy a Docker Elastic Beanstalk Up with GitHub Actions