مجلد nezal

ضع مجلدًا باسم nezal في جذر مستودعك وسينشر نزال ما بداخله. يحصل كل مطوّر على بيئة كاملة للتطبيق من فرعه هو. وتُبنى كل خدمة من ذلك الفرع وتعمل مع قاعدة بيانات وذاكرة تخزين مؤقت (cache) خاصتين بها. ولا يُشارَك أي شيء مع مطوّر آخر.

المستودع الذي لا يحتوي مجلد nezal يُقرأ كما كان يُقرأ دائمًا. والمجلد هو الطريقة التي تخبر بها نزال بما يجب أن يفعله بالضبط.

الحالة. كل ما في هذه الصفحة متاح: الصيغة وبيئات الوضع الكامل التي تُبنى وتُنشر من فرع كل مطوّر والأسرار والتجاوز من لوحة التحكم وأوامر CLI والوكيل الذكي.

البداية السريعة

نفّذ كل أمر داخل مستودعك.

  1. أنشئ مجلد nezal. يقرأ nezal init مستودعك ويكتب مسودة أولى تقرؤها وتصحّحها.

    nezal init
    
  2. ضع ملفات النشر فيه. مجلد لكل خدمة فيه مخطط Helm أو Kustomize أو ملفات Kubernetes عادية.

    nezal/
    ├── nezal.yaml
    ├── nezal.env
    ├── backend/    # Helm chart or manifests
    └── database/
    
  3. أعطِ خدمة رابطًا. يضم nezal.yaml إعدادات كل خدمة. وpublic: true يعطيها رابطًا.

    version: 1
    services:
      backend:
        public: true
    
  4. اضبط المتغيّرات. يضم nezal.env القيم والقوالب. ولكل بيئة قيمها الخاصة.

    LOG_LEVEL=debug
    ENVIRONMENT={{ .Environment }}
    PUBLIC_URL={{ .Service.backend.URL }}
    API_KEY={{ .Secret.API_KEY }}
    
  5. تحقّق وابدأ. تُحفظ الأسرار في الكلستر ولا تدخل git أبدًا.

    nezal secret set shop API_KEY
    nezal validate
    git push
    nezal up
    

تشرح بقية هذه الصفحة كل خيار.

ما يوضع فيه

nezal/
  nezal.yaml       what to build and which services get a URL
  nezal.env        the variables each service is given
  backend/         a Helm chart
  frontend/        a Helm chart
  huey/            plain manifests
  redis/           plain manifests
  database/        plain manifests or a chart

كل مجلد داخل nezal/ وحدة واحدة تُنشر. ويكتشف نزال ما يحتويه كل مجلد:

يحتوي المجلد يعامله نزال على أنه
Chart.yaml مخطط Helm
kustomization.yaml طبقة Kustomize (overlay)
أي ملف .yaml أو .yml أو .json ملفات Kubernetes عادية (manifests)
لا شيء مما سبق لا شيء يُنشر ويُتخطّى المجلد

يمكنك الجمع بين هذه الأنواع. وتُنشر كلها معًا تطبيقًا واحدًا. والملفان nezal.yaml وnezal.env اختياريان. ومجلد nezal الذي لا يحتوي إلا مخططات ينشر كل مخطط في الوضع الكامل دون متغيرات إضافية.

مساحة أسماء واحدة لكل مساحة عمل

تعمل كل بيئات مساحة العمل في مساحة الأسماء الخاصة بمساحة العمل. فمساحة العمل acme لها مساحة الأسماء acme وكل بيئة تُنشر هناك. ويحمل كل كائن اسم البيئة في اسمه:

في ملفات manifests لديك في البيئة amr في البيئة test
backend backend-preview-amr backend-preview-test
database database-preview-amr database-preview-test
redis redis-preview-amr redis-preview-test

تجيب الخدمة العامة على https://<service>-preview-<environment>.<domain>. ومع نطاق المعاينة dev.acme.test يكون الخادم الخلفي (backend) للبيئة amr هو https://backend-preview-amr.dev.acme.test.

لا تتغير مخططاتك. والعنوان الموجود في مخطط ويسمّي خدمة أخرى من التطبيق يُوجَّه إلى نسخة تلك الخدمة في البيئة نفسها. فيصبح http://collaboration-server-service:9600 هو http://collaboration-server-service-preview-amr:9600. وتُعرض كل إعادة كتابة في صفحة البيئة.

الشبكة

لكل بيئة سياسة شبكة (NetworkPolicy) خاصة بها. وهي تحدد ما تصل إليه Pods البيئة وما يصل إليها:

حركة المرور مسموحة
بين Pods البيئة نفسها نعم
من الإنترنت وإليه نعم
من متحكم الـ ingress (ingress controller) نعم
DNS نعم
من بيئة أخرى وإليها في مساحة العمل نفسها أو في أي مساحة عمل أخرى لا
إلى نظام نزال ومساحات الأسماء الأخرى والـ Pods التي لا يديرها نزال في مساحة الأسماء لا
إلى العقد وعنوان البيانات الوصفية للسحابة 169.254.169.254 لا
إلى شبكة داخلية يذكرها المشرف في egressCIDRs ضمن السياسة (Policy) نعم

والشبكة الداخلية تعني النطاقات الخاصة والمشتركة ونطاقات link-local وloopback: 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 169.254.0.0/16 127.0.0.0/8 وما يقابلها في IPv6. ويُوصَل إلى قاعدة بيانات الشركة أو خادمها الوسيط (proxy) على أحد هذه النطاقات بإضافة نطاقه إلى egressCIDRs. وكذلك الرابط العام للتطبيق نفسه حين يكون لموازن الحمل عنوان داخلي.

تفرض إضافة الشبكة (network plugin) في الكلستر هذه السياسة. والإضافة التي تتجاهل NetworkPolicy لا تترك بين البيئات فاصلًا سوى الأسماء.

