Skip to content

فصل ۱۷ — استقرار برنامه‌های دنیای واقعی

فصل‌های قبلی انواع مختلفی از اشیاء API موجود در یک کلاستر Kubernetes و روش‌هایی را که می‌توان از آن اشیاء برای ساخت سیستم‌های توزیع‌شدهٔ قابل اعتماد بهترین استفاده را کرد، توصیف کردند. با این حال، هیچ‌یک از فصل‌های پیشین واقعاً بحث نکردند که چگونه ممکن است در عمل از این اشیاء برای استقرار یک برنامهٔ کامل و دنیای واقعی استفاده کنید. تمرکز این فصل همین است.

به چهار برنامهٔ دنیای واقعی نگاه می‌کنیم:

  • Jupyter، یک دفترچهٔ علمی متن‌باز
  • Parse، یک سرور API متن‌باز برای برنامه‌های موبایل
  • Ghost، یک پلتفرم وبلاگ‌نویسی و مدیریت محتوا
  • Redis، یک key/value store سبک و پرکارا

این مثال‌های کامل باید تصویر بهتری به شما بدهند از اینکه چگونه استقرارهای خود را با Kubernetes ساختار دهید.

Jupyter

پروژهٔ Jupyter یک دفترچهٔ علمی تعاملی مبتنی بر وب برای کاوش و visualization است. دانش‌آموزان و دانشمندان در سراسر جهان از آن برای ساخت و کاوش داده و visualizationهای داده استفاده می‌کنند. چون هم استقرارش ساده است و هم استفاده از آن جالب است، سرویس اولی عالی برای استقرار روی Kubernetes است.

ابتدا یک namespace برای نگه‌داشتن برنامهٔ Jupyter می‌سازیم:

$ kubectl create namespace jupyter

سپس یک deployment با اندازهٔ یک از خود برنامه می‌سازیم:

yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    run: jupyter
  name: jupyter
  namespace: jupyter
spec:
  replicas: 1
  selector:
    matchLabels:
      run: jupyter
  template:
    metadata:
      labels:
        run: jupyter
    spec:
      containers
      - image: jupyter/scipy-notebook:abdb27a6dfbb
        name: jupyter
      dnsPolicy: ClusterFirst
      restartPolicy: Always

فایلی به نام jupyter.yaml با این محتوا بسازید. پس از ساخت این فایل، می‌توانید آن را با این دستور مستقر کنید:

$ kubectl create -f jupyter.yaml

حالا باید منتظر بمانید تا container ساخته شود. container Jupyter نسبتاً بزرگ است (در زمان نگارش ۲ گیگابایت)، بنابراین ممکن است چند دقیقه طول بکشد تا بالا بیاید.

می‌توانید با دستور watch منتظر آماده شدن container بمانید (در macOS باید این دستور را با brew install watch نصب کنید):

$ watch kubectl get pods --namespace jupyter

وقتی container Jupyter بالا و در حال اجرا شد، باید token ورود اولیه را به دست آورید. می‌توانید با نگاه کردن به logهای container این کار را انجام دهید:

$ pod_name=$(kubectl get pods --namespace jupyter --no-headers | awk '{print $1}') \
kubectl logs --namespace jupyter ${pod_name}

سپس token را کپی کنید (چیزی شبیه /?token=0195713c8e65088650fdd8b599db377b7ce6c9b10bd13766 خواهد بود).

بعد، port forwarding به container Jupyter راه بیندازید:

$ kubectl port-forward ${pod_name} 8888:8888

در نهایت می‌توانید به http://localhost:8888/?token=<token> سر بزنید و tokenی را که قبلاً از logها کپی کرده‌اید جایگزین کنید.

اکنون باید dashboard Jupyter در مرورگر شما بارگذاری شده باشد. اگر مایلید، می‌توانید در سایت پروژهٔ Jupyter آموزش‌هایی برای آشنایی با Jupyter پیدا کنید.

Parse

