في هذا المقال، سنتعلم ما يعنيه دفع التطبيق إلى الإنتاج وكيفية تحقيق ذلك آليًا، سنرى كيفية استخدام 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
لنعد الخادم جاهزًا
الأمر الأول الذي يجب القيام به هو إعداد بعض التكوينات الواحدية على الخادم، الهدف هو إعداد الخادم للخطوات القادمة. لهذا الغرض، يجب أن نأخذ في الاعتبار المواضيع التالية:
Docker Compose لتشغيل التطبيق وتباعده
دوكر كومبوز هو أداة تدير العديد من تطبيقات دوكر كخدم وتتيح لهم التواصل مع بعضهم البعض، بالإضافة إلى ميزات أخرى مثل الأجلدات لتخزين الملفات. دعونا أولاً نثبت دوكر ودوكر كومبوز على الخادم؛ يمكنك اتباع دليل التثبيت الرسمي هنا: docs.docker.com/engine/install/ubuntu. بعد ذلك، من المهم تشغيله كمستخدم غير الجذر؛ إليك ما تقوله الوثيقة الرسمية: docs.docker.com/engine/security/rootless.
قم بتهيئة بيانات اعتماد AWS ECR وCLI.
لا تنس أنك يمكنك تحقيق نفس الهدف باستخدام سجل آخر إذا كنت ترغب في ذلك، أنا فقط أستخدم 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/.
ملف Docker compose
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 لتحافظ على تشغيل كل شيء
في هذه المرحلة، يجب علينا إنشاء خدمة 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 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.
أتمنى أن تستمتع بالقراءة.
أنا هنا لمساعدتك في تنفيذ هذا ضمن شركتك أو فريقك، مما يتيح لك التركيز على مهامك الأساسية وتوفير المال قبل الإسهام في الإمكانات الكبيرة لمستودون.