الاسم الأطول من 63 حرفًا يُختصر من أوله مع بصمة تجزئة (hash) قصيرة ليبقى فريدًا. ويبقى اسم البيئة كاملًا في آخره دائمًا. واسم البيئة 20 حرفًا على الأكثر من الأحرف الصغيرة والأرقام والشرطات.

nezal.yaml

كل شيء اختياري ما عدا version.

version: 1
mode: full

definitions:                 # optional. Default: every folder under nezal/
  - path: backend
    values: [values-dev.yaml]
    releaseName: backend
  - path: redis

services:
  backend:
    build:
      context: components/backend
      dockerfile: docker/Dockerfile
      target: runtime
      args: {PYTHON_VERSION: "3.12"}
    public: true
    port: 8000
    migrate:
      command: [python, manage.py, migrate]
    dependsOn: [database, redis]
    replicas: 1
    resources:
      requests: {cpu: 100m, memory: 256Mi}
      limits: {memory: 1Gi}
  huey:
    build: {from: backend}
    dependsOn: [backend]
  frontend:
    build: {context: components/frontend}
    public: true

storage:
  size: 2Gi

environment:
  ttl: 72h

entrypoint: frontend

version

دائمًا 1. والملف الذي يحمل أي قيمة أخرى يُرفض برسالة واضحة بدل أن يُقرأ قراءة خاطئة.

mode

القيمة ما تحصل عليه البيئة
full القيمة الافتراضية. نسخة خاصة من كل خدمة بما فيها قاعدة البيانات وذاكرة التخزين المؤقت. لا يُشارَك أي شيء فلا يمكن لفرع أن يؤثر في مطوّر آخر أبدًا. وكلفتها نحو منظومة كاملة لكل بيئة.
shared تُنسخ فقط الخدمات التي يغيّرها المطوّر. ويخدم كل ما عداها أساس مشترك واحد للتطبيق. وهذا أقل كلفة للمنظومات الكبيرة. وتتشارك البيئتان قاعدة البيانات نفسها.

definitions

ما يُنشر. احذفه لنشر كل مجلد تحت nezal/. واذكره لاختيار بعض المجلدات أو لإعطاء مخطط Helm قيمه.

المفتاح المعنى
path مجلد داخل nezal/. ولا يمكن أن يخرج منه.
kind helm أو kustomize أو manifests. يُكتشف تلقائيًا إذا لم يُذكر. وإذا ذُكر يجب أن يطابق ما يحتويه المجلد.
values لـ Helm فقط. ملفات قيم مسارها نسبي إلى المخطط وتُدمج بالترتيب.
releaseName لـ Helm فقط. اسم الإصدار (release). والقيمة الافتراضية اسم المجلد.

services

إعدادات للخدمة تزيد على ما في ملفات manifests الخاصة بها. والمفتاح هو اسم الـ Deployment أو الـ StatefulSet الخاص بها. واسم الخدمة يتكون من أحرف صغيرة وأرقام وشرطات ولا يزيد على 40 حرفًا حتى تتسع لاحقة البيئة.

المفتاح المعنى
build.context ابنِ هذه الخدمة من الفرع. المجلد الذي يُبنى ومساره نسبي إلى جذر المستودع.
build.dockerfile ملف Dockerfile ومساره نسبي إلى السياق (context). والقيمة الافتراضية Dockerfile. وإذا لم يوجد أي Dockerfile يبني نزال الخدمة بـ buildpacks.
build.target مرحلة Dockerfile التي يتوقف عندها البناء.
build.args معاملات البناء. قد تحمل القيمة قوالب nezal.env وتُحلّ لكل بيئة قبل أن تبني. يُرفض السر ويُرفض كذلك الاسم الذي يشبه بيانات اعتماد لأن معاملات البناء تُسجَّل في الصورة.
build.from أعد استخدام الصورة التي تبنيها خدمة أخرى. وهذا مناسب لعامل (worker) مثل Huey يشغّل شيفرة الخادم الخلفي نفسها. ولا يقبل أي مفتاح آخر ويجب أن يسمّي خدمة تُبنى هي نفسها.
public أعطِ الخدمة رابطًا.
port المنفذ الذي يصل إليه الرابط. والقيمة الافتراضية أول منفذ للخدمة.
migrate.command أمر يعمل بوصفه Job بصورة الخدمة ومتغيراتها قبل أن تبدأ الخدمة. ويعمل مع كل نشر.
migrate.env متغيرات لمهمة الترحيل وحدها فوق متغيرات الخدمة. تقبل القيم قوالب nezal.env. السر قيمة كاملة {{ .Secret.NAME }} ويُرفض الاسم الذي يشبه بيانات اعتماد بقيمة صريحة. أي تغيير يشغّل الترحيل مرة أخرى.
dependsOn خدمات يجب أن تجهز أولًا. وتُرفض الاعتماديات الدائرية.
replicas عدد النسخ التي تعمل في البيئة. من 0 إلى 10. وغالبًا 1.
resources المعالج والذاكرة للخدمة داخل البيئة. والبيئات عادةً أصغر بكثير من الإنتاج.
scheduling keep يُبقي في النسخة محدد العُقد والتسامحات والتقارب وقيود التوزيع وفئة الأولوية كما في مخطط الخدمة. إن تُرك فارغًا تُحذف وتذكر البيئة ذلك. لا تُقبل أي قيمة أخرى.
ingress.annotations تعليقات توضيحية لمدخل الخدمة العامة توضع فوق القيم الافتراضية للتثبيت. إن تُركت تُنقل تعليقات المدخل الموجود في المجلد للخدمة نفسها بعد حذف ما يحدد مضيفًا أو شهادة أو فئة أو يصل إلى خارج النطاق.