سرور Parse یک cloud API اختصاص‌یافته برای فراهم کردن ذخیره‌سازی آسان برای برنامه‌های موبایل است. کتابخانه‌های client متنوعی دارد که ادغام با Android، iOS و سایر پلتفرم‌های موبایل را آسان می‌کند. Parse در سال ۲۰۱۳ توسط Facebook خریداری شد و بعداً خاموش شد. خوشبختانه برای ما، تیم اصلی Parse یک سرور سازگار را متن‌باز کرد که در دسترس ماست. این بخش نحوهٔ راه‌اندازی Parse در Kubernetes را توصیف می‌کند.

پیش‌نیازها

Parse برای ذخیره‌سازی از یک کلاستر MongoDB استفاده می‌کند. فصل ۱۵ توضیح داد چگونه یک کلاستر MongoDB تکرارشده با StatefulSetهای Kubernetes راه‌اندازی کنید. این بخش فرض می‌کند یک کلاستر Mongo با سه replica در Kubernetes در حال اجرا دارید با نام‌های mongo-0.mongo، mongo-1.mongo و mongo-2.mongo.

این دستورالعمل‌ها همچنین فرض می‌کنند login Docker دارید؛ اگر ندارید، می‌توانید رایگان در https://docker.com بگیرید.

در نهایت، فرض می‌کنیم یک کلاستر Kubernetes مستقر کرده‌اید و ابزار kubectl به‌درستی پیکربندی شده است.

ساخت parse-server

parse-server متن‌باز به‌طور پیش‌فرض با Dockerfile می‌آید تا containerization آسان باشد. ابتدا مخزن Parse را clone کنید:

$ git clone https://github.com/ParsePlatform/parse-server

سپس وارد آن دایرکتوری شوید و image را بسازید:

$ cd parse-server
$ docker build -t ${DOCKER_USER}/parse-server .

در نهایت آن image را به Docker hub push کنید:

$ docker push ${DOCKER_USER}/parse-server

استقرار parse-server

وقتی image container ساخته شد، استقرار parse-server در کلاستر شما نسبتاً ساده است. Parse هنگام پیکربندی به سه متغیر محیطی نگاه می‌کند:

PARSE_SERVER_APPLICATION_ID : شناسه‌ای برای مجاز کردن برنامهٔ شما

PARSE_SERVER_MASTER_KEY : شناسه‌ای که کاربر master (root) را مجاز می‌کند

PARSE_SERVER_DATABASE_URI : URI کلاستر MongoDB شما

با کنار هم گذاشتن همهٔ این‌ها، می‌توانید Parse را به‌عنوان یک deployment Kubernetes با فایل YAML در مثال ۱۷-۱ مستقر کنید.

مثال ۱۷-۱. parse.yaml

yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: parse-server
  namespace: default
spec:
  replicas: 1
  template:
    metadata:
      labels:
        run: parse-server
    spec:
      containers:
      - name: parse-server
        image: ${DOCKER_USER}/parse-server
        env:
        - name: PARSE_SERVER_DATABASE_URI
          value: "mongodb://mongo-0.mongo:27017,\
             mongo-1.mongo:27017,mongo-2.mongo\
             :27017/dev?replicaSet=rs0"
        - name: PARSE_SERVER_APP_ID
          value: my-app-id
        - name: PARSE_SERVER_MASTER_KEY
          value: my-master-key

آزمایش Parse

برای آزمایش استقرار، باید آن را به‌عنوان یک سرویس Kubernetes expose کنید. می‌توانید با تعریف سرویس در مثال ۱۷-۲ این کار را انجام دهید.

مثال ۱۷-۲. parse-service.yaml

yaml
apiVersion: v1
kind: Service
metadata:
  name: parse-server
  namespace: default
spec:
  ports:
  - port: 1337
    protocol: TCP
    targetPort: 1337
  selector:
    run: parse-server

حالا سرور Parse شما بالا و در حال اجرا و آمادهٔ دریافت درخواست از برنامه‌های موبایل است. البته در هر برنامهٔ واقعی احتمالاً می‌خواهید اتصال را با HTTPS امن کنید. برای جزئیات بیشتر چنین پیکربندی‌ای می‌توانید صفحهٔ GitHub parse-server را ببینید.

Ghost

