مجلد nezal
ضع مجلدًا باسم nezal في جذر مستودعك وسينشر نزال ما بداخله. يحصل كل مطوّر على بيئة كاملة
للتطبيق من فرعه هو. وتُبنى كل خدمة من ذلك الفرع وتعمل مع قاعدة بيانات وذاكرة تخزين مؤقت (cache)
خاصتين بها. ولا يُشارَك أي شيء مع مطوّر آخر.
المستودع الذي لا يحتوي مجلد nezal يُقرأ كما كان يُقرأ دائمًا. والمجلد هو الطريقة التي تخبر بها
نزال بما يجب أن يفعله بالضبط.
الحالة. كل ما في هذه الصفحة متاح: الصيغة وبيئات الوضع الكامل التي تُبنى وتُنشر من فرع كل مطوّر والأسرار والتجاوز من لوحة التحكم وأوامر CLI والوكيل الذكي.
البداية السريعة
نفّذ كل أمر داخل مستودعك.
أنشئ مجلد nezal. يقرأ
nezal initمستودعك ويكتب مسودة أولى تقرؤها وتصحّحها.nezal initضع ملفات النشر فيه. مجلد لكل خدمة فيه مخطط Helm أو Kustomize أو ملفات Kubernetes عادية.
nezal/ ├── nezal.yaml ├── nezal.env ├── backend/ # Helm chart or manifests └── database/أعطِ خدمة رابطًا. يضم
nezal.yamlإعدادات كل خدمة. وpublic: trueيعطيها رابطًا.version: 1 services: backend: public: trueاضبط المتغيّرات. يضم
nezal.envالقيم والقوالب. ولكل بيئة قيمها الخاصة.LOG_LEVEL=debug ENVIRONMENT={{ .Environment }} PUBLIC_URL={{ .Service.backend.URL }} API_KEY={{ .Secret.API_KEY }}تحقّق وابدأ. تُحفظ الأسرار في الكلستر ولا تدخل 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:
- يعمل البناء داخل الكلستر لديك بوصفه Job دون أي صلاحيات مميزة. ويبني BuildKit دون صلاحيات الجذر (rootless) ملف Dockerfile وتبني buildpacks المجلد الذي لا يحتوي Dockerfile.
- تُدفع الصورة إلى سجل الصور الخاص بمساحة العمل وتُوسم بالإيداع.
- الخدمة التي لم يتغير سياق بنائها تعيد استخدام الصورة المبنية مسبقًا لذلك الإيداع. لذلك لا تُبنى الشيفرة نفسها مرتين لبيئتين.
- تبدأ قاعدة البيانات وذاكرة التخزين المؤقت أولًا. ثم تعمل أوامر الترحيل (migrate). ثم تبدأ
الخدمات بالترتيب الذي يحدده
dependsOn.
الدفع إلى الفرع يعيد بناء الخدمات التي تغيّرت شيفرتها فقط ويعيد نشرها.
أدلة عملية
كل دليل أدناه مهمة واحدة من البداية إلى النهاية مع مثال يمكنك نسخه.
أعطِ البناء عناوين بيئته
كثيرًا ما تُثبّت الواجهة الأمامية عنوان واجهتها البرمجية داخل الحزمة وقت البناء. اكتب هذا العنوان قالبًا في build.args فتبني كل بيئة صورتها الخاصة التي تشير إلى خادمها الخاص.
- اجعل الخدمة التي يشير إليها العنوان عامة إن كان المتصفح يطلبها.
- أضف تحت الخدمة المبنية
build.argsوفيه قالب لكل قيمة تختلف من بيئة إلى أخرى. القوالب هي نفسها التي يقبلهاnezal.env. - ادفع الفرع. تحلّ كل بيئة المعاملات قبل أن تبني. البيئتان اللتان تخرج معاملاتهما متطابقة تتشاركان صورة واحدة. والبيئتان اللتان تختلف معاملاتهما تحصل كل منهما على صورة.
- افتح صفحة البيئة. القالب الذي يذكر خدمة غير موجودة يوقف البيئة مع اسم الحقل بالضبط والسبب.
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 هذه الإعدادات من كل نسخة ويذكر ذلك في صفحة البيئة.
- انشر المخطط كما هو. تفقد كل نسخة
nodeSelectorوnodeNameوtolerationsوaffinityوtopologySpreadConstraintsوpriorityClassName. - افتح صفحة البيئة. يذكر تحذير كل خدمة وما حُذف منها.
- إن كانت خدمة واحدة تحتاج قيودها فعلًا على هذا العنقود مثل عامل يحتاج عقدة فيها معالج رسوميات فاضبط عليها
scheduling: keep. - ادفع التغيير. تحتفظ تلك الخدمة بما كتبه مخططها وتبقى كل خدمة أخرى حرة في العمل على أي عقدة.
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 مدخلًا واحدًا لكل خدمة عامة باسم مضيف وشهادة خاصين بها. تتيح لك التعليقات التوضيحية ضبط الطريقة التي يخدمها بها متحكم الدخول مثل أكبر حجم للرفع أو مهلة الانتظار.
- اجعل الخدمة عامة.
- أضف تحت
ingress.annotationsالتعليقات التوضيحية التي يقرؤها متحكم الدخول لديك. توضع فوق القيم الافتراضية التي ضبطها المسؤول للتثبيت. - ادفع الفرع وافتح عنوان البيئة. التعليقات التوضيحية موجودة على مدخل البيئة.
- إن تركت
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) مجموعة من التعريفات والخدمات. تنشر البيئة ما تذكره المجموعة فقط حين تختارها وكل ما لا تذكره أي مجموعة يُنشر دائمًا. استخدمها لجزء من التطبيق لا يحتاجه أغلب المطورين مثل محرك بحث أو مجموعة من العمّال.
ضع الجزء الاختياري في مجلد خاص به تحت
nezal/. هنا محرك البحث والعامل الذي يملأ فهرسه فيnezal/search/.اذكر المجلد تحت
profilesفيnezal.yaml. يمكن للمجموعة أن تذكر مجلدات تحتdefinitionsوخدمات مفردة تحتservices. اسم المجموعة أحرف صغيرة وأرقام وشرطات.ضع كل متغير يذكر خدمة من المجموعة تحت
[section]خدمة في المجموعة نفسها. الخدمة التي تحصل على متغير يذكر خدمة لا تشغّلها البيئة لا تُنشر وتذكر البيئة السبب. أما بقية الخدمات فتُنشر.لا تجعل خدمة خارج المجموعة تعتمد عبر
dependsOnعلى خدمة داخلها. تتوقف مثل هذه البيئة بجملة مثلbackend depends on opensearch which is in the profile search that this environment did not choose.تحقق بالطريقتين قبل أن تدفع. لا يُرسل شيء إلى أي مكان.
nezal validate nezal validate --profile searchادفع الفرع.
من لوحة التحكم. افتح البيئات واضغط بيئة جديدة. اختر التطبيق وفرعًا. تعرض خطوة المراجعة المجموعات الاختيارية كمربعات اختيار ولا يكون أي منها محددًا. حدّد
searchواضغط إنشاء. تعرض صفحة البيئة المجموعات التي اختارتها بجوار فرعها.من سطر الأوامر. اختر المجموعة بـ
--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 على الخدمة التي تشغّل الصورة نفسها.
- ضع المخطط في مجلد داخل
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 }}
- أبقِ القيم التي يقرؤها الـ Job في ملف
values.yamlالخاص بالمخطط:
# nezal/api/values.yaml
image:
repository: registry.example.test/api
tag: "1.4.2"
migrations:
timeout: 300
- شغّل init في جذر المستودع:
nezal init
- اقرأ ما صاغه. الأمر هو
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"
اقرأ كل تعليق كتبه init فوق
migrateواعمل به:- التعبير الذي لم يُعثر على قيمته يبقى في الأمر كما كُتب. استبدله بالقيمة الحقيقية.
- المتغير الذي لم يُعثر على قيمته يُترك خارج
migrate.env. أضفه يدوياً. - المتغير الذي يقرؤه الـ Job من Secret أو ConfigMap يُترك خارجاً. اضبطه في
nezal.envأو تحتmigrate.envبصيغة{{ .Secret.NAME }}. - المتغير الذي يبدو اسمه كبيانات اعتماد يُكتب بصيغة
{{ .Secret.NAME }}. اضبط ذلك السر بالأمرnezal secret set.
افحص المجلد:
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.
- أبقِ ملف 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:
- شغّل 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
- اقرأ كل ملف صاغه. خدمة 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
- اقرأ
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
- اقرأ
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]
- اضبط كل سر ذكره init ثم افحص المجلد:
nezal secret set <app> POSTGRES_PASSWORD
nezal validate --env amr --domain dev.example.test
لا يُستبدل أبداً ملف موجود مسبقاً ويذكر init أنه تخطاه. شغّل nezal init --force لاستبداله. ويُترك دائماً كما هو المجلد تحت nezal/ الذي يحوي مسبقاً مخطط Helm أو kustomization.
أعطِ الترحيل متغيراته الخاصة
يحتاج الترحيل أحيانًا إعدادًا لا يجوز أن تحمله الخدمة العاملة مثل وحدة إعدادات للترحيل أو مستخدم قاعدة بيانات يحق له تغيير المخطط. يعطي migrate.env متغيرات لمهمة الترحيل وحدها.
- أضف
migrate.commandإلى الخدمة. - أضف تحت
migrate.envكل متغير بالشكلNAME: value. يحصل الترحيل على متغيرات الخدمة نفسها أولًا ثم هذه فوقها. - استخدم قوالب
nezal.envفي القيمة حين تختلف من بيئة إلى أخرى. بيانات الاعتماد تُكتب إشارة إلى سر تشغل القيمة كلها مثل"{{ .Secret.DB_ADMIN_PASSWORD }}". الاسم الذي يشبه بيانات اعتماد بقيمة صريحة يُرفض لأنnezal.yamlموجود في git. - ادفع الفرع. تحمل مهمة الترحيل المتغيرات ولا تحملها الخدمة. أي تغيير في
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
من لوحة التحكم
- المستودعات. اربط المستودع.
- سجلات الصور. اربط سجل الصور الذي تُدفع إليه الصور.
- التطبيقات. اربط تطبيقًا من المستودع. يجد نزال مجلد
nezalويعرض كل خدمة مع طريقة نشرها وهل تُبنى وهل هي عامة. ويعرض أيضًا القيمة التي يأخذها كل متغير في بيئة على سبيل المثال. - البيئات. اختر التطبيق وفرعًا واسمًا مثل
amr. وتعرض الصفحة الروابط قبل إنشاء أي شيء. اضغط زر الإنشاء وتابع كل عملية بناء. - صفحة البيئة. روابطها. وكل متغير بقيمته ومصدره. والسجلات وطرفية لكل خدمة. وإعادة البناء والحذف.
يمكنك أيضًا تعديل 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
- بيئتك وحدها. يربط الخادم أدوات الوكيل ببيئتك قبل أن يُسأل النموذج أي شيء. فلا يستطيع أن يسمّي بيئة أخرى أو مساحة أسماء أو Pod خارج بيئتك. وبيئة زميلك في مساحة العمل نفسها خارج متناوله.
- أنت وحدك. الوكيل الذكي لبيئة ما متاح لمالكها ولا أحد غيره.
- يقرأ بنفسه. الحالة والـ Pods والسجلات والأحداث والمتغيرات.
- يسألك قبل أن يغيّر أي شيء. تنفيذ أمر في Pod أو إعادة تشغيل خدمة أو إعادة النشر من الفرع أو تشغيل الترحيل مجددًا. ترى الإجراء بالضبط فتوافق عليه أو ترفضه. ويمرّ الإجراء الموافَق عليه بالصلاحيات نفسها التي تمر بها لو نفّذته بنفسك ولا ينفّذ أمرًا إلا حيث يُسمح بالطرفية.
- كل شيء يُسجَّل في سجل التدقيق باسمك واسم النموذج.
- لا تصل الأسرار إلى النموذج أبدًا. يُزال كل نص يشبه بيانات الاعتماد من كل ما يقرؤه ولا يظهر متغير السر إلا باسمه.
النموذج هو الذي ضبطه المشرف في قسم الإدارة. والنموذج الذي تشغّله داخل الكلستر لا يحتاج إلى الإنترنت إطلاقًا. أما النموذج المستضاف خارجه فيصل إلى عنوانه وحده ولا يصل إلى أي مضيف آخر.
الحدود
- تنشر النسخة الكاملة كائنات Deployment وStatefulSet مع كائنات Service وConfigMap وSecret وطلبات وحدات التخزين التابعة لها. ولا تُنسخ كائنات Ingress وNetworkPolicy وServiceAccount وRole وJob وCronJob الخاصة بالمخطط وتذكر البيئة ذلك. فنزال يعطي كل خدمة عامة رابطها الخاص ويعزل كل بيئة بسياستها الخاصة.
- لا تُشغَّل خطافات Helm (hooks). والمهمة التي تعمل مرة واحدة مثل الترحيل مكانها
migrate. - تُولَّد القيم في المخطط كما كُتبت. والعنصر النائب الذي يملؤه نظام CI لديك مثل
${{ secrets.X }}لا يملؤه نزال. استخدم قيمًا عادية في المخطط الذي تضعه فيnezal/واستخدم الأسرار لكل ما هو حساس. - النوم عند الخمول معطّل في النسخة الكاملة لأنها بلا موجّه يرى حركة المرور. لكن مدة بقائها (TTL) تبقى سارية.
- كل بيئة في الوضع الكامل تشغّل المنظومة كلها. خطّط سعة الكلستر على أساس منظومة كاملة لكل مطوّر واستخدم سياسة الفريق لتحديد عدد البيئات التي تعمل في الوقت نفسه.