كيفية نشر تطبيق إنتاجي على VPS وتحسين العملية باستخدام Docker، GitHub Actions، و AWS ECR.

في هذا المقال، سنتعلم ما يعنيه دفع التطبيق إلى الإنتاج وكيفية تحقيق ذلك آليًا، سنرى كيفية استخدام Docker وGitHub Actions لذلك. Since we are using docker I won’t spend too much time on what technologies have been used to write the application rather than what to do with the docker image itself. المبدأ هنا يعني خادم بسيط، دعوه يكون خادم VPS (الخادم الخاص الافتراضي) أو خادم مخصص أو أي خادم يمكنك استخدام SSH عليه. علينا تنفيذ إجراءات، إعداد الخادم لاستقبال وتشغيل التطبيق، وتهيئة مسار التوزيع. For the upcoming part of this article, I will consider you have a Ubuntu server

الأمر الأول الذي يجب القيام به هو إعداد بعض التكوينات الواحدية على الخادم، الهدف هو إعداد الخادم للخطوات القادمة. لهذا الغرض، يجب أن نأخذ في الاعتبار المواضيع التالية:

دوكر كومبوز هو أداة تدير العديد من تطبيقات دوكر كخدم وتتيح لهم التواصل مع بعضهم البعض، بالإضافة إلى ميزات أخرى مثل الأجلدات لتخزين الملفات. دعونا أولاً نثبت دوكر ودوكر كومبوز على الخادم؛ يمكنك اتباع دليل التثبيت الرسمي هنا: docs.docker.com/engine/install/ubuntu. بعد ذلك، من المهم تشغيله كمستخدم غير الجذر؛ إليك ما تقوله الوثيقة الرسمية: docs.docker.com/engine/security/rootless.

لا تنس أنك يمكنك تحقيق نفس الهدف باستخدام سجل آخر إذا كنت ترغب في ذلك، أنا فقط أستخدم AWS هنا لأنه أكثر بساطة لي.

Since we are using Docker, the images will have to be stored and retrieved from somewhere. For this purpose, I am using AWS ECR (Amazon Web Services Elastic Container Registry). It’s a Docker registry within an AWS account. It’s very cheap to use and easy to set up. You can also use Docker Hub to create a private repository for your images. It all starts by creating an ECR private registry in the AWS account. You will click on “Create Repository” and fill in the name of the repository.

بعد إنشاء المستودع يمكنك نسخ URI للمستودع وتخزينه للeeaer. يحتوي على تنسيق التالي AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com/اسم_المستودع .

也将需要设置AWS IAM الصلاحيات التي تملك الحق في سحب/إرسال إلى/from هذا المستودع. دعونا ننتقل إلى خدمة IAM، انقر على مستخدم جديد وقم بالربط بالسياسة التالية: AmazonEC2ContainerRegistryFullAccess، لا تحتاج إلى تمكين الوصول إلى وحدة التحكم في AWS له. في نهاية هذا العملية، تحصل على مفاتيحين من AWS، مفتاح السر و معرف مفتاح السر، احتفظ بهما جانباً سنحتاجهم للعمل القادم.

عند العودة إلى خادمنا، نحتاج إلى تثبيت AWS CLI. الطريقة الرسمية لتنصيبه متاحة هنا. https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions. بعد ذلك يمكنك اختبار التثبيت عن طريق تشغيل الأمر aws --version. في هذه الخطوة، ستحتاج إلى تنفيذ الأمر aws configure والرد على الأسئلة بتقديم المفاتيح السابقة التي تم إنشاؤها على AWS، وهي مفتاح السر و معرف مفتاح السر. سيُطلب منك أيضًا اختيار تنسيق الناتج، ببساطة JSON، وتقديم منطقة افتراضية، من الأفضل اختيار المنطقة التي أنشأت فيها سجل ECR في وقت سابق.

في سير عملي، كتبت سكريبت shell صغيرًا يقوم ببعض الأنشطة، وهو الجزء الأساسي من هذه العملية، حيث ي=logging إلى السجل، ينزّل الصورة، ويعيد تشغيل الخدمة الم相对应ة لـ Docker، أنادي به redeploy.sh وأقوم بإنشائه في مجلد من أريد تشغيل تطبيقي منه، هاهي محتوياته:

#!/bin/bash

# Retrieve AWS ECR login command
aws ecr get-login-password --region [SWS_REGION] | docker login --username AWS --password-stdin [AWS_REGION].dkr.ecr.us-west-2.amazonaws.com

# Associating repositories with identifiers
declare -A repositories=(
    ["web"]="[REGISTRY_NAME]:latest"
)