Ghost یک موتور وبلاگ محبوب با رابط تمیز است که با JavaScript نوشته شده. می‌تواند از پایگاه دادهٔ فایل‌محور SQLite یا MySQL برای ذخیره‌سازی استفاده کند.

پیکربندی Ghost

Ghost با یک فایل JavaScript ساده که سرور را توصیف می‌کند پیکربندی می‌شود. این فایل را به‌عنوان یک ConfigMap ذخیره می‌کنیم. یک پیکربندی توسعهٔ ساده برای Ghost شبیه مثال ۱۷-۳ است.

مثال ۱۷-۳. ghost-config.js

javascript
var path = require('path'),
    config;

config = {
    development: {
        url: 'http://localhost:2368',
        database: {
            client: 'sqlite3',
            connection: {
                filename: path.join(process.env.GHOST_CONTENT,
                                    '/data/ghost-dev.db')
            },
            debug: false
        },
        server: {
            host: '0.0.0.0',
            port: '2368'
        },
        paths: {
            contentPath: path.join(process.env.GHOST_CONTENT, '/')
        }
    }
};

module.exports = config;

وقتی این فایل پیکربندی را در ghost-config.js ذخیره کردید، می‌توانید یک شیء ConfigMap Kubernetes بسازید:

$ kubectl create cm --from-file ghost-config.js ghost-config

این یک ConfigMap به نام ghost-config می‌سازد. مثل مثال Parse، این فایل پیکربندی را به‌عنوان volume داخل container mount می‌کنیم. Ghost را به‌عنوان یک شیء Deployment مستقر می‌کنیم که این volume mount را به‌عنوان بخشی از template Pod تعریف می‌کند (مثال ۱۷-۴).

مثال ۱۷-۴. ghost.yaml

yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: ghost
spec:
  replicas: 1
  selector:
    matchLabels:
      run: ghost
  template:
    metadata:
      labels:
        run: ghost
    spec:
      containers:
      - image: ghost
        name: ghost
        command:
        - sh
        - -c
        - cp /ghost-config/ghost-config.js /var/lib/ghost/config.js
          && /usr/local/bin/docker-entrypoint.sh node current/index.js
        volumeMounts:
        - mountPath: /ghost-config
          name: config
      volumes:
      - name: config
        configMap:
          defaultMode: 420
          name: ghost-config

یک نکته اینجا این است که فایل config.js را از مکانی دیگر به جایی که Ghost انتظار دارد پیدایش کند کپی می‌کنیم، چون ConfigMap فقط می‌تواند دایرکتوری‌ها را mount کند، نه فایل‌های تکی. Ghost انتظار دارد فایل‌های دیگری که در آن ConfigMap نیستند در دایرکتوری‌اش حضور داشته باشند، بنابراین نمی‌توانیم کل ConfigMap را ساده mount کنیم به /var/lib/ghost.

می‌توانید با این دستور اجرا کنید:

$ kubectl apply -f ghost.yaml

وقتی Pod بالا و در حال اجرا شد، می‌توانید آن را به‌عنوان سرویس expose کنید:

$ kubectl expose deployments ghost --port=2368

وقتی سرویس expose شد، می‌توانید با دستور kubectl proxy به سرور Ghost دسترسی پیدا کنید:

$ kubectl proxy

سپس در مرورگر به http://localhost:8001/api/v1/namespaces/default/services/ghost/proxy/ سر بزنید تا با Ghost تعامل کنید.

Ghost + MySQL

البته این مثال خیلی scalable یا حتی reliable نیست، چون محتوای وبلاگ در یک فایل محلی داخل container ذخیره می‌شود. رویکرد scalableتر ذخیرهٔ دادهٔ وبلاگ در پایگاه دادهٔ MySQL است.

برای این کار، ابتدا config.js را طوری تغییر دهید که شامل این باشد:

javascript
...
database: {
    client: 'mysql',
    connection: {
      host     : 'mysql',
      user     : 'root',
      password : 'root',
      database : 'ghost_db',
      charset : 'utf8'
    }
 },
...

واضح است که در استقرار دنیای واقعی می‌خواهید رمز عبور را از root به چیزی محرمانه‌تر تغییر دهید.

