Автоматизація (CI/CD) та зворотний проксі для Docker на Elastic Beanstalk за допомогою GitHub Actions

Заголовок намагається підсумувати безліч дрібних деталей всього кількома словами.

Повний код можна побачити тут, і, коротко кажучи, ідея цієї статті — показати спосіб створення:

  • докеризованої повноцінної стекової програми
  • використання робочих простірів для спільного використання коду між api та ui
  • доступність для всіх за допомогою Elastic Beanstalk
  • використання docker-compose з nginx для зворотного проксі та запуску лише одного екземпляра
  • автоматизація за допомогою GitHub Actions

Ідея цієї повноцінної стекової програми:

Стаття буде поділена на кілька розділів:

  • базова конфігурація робочих просторів
  • докеризація 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.

Для цілей цієї статті ми створимо політику, яка буде використовуватися в ролі, створеній нижче, з повним доступом до 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

pic

Це створить папку .elasticbeastalk та файл config.yml у вашому проєкті і додасть деякі рядки до вашого gitignore. Ідея цього файлу в тому, що він буде використовувати ці налаштування для взаємодії з сервісом Elastic Beanstalk. Після цього кроку навіть буде створено застосунок на AWS, якщо ви перейдете до консолі AWS.

pic

Просунуте використання 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):

pic

всі люблять здоров'я ok

Загальний стан здоров'я показує порожні дані:

pic

ми хочемо більше інформації, будь ласка

Це відбувається через те:

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, що був зроблений, щоб загальний стан здоров'я почав працювати.

pic

ми любимо інформацію

Бонус 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

Leave a Reply

Your email address will not be published. Required fields are marked *