איך להפעיל אפליקציה בייצור על VPS ולאוטומט את התהליך עם Docker, GitHub Actions, ו-AWS ECR.

במאמר זה, נלמד מה שזה אומר לדחוף אפליקציה לתפעול ואיך לגרום לכך שזה יקרה אוטומטית, נראה איך להשתמש בדוקר ובפעולות GitHub לשם כך. מאחר ואנחנו משתמשים בדוקר, לא אשקיע הרבה זמן בלהסביר אילו טכנולוגיות שונות שומשו לכתיבת האפליקציה, אלא מה לעשות עם תמונת הדוקר עצמה. ההנחה כאן היא שיש לנו שרת פשוט, נאמר VPS (שרת פרטי וירטואלי), שרת מוקצה או כל שרת שיש לנו SSH כדי להשתמש בו. עלינו לבצע פעולות, להכין את השרת לקבל ולהריץ את האפליקציה, ולהכין את תפיסת ההפצה. בחלק הבא של המאמר, אני אנחש שיש לך שרת Ubuntu

הדבר הראשון שצריך לבצע הוא תצורה חד-פעמית על השרת, המטרה היא להכין אותו לשלבים הבאים. לשם כך, עלינו לשקול את הנושאים הבאים:

Docker Compose הוא כלי שמריץ מספר אפליקציות Docker כשירותים ומאפשר להם לתקשר זה עם זה, כמו גם תכונות נוספות כמו ווליומים לאחסון קבצים. בואו קודם כל נתקין את Docker ו-Docker Compose על השרת; אתם יכולים ללכת להוראות ההתקנה הרשמיות כאן: docs.docker.com/engine/install/ubuntu. לאחר זה, חשוב להריץ אותו כמשתמש לא-מנהל; זה מה שאומרת התיעוד הרשמי: docs.docker.com/engine/security/rootless.

שים לב שאפשר להשיג את אותו מטרה עם רשם אחר אם רוצה, אני רק משתמש ב-AWS כי זה יותר פשוט בשבילי.

מאחר שאנחנו משתמשים ב-Docker, התמונות יהיו צריכות להיות מאוחסנות ומוחזרות ממקום מסוים. לשם כך, אני משתמש ב-AWS ECR (Amazon Web Services Elastic Container Registry). זה רשם Docker בתוך חשבון AWS. זה מאוד זול לשימוש וקל להגדרה. אתה יכול גם להשתמש ב-Docker Hub ליצירת מאגר פרטי עבור התמונות שלך. הכל מתחיל ביצירת רשם ECR פרטי בחשבון AWS. אתה תלחץ על "יצור מאגר" ותמלא את שם המאגר.

לאחר יצירת מאגר, אתה יכול להעתיק URI של המאגר ולשמור אותו לימים הבאים. יש לו את הפורמט הבא AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com/שם_ה_מאגר .

אתה גם צריך להגדיר את איברי AWS IAM שיש להם את הזכות לשלוף/להעלות למאגר זה. בוא נעבור לשרות 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 ולענות על שאלות על ידי ספיגת המפתחות שנוצרו באס"ו, המפתח סודי והמזהה מפתח סודי. גם תוכל לבחור פורמט יצוא, פשוט JSON, ולציין אזור ברירת המחדל, זה טוב יותר לבחור את האזור שבו יצרת את רישום ECR לפני.

בתהליך העבודה שלי כתבתי סקריפט קטן של shell שמבצע פעולות מסוימות, זה החלק המרכזי של התהליך, הוא מתחבר לרישום, מוריד את התמונה ומאתחל את שירות 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 ימפה את תוכן שרת ה-Postgres לדיסק המקומי כך שהוא לא יאבד כאשר קונטיינר הדוקר עוצר. למדו יותר על הרצת Postgres עם docker-compose כאן https://medium.com/@agusmahari/docker-how-to-install-postgresql-using-docker-compose-d646c793f216. השתמשתי בהוראה שנקראת env_file זה מאפשר ל-docker-compose לקרוא קובץ ולטעון את תוכנו לתוך קונטיינר הדוקר בזמן הרצה, עשיתי זאת כי בדרך כלל, קובץ docker-compose נשמר במערכת ניהול גרסאות ואני לא רוצה לשמור על משתני הסביבה ישירות בתוך ההוראה environment בשירות. שימו לב ששירות שלנו נקרא web כאן, מוקדם יותר כתבנו קובץ redeploy.sh ואנחנו מתכוונים להריצו כך:

 ./redeploy.sh web