# Check if service identifier is provided as a command line argument
if [ -z "$1" ]; then
    echo "Please provide a service identifier as a command line argument."
    exit 1
fi

service_identifier=$1

# Check if the provided service identifier exists in the repositories array
if [ -z "${repositories[$service_identifier]}" ]; then
    echo "Invalid service identifier. Available identifiers: ${!repositories[@]}"
    exit 1
fi

# pull the new image from the registry
repository=${repositories[$service_identifier]}
echo "Pulling [AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/$repository"
docker pull "[AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/$repository"

# Change directory to [APP_FOLDER] 
cd /home/ubuntu/[APP_FOLDER] || {
    echo "Failed to change directory to /home/ubuntu/[APP_FOLDER]"
    exit 1
}

# stop and restart the service, this wil force docker compose to redownload the lates image
echo "Re-running service $service_identifier"
docker compose stop "$service_identifier"
docker compose up --no-deps "$service_identifier" -d

# Remove old and un-used docker images
echo "Removing unused Docker images"
docker image prune -fa
echo "Removed Dangling Images"

第一步 من هذا السكريبت يتكون من الولوج إلى حساب AWS باستخدام AWS CLI للحصول على رمز سيستخدمه Docker عند استرجاع الصورة، تذكر أن السجل هو خاص، لا يمكننا سحبها دون التحقق من الهوية.

ثم نعلن قائمة المستودعات ونربطها ببعض معرفات، سيتم استخدام المعرف المحدد كأمر سطر الأوامر، سأشرح المزيد حول هذا لاحقاً. بعد ذلك، نتحقق ما إذا قدم المستخدم حجة تطابق معرف خدمة موجود، نريد منه كتابة شيء مثل ./redeploy web على سبيل المثال، سيربط السكريبت الحجة web بالمستودع web كما في الخطوة الثانية.

بعد الحصول على معرف الخدمة، ننشئ عنوان المستودع ديناميكياً وننفذ أمر docker pull باستخدامه. هذا يضمن تنزيل صورة Docker إلى نظامنا.

الآن، سينتقل السكريبت إلى مجلد التطبيق، /home/ubuntu/[APP_FOLDER] هذا يفترض أنك تقوم بكل شيء تحت المستخدم ubuntu وأن مجلد HOME الخاص به مسمى ubuntu، APP_FOLDER يحتوي على كل الإعدادات.

الخطوة التالية تتكون من إيقاف تشغيل الخدمة وإعادة تشغيلها، بعد ذلك نزيل الصور القديمة والغير مستخدمة باستخدام الأمر docker image prune -fa يمكنك معرفة المزيد هنا: https://docs.docker.com/reference/cli/docker/system/prune/.

Compose هو الأداة التي تشغل نظامنا بالكامل، يحتاج إلى ملف مسمى docker-compose.yml حيث ستعرف كل شيء، لنفترض أن تطبيقنا يحتاج إلى خدمة redis وخدمة postgres للعمل، هكذا سيبدو الأمر:

version: '3.9'
services:
  web:
    image: "[AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/myapp:latest"
    ports:
        - 8080:8080
    depends_on:
        - redis
        - db
    env_file:
        - .env

  redis:
    image: 'redis:alpine'
    ports:
      - '6379:6379'
  db:    
    image: 'postgres:14'
    restart: always
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_USER: postgres
      PGDATA: /var/lib/postgresql/data/pgdata
    healthcheck:
      test: [ "CMD", "pg_isready", "-q", "-d", "postgres", "-U", "postgres" ]
      timeout: 45s
      interval: 10s
      retries: 10
    ports: 
      - '5437:5432'
    volumes:
      - ./opt/postgres/data:/var/lib/postgresql/data

سيتم توجيه محتوى وحدة التخزين الخاصة بك ./opt/postgres/data:/var/lib/postgresql/data إلى قرص Local بحيث لا يتم فقدانه عند توقف تشغيل حاوية Docker. اكتشف المزيد حول تشغيل Postgres باستخدام docker-compose هنا https://medium.com/@agusmahari/docker-how-to-install-postgresql-using-docker-compose-d646c793f216. استخدمت توجيهًا باسم env_file والذي يسمح لـ docker-compose بقراءة ملف وتحميل محتواه إلى حاوية Docker أثناء التشغيل، قمت بذلك لأنه عادة ما يتم إيداع ملف docker-compose في VCS هناك لا أريد الاحتفاظ بالمتغيرات البيئية فيه مباشرة عبر التوجيه environment في الخدمة. لاحظ أن الخدمة الخاصة بنا تُدعى web هنا، في وقت سابق كتبنا ملفًا redeploy.sh ونقصد تشغيله بهذه الطريقة:

 ./redeploy.sh web