بعد، یک شیء ConfigMap جدید ghost-config بسازید:

$ kubectl create configmap ghost-config-mysql --from-file ghost-config.js

سپس deployment Ghost را به‌روز کنید تا نام ConfigMap mount‌شده از config-map به config-map-mysql تغییر کند:

yaml
...
        - configMap:
            name: ghost-config-mysql
...

با دستورالعمل‌های «ذخیره‌سازی بومی Kubernetes با StatefulSetها» در صفحهٔ ۱۸۶، یک سرور MySQL در کلاستر Kubernetes مستقر کنید. مطمئن شوید سرویسی به نام mysql هم تعریف شده باشد.

باید پایگاه داده را در MySQL بسازید:

$ kubectl exec -it mysql-zzmlw -- mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
...

mysql> create database ghost_db;
...

در نهایت rollout انجام دهید تا این پیکربندی جدید مستقر شود:

$ kubectl apply -f ghost.yaml

چون سرور Ghost شما حالا از پایگاه داده‌اش جدا شده، می‌توانید سرور Ghost را scale up کنید و داده در همهٔ replicaها مشترک بماند.

ghost.yaml را ویرایش کنید و spec.replicas را روی ۳ بگذارید، سپس اجرا کنید:

$ kubectl apply -f ghost.yaml

نصب Ghost شما حالا به سه replica scale up شده است.

Redis

Redis یک key/value store در حافظهٔ محبوب است با ویژگی‌های اضافی متعدد. برنامهٔ جالبی برای استقرار است چون مثال خوبی از ارزش abstraction Pod در Kubernetes است. دلیلش این است که یک نصب Redis قابل اعتماد در واقع دو برنامه است که با هم کار می‌کنند. اولی redis-server است که key/value store را پیاده می‌کند، و دیگری redis-sentinel که health checking و failover برای یک کلاستر Redis تکرارشده را پیاده می‌کند.

وقتی Redis به‌صورت تکرارشده مستقر می‌شود، یک سرور master وجود دارد که هم برای خواندن و هم نوشتن قابل استفاده است. علاوه بر این، replicaهای دیگری هستند که دادهٔ نوشته‌شده روی master را تکرار می‌کنند و می‌توانند برای load-balance کردن عملیات خواندن استفاده شوند. هر یک از این replicaها می‌توانند در صورت خرابی master اصلی، failover کنند و master شوند. این failover توسط Redis sentinel انجام می‌شود. در استقرار ما، هم سرور Redis و هم Redis sentinel در همان فایل colocate شده‌اند.

پیکربندی Redis

مثل قبل، از ConfigMapهای Kubernetes برای پیکربندی نصب Redis استفاده می‌کنیم. Redis به پیکربندی‌های جدا برای master و replicaهای slave نیاز دارد. برای پیکربندی master، فایلی به نام master.conf بسازید که کد مثال ۱۷-۵ را دارد.

مثال ۱۷-۵. master.conf

bind 0.0.0.0
port 6379

dir /redis-data

این به Redis می‌گوید روی همهٔ interfaceهای شبکه روی پورت ۶۳۷۹ (پورت پیش‌فرض Redis) bind کند و فایل‌هایش را در دایرکتوری /redis-data ذخیره کند.

پیکربندی slave یکسان است، اما یک directive slaveof اضافه می‌کند. فایلی به نام slave.conf بسازید که محتوای مثال ۱۷-۶ را دارد.

مثال ۱۷-۶. slave.conf

bind 0.0.0.0
port 6379

dir .

slaveof redis-0.redis 6379

توجه کنید برای نام master از redis-0.redis استفاده می‌کنیم. این نام را با یک سرویس و StatefulSet راه می‌اندازیم.

همچنین به پیکربندی Redis sentinel نیاز داریم. فایلی به نام sentinel.conf با محتوای مثال ۱۷-۷ بسازید.

مثال ۱۷-۷. sentinel.conf

bind 0.0.0.0
port 26379

sentinel monitor redis redis-0.redis 6379 2
sentinel parallel-syncs redis 1
sentinel down-after-milliseconds redis 10000
sentinel failover-timeout redis 20000

