نحن نتقن مهمة النشر في GKE بدون المكونات الإضافية والرسائل القصيرة والتسجيل. نظرة خاطفة تحت سترة جينكينز بعين واحدة

بدأ كل شيء عندما طلب منا قائد فريق أحد فرق التطوير لدينا اختبار تطبيقهم الجديد ، والذي تم وضعه في حاويات في اليوم السابق. أضعه. بعد حوالي 20 دقيقة ، تم استلام طلب لتحديث التطبيق ، لأنه تم الانتهاء من قطعة ضرورية للغاية هناك. أنا جددت. بعد ساعتين أخريين ... حسنًا ،



لقد خمنت بالفعل ما بدأ يحدث بعد ذلك ... يجب أن أعترف ، أنا كسول نوعًا ما (هل اعترفت بذلك سابقًا؟ لا؟) ، وبالنظر إلى حقيقة أن قادة الفريق يمكنهم الوصول إلى جينكينز ، حيث لقد فكرت في أن لدينا جميع CI / CD: دعه ينشر نفسه بقدر ما يشاء! تذكرت نكتة: أعط الرجل سمكة فيشبع اليوم ؛ اتصل بشخص متخم وسيظل مشبعًا طوال حياته. وذهب لأداء المهمة، والتي ستكون قادرة على نشر حاوية في Kuber مع تطبيق أي إصدار تم تجميعه بنجاح ونقل أي قيم ENV إليها (جدي ، عالم فقه اللغة ، مدرس اللغة الإنجليزية في الماضي ، كان الآن يلف إصبعه على معبده وينظر إلي بشكل واضح للغاية بعد قراءة هذا جملة او حكم على).



لذلك ، في منشور سأتحدث عن كيف تعلمت:



  1. تحديث الوظائف في Jenkins ديناميكيًا من الوظيفة نفسها أو من وظائف أخرى ؛
  2. الاتصال بوحدة التحكم السحابية (Cloud shell) من عقدة مثبت عليها وكيل Jenkins ؛
  3. انشر حمل العمل على Google Kubernetes Engine.


في الحقيقة ، أنا بالطبع ماكر قليلاً. من المفترض أن جزءًا على الأقل من بنيتك الأساسية موجود في google cloud ، وبالتالي فأنت مستخدمها ، وبالطبع لديك حساب GCP. لكن المذكرة لا تتعلق بذلك.



هذه هي ورقة الغش التالية الخاصة بي. أريد أن أكتب مثل هذه الملاحظات في حالة واحدة فقط: كانت لدي مشكلة قبلي ، في البداية لم أكن أعرف كيف أحلها ، ولم يكن الحل في شكله النهائي على Google ، لذلك بحثت عنه في جوجل في أجزاء وحلت المشكلة في النهاية. وهكذا في المستقبل ، عندما أنسى كيف فعلت ذلك ، لست مضطرًا إلى البحث عن كل شيء على google مرة أخرى قطعة قطعة وجمعها معًا ، أكتب لنفسي أوراق الغش هذه.

Disclaimer: 1. « », best practice . « » .

2. , , , — .

Jenkins



أتوقع سؤالك: ما علاقة التحديث الوظيفي الديناميكي به؟ لقد أدخلت قيمة معلمة السلسلة بالمقابض وامض قدمًا!



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



الخطة هي كما يلي: إنشاء وظيفة في Jenkins ، والتي ، قبل الإطلاق ، سيكون من الممكن تحديد إصدار من القائمة ، وتحديد قيم المعلمات التي تم تمريرها إلى الحاوية عبر ENV ، ثم تقوم بتجميع الحاوية ودفعها إلى Container Registry. علاوة على ذلك ، يتم إطلاق الحاوية في kubera باسمعبء العمل مع المعلمات المحددة في الوظيفة.



لن نفكر في عملية إنشاء وتكوين وظيفة في Jenkins ، فهذا أمر غير موضوعي. سنفترض أن المهمة جاهزة. لتنفيذ قائمة إصدارات قابلة للتحديث ، نحتاج إلى شيئين: قائمة مصادر موجودة بأرقام إصدار صالحة مسبقًا ومتغير نوع معلمة Choice في المهمة. في مثالنا ، دع المتغير يسمى BUILD_VERSION ، لن نتناوله بالتفصيل. لكن دعونا نلقي نظرة فاحصة على قائمة المصادر.