الخدمة التي تُبنى من الفرع لا تحتاج إلى صورة في مخططها. فنزال يضبط الصورة على الصورة التي بناها لذلك الإيداع (commit).

storage

size هو حجم كل وحدة تخزين تطلبها البيئة. والقيمة الافتراضية 1Gi. وتحتفظ قاعدة البيانات ببياناتها بعد إعادة التشغيل وبعد النوم. وتُحذف وحدة التخزين مع البيئة.

environment

القيم الافتراضية للبيئات الجديدة لهذا التطبيق.

المفتاح المعنى
ttl مدة بقاء البيئة. مثل 72h.
sleepAfterIdle قلّص البيئة إلى الصفر بعد هذه المدة دون حركة مرور. مثل 2h. في الوضع المشترك فقط لأن النسخة الكاملة لا موجّه لها يرى حركة المرور.

وتبقى سياسة الفريق هي التي تحدد الحد الأقصى.

entrypoint

الخدمة التي تعرض صفحة البيئة رابطها أولًا. ويجب أن تكون عامة.

profiles

مجموعات اختيارية من التعريفات والخدمات. لا تنشر البيئة مجموعة إلا حين تختارها. وكل ما ليس في أي مجموعة يُنشر دائمًا.

المفتاح المعنى
profiles.<name> مجموعة اختيارية. تنشرها البيئة فقط حين تختارها. الاسم أحرف صغيرة وأرقام وشرطات و 63 حرفًا على الأكثر.
profiles.<name>.definitions مجلدات داخل nezal/ لا تُنشر إلا مع هذه المجموعة. يجب أن يكون كل منها تعريفًا في المجلد.
profiles.<name>.services خدمات لا تعمل إلا مع هذه المجموعة. دونها يُترك عبء عملها وخدمتها وترحيلها وبناؤها. يجب أن تكون كل منها خدمة في المجلد. والخدمة التي لا يصنعها إلا مخطط Helm أو kustomization تنبيه في nezal validate لأنها تُعرف حين يُعرض المخطط.

nezal.env

المتغيرات التي تُعطى لكل خدمة. وله الشكل نفسه لملف .env الذي يحتفظ به فريقك بجوار شيفرته.

# For every service
MINIO_TIMEOUT=6000
LOG_LEVEL=debug
BACKEND_URL={{ .Service.backend.URL }}
REDIS_URL=redis://{{ .Service.redis.Host }}:6379
DATABASE_URL=postgres://app:app@{{ .Service.database.Host }}:5432/app
DB_PASSWORD={{ .Secret.DB_PASSWORD }}
ENVIRONMENT={{ .Environment }}

# Only for the frontend. A value here wins over the same name above.
[frontend]
VITE_API_URL={{ .Service.backend.URL }}/api
LOG_LEVEL=warn

الصيغة

السطر المعنى
NAME=value متغير. ويعمل export NAME=value أيضًا.
[service] كل سطر بعده ينطبق على تلك الخدمة وحدها.
# text تعليق. وتنتهي القيمة غير المحاطة بعلامات اقتباس أيضًا عند #.
NAME="a\nb" علامات الاقتباس المزدوجة تسمح بـ \n و\t و\" و\\.
NAME='{{ raw }}' علامات الاقتباس المفردة حرفية ولا تُقرأ قالبًا أبدًا.

يُرفض الاسم المضبوط مرتين في القسم نفسه. وتُرفض القيمة تحت اسم يبدو اسم بيانات اعتماد مثل DB_PASSWORD ما لم تكن مرجعًا إلى سر. فهذا الملف في git ويستطيع قراءته كل من يستطيع استنساخ المستودع.

القوالب

يمكن أن تحتوي القيمة على {{ }} وتُملأ لكل بيئة بعد نشرها.

القالب في البيئة amr
{{ .Environment }} amr
{{ .Owner }} من أنشأها
{{ .Branch }} الفرع الذي أُنشئت منه
{{ .Commit }} الإيداع (commit) الذي تشغّله
{{ .Namespace }} مساحة أسماء مساحة العمل
{{ .Domain }} نطاق المعاينة
{{ .Service.backend.Host }} backend-preview-amr
{{ .Service.backend.Port }} أول منفذ لها
{{ .Service.backend.Internal }} http://backend-preview-amr:8000 للاستدعاءات داخل الكلستر
{{ .Service.backend.URL }} https://backend-preview-amr.<domain> للخدمة العامة
{{ .Secret.NAME }} سر. انظر أدناه.

أي خطأ يوقف النشر برسالة تسمّيه. والخدمة غير الموجودة أو .URL على خدمة غير عامة لا تتحول أبدًا إلى قيمة فارغة تبدو كأنها تعمل.

الأسرار

اضبط السر مرة واحدة للتطبيق من لوحة التحكم أو عبر CLI. ثم أشر إليه بوصفه القيمة الكاملة لمتغير:

DB_PASSWORD={{ .Secret.DB_PASSWORD }}

يقرأ المتغير القيمة من Secret في Kubernetes عند بدء الحاوية. ولا يضع نزال القيمة في المتغير أبدًا ولا يعرضها في صفحة البيئة. ويجب أن يكون السر هو القيمة كلها. ويُرفض prefix-{{ .Secret.X }}.

عندما يتغير الملف

يُقرأ nezal.env من فرع البيئة نفسها. ادفع (push) تغييرًا عليه وخلال دقيقتين تقريبًا يُعاد تشغيل الخدمات التي تغيّرت متغيراتها بالقيم الجديدة. ولا يُعاد بناء أي شيء. ويقرأ nezal sync <name> الفرع فورًا.

البناء