הארגומנט web קשור לשם שירות שלנו, קובץ זה פשוט ממפה את הארגומנט לשם שירות בקובץ הדוקר.

בשלב הזה, אנחנו צריכים ליצור שירות לינוקס שידאג להתחלת היישום בכל פעם שהשרת מתחיל או שהיישום עוצר. הסקריפט הבא יעזור לכם לעשות זאת:

[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 מתאר את שירותנו ומציין איזה שירות היחידה שלנו היא חלק ממנו, במקרה זה, זה שירות הדוקר, זה יוודא שהשירות שלנו תמיד ירוץ כאשר שירות הדוקר רץ גם הוא.

  • החלק שירות מתאר איך להריץ את שירותנו, החלקים המעניינים הם WorkingDirectory, ExecStart ופקודות ExecStop, הם יהיו בשימוש לפי משמעות שמם, למשל, אם השירות נקרא myapp כשאתה מקליד את הפקודה systemctl start myapp הפקודה ExecStart תוצא לפעולה. אתה יכול ללמוד יותר על שירותים בלינוקס כאן 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

בשלב זה הוא מוכר כשירות לינוקס, אתה יכול להריץ את systemctl start myapp כדי להריץ אותו. הפקודה הדרושה הבאה היא

systemctl enable myapp.service

זה יוודא שהשירות יורץ אוטומטית על ידי השרת בכל התחלה. אתה יכול ללמוד יותר כאן: https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6

השתמשתי ב-Nginx למשימה זו, הוא קטן וחזק, נפוץ מאוד ויכול לפעול כמאזן עומסים, שרת קבצים סטטיים, פרוקסי הפוך ועוד הרבה. הדבר הראשון לעשות הוא להתקין אותו.

sudo apt-get install nginx

בשלב זה התמונה של Docker כביכול רצה, בואו נניח שהיא מכילה אפליקציה שרצה בפורט 8080, ושהפורט הזה קושר לשרת דרך קובץ docker-compose. עלינו להגדיר הגדרות פרוקסי הפוך בין 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.

לאחר שהגדרנו את השרת, עלינו להקים עכשיו את קו הבנייה, שמורכב בעיקר מ 1 שלב, לכתוב קובץ תהליך של GitHub Action ולהוסיפו למיזם, בואו נתחיל.

GitHub action ישמש לבניית תמונת Docker מקוד המקור שלנו ולהעלות אותה לרשם, ממנו יושך התמונה ויבוצע על השרת. אני אקח דוגמה של Dockerfile לשם דוגמה, אך במעשה, עליך לכתוב Dockerfile משלך. ליישום express.js, קובץ הdocker יהיה כזה:

# 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']

בשלב זה כל המערכת מושלמת וקשורה, עכשיו אפשר לערוך וליישם אותה למקרה הספציפי שלך. במאמר עתידי, אשתף את התהליך של בניית היישום עצמו לשם הפעלה והרצה בקובץ Docker.

מאמר זה מנסה לתאר את התהליך שאני משתמש בו כדי להקים VPS לאוטומציה כשמדובר בהפצה. הוא מתאר איך לקבוע את התהליך של הרצת היישום בתוך השרת ואת תהליך בניית היישום, כל חלק יכול להתבצע עם כלי אחר, למשל, אפשר להחליף את nginx ב-Treafik אם רוצים, ולהחליף את השירות systemd בתוכנה ב-supervisor ועוד. תהליך זה לא כולל דברים נוספים כמו גיבוי של השרת או סגירת פורטים ברירת המחדל בשרתים, אלה יוסבירו במאמרים עתידיים. אני פתוח לשאלות אם אתה רוצה להתאים את זה לתהליך שלך. במאמר אחר אשתף איך להקים יישום כך שיהיה מוכן להפעלה במונחים של הפצה, זהו החלק של התהליך שבא לפני בניית תמונת 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