لا يوجد الكثير من الخيارات. حدث لي اثنان على الفور:



  • استخدام واجهة برمجة تطبيقات الوصول عن بُعد التي تقدمها Jenkins لمستخدميها ؛
  • استعلم عن محتويات مجلد المستودع البعيد (في حالتنا ، هذا هو JFrog Artifactory ، وهو أمر غير مهم).


Jenkins Remote Access API



وفقًا للتقاليد الراسخة ، أفضل تجنب التفسيرات المطولة.

سأسمح لنفسي فقط بترجمة جزء من الفقرة الأولى من الصفحة الأولى من وثائق API بحرية :

يوفر Jenkins واجهة برمجة تطبيقات للوصول القابل للقراءة آليًا عن بُعد إلى وظائفه. <...> يتم تقديم الوصول عن بُعد بأسلوب يشبه REST. هذا يعني أنه لا توجد نقطة إدخال واحدة لجميع الإمكانات ، وبدلاً من ذلك يتم استخدام عنوان URL مثل " ... / api / " ، حيث " ... " هو الكائن الذي يتم تطبيق إمكانيات واجهة برمجة التطبيقات عليه.
بعبارة أخرى ، إذا كانت مهمة النشر ، التي نتحدث عنها في الوقت الحالي ، متاحة على العنوان http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build، فإن صفارات API لهذه المهمة متاحة في Next ، فلدينا خيار في أي شكل لتلقي الإخراج. دعنا نتحدث عن XML ، لأن واجهة برمجة التطبيقات تسمح فقط بالتصفية في هذه الحالة. دعنا نحاول فقط الحصول على قائمة بجميع مهام التشغيل. نحن مهتمون فقط باسم التجميع (اسم العرض) ونتيجته ( النتيجة ):http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/











http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]


حدث؟



الآن دعنا نقوم بتصفية عمليات الإطلاق التي تنتهي بنتيجة النجاح . نستخدم وسيطة الاستبعاد & ونمررها المسار إلى قيمة لا تساوي SUCCESS كمعامل . نعم نعم. النفي المزدوج هو بيان. نستبعد كل ما لا يهمنا:



http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result!='SUCCESS']


لقطة من قائمة الناجحين




حسنًا ، من أجل التدليل فقط ، دعنا نتأكد من أن الفلتر لم يخدعنا (المرشحات لا تكذب أبدًا!) وعرض قائمة بالمرشحات "غير الناجحة":



http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result='SUCCESS']


لقطة شاشة لقائمة فاشلة




قائمة الإصدارات من مجلد على خادم بعيد



هناك طريقة ثانية للحصول على قائمة الإصدارات. أنا أحبها أكثر من استدعاء Jenkins API. حسنًا ، لأنه إذا تم إنشاء التطبيق بنجاح ، فقد تم تجميعه ووضعه في المستودع في المجلد المناسب. مثل ، المستودع هو المستودع الافتراضي لإصدارات التطبيقات العاملة. مثل. حسنًا ، دعنا نسأله عن الإصدارات المخزنة. سنقوم بلف المجلد البعيد و grep و awk. إذا كان شخص ما مهتمًا بالبطانة ، فسيكون تحت المفسد.



أمر سطر واحد
: , , . :



curl -H "X-JFrog-Art-Api:VeryLongAPIKey" -s http://arts.myre.po/artifactory/awesomeapp/ | sed 's/a href=//' | grep "$(date +%b)-$(date +%Y)\|$(date +%b --date='-1 month')-$(date +%Y)" | awk '{print $1}' | grep -oP '>\K[^/]+' )




ملف إعداد الوظيفة وتكوين الوظيفة في جينكينز



لقد تعاملنا مع مصدر قائمة الإصدارات. دعنا الآن نحلل القائمة الناتجة في المهمة. بالنسبة لي ، كان الحل الواضح هو إضافة خطوة في وظيفة إنشاء التطبيق. الخطوة التي سيتم تنفيذها إذا كانت النتيجة "نجاح".



افتح إعدادات مهمة التجميع وانتقل إلى الأسفل. انقر فوق الأزرار: أضف خطوة بناء -> خطوة شرطية (مفردة) . في إعدادات الخطوة ، حدد شرط حالة البناء الحالي ، وقم بتعيين قيمة النجاح ، والإجراء الذي سيتم تنفيذه في حالة نجاح الأمر Run shell .