لكل بيئة يبني نزال من فرع البيئة كل خدمة لها مدخل build:

  1. يعمل البناء داخل الكلستر لديك بوصفه Job دون أي صلاحيات مميزة. ويبني BuildKit دون صلاحيات الجذر (rootless) ملف Dockerfile وتبني buildpacks المجلد الذي لا يحتوي Dockerfile.
  2. تُدفع الصورة إلى سجل الصور الخاص بمساحة العمل وتُوسم بالإيداع.
  3. الخدمة التي لم يتغير سياق بنائها تعيد استخدام الصورة المبنية مسبقًا لذلك الإيداع. لذلك لا تُبنى الشيفرة نفسها مرتين لبيئتين.
  4. تبدأ قاعدة البيانات وذاكرة التخزين المؤقت أولًا. ثم تعمل أوامر الترحيل (migrate). ثم تبدأ الخدمات بالترتيب الذي يحدده dependsOn.

الدفع إلى الفرع يعيد بناء الخدمات التي تغيّرت شيفرتها فقط ويعيد نشرها.

أدلة عملية

كل دليل أدناه مهمة واحدة من البداية إلى النهاية مع مثال يمكنك نسخه.

أعطِ البناء عناوين بيئته

كثيرًا ما تُثبّت الواجهة الأمامية عنوان واجهتها البرمجية داخل الحزمة وقت البناء. اكتب هذا العنوان قالبًا في build.args فتبني كل بيئة صورتها الخاصة التي تشير إلى خادمها الخاص.

  1. اجعل الخدمة التي يشير إليها العنوان عامة إن كان المتصفح يطلبها.
  2. أضف تحت الخدمة المبنية build.args وفيه قالب لكل قيمة تختلف من بيئة إلى أخرى. القوالب هي نفسها التي يقبلها nezal.env.
  3. ادفع الفرع. تحلّ كل بيئة المعاملات قبل أن تبني. البيئتان اللتان تخرج معاملاتهما متطابقة تتشاركان صورة واحدة. والبيئتان اللتان تختلف معاملاتهما تحصل كل منهما على صورة.
  4. افتح صفحة البيئة. القالب الذي يذكر خدمة غير موجودة يوقف البيئة مع اسم الحقل بالضبط والسبب.
version: 1
services:
  backend:
    public: true
    port: 8000
  frontend:
    public: true
    build:
      context: components/frontend
      args:
        VITE_API_URL: "{{ .Service.backend.URL }}/api"
        VITE_ENVIRONMENT: "{{ .Environment }}"
        NODE_VERSION: "20"
entrypoint: frontend
FROM node:20 AS build
ARG VITE_API_URL
ARG VITE_ENVIRONMENT
WORKDIR /app
COPY . .
RUN npm ci && npm run build

السر لا يكون معامل بناء أبدًا. معاملات البناء تُسجَّل في الصورة ويقرؤها كل من يستطيع سحبها. يُرفض {{ .Secret.NAME }} في build.args. أعطِ السر للخدمة العاملة في nezal.env بدلًا من ذلك.

أبقِ أو احذف مكان جدولة النسخة

كثيرًا ما يثبّت مخطط الإنتاج حاوياته على مجموعة عُقد بمحدد عُقد أو بتسامحات أو بتقارب. البيئة تعمل على أي عُقد في عنقودك لذلك يحذف Nezal هذه الإعدادات من كل نسخة ويذكر ذلك في صفحة البيئة.

  1. انشر المخطط كما هو. تفقد كل نسخة nodeSelector و nodeName و tolerations و affinity و topologySpreadConstraints و priorityClassName.
  2. افتح صفحة البيئة. يذكر تحذير كل خدمة وما حُذف منها.
  3. إن كانت خدمة واحدة تحتاج قيودها فعلًا على هذا العنقود مثل عامل يحتاج عقدة فيها معالج رسوميات فاضبط عليها scheduling: keep.
  4. ادفع التغيير. تحتفظ تلك الخدمة بما كتبه مخططها وتبقى كل خدمة أخرى حرة في العمل على أي عقدة.
version: 1
services:
  web:
    public: true
  ml-worker:
    scheduling: keep
# nezal/ml-worker/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata: {name: ml-worker}
spec:
  selector: {matchLabels: {app: ml-worker}}
  template:
    metadata: {labels: {app: ml-worker}}
    spec:
      nodeSelector: {accelerator: gpu}
      tolerations: [{key: gpu, operator: Exists, effect: NoSchedule}]
      containers: [{name: worker, image: "registry.example.test/ml-worker:1"}]

اضبط عنوان الخدمة العامة