Argument web مرتبط باسم خدمتنا، هذا الملف يُقوم بتعيين Argument إلى اسم خدمة في ملف Docker.

في هذه المرحلة، يجب علينا إنشاء خدمة Linux التي ستضمن بدء التطبيق في كل مرة يبدأ فيها الخادم أو يتوقف تطبيقنا. النص التالي سيساعدك في القيام بذلك:

[Unit]
Description=[APP_NAME] service executed by docker compose
PartOf=docker.service
After=docker.service
After=network.target

[Service]
Type=oneshot
RemainAfterExit=true
WorkingDirectory=/home/ubuntu/[APP_FOLDER]
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down

[Install]
WantedBy=multi-user.target

دعونا نحلل ذلك !!!

  • قسم Unit يصف خدمتنا ويحدد ما هي خدمة الوحدة جزءًا منها، في هذه الحالة، إنها خدمة Docker، هذا سيعني أن خدمتنا دائمًا ما تعمل عندما تعمل خدمة Docker أيضًا.

  • القسم Service يصف كيفية تشغيل خدمتنا، والأجزاء المثيرة للاهتمام هي WorkingDirectory، ExecStart وأمر ExecStop، سيتم استخدامها وفقًا لماهية أسمائها، على سبيل المثال، إذا كانت الخدمة مسماة بـ myapp فإنه عند كتابة الأمر systemctl start myapp سيتم تنفيذ الأمر ExecStart. يمكنك تعلم المزيد عن خدمات Linux هنا https://www.redhat.com/sysadmin/systemd-oneshot-service. تعلم المزيد حول كيفية تشغيل خدمة Docker باستخدام systemd هنا: https://bootvar.com/systemd-service-for-docker-compose/

يجب تثبيت هذه الخدمة بطريقة تسمح للنظام بتشغيلها عند الحاجة، ستحتاج إلى حفظها في ملف باسم مثلاً: myapp.service

touch myapp.service
# open it
nano myapp.service
# paste the previous scrip in it
cp myapp.service /etc/systemd/system/myapp.serivce

في هذه المرحلة، يتم التعرف عليها كخدمة Linux، يمكنك تشغيل الأمر systemctl start myapp لبدء تشغيلها. الأمر المطلوب التالي هو

systemctl enable myapp.service

هذا سيضمن تنفيذ الخدمة تلقائيًا بواسطة الخادم في كل إعادة تشغيل. يمكنك التعرف على المزيد هنا: https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6

استخدمت Nginx لهذه المهمة، إنه صغير وقوي، ويُستخدم على نطاق واسع، ويمكنه أن يعمل كموزع حمل، وكمخدم ملفات ثابتة، وكم proxy معكوس، وأكثر من ذلك. أول شيء يجب القيام به هو تثبيته.

sudo apt-get install nginx

في هذا الخطوة، من المفترض أن الصورة Docker قيد التشغيل، لنفترض أنها تحتوي على تطبيق يعمل على منفذ 8080، وأن هذا المنفذ مرتبط بالخادم عبر ملف docker-compose. نحتاج إلى إعداد تكوين proxy معكوس بين Nginx ومنفذنا. إليك التكوين المطلوب :

upstream app_backend {
  server localhost:8080; # the appliction port
}

server {
  listen 80;
  server_name [DOMAIN_NAME];

  location / {
    proxy_pass http://app_backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_cache_bypass $http_upgrade;
  }
}

لنسمي هذا التكوين myapp.conf ونقوم ب حفظه في مجلد حيث يمكن لـ nginx إيجاده، هذا المجلد من بين آخرين يُدعى /etc/nginx/conf.d/.

sudo touch /etc/nginx/conf.d/myapp.conf
sudo nano /etc/nginx/conf.d/myapp.conf
# paste the content there

الآن كل ما نحتاجه هو اختباره وإعادة تشغيل خدمة NGINX بالأوامر التالية

sudo nginx -t # test if the config is valid 
sudo nginx -s reload # reload the nginx service so it will consider it