والآن الجزء الممتع. يخزن Jenkins تكوينات الوظائف في ملفات. بتنسيق XML. على طول الطريقhttp://--/config.xmlوفقًا لذلك ، يمكنك تنزيل ملف التكوين وتعديله حسب الحاجة ووضعه في المكان الذي تم نقله منه.



تذكر ، اتفقنا أعلاه على أننا سننشئ معلمة BUILD_VERSION لقائمة الإصدارات ؟



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



لقطة من تحت الجناح.



يجب أن يظهر مقتطف config.xml بنفس الشكل. إلا أن محتوى عنصر الخيارات غير موجود بعد




هل اقتنعت؟ حسنًا ، نحن نكتب نصًا سيتم تنفيذه في حالة بناء ناجح.

سيتلقى البرنامج النصي قائمة بالإصدارات ، وقم بتنزيل ملف تكوين ، وكتابة قائمة بالإصدارات فيه في المكان الذي نحتاجه ، ثم أعده مرة أخرى. نعم. كل شيء صحيح. اكتب قائمة الإصدارات في XML في المكان الذي توجد فيه بالفعل قائمة بالإصدارات (ستكون في المستقبل ، بعد الإطلاق الأول للبرنامج النصي). أعلم أنه لا يزال هناك بعض عشاق التعبيرات العادية في العالم. أنا لا أنتمي إليهم. الرجاء تثبيت xmlstarler على الجهاز حيث سيتم تحرير التكوين. يبدو لي أن هذا ليس ثمنًا كبيرًا يجب دفعه مقابل تجنب تحرير XML باستخدام sed.



تحت المفسد ، أذكر الكود الذي يؤدي التسلسل الكامل الموصوف أعلاه.



نكتب إلى التكوين قائمة الإصدارات من المجلد على الخادم البعيد
#!/bin/bash
##############  
curl -X GET -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml -o appConfig.xml

##############     xml-   
xmlstarlet ed --inplace -d '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' appConfig.xml

xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]' --type elem -n a appConfig.xml

xmlstarlet ed --inplace --insert '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a' --type attr -n class -v string-array appConfig.xml

##############       
readarray -t vers < <( curl -H "X-JFrog-Art-Api:Api:VeryLongAPIKey" -s http://arts.myre.po/artifactory/awesomeapp/ | sed 's/a href=//' | grep "$(date +%b)-$(date +%Y)\|$(date +%b --date='-1 month')-$(date +%Y)" | awk '{print $1}' | grep -oP '>\K[^/]+' )

##############       
printf '%s\n' "${vers[@]}" | sort -r | \
                while IFS= read -r line
                do
                    xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' --type elem -n string -v "$line" appConfig.xml
                done

##############   
curl -X POST -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml --data-binary @appConfig.xml

##############     
rm -f appConfig.xml




إذا أعجبك خيار الحصول على إصدارات من Jenkins أكثر وكنت كسولًا مثلي ، فعندئذٍ تحت المفسد نفس الكود ، لكن القائمة من Jenkins:



نكتب قائمة بالإصدارات من Jenkins إلى ملف config
: , . , awk . .



#!/bin/bash
##############  
curl -X GET -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml -o appConfig.xml

##############     xml-   
xmlstarlet ed --inplace -d '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' appConfig.xml

xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]' --type elem -n a appConfig.xml

xmlstarlet ed --inplace --insert '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a' --type attr -n class -v string-array appConfig.xml

##############       Jenkins
curl -g -X GET -u username:apiKey 'http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result!=%22SUCCESS%22]&pretty=true' -o builds.xml

##############       XML
readarray vers < <(xmlstarlet sel -t -v "freeStyleProject/allBuild/displayName" builds.xml | awk -F":" '{print $2}')

##############       
printf '%s\n' "${vers[@]}" | sort -r | \
                while IFS= read -r line
                do
                    xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' --type elem -n string -v "$line" appConfig.xml
                done

##############   
curl -X POST -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml --data-binary @appConfig.xml

##############     
rm -f appConfig.xml