يكتب Nezal مدخلًا واحدًا لكل خدمة عامة باسم مضيف وشهادة خاصين بها. تتيح لك التعليقات التوضيحية ضبط الطريقة التي يخدمها بها متحكم الدخول مثل أكبر حجم للرفع أو مهلة الانتظار.

  1. اجعل الخدمة عامة.
  2. أضف تحت ingress.annotations التعليقات التوضيحية التي يقرؤها متحكم الدخول لديك. توضع فوق القيم الافتراضية التي ضبطها المسؤول للتثبيت.
  3. ادفع الفرع وافتح عنوان البيئة. التعليقات التوضيحية موجودة على مدخل البيئة.
  4. إن تركت ingress.annotations فإن Nezal ينقل التعليقات التوضيحية للمدخل الموجود في مجلدك الذي يرسل الحركة إلى الخدمة نفسها. ويحذف منها ما يحدد مضيفًا أو شهادة أو فئة الدخول أو ينشئ شيئًا خارج النطاق مثل cert-manager.io/* أو kubernetes.io/ingress.class أو external-dns.* أو alb.ingress.kubernetes.io/*. يذكر تحذير على البيئة كل ما حُذف.
version: 1
services:
  backend:
    public: true
    port: 8000
    ingress:
      annotations:
        nginx.ingress.kubernetes.io/proxy-body-size: 50m
        nginx.ingress.kubernetes.io/proxy-read-timeout: "120"

التعليقات التوضيحية التي تختار مضيفًا أو شهادة أو فئة أو تنشئ سجل أسماء نطاقات أو موازن أحمال أو شهادة تُرفض في nezal.yaml أيضًا. وكذلك مقتطفات الإعداد والإشارات إلى سر في نطاق آخر.

انشر جزءًا اختياريًا فقط حين تطلبه

المجموعة الاختيارية (profile) مجموعة من التعريفات والخدمات. تنشر البيئة ما تذكره المجموعة فقط حين تختارها وكل ما لا تذكره أي مجموعة يُنشر دائمًا. استخدمها لجزء من التطبيق لا يحتاجه أغلب المطورين مثل محرك بحث أو مجموعة من العمّال.

  1. ضع الجزء الاختياري في مجلد خاص به تحت nezal/. هنا محرك البحث والعامل الذي يملأ فهرسه في nezal/search/.

  2. اذكر المجلد تحت profiles في nezal.yaml. يمكن للمجموعة أن تذكر مجلدات تحت definitions وخدمات مفردة تحت services. اسم المجموعة أحرف صغيرة وأرقام وشرطات.

  3. ضع كل متغير يذكر خدمة من المجموعة تحت [section] خدمة في المجموعة نفسها. الخدمة التي تحصل على متغير يذكر خدمة لا تشغّلها البيئة لا تُنشر وتذكر البيئة السبب. أما بقية الخدمات فتُنشر.

  4. لا تجعل خدمة خارج المجموعة تعتمد عبر dependsOn على خدمة داخلها. تتوقف مثل هذه البيئة بجملة مثل backend depends on opensearch which is in the profile search that this environment did not choose.

  5. تحقق بالطريقتين قبل أن تدفع. لا يُرسل شيء إلى أي مكان.

    nezal validate
    nezal validate --profile search
    
  6. ادفع الفرع.

  7. من لوحة التحكم. افتح البيئات واضغط بيئة جديدة. اختر التطبيق وفرعًا. تعرض خطوة المراجعة المجموعات الاختيارية كمربعات اختيار ولا يكون أي منها محددًا. حدّد search واضغط إنشاء. تعرض صفحة البيئة المجموعات التي اختارتها بجوار فرعها.

  8. من سطر الأوامر. اختر المجموعة بـ --profile. كرّر الخيار لتختار أكثر من مجموعة.

    nezal up amr --profile search
    nezal status amr
    

    دون --profile تشغّل البيئة backend و db فقط.

المجلد:

nezal/
├── nezal.yaml
├── nezal.env
├── backend/
│   └── backend.yaml
├── db/
│   └── db.yaml
└── search/
    ├── opensearch.yaml
    └── indexer.yaml

nezal/nezal.yaml:

version: 1
services:
  backend:
    public: true
    dependsOn: [db]
  search-indexer:
    dependsOn: [opensearch, db]
profiles:
  search:
    definitions: [search]

nezal/nezal.env:

DATABASE_URL=postgres://app:app@{{ .Service.db.Host }}:5432/app

[search-indexer]
OPENSEARCH_URL={{ .Service.opensearch.Internal }}

nezal/backend/backend.yaml:

apiVersion: apps/v1
kind: Deployment
metadata: {name: backend}
spec:
  selector: {matchLabels: {app: backend}}
  template:
    metadata: {labels: {app: backend}}
    spec:
      containers:
      - name: backend
        image: registry.example.test/shop/backend:1
        ports: [{containerPort: 8000}]
---
apiVersion: v1
kind: Service
metadata: {name: backend}
spec:
  selector: {app: backend}
  ports: [{port: 8000}]

nezal/db/db.yaml:

apiVersion: apps/v1
kind: StatefulSet
metadata: {name: db}
spec:
  serviceName: db
  selector: {matchLabels: {app: db}}
  template:
    metadata: {labels: {app: db}}
    spec:
      containers:
      - name: postgres
        image: postgres:16
        env:
        - {name: POSTGRES_USER, value: app}
        - {name: POSTGRES_PASSWORD, value: app}
        - {name: POSTGRES_DB, value: app}
        ports: [{containerPort: 5432}]
        volumeMounts: [{name: data, mountPath: /var/lib/postgresql/data}]
  volumeClaimTemplates:
  - metadata: {name: data}
    spec: {accessModes: [ReadWriteOnce]}
---
apiVersion: v1
kind: Service
metadata: {name: db}
spec:
  clusterIP: None
  selector: {app: db}
  ports: [{port: 5432}]

nezal/search/opensearch.yaml:

apiVersion: apps/v1
kind: Deployment
metadata: {name: opensearch}
spec:
  selector: {matchLabels: {app: opensearch}}
  template:
    metadata: {labels: {app: opensearch}}
    spec:
      containers:
      - name: opensearch
        image: opensearchproject/opensearch:2
        env:
        - {name: discovery.type, value: single-node}
        - {name: DISABLE_SECURITY_PLUGIN, value: "true"}
        ports: [{containerPort: 9200}]
---
apiVersion: v1
kind: Service
metadata: {name: opensearch}
spec:
  selector: {app: opensearch}
  ports: [{port: 9200}]

nezal/search/indexer.yaml:

apiVersion: apps/v1
kind: Deployment
metadata: {name: search-indexer}
spec:
  selector: {matchLabels: {app: search-indexer}}
  template:
    metadata: {labels: {app: search-indexer}}
    spec:
      containers:
      - name: indexer
        image: registry.example.test/shop/indexer:1

مع --profile search تشغّل البيئة amr كلًّا من backend-preview-amr و db-preview-amr و opensearch-preview-amr و search-indexer-preview-amr. ودونه تعمل الأوليان فقط ولا يُستخدم [search-indexer] في nezal.env.

تنطبق المجموعة الاختيارية على تطبيق في الوضع الكامل. تختار البيئة 10 مجموعات على الأكثر. والاسم الذي لا يعرّفه المجلد يوقف البيئة بمشكلة تذكر المجموعات التي يعرّفها.

صياغة migrate من خطاف Helm

كثير من المخططات يرحّل قاعدة بياناته في Job يشغّله Helm كخطاف pre-install أو pre-upgrade. لا يشغّل Nezal خطافات Helm أبداً لذا يقرأ nezal init ذلك الـ Job ويقترح الأمر نفسه في migrate.command على الخدمة التي تشغّل الصورة نفسها.

  1. ضع المخطط في مجلد داخل nezal/. ويُقرأ أيضاً المخطط الموجود في مكان آخر من المستودع. هنا يحوي المخطط Deployment باسم api وهذا الـ Job الخطاف:
# nezal/api/templates/migrate-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "api.fullname" . }}-migrate
  annotations:
    "helm.sh/hook": pre-install,pre-upgrade
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          command: ["python", "manage.py"]
          args: ["migrate", "--noinput"]
          env:
            - name: MIGRATION_TIMEOUT
              value: {{ .Values.migrations.timeout | quote }}
  1. أبقِ القيم التي يقرؤها الـ Job في ملف values.yaml الخاص بالمخطط:
# nezal/api/values.yaml
image:
  repository: registry.example.test/api
  tag: "1.4.2"
migrations:
  timeout: 300
  1. شغّل init في جذر المستودع:
nezal init
  1. اقرأ ما صاغه. الأمر هو command لأول حاوية في الـ Job يليه args. يُبحث عن كل {{ .Values.x }} بسيط في values.yaml. المتغير الذي يضبطه الـ Job ولا تضبطه الخدمة يوضع تحت migrate.env. ويذكر تعليق فوق migrate الملف والـ Job الذي جاء منه:
# nezal/nezal.yaml as nezal init drafts it
version: 1
mode: full
services:
  api:
    build: {context: api}
    public: true
    # From the Helm hook Job api-migrate in nezal/api/templates/migrate-job.yaml.
    # Nezal runs it with this service's image and variables before the service starts.
    migrate:
      command: ["python", "manage.py", "migrate", "--noinput"]
      env:
        MIGRATION_TIMEOUT: "300"
  1. اقرأ كل تعليق كتبه init فوق migrate واعمل به:

    • التعبير الذي لم يُعثر على قيمته يبقى في الأمر كما كُتب. استبدله بالقيمة الحقيقية.
    • المتغير الذي لم يُعثر على قيمته يُترك خارج migrate.env. أضفه يدوياً.
    • المتغير الذي يقرؤه الـ Job من Secret أو ConfigMap يُترك خارجاً. اضبطه في nezal.env أو تحت migrate.env بصيغة {{ .Secret.NAME }}.
    • المتغير الذي يبدو اسمه كبيانات اعتماد يُكتب بصيغة {{ .Secret.NAME }}. اضبط ذلك السر بالأمر nezal secret set.
  2. افحص المجلد:

nezal validate

كيف تُختار الخدمة. يحصل على الترحيل الـ Deployment أو الـ StatefulSet الذي تطابق صورة حاويته صورة الـ Job. ويُعدّ تعبير المخطط نفسه مثل {{ .Values.image }} صورةً مطابقة. وإن لم يطابق شيء وكان في المخطط Deployment واحد فقط فهو يحصل على الترحيل ويذكر تعليق ذلك. والـ Deployment الذي اسمه قالب مثل {{ include "api.fullname" . }} يُسمّى باسم مجلد مخططه. وهذا هو اسم الإصدار الذي يستخدمه Nezal افتراضياً. ولا تُقترح أبداً الخطافات التي تعمل بعد التثبيت أو الترقية.

صياغة الاعتماديات من docker-compose

الفريق الذي يشغّل بيئته بـ docker-compose يسرد مسبقاً قاعدة البيانات وذاكرة التخزين المؤقت وملتقط البريد بجانب شيفرته. يقرأ nezal init ملف compose مرة واحدة ليصوغ لكل منها ملفاً تحت nezal/ ومتغيراتها في nezal.env. يُقرأ compose لصياغة الملفات فقط. ولا يُنشر أبداً ولا يقرؤه شيء بعد init. ما يعمل في البيئة هو الملفات تحت nezal/ التي تقرؤها وتعتمدها في git.

  1. أبقِ ملف compose في جذر المستودع. واسمه compose.yaml أو compose.yml أو docker-compose.yml أو docker-compose.yaml:
# docker-compose.yml
services:
  api:
    build: ./api
    ports: ["8000:8000"]
    environment:
      DATABASE_URL: postgres://app:app@db:5432/app
      REDIS_URL: redis://redis:6379/0
      SMTP_HOST: mail
    depends_on: [db, redis, mail]
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: app
    volumes:
      - dbdata:/var/lib/postgresql/data
    ports: ["5432:5432"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
  redis:
    image: redis:7-alpine
    expose: ["6379"]
  mail:
    image: axllent/mailpit:v1.20
    ports: ["8025:8025", "1025:1025"]
volumes:
  dbdata:
  1. شغّل init في جذر المستودع:
nezal init

يذكر ما كتبه وما تخطاه والأسرار التي يجب ضبطها:

Wrote nezal/nezal.yaml
Wrote nezal/nezal.env
Wrote nezal/db/db.yaml
Wrote nezal/mail/mail.yaml
Wrote nezal/redis/redis.yaml
The compose file was read only to draft these files. Compose is never deployed: docker-compose.yml
Built from the repository so no manifest was drafted for api
Set these secrets before the first environment:
  nezal secret set <app> POSTGRES_PASSWORD
  1. اقرأ كل ملف صاغه. خدمة compose التي فيها image: وليس فيها build: تصبح nezal/<service>/<service>.yaml:
    • Deployment. أو StatefulSet مع مطالبة تخزين حين تركّب الخدمة وحدة تخزين مسمّاة. تُركّب المطالبة على مسار الحاوية لأول وحدة تخزين مسمّاة. ولا يُركّب أبداً مجلد من جهازك.
    • Service على منافذ الحاوية من ports: وexpose:. الخدمة التي بلا منفذ لا تحصل على Service ويذكر init ذلك.
    • يصبح entrypoint: هو command للحاوية ويصبح command: هو args.
    • فحص الصحة من نوع CMD أو CMD-SHELL يصبح فحص جاهزية exec بفاصله ومهلته وعدد محاولاته.
# nezal/db/db.yaml
# Drafted by nezal init from docker-compose.yml. Compose is read once to draft this file and never deployed.
# Read it and correct it before you commit it. Its variables are in nezal/nezal.env under [db].
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: db
  labels:
    app.kubernetes.io/name: db
spec:
  serviceName: db
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: db
  template:
    metadata:
      labels:
        app.kubernetes.io/name: db
    spec:
      containers:
        - name: db
          image: "postgres:16"
          ports:
            - containerPort: 5432
              protocol: TCP
          readinessProbe:
            exec:
              command: ["sh", "-c", "pg_isready -U app"]
            periodSeconds: 5
          volumeMounts:
            - name: data
              mountPath: "/var/lib/postgresql/data"
              subPath: data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: [ReadWriteOnce]
        resources:
          requests:
            storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
  name: db
  labels:
    app.kubernetes.io/name: db
spec:
  selector:
    app.kubernetes.io/name: db
  ports:
    - name: tcp-5432
      port: 5432
      targetPort: 5432
      protocol: TCP
  1. اقرأ nezal.env. قسم environment: لكل اعتمادية يوضع تحت قسم [<service>]. الاسم الذي يبدو كبيانات اعتماد يصبح {{ .Secret.NAME }} ولا تُنسخ قيمته أبداً. والقيمة التي تسمّي خدمة compose أخرى مثل db أو redis:6379 تصبح {{ .Service.<name>.Host }} لتصل إلى نسخة تلك الخدمة في البيئة. ومتغيرات الخدمة التي يبنيها compose تذهب إلى الخدمة التي يبنيها nezal.yaml من المجلد نفسه:
# nezal/nezal.env
# Drafted by nezal init from the .env files in this repository.
# The variables in docker-compose.yml are drafted here too. Compose is read once and never deployed.
# A value may be fixed or a template filled in for each environment such as
# {{ .Service.backend.URL }} or {{ .Service.redis.Host }}. Secrets are set with nezal secret set.

[api]
DATABASE_URL=postgres://app:app@{{ .Service.db.Host }}:5432/app
REDIS_URL=redis://{{ .Service.redis.Host }}:6379/0
SMTP_HOST={{ .Service.mail.Host }}

[db]
POSTGRES_DB=app
POSTGRES_PASSWORD={{ .Secret.POSTGRES_PASSWORD }}
POSTGRES_USER=app
  1. اقرأ nezal.yaml. لا تحتاج الاعتمادية إلى شيء فيه. والخدمة المذكورة فيه مسبقاً تحصل على dependsOn من depends_on في compose. ولا تُصاغ الخدمة التي يبنيها compose كملف لأن nezal.yaml يبنيها من ملف Dockerfile الخاص بها:
# nezal/nezal.yaml
# Drafted by nezal init. Read it and correct it before you commit it.
# Every folder under nezal/ holding a Helm chart or a kustomization or manifests is deployed.
# The manifests drafted from docker-compose.yml are under nezal/ too. Compose is never deployed.
# See https://nezal.dev/docs/guide/nezal-folder/ for every option.
version: 1
mode: full
services:
  api:
    build: {context: api}
    public: true
    dependsOn: [db, mail, redis]
  1. اضبط كل سر ذكره init ثم افحص المجلد:
nezal secret set <app> POSTGRES_PASSWORD
nezal validate --env amr --domain dev.example.test

لا يُستبدل أبداً ملف موجود مسبقاً ويذكر init أنه تخطاه. شغّل nezal init --force لاستبداله. ويُترك دائماً كما هو المجلد تحت nezal/ الذي يحوي مسبقاً مخطط Helm أو kustomization.

أعطِ الترحيل متغيراته الخاصة

يحتاج الترحيل أحيانًا إعدادًا لا يجوز أن تحمله الخدمة العاملة مثل وحدة إعدادات للترحيل أو مستخدم قاعدة بيانات يحق له تغيير المخطط. يعطي migrate.env متغيرات لمهمة الترحيل وحدها.

  1. أضف migrate.command إلى الخدمة.
  2. أضف تحت migrate.env كل متغير بالشكل NAME: value. يحصل الترحيل على متغيرات الخدمة نفسها أولًا ثم هذه فوقها.
  3. استخدم قوالب nezal.env في القيمة حين تختلف من بيئة إلى أخرى. بيانات الاعتماد تُكتب إشارة إلى سر تشغل القيمة كلها مثل "{{ .Secret.DB_ADMIN_PASSWORD }}". الاسم الذي يشبه بيانات اعتماد بقيمة صريحة يُرفض لأن nezal.yaml موجود في git.
  4. ادفع الفرع. تحمل مهمة الترحيل المتغيرات ولا تحملها الخدمة. أي تغيير في migrate.env يشغّل الترحيل مرة أخرى.
version: 1
services:
  backend:
    public: true
    build: {context: components/backend}
    dependsOn: [database]
    migrate:
      command: [python, manage.py, migrate]
      env:
        DJANGO_SETTINGS_MODULE: app.settings.migrate
        DB_USER: schema_owner
        DB_PASSWORD: "{{ .Secret.DB_ADMIN_PASSWORD }}"
        MIGRATION_LABEL: "{{ .Environment }}-{{ .Commit }}"
nezal secret set backend-app DB_ADMIN_PASSWORD=change-me

من لوحة التحكم

  1. المستودعات. اربط المستودع.
  2. سجلات الصور. اربط سجل الصور الذي تُدفع إليه الصور.
  3. التطبيقات. اربط تطبيقًا من المستودع. يجد نزال مجلد nezal ويعرض كل خدمة مع طريقة نشرها وهل تُبنى وهل هي عامة. ويعرض أيضًا القيمة التي يأخذها كل متغير في بيئة على سبيل المثال.
  4. البيئات. اختر التطبيق وفرعًا واسمًا مثل amr. وتعرض الصفحة الروابط قبل إنشاء أي شيء. اضغط زر الإنشاء وتابع كل عملية بناء.
  5. صفحة البيئة. روابطها. وكل متغير بقيمته ومصدره. والسجلات وطرفية لكل خدمة. وإعادة البناء والحذف.

يمكنك أيضًا تعديل nezal.yaml من لوحة التحكم. ويُحفظ التعديل تجاوزًا على التطبيق ويُطبَّق فوق الملف في كل فرع. ويبقى الملف في git هو المرجع. انسخ التغيير إلى nezal.yaml وأودِعه حين تطمئن إليه.

من CLI

الأمر ما يفعله
nezal login --server <url> يسجّل الدخول إلى تثبيتك عبر المتصفح. اضغط الموافقة في المتصفح وتخبرك الطرفية بتسجيل الدخول. ويُحفظ الرمز في ~/.nezal/config.json على جهازك. وإن عرض المتصفح الرمز بدلًا من ذلك فالصقه في الطرفية.
nezal init [folder] يكتب مسودة nezal.yaml وnezal.env من ملفات Dockerfile وملفات .env في المستودع. وتصبح مهمة Job من نوع pre-install أو pre-upgrade في Helm خطوة migrate. وتصبح كل خدمة compose تشغّل صورة ملف manifest تحت nezal/. وتحصل كل خدمة تُبنى من Dockerfile على Deployment وService تحت nezal/<service>/ بالمنفذ الذي يعرضه Dockerfile ويشغّل الصورة التي يبنيها نزال. ويُشار إلى أي مخطط Helm في مكان آخر من المستودع لتنقله إلى الداخل بدلًا من ذلك. ولا يُنشر compose أبدًا.
nezal validate [folder] يتحقق من مجلد nezal على جهازك ويعرض الخدمات وروابطها وكل متغير لبيئة واحدة. لا يُرسل شيء إلى أي مكان. استخدم --env و --domain لاختيار المثال و --profile <name> للتحقق منه كبيئة اختارت تلك المجموعة.
nezal up [name] ينشئ بيئتك أو يحدّثها من الفرع الحالي ويطبع روابطها. يختار --ref فرعًا آخر. ويضبط --ttl مدة بقائها. وينشر --profile <name> تلك المجموعة من مجلد nezal أيضًا ويمكن تكراره.
nezal list بيئاتك. و--scope team أو all يعرض المزيد.
nezal status <name> خدماتها وعمليات بنائها وروابطها والمجموعات التي اختارتها.
nezal logs <name> كل وحدات Pod والحاويات مع سبب آخر توقف لكل منها. أضف -s <service> أو --pod <pod> مع -c <container> لمتابعة إحداها. و-f للمتابعة المستمرة. و-p لسجل ما قبل آخر إعادة تشغيل. و--grep <text> لإبقاء الأسطر المطابقة فقط. ولا يقرأ السجلات إلا مالك البيئة.
nezal terminal <name> -s <service> يفتح طرفية في حاوية من بيئتك أنت. ويختار -c <container> الحاوية.
nezal env <name> كل متغير حصلت عليه كل خدمة.
nezal sync <name> يعيد بناء ما تغيّر في الفرع ويعيد نشره.
nezal extend <name> يُبقي البيئة مدة أطول.
nezal secret set|list|rm <app> NAME يدير الأسرار التي يقرؤها nezal.env. وبلا قيمة يُقرأ السر من الإدخال القياسي.
nezal agent <name> "<question>" يسأل الوكيل الذكي عن بيئتك. ويستأذنك قبل أن يغيّر أي شيء.
nezal down <name> يحذف البيئة وبياناتها.
nezal app list تطبيقات مساحة العمل وهل يعمل كل منها بالوضع الكامل أو المشترك. ومرّر أحد الأسماء مع --app.
nezal registry nezal config سجلات الصور والإعدادات المحفوظة.
nezal version إصدار الأداة.
nezal <command> --help كل خيارات أمر ما.

نفّذ nezal validate قبل أن تدفع تغييراتك:

$ nezal validate --env amr --domain dev.acme.test
Mode: full
Deploys:
  nezal/backend  helm
  nezal/database  manifests
  nezal/redis  manifests
Services in environment amr
  backend  backend-preview-amr · https://backend-preview-amr.dev.acme.test · built from components/backend
  database  database-preview-amr
  huey  huey-preview-amr · uses the image of backend
  redis  redis-preview-amr
The nezal folder is valid.

الخدمة التي لا يعرّفها إلا مخطط Helm أو ملف kustomization لا تُعرف إلا بعد توليد المخطط وهذا يحدث في الكلستر. لذلك ينبّه nezal validate إلى القالب الذي يسمّي مثل هذه الخدمة ولا يفشل.

الوكيل الذكي

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

nezal agent amr why is the backend restarting

النموذج هو الذي ضبطه المشرف في قسم الإدارة. والنموذج الذي تشغّله داخل الكلستر لا يحتاج إلى الإنترنت إطلاقًا. أما النموذج المستضاف خارجه فيصل إلى عنوانه وحده ولا يصل إلى أي مضيف آخر.

الحدود