هذا التكوين سيعين nginx للاستماع إلى حركة المرور على المنفذ 80 وباسم النطاق [DOMAIN_NAME] ويرسلها إلى خادم تطبيقك على المنفذ 8080 عبر توجيه proxy_pass، السطر location / { ببساطة يعني التقاط جميع الطلبات التي تبدأ بـ / وتنفيذ الإجراءات المكتوبة تحت الكتلة location. اكتشف المزيد هنا https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/How-to-setup-Nginx-reverse-proxy-servers-by-example.

بعد إعداد الخادم، علينا الآن إعداد مسار البناء، والذي يتكون بشكل رئيسي من خطوة واحدة، وهي كتابة ملف سير عمل Github Actions وإضافته إلى المشروع، هيا بنا.

سيتم استخدام GitHub action لبناء صورة Docker من كود المصدر لدينا وت上传ها إلى السجل من حيث يتم سحب الصورة وتشغيلها على الخادم. سأستخدم ملف Dockerfile عينة لهذا المثال، ولكن في الممارسة، سيتعين عليك كتابة Dockerfile الخاص بك. ل application express.js، سيكون ملف Dockerfile كالتالي:

# Fetching the minified node image on apline linux
FROM node:slim

# Declaring env
ENV NODE_ENV production

# Setting up the work directory
WORKDIR /express-docker

# Copying all the files in our project
COPY . .

# Installing dependencies
RUN npm install

# Installing pm2 globally
RUN npm install pm2 -g

# Exposing server port
EXPOSE 8080

# Starting our application
CMD pm2 start process.yml && tail -f /dev/null

بناء وتشغيل هذا الملف Docker سيتيح لنا بدء التطبيق على المنفذ 8000، ولكن في إعدادنا، سنحتاج إلى تشغيله باستخدام docker-compose.

الشيء التالي هو إعداد مسار سير عمل GitHub Actions. لذلك، قم بإنشاء مجلد .github/workflows في جذر المشروع وإنشاء ملف باسم docker-build.yml، سنكتب مسار السير فيه.

name: Build, Push to ECS and Deploy to Server

on:
  push:
    branches: ['deploy/main']

jobs:
  build:
    name: Build Web Image
    runs-on: ubuntu-latest

    steps:
      - name: Check out code
        uses: actions/checkout@v2

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: [AWS_REGION]

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Build, tag, and push image to Amazon ECR
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: [REPOSITORY_NAME]
          IMAGE_TAG: latest
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . 
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

      - name: Restart the service via SSH
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          password: ${{ secrets.PASSWORD }}
          port: ${{ secrets.PORT }}
          script: /home/ubntu/[APP_DIRECTORY]/redeploy.sh web

هناك عدة خطوات لمراجعتها هنا:

  • تهيئة بيانات الاعتماد AWS: هنا النظام سيحمّل مفتاح AWS السابق الذي أنشأته، سيتعين عليك تسجيله في أسرار حساب GitHub الخاص بك

  • بناء، تحديد الوسم، وت上传 صورة إلى Amazon ECR: هذه الخطوة ستعمل الأمر docker build وdocker push لإنشاء صورة Docker

  • إعادة تشغيل الخدمة عبر SSH ستقوم هذه الخطوة بالاتصال بالخادم وإعادة تشغيل التطبيق بالكامل دفعة واحدة.

ستعمل هذه الأتمتة في كل مرة يتم فيها دمج طلب السحب ضد الفرع deploy/main.

on:
  push:
    branches: ['deploy/main']

في هذه المرحلة، النظام بالكامل في مكانه ومربوط، والآن يمكن تحريره وتطبيقه لkasusك المحدد. في مقال مستقبلي، سأشارك عملية بناء التطبيق نفسه للإنتاج و تشغيله في ملف Docker.

يحاول هذا المقال وصف العملية التي أستخدمها لتهيئة VPS لأتمتة التوزيع. يصف كيفية إعداد عملية تنفيذ التطبيق داخل الخادم و عملية بناء التطبيق، ويمكن تنفيذ كل جزء باستخدام أداة أخرى، على سبيل المثال، يمكنك استبدال nginx ب Treafik إذا كنت ترغب في ذلك، ويمكنك استبدال خدمة systemd ببرنامج في supervisor وغيرها. لا تغطي هذه العملية الأمور الإضافية مثل نسخ احتياطي للخادم أو إغلاق المنافذ الافتراضية على الخوادم، سيتم تفسير هذه الأمور في مقالات مستقبلية. لا تتردد في طرح سؤال إذا كنت ترغب في تكييف هذا لkasusك. في مقال آخر سأركز على كيفية إعداد التطبيق ليكون جاهزًا للإنتاج من حيث التوزيع، هذه هي المرحلة التي تأتي قبل بناء صورة Docker.

أتمنى أن تستمتع بالقراءة.

أنا هنا لمساعدتك في تنفيذ هذا ضمن شركتك أو فريقك، مما يتيح لك التركيز على مهامك الأساسية وتوفير المال قبل الإسهام في الإمكانات الكبيرة لمستودون.

Source:
https://blog.adonissimo.com/how-to-deploy-a-production-app-on-a-vps-and-automate-the-process-with-docker-github-actions-and-aws-ecr