نظريًا ، إذا اختبرت الكود المكتوب بناءً على الأمثلة أعلاه ، ففي مهمة النشر ، يجب أن يكون لديك بالفعل قائمة منسدلة بالإصدارات. إليك شيء مثل لقطة الشاشة أسفل المفسد.



قائمة النسخة المكتملة بشكل صحيح




إذا نجح كل شيء ، فقم بنسخ البرنامج النصي ولصقه في أمر تشغيل shell وحفظ التغييرات.



اتصال Cloud shell



هواة الجمع في حاوياتنا. نحن نستخدم Ansible كمدير لتوصيل التطبيق والتكوين. وفقًا لذلك ، عندما يتعلق الأمر ببناء الحاويات ، تتبادر إلى الذهن ثلاثة خيارات: تثبيت Docker في Docker ، أو تثبيت Docker على جهاز مزود بـ Ansible ، أو إنشاء حاويات في وحدة التحكم السحابية. لقد اتفقنا على التزام الصمت بشأن المكونات الإضافية لـ Jenkins في هذه المقالة. تذكر؟



قررت: حسنًا ، نظرًا لأن الحاويات "خارج الصندوق" يمكن تجميعها في وحدة التحكم السحابية ، فلماذا تسييج حديقة نباتية؟ ابقها نظيفة ، أليس كذلك؟ أرغب في إنشاء حاويات باستخدام Jenkins في وحدة التحكم السحابية ، ثم إطلاقها في Kuber من هناك. علاوة على ذلك ، داخل البنية التحتية ، تمتلك Google قنوات كبيرة جدًا ، والتي سيكون لها تأثير مفيد على سرعة النشر. يلزم



شيئين للاتصال بوحدة التحكم السحابية: gcloudوحقوق الوصول إلى Google Cloud API لمثيل VM الذي سيتم من خلاله إجراء هذا الاتصال.



بالنسبة لأولئك الذين يخططون للاتصال ليس من Google cloud على الإطلاق
. , *nix' .



, — . — .



أسهل طريقة لمنح الأذونات هي من خلال واجهة الويب.



  1. أوقف مثيل VM الذي ستتصل منه بوحدة التحكم السحابية في المستقبل.
  2. افتح تفاصيل المثيل وانقر فوق تحرير .
  3. في الجزء السفلي من الصفحة ، حدد نطاق الوصول إلى المثيل وصول كامل لجميع واجهات برمجة التطبيقات السحابية .



    لقطة شاشة


  4. احفظ التغييرات وابدأ المثيل.


بعد انتهاء VM من الإقلاع ، اتصل به عبر SSH وتأكد من نجاح الاتصال. استخدم الأمر:



gcloud alpha cloud-shell ssh


يبدو الاتصال الناجح شيئًا كهذا




انشر في GKE



نظرًا لأننا نسعى بكل طريقة ممكنة للتبديل تمامًا إلى IaC (البنية التحتية كرمز) ، فإننا نخزن ملفات dockerfiles في gita. هذا من جهة. يتم وصف النشر في kubernetes بواسطة ملف yaml ، والذي يتم استخدامه فقط بواسطة هذه المهمة ، والتي هي نفسها أيضًا مثل الكود. هذا على الجانب الآخر. بشكل عام أعني أن الخطة كالتالي:



  1. نأخذ قيم المتغيرات BUILD_VERSION ، واختيارياً ، قيم المتغيرات التي سيتم تمريرها عبر ENV .
  2. تحميل ملف dockerfile من gita.
  3. توليد yaml للنشر.
  4. قم بتحميل كلا الملفين عبر scp إلى وحدة التحكم السحابية.
  5. قم ببناء حاوية هناك وادفعها إلى سجل الحاوية
  6. نقوم بتطبيق ملف نشر التحميل على Kuber.


لنكن أكثر تحديدًا. نظرًا لأننا نتحدث عن ENV ، فلنفترض أننا بحاجة إلى تمرير قيم معلمتين: PARAM1 و PARAM2 . أضف مهمتهم للنشر ، اكتب - String Parameter .



لقطة شاشة




سننشئ yaml ببساطة عن طريق إعادة توجيه echo إلى ملف. ومن المفترض، بطبيعة الحال، أن لديك PARAM1 و PARAM2 في dockerfile ، أن اسم الحمل سيكون awesomeapp ، والحاوية تجميعها مع تطبيق الإصدار المحدد هو في التسجيل الحاويات على طول مسار gcr.io/awesomeapp/awesomeapp- $ BUILD_VERSION ، حيث $ BUILD_VERSION هو فقط تم اختياره من القائمة المنسدلة.



