حالت تاریک
فصل ۱۷ — استقرار برنامههای دنیای واقعی
فصلهای قبلی انواع مختلفی از اشیاء 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 را برای خودتان به کار بیندازید.