حالا که همهٔ فایل‌های پیکربندی را داریم، باید چند اسکریپت wrapper ساده برای استفاده در استقرار StatefulSet بسازیم.

اولین اسکریپت hostname Pod را نگاه می‌کند و تعیین می‌کند این master است یا slave، و Redis را با پیکربندی مناسب اجرا می‌کند. فایلی به نام init.sh با کد مثال ۱۷-۸ بسازید.

مثال ۱۷-۸. init.sh

bash
#!/bin/bash
if [[ ${HOSTNAME} == 'redis-0' ]]; then
  redis-server /redis-config/master.conf
else
  redis-server /redis-config/slave.conf
fi

اسکریپت دیگر برای sentinel است. در این مورد لازم است چون باید منتظر بمانیم تا نام DNS redis-0.redis در دسترس شود. اسکریپتی به نام sentinel.sh با کد مثال ۱۷-۹ بسازید.

مثال ۱۷-۹. sentinel.sh

bash
#!/bin/bash
cp /redis-config-src/*.* /redis-config

while ! ping -c 1 redis-0.redis; do
  echo 'Waiting for server'
  sleep 1
done

redis-sentinel /redis-config/sentinel.conf

حالا باید همهٔ این فایل‌ها را در یک شیء ConfigMap بسته‌بندی کنیم. می‌توانید با یک خط دستور این کار را انجام دهید:

$ kubectl create configmap \
  --from-file=slave.conf=./slave.conf \
  --from-file=master.conf=./master.conf \
  --from-file=sentinel.conf=./sentinel.conf \
  --from-file=init.sh=./init.sh \
  --from-file=sentinel.sh=./sentinel.sh \
  redis-config

ساخت یک سرویس Redis

گام بعدی در استقرار Redis ساخت یک سرویس Kubernetes است که naming و discovery برای replicaهای Redis فراهم کند (مثلاً redis-0.redis). برای این کار سرویسی بدون آدرس IP کلاستر می‌سازیم (مثال ۱۷-۱۰).

مثال ۱۷-۱۰. redis-service.yaml

yaml
apiVersion: v1
kind: Service
metadata:
  name: redis
spec:
  ports:
  - port: 6379
    name: peer
  clusterIP: None
  selector:
    app: redis

می‌توانید این سرویس را با kubectl apply -f redis-service.yaml بسازید. نگران نباشید که Podهای سرویس هنوز وجود ندارند. Kubernetes اهمیتی نمی‌دهد؛ وقتی Podها ساخته شوند نام‌های درست را اضافه می‌کند.

استقرار Redis

آماده‌ایم کلاستر Redis را مستقر کنیم. برای این کار از StatefulSet استفاده می‌کنیم. StatefulSetها را در «MongoDB تکرارشدهٔ دستی با StatefulSetها» در صفحهٔ ۱۸۷ معرفی کردیم، وقتی نصب MongoDB را بحث کردیم. StatefulSetها indexing (مثلاً redis-0.redis) و همچنین semantics ایجاد و حذف مرتب‌شده (همیشه redis-0 قبل از redis-1 ساخته می‌شود و غیره) را فراهم می‌کنند. برای برنامه‌های stateful مثل Redis بسیار مفیدند، اما صادقانه بگوییم، اساساً شبیه deploymentهای Kubernetes به نظر می‌رسند. مثال ۱۷-۱۱ نشان می‌دهد StatefulSet کلاستر Redis ما چگونه است.

مثال ۱۷-۱۱. redis.yaml

yaml
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: redis
spec:
  replicas: 3
  serviceName: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - command: [sh, -c, source /redis-config/init.sh ]
        image: redis:4.0.11-alpine
        name: redis
        ports:
        - containerPort: 6379
          name: redis
        volumeMounts:
        - mountPath: /redis-config
          name: config
        - mountPath: /redis-data
          name: data
      - command: [sh, -c, source /redis-config-src/sentinel.sh]
        image: redis:4.0.11-alpine
        name: sentinel
        volumeMounts:
        - mountPath: /redis-config-src
          name: config
        - mountPath: /redis-config
          name: data
      volumes:
      - configMap:
          defaultMode: 420
          name: redis-config
        name: config
      - emptyDir:
        name: data
        volumeMounts:
        - mountPath: /redis-config
          name: config
        - mountPath: /redis-data
          name: data
      - command: [sh, -c, source /redis-config/sentinel.sh]
        image: redis:3.2.7-alpine
        name: sentinel
        volumeMounts:
        - mountPath: /redis-config
          name: config

می‌بینید در این Pod دو container وجود دارد. یکی اسکریپت init.sh که ساختیم و سرور اصلی Redis را اجرا می‌کند، و دیگری sentinel است که سرورها را مانیتور می‌کند.

همچنین می‌بینید دو volume در Pod تعریف شده. یکی volumeای است که از ConfigMap ما برای پیکربندی دو برنامهٔ Redis استفاده می‌کند، و دیگری یک volume سادهٔ emptyDir است که به container سرور Redis map شده تا دادهٔ برنامه را نگه دارد تا restart container زنده بماند. برای نصب Redis قابل اعتمادتر، این می‌تواند یک دیسک متصل به شبکه باشد، همان‌طور که در فصل ۱۵ بحث شد.

حالا که کلاستر Redis را تعریف کردیم، می‌توانیم با این دستور بسازیم:

$ kubectl apply -f redis.yaml

بازی با کلاستر Redis

برای نشان دادن اینکه واقعاً یک کلاستر Redis ساخته‌ایم، می‌توانیم چند آزمایش انجام دهیم.

ابتدا می‌توانیم تعیین کنیم sentinel Redis کدام سرور را master می‌داند. برای این کار می‌توانیم دستور redis-cli را در یکی از Podها اجرا کنیم:

$ kubectl exec redis-2 -c redis \
  -- redis-cli -p 26379 sentinel get-master-addr-by-name redis

این باید آدرس IP Pod redis-0 را چاپ کند. می‌توانید با kubectl get pods -o wide تأیید کنید.

بعد، تأیید می‌کنیم replication واقعاً کار می‌کند.

برای این کار، ابتدا مقدار foo را از یکی از replicaها بخوانید:

$ kubectl exec redis-2 -c redis -- redis-cli -p 6379 get foo

باید هیچ داده‌ای در پاسخ نبینید.

بعد، سعی کنید آن داده را روی یک replica بنویسید:

$ kubectl exec redis-2 -c redis -- redis-cli -p 6379 set foo 10
READONLY You can't write against a read only slave.

نمی‌توانید روی replica بنویسید، چون فقط خواندنی است. همان دستور را روی redis-0 که master است امتحان کنیم:

$ kubectl exec redis-0 -c redis -- redis-cli -p 6379 set foo 10
OK

حالا خواندن اولیه را از یک replica دوباره امتحان کنید:

$ kubectl exec redis-2 -c redis -- redis-cli -p 6379 get foo
10

این نشان می‌دهد کلاستر ما درست راه‌اندازی شده و داده بین masterها و slaveها replicate می‌شود.

خلاصه

در این فصل توضیح دادیم چگونه انواع مختلفی از برنامه‌ها را با مفاهیم گوناگون Kubernetes مستقر کنیم. دیدیم چگونه naming و discovery مبتنی بر سرویس را کنار هم بگذاریم تا frontendهای وب مثل Ghost و هم سرورهای API مثل Parse را مستقر کنیم، و دیدیم abstraction Pod چگونه استقرار اجزایی که یک کلاستر Redis قابل اعتماد می‌سازند را آسان می‌کند. صرف‌نظر از اینکه واقعاً این برنامه‌ها را به production مستقر می‌کنید یا نه، مثال‌ها الگوهایی را نشان دادند که می‌توانید برای مدیریت برنامه‌هایتان با Kubernetes تکرار کنید. امیدواریم دیدن مفاهیمی که در فصل‌های قبلی توصیف کردیم در مثال‌های دنیای واقعی زنده شوند، به شما کمک کند بهتر بفهمید چگونه Kubernetes را برای خودتان به کار بیندازید.