أوامر الإدراج
touch deploy.yaml
echo "apiVersion: apps/v1" >> deploy.yaml
echo "kind: Deployment" >> deploy.yaml
echo "metadata:" >> deploy.yaml
echo "  name: awesomeapp" >> deploy.yaml
echo "spec:" >> deploy.yaml
echo "  replicas: 1" >> deploy.yaml
echo "  selector:" >> deploy.yaml
echo "    matchLabels:" >> deploy.yaml
echo "      run: awesomeapp" >> deploy.yaml
echo "  template:" >> deploy.yaml
echo "    metadata:" >> deploy.yaml
echo "      labels:" >> deploy.yaml
echo "        run: awesomeapp" >> deploy.yaml
echo "    spec:" >> deploy.yaml
echo "      containers:" >> deploy.yaml
echo "      - name: awesomeapp" >> deploy.yaml
echo "        image: gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION:latest" >> deploy.yaml
echo "        env:" >> deploy.yaml
echo "        - name: PARAM1" >> deploy.yaml
echo "          value: $PARAM1" >> deploy.yaml
echo "        - name: PARAM2" >> deploy.yaml
echo "          value: $PARAM2" >> deploy.yaml




بعد الاتصال باستخدام gcloud alpha cloud-shell ssh بعامل Jenkins ، لا يتوفر الوضع التفاعلي ، لذلك نرسل أوامر إلى وحدة التحكم السحابية باستخدام المعلمة --command .



نقوم بتنظيف المجلد الرئيسي في وحدة التحكم السحابية من ملف dockerfile القديم:



gcloud alpha cloud-shell ssh --command="rm -f Dockerfile"


وضعنا ملف dockerfile الذي تم تنزيله حديثًا في المجلد الرئيسي لوحدة التحكم السحابية باستخدام scp:



gcloud alpha cloud-shell scp localhost:./Dockerfile cloudshell:~


نقوم بجمع الحاوية ووضع علامة عليها ودفعها إلى سجل الحاوية:



gcloud alpha cloud-shell ssh --command="docker build -t awesomeapp-$BUILD_VERSION ./ --build-arg BUILD_VERSION=$BUILD_VERSION --no-cache"
gcloud alpha cloud-shell ssh --command="docker tag awesomeapp-$BUILD_VERSION gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION"
gcloud alpha cloud-shell ssh --command="docker push gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION"


نفعل نفس الشيء مع ملف النشر. لاحظ أن الأوامر أدناه تستخدم أسماء وهمية للعنقود حيث يتم النشر ( awsm-cluster ) واسم المشروع ( awesome-project ) حيث توجد الكتلة.



gcloud alpha cloud-shell ssh --command="rm -f deploy.yaml"
gcloud alpha cloud-shell scp localhost:./deploy.yaml cloudshell:~
gcloud alpha cloud-shell ssh --command="gcloud container clusters get-credentials awsm-cluster --zone us-central1-c --project awesome-project && \
kubectl apply -f deploy.yaml"


نبدأ المهمة ، ونفتح إخراج وحدة التحكم ونأمل أن نرى بناء حاوية ناجحًا.



لقطة شاشة




ثم النشر الناجح للحاوية المجمعة



لقطة شاشة




أنا تعمدت تجاهل تنظيم الخروج الإعداد . لسبب واحد بسيط: بمجرد تكوينه لحمل عمل باسم معين ، سيظل قيد التشغيل ، بغض النظر عن عدد عمليات النشر التي يتم إجراؤها بهذا الاسم. حسنًا ، بشكل عام ، هذا أبعد قليلاً عن نطاق التاريخ.



بدلا من الاستنتاجات



ربما لا يمكن تنفيذ جميع الخطوات المذكورة أعلاه ، ولكن ببساطة قم بتثبيت بعض المكونات الإضافية لـ Jenkins ، muuulon. لكن بطريقة ما لا أحب المكونات الإضافية. حسنًا ، بتعبير أدق ، أنا ألجأ إليهم فقط بدافع اليأس.



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



All Articles