آزمایش ابزارهای جدید برای ساخت و استقرار خودکار در Kubernetes

آزمایش ابزارهای جدید برای ساخت و استقرار خودکار در Kubernetes

سلام! اخیراً بسیاری از ابزارهای اتوماسیون جالب هم برای ساخت تصاویر Docker و هم برای استقرار در Kubernetes منتشر شده اند. در این راستا، تصمیم گرفتم با GitLab بازی کنم، قابلیت های آن را به طور کامل مطالعه کنم و البته خط لوله را راه اندازی کنم.

این اثر از وب سایت الهام گرفته شده است kubernetes.io، که از تولید می شود کدهای منبع به صورت خودکار و به ازای هر درخواست pool ارسال شده، ربات به طور خودکار یک نسخه پیش نمایش از سایت با تغییرات شما ایجاد می کند و یک لینک برای مشاهده ارائه می دهد.

من سعی کردم یک فرآیند مشابه را از ابتدا بسازم، اما کاملاً بر اساس Gitlab CI و ابزارهای رایگانی که عادت دارم از آنها برای استقرار برنامه ها در Kubernetes استفاده کنم ساخته شده است. امروز بالاخره در مورد آنها بیشتر به شما خواهم گفت.

در این مقاله ابزارهایی مانند:
هوگو, qbec, کانیکو, git-crypt и GitLab CI با ایجاد محیط های پویا

محتوا

  1. با هوگو آشنا شوید
  2. آماده سازی Dockerfile
  3. آشنایی با کانیکو
  4. آشنایی با qbec
  5. آزمایش Gitlab-runner با Kubernetes-executor
  6. استقرار نمودارهای Helm با qbec
  7. معرفی git-crypt
  8. ایجاد تصویر جعبه ابزار
  9. اولین خط لوله و مونتاژ تصاویر ما توسط برچسب ها
  10. اتوماسیون استقرار
  11. مصنوعات و مونتاژ هنگام فشار دادن به استاد
  12. محیط های پویا
  13. مرور برنامه ها

1. آشنایی با هوگو

به عنوان نمونه ای از پروژه خود، ما سعی خواهیم کرد یک سایت انتشار اسناد ساخته شده بر روی هوگو ایجاد کنیم. Hugo یک تولید کننده محتوای ثابت است.

برای کسانی که با ژنراتورهای استاتیک آشنایی ندارند، کمی بیشتر در مورد آنها توضیح خواهم داد. برخلاف موتورهای وب‌سایت معمولی با پایگاه داده و مقداری PHP، که در صورت درخواست کاربر، صفحات را به سرعت تولید می‌کنند، ژنراتورهای استاتیک کمی متفاوت طراحی شده‌اند. آنها به شما این امکان را می‌دهند که منابع، معمولاً مجموعه‌ای از فایل‌ها را در نشانه‌گذاری Markdown و قالب‌های تم دریافت کنید، سپس آنها را در یک وب‌سایت کاملاً کامل کامپایل کنید.

یعنی در نتیجه یک ساختار دایرکتوری و مجموعه‌ای از فایل‌های HTML تولید شده دریافت خواهید کرد که می‌توانید به سادگی آن‌ها را در هر هاست ارزانی آپلود کنید و یک وب‌سایت کارآمد دریافت کنید.

می توانید Hugo را به صورت محلی نصب کنید و آن را امتحان کنید:

راه اندازی یک سایت جدید:

hugo new site docs.example.org

و در عین حال مخزن git:

cd docs.example.org
git init

تا اینجای کار، سایت ما بکر است و برای اینکه چیزی در آن ظاهر شود، ابتدا باید یک تم را به هم وصل کنیم؛ یک تم فقط مجموعه ای از قالب ها و قوانین مشخصی است که سایت ما توسط آن تولید می شود.

برای موضوعی که استفاده خواهیم کرد بدانید، که به نظر من برای سایت مستندسازی کاملاً مناسب است.

من می خواهم به این واقعیت توجه ویژه داشته باشم که ما نیازی به ذخیره فایل های موضوعی در مخزن پروژه خود نداریم، در عوض، می توانیم به سادگی آن را با استفاده از آن متصل کنیم. زیر ماژول git:

git submodule add https://github.com/matcornic/hugo-theme-learn themes/learn

بنابراین، مخزن ما فقط حاوی فایل‌هایی است که مستقیماً با پروژه ما مرتبط هستند و موضوع متصل به عنوان پیوندی به یک مخزن خاص و یک تعهد در آن باقی می‌ماند، یعنی همیشه می‌توان آن را از منبع اصلی بیرون کشید و از آن ترسی نداشت. تغییرات ناسازگار

بیایید پیکربندی را اصلاح کنیم config.toml:

baseURL = "http://docs.example.org/"
languageCode = "en-us"
title = "My Docs Site"
theme = "learn"

در حال حاضر در این مرحله می توانید اجرا کنید:

hugo server

و در آدرس http://localhost:1313/ وب سایت تازه ایجاد شده ما را بررسی کنید، تمام تغییرات ایجاد شده در فهرست به طور خودکار صفحه باز شده در مرورگر را به روز می کند، بسیار راحت!

بیایید سعی کنیم یک صفحه جلد ایجاد کنیم content/_index.md:

# My docs site

## Welcome to the docs!

You will be very smart :-)

اسکرین شات از صفحه تازه ایجاد شده

آزمایش ابزارهای جدید برای ساخت و استقرار خودکار در Kubernetes

برای ایجاد یک سایت، فقط اجرا کنید:

hugo

محتویات دایرکتوری عمومی/ و وب سایت شما خواهد بود.
بله، به هر حال، بیایید بلافاصله آن را به آن اضافه کنیم .گیتیگنور:

echo /public > .gitignore

فراموش نکنید که تغییرات ما را انجام دهید:

git add .
git commit -m "New site created"

2. آماده سازی Dockerfile

وقت آن است که ساختار مخزن خود را تعریف کنیم. من معمولا از چیزی مانند:

.
├── deploy
│   ├── app1
│   └── app2
└── dockerfiles
    ├── image1
    └── image2

  • dockerfiles/ - حاوی دایرکتوری هایی با Dockerfiles و هر چیزی که برای ساختن تصاویر داکر ما لازم است.
  • استقرار/ - شامل دایرکتوری هایی برای استقرار برنامه های ما در Kubernetes است

بنابراین، ما اولین Dockerfile خود را در طول مسیر ایجاد خواهیم کرد dockerfiles/website/Dockerfile

FROM alpine:3.11 as builder
ARG HUGO_VERSION=0.62.0
RUN wget -O- https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_linux-64bit.tar.gz | tar -xz -C /usr/local/bin
ADD . /src
RUN hugo -s /src

FROM alpine:3.11
RUN apk add --no-cache darkhttpd
COPY --from=builder /src/public /var/www
ENTRYPOINT [ "/usr/bin/darkhttpd" ]
CMD [ "/var/www" ]

همانطور که می بینید، Dockerfile شامل دو مورد است از، این فرصت نامیده می شود ساخت چند مرحله ای و به شما این امکان را می دهد که هر چیز غیر ضروری را از تصویر نهایی Docker حذف کنید.
بنابراین، تصویر نهایی فقط شامل darkhttpd (سرور HTTP سبک وزن) و عمومی/ - محتوای وب سایت ما که به صورت ایستا تولید شده است.

فراموش نکنید که تغییرات ما را انجام دهید:

git add dockerfiles/website
git commit -m "Add Dockerfile for website"

3. آشنایی با کانیکو

به عنوان یک سازنده تصویر docker، تصمیم گرفتم از آن استفاده کنم کانیکو، از آنجایی که عملکرد آن نیازی به داکر دیمون ندارد و خود ساخت می تواند بر روی هر ماشینی انجام شود و کش می تواند مستقیماً در رجیستری ذخیره شود و در نتیجه نیاز به ذخیره سازی دائمی کامل را از بین می برد.

برای ساخت تصویر، کافیست ظرف را با آن اجرا کنید مجری kaniko و آن را به متن ساخت فعلی منتقل کنید؛ این را می توان به صورت محلی، از طریق docker نیز انجام داد:

docker run -ti --rm 
  -v $PWD:/workspace 
  -v ~/.docker/config.json:/kaniko/.docker/config.json:ro 
  gcr.io/kaniko-project/executor:v0.15.0 
  --cache 
  --dockerfile=dockerfiles/website/Dockerfile 
  --destination=registry.gitlab.com/kvaps/docs.example.org/website:v0.0.1

Где registry.gitlab.com/kvaps/docs.example.org/website - نام تصویر داکر شما؛ پس از ساخت، به طور خودکار در رجیستری داکر راه اندازی می شود.

پارامتر -- کش به شما امکان می دهد لایه ها را در رجیستری docker ذخیره کنید؛ برای مثال ذکر شده، آنها در آن ذخیره می شوند registry.gitlab.com/kvaps/docs.example.org/website/cache، اما می توانید مسیر دیگری را با استفاده از پارامتر مشخص کنید --cache-repo.

اسکرین شات از docker-registry

آزمایش ابزارهای جدید برای ساخت و استقرار خودکار در Kubernetes

4. آشنایی با qbec

Qbec یک ابزار استقرار است که به شما این امکان را می دهد تا به طور شفاف مانیفست های برنامه خود را توصیف کرده و آنها را در Kubernetes مستقر کنید. استفاده از Jsonnet به‌عنوان سینتکس اصلی به شما امکان می‌دهد تا توصیف تفاوت‌ها در محیط‌های مختلف را تا حد زیادی ساده کنید، و همچنین تقریباً به طور کامل تکرار کد را حذف می‌کند.

این می تواند به ویژه در مواردی که شما نیاز به استقرار یک برنامه در چندین کلاستر با پارامترهای مختلف دارید و می خواهید آنها را به طور شفاف در Git توصیف کنید، صادق است.

Qbec همچنین به شما این امکان را می دهد که نمودارهای Helm را با پاس دادن پارامترهای لازم به آنها رندر کنید و سپس آنها را به همان روشی که مانیفست های معمولی انجام می دهید، از جمله می توانید جهش های مختلفی را روی آنها اعمال کنید و این به نوبه خود به شما این امکان را می دهد که از شر نیاز خلاص شوید. از ChartMuseum استفاده کنید یعنی می‌توانید نمودارها را مستقیماً از git، جایی که به آن تعلق دارند، ذخیره و رندر کنید.

همانطور که قبلاً گفتم، ما همه استقرارها را در یک فهرست ذخیره می کنیم استقرار/:

mkdir deploy
cd deploy

بیایید اولین برنامه خود را مقداردهی کنیم:

qbec init website
cd website

اکنون ساختار برنامه ما به صورت زیر است:

.
├── components
├── environments
│   ├── base.libsonnet
│   └── default.libsonnet
├── params.libsonnet
└── qbec.yaml

بیایید به فایل نگاه کنیم qbec.yaml:

apiVersion: qbec.io/v1alpha1
kind: App
metadata:
  name: website
spec:
  environments:
    default:
      defaultNamespace: docs
      server: https://kubernetes.example.org:8443
  vars: {}

در اینجا ما در درجه اول به آن علاقه داریم spec.محیط ها، qbec قبلاً یک محیط پیش فرض برای ما ایجاد کرده و آدرس سرور و همچنین فضای نام را از kubeconfig فعلی ما گرفته است.
اکنون هنگام استقرار به به طور پیش فرض در محیط، qbec همیشه فقط در خوشه Kubernetes مشخص شده و در فضای نام مشخص شده مستقر می شود، یعنی دیگر لازم نیست برای اجرای یک استقرار بین زمینه ها و فضاهای نام جابجا شوید.
در صورت لزوم، همیشه می توانید تنظیمات این فایل را به روز کنید.

تمام محیط های شما در آن شرح داده شده است qbec.yaml، و در فایل params.libsonnet، جایی که می گوید از کجا باید پارامترها را برای آنها دریافت کرد.

در ادامه دو دایرکتوری را می بینیم:

  • قطعات / - همه مانیفست‌های برنامه ما در اینجا ذخیره می‌شوند؛ آنها را می‌توان هم در فایل‌های jsonnet و هم در فایل‌های Yaml معمولی توصیف کرد.
  • محیط ها/ - در اینجا ما تمام متغیرها (پارامترها) را برای محیط خود شرح خواهیم داد.

به طور پیش فرض دو فایل داریم:

  • ambients/base.libsonnet - شامل پارامترهای مشترک برای همه محیط ها خواهد بود
  • ambients/default.libsonnet - شامل پارامترهای نادیده گرفته شده برای محیط است به طور پیش فرض

بیا باز کنیم ambients/base.libsonnet و پارامترهایی را برای اولین مؤلفه خود در آنجا اضافه کنید:

{
  components: {
    website: {
      name: 'example-docs',
      image: 'registry.gitlab.com/kvaps/docs.example.org/website:v0.0.1',
      replicas: 1,
      containerPort: 80,
      servicePort: 80,
      nodeSelector: {},
      tolerations: [],
      ingressClass: 'nginx',
      domain: 'docs.example.org',
    },
  },
}

بیایید اولین مؤلفه خود را نیز ایجاد کنیم components/website.jsonnet:

local env = {
  name: std.extVar('qbec.io/env'),
  namespace: std.extVar('qbec.io/defaultNs'),
};
local p = import '../params.libsonnet';
local params = p.components.website;

[
  {
    apiVersion: 'apps/v1',
    kind: 'Deployment',
    metadata: {
      labels: { app: params.name },
      name: params.name,
    },
    spec: {
      replicas: params.replicas,
      selector: {
        matchLabels: {
          app: params.name,
        },
      },
      template: {
        metadata: {
          labels: { app: params.name },
        },
        spec: {
          containers: [
            {
              name: 'darkhttpd',
              image: params.image,
              ports: [
                {
                  containerPort: params.containerPort,
                },
              ],
            },
          ],
          nodeSelector: params.nodeSelector,
          tolerations: params.tolerations,
          imagePullSecrets: [{ name: 'regsecret' }],
        },
      },
    },
  },
  {
    apiVersion: 'v1',
    kind: 'Service',
    metadata: {
      labels: { app: params.name },
      name: params.name,
    },
    spec: {
      selector: {
        app: params.name,
      },
      ports: [
        {
          port: params.servicePort,
          targetPort: params.containerPort,
        },
      ],
    },
  },
  {
    apiVersion: 'extensions/v1beta1',
    kind: 'Ingress',
    metadata: {
      annotations: {
        'kubernetes.io/ingress.class': params.ingressClass,
      },
      labels: { app: params.name },
      name: params.name,
    },
    spec: {
      rules: [
        {
          host: params.domain,
          http: {
            paths: [
              {
                backend: {
                  serviceName: params.name,
                  servicePort: params.servicePort,
                },
              },
            ],
          },
        },
      ],
    },
  },
]

در این فایل ما سه موجودیت Kubernetes را به طور همزمان شرح دادیم که عبارتند از: گسترش, محصولات и ورود. اگر می خواستیم می توانستیم آنها را در اجزای مختلف قرار دهیم، اما در این مرحله یکی برای ما کافی است.

نحو jsonnet بسیار شبیه به json معمولی است، در اصل، json معمولی از قبل jsonnet معتبر است، بنابراین در ابتدا ممکن است استفاده از خدمات آنلاین مانند yaml2json برای تبدیل yaml معمولی خود به json، یا اگر اجزای شما حاوی هیچ متغیری نیستند، می توان آنها را به شکل yaml معمولی توصیف کرد.

هنگام کار با jsonnet من به شدت توصیه می کنم یک افزونه برای ویرایشگر خود نصب کنید

به عنوان مثال، یک افزونه برای vim وجود دارد vim-jsonnet، که برجسته کردن نحو را روشن می کند و به طور خودکار اجرا می شود jsonnet fmt هر بار که ذخیره می کنید (نیاز به نصب jsonnet دارد).

همه چیز آماده است، اکنون می توانیم استقرار را شروع کنیم:

برای اینکه ببینیم چه چیزی بدست آوردیم، اجرا کنیم:

qbec show default

در خروجی، مانیفست های رندر شده yaml را مشاهده خواهید کرد که روی خوشه پیش فرض اعمال خواهند شد.

عالی است، اکنون درخواست دهید:

qbec apply default

در خروجی همیشه خواهید دید که در خوشه خود چه کاری انجام می شود، qbec از شما می خواهد با تایپ کردن با تغییرات موافقت کنید. y شما قادر خواهید بود نیت خود را تأیید کنید.

برنامه ما آماده و مستقر است!

اگر تغییراتی ایجاد کنید، همیشه می توانید انجام دهید:

qbec diff default

ببینیم این تغییرات چگونه بر استقرار فعلی تأثیر می گذارد

فراموش نکنید که تغییرات ما را انجام دهید:

cd ../..
git add deploy/website
git commit -m "Add deploy for website"

5. آزمایش Gitlab-runner با Kubernetes-executor

تا همین اواخر فقط معمولی استفاده می کردم gitlab-runner روی ماشین از پیش آماده شده (ظرف LXC) با پوسته یا داکر-اجری. در ابتدا، ما چندین دونده از این قبیل داشتیم که به صورت جهانی در gitlab خود تعریف شده بودند. آنها تصاویر داکر را برای همه پروژه ها جمع آوری کردند.

اما همانطور که تمرین نشان داده است، این گزینه هم از نظر عملی و هم از نظر ایمنی ایده آل ترین گزینه نیست. بسیار بهتر و از نظر ایدئولوژیکی صحیح تر است که برای هر پروژه یا حتی برای هر محیط، دونده های جداگانه مستقر شوند.

خوشبختانه، این به هیچ وجه مشکلی نیست، زیرا اکنون ما مستقر خواهیم شد gitlab-runner به طور مستقیم به عنوان بخشی از پروژه ما درست در Kubernetes.

Gitlab یک نمودار فرمان آماده برای استقرار gitlab-runner در Kubernetes ارائه می دهد. بنابراین تنها کاری که باید انجام دهید این است که بفهمید رمز ثبت نام برای پروژه ما در تنظیمات -> CI / CD -> Runners و آن را به سکان هدایت کنید:

helm repo add gitlab https://charts.gitlab.io

helm install gitlab-runner 
  --set gitlabUrl=https://gitlab.com 
  --set runnerRegistrationToken=yga8y-jdCusVDn_t4Wxc 
  --set rbac.create=true 
  gitlab/gitlab-runner

که در آن:

  • https://gitlab.com - آدرس سرور Gitlab شما.
  • yga8y-jdCusVDn_t4Wxc - رمز ثبت نام برای پروژه شما.
  • rbac.create=true — مقدار لازم از امتیازات را در اختیار رانر قرار می دهد تا بتواند با استفاده از kubernetes-executor پادهایی ایجاد کند تا وظایف ما را انجام دهد.

اگر همه چیز به درستی انجام شود، باید یک دونده ثبت نام شده در بخش مشاهده کنید دوندگان، در تنظیمات پروژه شما.

اسکرین شات از دونده اضافه شده

آزمایش ابزارهای جدید برای ساخت و استقرار خودکار در Kubernetes

به همین سادگی است؟ - بله، به همین سادگی است! با ثبت نام دستی دونده ها دیگر مشکلی وجود ندارد، از این پس رانرها به صورت خودکار ایجاد و نابود می شوند.

6. نمودارهای Helm را با QBEC مستقر کنید

از آنجایی که تصمیم گرفتیم در نظر بگیریم gitlab-runner بخشی از پروژه ما، وقت آن است که آن را در مخزن Git خود توضیح دهیم.

ما می توانیم آن را به عنوان یک جزء جداگانه توصیف کنیم سایت اینترنتی، اما در آینده قصد داریم کپی های مختلفی را مستقر کنیم سایت اینترنتی اغلب، بر خلاف gitlab-runner، که فقط یک بار در هر خوشه Kubernetes مستقر می شود. بنابراین اجازه دهید یک برنامه جداگانه برای آن مقداردهی اولیه کنیم:

cd deploy
qbec init gitlab-runner
cd gitlab-runner

این بار ما موجودیت های Kubernetes را به صورت دستی توصیف نمی کنیم، بلکه یک نمودار Helm آماده می گیریم. یکی از مزایای qbec توانایی ارائه نمودارهای Helm به طور مستقیم از یک مخزن Git است.

بیایید آن را با استفاده از زیر ماژول git وصل کنیم:

git submodule add https://gitlab.com/gitlab-org/charts/gitlab-runner vendor/gitlab-runner

حالا دایرکتوری فروشنده/gitlab-runner ما یک مخزن با نمودار برای gitlab-runner داریم.

به روشی مشابه، می توانید مخازن دیگر، به عنوان مثال، کل مخزن را با نمودارهای رسمی متصل کنید. https://github.com/helm/charts

بیایید مولفه را توضیح دهیم components/gitlab-runner.jsonnet:

local env = {
  name: std.extVar('qbec.io/env'),
  namespace: std.extVar('qbec.io/defaultNs'),
};
local p = import '../params.libsonnet';
local params = p.components.gitlabRunner;

std.native('expandHelmTemplate')(
  '../vendor/gitlab-runner',
  params.values,
  {
    nameTemplate: params.name,
    namespace: env.namespace,
    thisFile: std.thisFile,
    verbose: true,
  }
)

اولین استدلال به ExpandHelmTemplate سپس از مسیر نمودار عبور می کنیم پارامترها. ارزش ها، که از پارامترهای محیط می گیریم، سپس شی با آن می آید

  • nameTemplate - نام انتشار
  • فضای نام - فضای نام به فرمان منتقل شده است
  • این فایل - یک پارامتر مورد نیاز که مسیر فایل فعلی را ارسال می کند
  • واژگان - فرمان را نشان می دهد قالب فرمان با تمام آرگومان ها هنگام ارائه نمودار

حالا بیایید پارامترهای کامپوننت خود را در آن توضیح دهیم ambients/base.libsonnet:

local secrets = import '../secrets/base.libsonnet';

{
  components: {
    gitlabRunner: {
      name: 'gitlab-runner',
      values: {
        gitlabUrl: 'https://gitlab.com/',
        rbac: {
          create: true,
        },
        runnerRegistrationToken: secrets.runnerRegistrationToken,
      },
    },
  },
}

یادداشت runnerRegistrationToken ما از یک فایل خارجی می گیریم Secrets/base.libsonnet، بیایید آن را ایجاد کنیم:

{
  runnerRegistrationToken: 'yga8y-jdCusVDn_t4Wxc',
}

بیایید بررسی کنیم که آیا همه چیز کار می کند:

qbec show default

اگر همه چیز درست باشد، می‌توانیم نسخه‌ای که قبلاً مستقر شده‌ایم را از طریق Helm حذف کنیم:

helm uninstall gitlab-runner

و آن را به همان روش اجرا کنید، اما از طریق qbec:

qbec apply default

7. مقدمه ای بر git-crypt

Git-crypt ابزاری است که به شما امکان می دهد رمزگذاری شفاف را برای مخزن خود تنظیم کنید.

در حال حاضر، ساختار دایرکتوری ما برای gitlab-runner به شکل زیر است:

.
├── components
│   ├── gitlab-runner.jsonnet
├── environments
│   ├── base.libsonnet
│   └── default.libsonnet
├── params.libsonnet
├── qbec.yaml
├── secrets
│   └── base.libsonnet
└── vendor
    └── gitlab-runner (submodule)

اما ذخیره اسرار در Git ایمن نیست، درست است؟ بنابراین باید آنها را به درستی رمزگذاری کنیم.

معمولاً به خاطر یک متغیر، این همیشه منطقی نیست. شما می توانید اسرار را به qbec و از طریق متغیرهای محیطی سیستم CI شما.
اما شایان ذکر است که پروژه‌های پیچیده‌تری نیز وجود دارند که می‌توانند اسرار بیشتری را در خود داشته باشند؛ انتقال همه آنها از طریق متغیرهای محیطی بسیار دشوار خواهد بود.

علاوه بر این، در این مورد من نمی توانم در مورد چنین ابزار فوق العاده ای به شما بگویم git-crypt.

git-crypt همچنین از این جهت راحت است که به شما امکان می دهد کل تاریخچه اسرار را ذخیره کنید، و همچنین به همان روشی که در مورد Git عادت کرده ایم، تضادها را مقایسه، ادغام و حل کنید.

اولین کار بعد از نصب git-crypt ما باید کلیدهایی را برای مخزن خود تولید کنیم:

git crypt init

اگر یک کلید PGP دارید، می توانید بلافاصله خود را به عنوان یک همکار برای این پروژه اضافه کنید:

git-crypt add-gpg-user [email protected]

به این ترتیب همیشه می توانید این مخزن را با استفاده از کلید خصوصی خود رمزگشایی کنید.

اگر کلید PGP ندارید و انتظار آن را ندارید، می توانید از راه دیگر بروید و کلید پروژه را صادر کنید:

git crypt export-key /path/to/keyfile

بنابراین، هر کسی که صادر می کند فایل کلیدی قادر به رمزگشایی مخزن شما خواهد بود.

وقت آن است که اولین راز خود را تنظیم کنیم.
اجازه دهید یادآوری کنم که ما هنوز در دایرکتوری هستیم deploy/gitlab-runner/، جایی که ما یک دایرکتوری داریم اسرار/بیایید تمام فایل های موجود در آن را رمزگذاری کنیم، برای این کار یک فایل ایجاد می کنیم راز/.gitattributes با محتوای زیر:

* filter=git-crypt diff=git-crypt
.gitattributes !filter !diff

همانطور که از محتوا پیداست، تمام فایل ها ماسک شده اند * رانده خواهد شد git-crypt، به جز بیشتر .gitattributes

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

git crypt status -e

خروجی لیستی از تمام فایل های موجود در مخزن خواهد بود که رمزگذاری برای آنها فعال است

این همه است، اکنون می توانیم با خیال راحت تغییرات خود را انجام دهیم:

cd ../..
git add .
git commit -m "Add deploy for gitlab-runner"

برای مسدود کردن یک مخزن، کافی است:

git crypt lock

و بلافاصله تمام فایل های رمزگذاری شده به چیزی باینری تبدیل می شوند، خواندن آنها غیرممکن خواهد بود.
برای رمزگشایی مخزن، اجرا کنید:

git crypt unlock

8. یک تصویر جعبه ابزار ایجاد کنید

تصویر جعبه ابزار تصویری است با تمام ابزارهایی که برای اجرای پروژه خود استفاده خواهیم کرد. توسط Runer Gitlab برای انجام وظایف استقرار معمولی استفاده خواهد شد.

همه چیز در اینجا ساده است، بیایید یک مورد جدید ایجاد کنیم dockerfiles/toolbox/Dockerfile با محتوای زیر:

FROM alpine:3.11

RUN apk add --no-cache git git-crypt

RUN QBEC_VER=0.10.3 
 && wget -O- https://github.com/splunk/qbec/releases/download/v${QBEC_VER}/qbec-linux-amd64.tar.gz 
     | tar -C /tmp -xzf - 
 && mv /tmp/qbec /tmp/jsonnet-qbec /usr/local/bin/

RUN KUBECTL_VER=1.17.0 
 && wget -O /usr/local/bin/kubectl 
      https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VER}/bin/linux/amd64/kubectl 
 && chmod +x /usr/local/bin/kubectl

RUN HELM_VER=3.0.2 
 && wget -O- https://get.helm.sh/helm-v${HELM_VER}-linux-amd64.tar.gz 
     | tar -C /tmp -zxf - 
 && mv /tmp/linux-amd64/helm /usr/local/bin/helm

همانطور که می بینید، در این تصویر ما تمام ابزارهایی را که برای استقرار برنامه خود استفاده کرده ایم نصب می کنیم. ما در اینجا به آن نیاز نداریم مگر اینکه کوبکتل، اما ممکن است بخواهید در مرحله راه اندازی خط لوله با آن بازی کنید.

همچنین، برای اینکه بتوانیم با Kubernetes ارتباط برقرار کنیم و در آن مستقر شویم، باید یک نقش برای پادهای تولید شده توسط gitlab-runner پیکربندی کنیم.

برای انجام این کار، اجازه دهید به دایرکتوری با gitlab-runner برویم:

cd deploy/gitlab-runner

و یک جزء جدید اضافه کنید components/rbac.jsonnet:

local env = {
  name: std.extVar('qbec.io/env'),
  namespace: std.extVar('qbec.io/defaultNs'),
};
local p = import '../params.libsonnet';
local params = p.components.rbac;

[
  {
    apiVersion: 'v1',
    kind: 'ServiceAccount',
    metadata: {
      labels: {
        app: params.name,
      },
      name: params.name,
    },
  },
  {
    apiVersion: 'rbac.authorization.k8s.io/v1',
    kind: 'Role',
    metadata: {
      labels: {
        app: params.name,
      },
      name: params.name,
    },
    rules: [
      {
        apiGroups: [
          '*',
        ],
        resources: [
          '*',
        ],
        verbs: [
          '*',
        ],
      },
    ],
  },
  {
    apiVersion: 'rbac.authorization.k8s.io/v1',
    kind: 'RoleBinding',
    metadata: {
      labels: {
        app: params.name,
      },
      name: params.name,
    },
    roleRef: {
      apiGroup: 'rbac.authorization.k8s.io',
      kind: 'Role',
      name: params.name,
    },
    subjects: [
      {
        kind: 'ServiceAccount',
        name: params.name,
        namespace: env.namespace,
      },
    ],
  },
]

ما همچنین پارامترهای جدید را در آن شرح خواهیم داد ambients/base.libsonnet، که اکنون به شکل زیر است:

local secrets = import '../secrets/base.libsonnet';

{
  components: {
    gitlabRunner: {
      name: 'gitlab-runner',
      values: {
        gitlabUrl: 'https://gitlab.com/',
        rbac: {
          create: true,
        },
        runnerRegistrationToken: secrets.runnerRegistrationToken,
        runners: {
          serviceAccountName: $.components.rbac.name,
          image: 'registry.gitlab.com/kvaps/docs.example.org/toolbox:v0.0.1',
        },
      },
    },
    rbac: {
      name: 'gitlab-runner-deploy',
    },
  },
}

یادداشت $.components.rbac.name اشاره دارد به نام برای جزء rbac

بیایید بررسی کنیم که چه چیزی تغییر کرده است:

qbec diff default

و تغییرات ما را در Kubernetes اعمال کنید:

qbec apply default

همچنین، فراموش نکنید که تغییرات ما را در git انجام دهید:

cd ../..
git add dockerfiles/toolbox
git commit -m "Add Dockerfile for toolbox"
git add deploy/gitlab-runner
git commit -m "Configure gitlab-runner to use toolbox"

9. اولین خط لوله و مونتاژ تصاویر توسط برچسب ها

در ریشه پروژه ما ایجاد خواهیم کرد .gitlab-ci.yml با محتوای زیر:

.build_docker_image:
  stage: build
  image:
    name: gcr.io/kaniko-project/executor:debug-v0.15.0
    entrypoint: [""]
  before_script:
    - echo "{"auths":{"$CI_REGISTRY":{"username":"$CI_REGISTRY_USER","password":"$CI_REGISTRY_PASSWORD"}}}" > /kaniko/.docker/config.json

build_toolbox:
  extends: .build_docker_image
  script:
    - /kaniko/executor --cache --context $CI_PROJECT_DIR/dockerfiles/toolbox --dockerfile $CI_PROJECT_DIR/dockerfiles/toolbox/Dockerfile --destination $CI_REGISTRY_IMAGE/toolbox:$CI_COMMIT_TAG
  only:
    refs:
      - tags

build_website:
  extends: .build_docker_image
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  script:
    - /kaniko/executor --cache --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dockerfiles/website/Dockerfile --destination $CI_REGISTRY_IMAGE/website:$CI_COMMIT_TAG
  only:
    refs:
      - tags

لطفا توجه داشته باشید که ما استفاده می کنیم GIT_SUBMODULE_STRATEGY: عادی برای مشاغلی که باید به صراحت زیر ماژول ها را قبل از اجرا مقداردهی اولیه کنید.

فراموش نکنید که تغییرات ما را انجام دهید:

git add .gitlab-ci.yml
git commit -m "Automate docker build"

من فکر می کنم ما می توانیم با خیال راحت این را یک نسخه بنامیم v0.0.1 و تگ را اضافه کنید:

git tag v0.0.1

هر زمان که نیاز به انتشار نسخه جدید داشته باشیم، برچسب ها را اضافه خواهیم کرد. برچسب ها در تصاویر داکر به تگ های Git گره می خورند. هر فشار با یک تگ جدید، ساخت تصاویر با این تگ را مقداردهی اولیه می کند.

بیایید آن را انجام دهیم git push -- برچسب هاو بیایید به اولین خط لوله خود نگاه کنیم:

اسکرین شات از اولین خط لوله

آزمایش ابزارهای جدید برای ساخت و استقرار خودکار در Kubernetes

شایان ذکر است که مونتاژ با برچسب برای ساخت تصاویر داکر مناسب است، اما برای استقرار یک برنامه در Kubernetes مناسب نیست. از آنجایی که تگ های جدید را می توان به commit های قدیمی اختصاص داد، در این صورت، مقداردهی اولیه خط لوله برای آنها منجر به استقرار نسخه قدیمی می شود.

برای حل این مشکل، معمولاً ساخت تصاویر docker به برچسب ها و استقرار برنامه به یک شاخه گره خورده است. استاد، که در آن نسخه هایی از تصاویر جمع آوری شده هاردکد شده است. این جایی است که می توانید با یک برگرداندن ساده، بازگشت به عقب را مقداردهی اولیه کنید استاد-شاخه ها.

10. اتوماسیون استقرار

برای اینکه Gitlab-runner اسرار ما را رمزگشایی کند، باید کلید مخزن را صادر کنیم و آن را به متغیرهای محیط CI خود اضافه کنیم:

git crypt export-key /tmp/docs-repo.key
base64 -w0 /tmp/docs-repo.key; echo

ما خط به دست آمده را در Gitlab ذخیره می کنیم؛ برای انجام این کار، به تنظیمات پروژه خود می رویم:
تنظیمات -> CI / CD -> متغیرها

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

نوع
کلید
ارزش
حفاظت شده
ماسک زده
حوزه

File
GITCRYPT_KEY
<your string>
true (در طول آموزش می توانید false)
true
All environments

اسکرین شات از متغیر اضافه شده

آزمایش ابزارهای جدید برای ساخت و استقرار خودکار در Kubernetes

حالا بیایید خودمان را به روز کنیم .gitlab-ci.yml اضافه کردن به آن:

.deploy_qbec_app:
  stage: deploy
  only:
    refs:
      - master

deploy_gitlab_runner:
  extends: .deploy_qbec_app
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  before_script:
    - base64 -d "$GITCRYPT_KEY" | git-crypt unlock -
  script:
    - qbec apply default --root deploy/gitlab-runner --force:k8s-context __incluster__ --wait --yes

deploy_website:
  extends: .deploy_qbec_app
  script:
    - qbec apply default --root deploy/website --force:k8s-context __incluster__ --wait --yes

در اینجا ما چندین گزینه جدید را برای qbec فعال کرده ایم:

  • -روت برخی/برنامه - به شما امکان می دهد دایرکتوری یک برنامه خاص را تعیین کنید
  • --force:k8s-context __incluster__ - این یک متغیر جادویی است که می گوید استقرار در همان خوشه ای رخ می دهد که gtilab-runner در آن اجرا می شود. این ضروری است زیرا در غیر این صورت qbec سعی خواهد کرد یک سرور Kubernetes مناسب را در kubeconfig شما پیدا کند
  • --صبر کن - qbec را مجبور می کند منتظر بماند تا منابعی که ایجاد می کند به حالت آماده بروند و تنها پس از آن با یک کد خروج موفق خارج شود.
  • -آره - به سادگی پوسته تعاملی را غیرفعال می کند شما مطمئن هستید؟ هنگام استقرار

فراموش نکنید که تغییرات ما را انجام دهید:

git add .gitlab-ci.yml
git commit -m "Automate deploy"

و بعد از فشار دادن خواهیم دید که برنامه های ما چگونه مستقر شده اند:

اسکرین شات از خط لوله دوم

آزمایش ابزارهای جدید برای ساخت و استقرار خودکار در Kubernetes

11. مصنوعات و مونتاژ هنگام هل دادن به استاد

به طور معمول، مراحلی که در بالا توضیح داده شد برای ساخت و ارائه تقریباً هر میکروسرویس کافی است، اما ما نمی‌خواهیم هر بار که نیاز به به‌روزرسانی سایت داریم، یک برچسب اضافه کنیم. بنابراین، ما مسیر پویاتری را در پیش خواهیم گرفت و یک Digest Deployment را در شاخه اصلی راه اندازی خواهیم کرد.

ایده ساده است: اکنون تصویر ماست سایت اینترنتی هر بار که به داخل فشار می آورید بازسازی می شود استاد، و سپس به طور خودکار در Kubernetes مستقر شود.

بیایید این دو شغل را در ما به روز کنیم .gitlab-ci.yml:

build_website:
  extends: .build_docker_image
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  script:
    - mkdir -p $CI_PROJECT_DIR/artifacts
    - /kaniko/executor --cache --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dockerfiles/website/Dockerfile --destination $CI_REGISTRY_IMAGE/website:$CI_COMMIT_REF_NAME --digest-file $CI_PROJECT_DIR/artifacts/website.digest
  artifacts:
    paths:
      - artifacts/
  only:
    refs:
      - master
      - tags

deploy_website:
  extends: .deploy_qbec_app
  script:
    - DIGEST="$(cat artifacts/website.digest)"
    - qbec apply default --root deploy/website --force:k8s-context __incluster__ --wait --yes --vm:ext-str digest="$DIGEST"

لطفا توجه داشته باشید که ما یک موضوع اضافه کرده ایم استاد к رفر برای مشاغل build_website و اکنون استفاده می کنیم $CI_COMMIT_REF_NAME به جای $CI_COMMIT_TAG، یعنی از تگ ها در Git جدا شده ایم و اکنون تصویری را با نام شاخه commit که خط لوله را مقداردهی اولیه کرده است فشار می دهیم. شایان ذکر است که این کار با برچسب‌ها نیز کار می‌کند، که به ما امکان می‌دهد عکس‌های فوری یک سایت را با یک نسخه خاص در رجیستری docker ذخیره کنیم.

هنگامی که نام تگ docker برای یک نسخه جدید از سایت بدون تغییر باشد، ما هنوز باید تغییرات را در Kubernetes توصیف کنیم، در غیر این صورت به سادگی برنامه را از تصویر جدید مجدداً نصب نخواهد کرد، زیرا هیچ تغییری در تصویر مشاهده نخواهد کرد. مانیفست استقرار

گزینه —vm:ext-str digest="$DIGEST" برای qbec - به شما امکان می دهد یک متغیر خارجی را به jsonnet ارسال کنید. ما می خواهیم که با هر نسخه از برنامه ما، آن را دوباره در کلاستر مستقر کنیم. ما دیگر نمی‌توانیم از نام برچسب استفاده کنیم، که اکنون می‌تواند غیرقابل تغییر باشد، زیرا باید به نسخه خاصی از تصویر مرتبط باشیم و هنگام تغییر آن، استقرار آن را آغاز کنیم.

در اینجا از توانایی Kaniko برای ذخیره یک تصویر خلاصه در یک فایل (گزینه --digest-file)
سپس این فایل را انتقال داده و در زمان استقرار می خوانیم.

بیایید پارامترهای خود را به روز کنیم deploy/website/environments/base.libsonnet که اکنون به شکل زیر خواهد بود:

{
  components: {
    website: {
      name: 'example-docs',
      image: 'registry.gitlab.com/kvaps/docs.example.org/website@' + std.extVar('digest'),
      replicas: 1,
      containerPort: 80,
      servicePort: 80,
      nodeSelector: {},
      tolerations: [],
      ingressClass: 'nginx',
      domain: 'docs.example.org',
    },
  },
}

انجام شد، اکنون هر تعهدی وارد شود استاد ساخت تصویر docker را برای سایت اینترنتیو سپس آن را در Kubernetes مستقر کنید.

فراموش نکنید که تغییرات ما را انجام دهید:

git add .
git commit -m "Configure dynamic build"

بعدا بررسی می کنیم فشار دادن باید چیزی شبیه به این ببینیم:

تصویری از خط لوله برای استاد

آزمایش ابزارهای جدید برای ساخت و استقرار خودکار در Kubernetes

در اصل، نیازی نیست که با هر فشار، gitlab-runner را دوباره مستقر کنیم، مگر اینکه، البته، چیزی در پیکربندی آن تغییر نکرده باشد، اجازه دهید آن را در .gitlab-ci.yml:

deploy_gitlab_runner:
  extends: .deploy_qbec_app
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  before_script:
    - base64 -d "$GITCRYPT_KEY" | git-crypt unlock -
  script:
    - qbec apply default --root deploy/gitlab-runner --force:k8s-context __incluster__ --wait --yes
  only:
    changes:
      - deploy/gitlab-runner/**/*

تغییرات به شما این امکان را می دهد که تغییرات را در deploy/gitlab-runner/ و فقط در صورت وجود کار ما را آغاز می کند

فراموش نکنید که تغییرات ما را انجام دهید:

git add .gitlab-ci.yml
git commit -m "Reduce gitlab-runner deploy"

فشار دادن، بهتر است:

تصویری از خط لوله به روز شده

آزمایش ابزارهای جدید برای ساخت و استقرار خودکار در Kubernetes

12. محیط های پویا

زمان آن رسیده است که خط لوله خود را با محیط های پویا متنوع کنیم.

ابتدا بیایید کار را به روز کنیم build_website در ما .gitlab-ci.yml، بلوک را از آن حذف کنید فقط، که Gitlab را مجبور می کند تا آن را در هر commit به هر شاخه ای راه اندازی کند:

build_website:
  extends: .build_docker_image
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  script:
    - mkdir -p $CI_PROJECT_DIR/artifacts
    - /kaniko/executor --cache --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dockerfiles/website/Dockerfile --destination $CI_REGISTRY_IMAGE/website:$CI_COMMIT_REF_NAME --digest-file $CI_PROJECT_DIR/artifacts/website.digest
  artifacts:
    paths:
      - artifacts/

سپس کار را به روز کنید deploy_website، یک بلوک را در آنجا اضافه کنید محیط:

deploy_website:
  extends: .deploy_qbec_app
  environment:
    name: prod
    url: https://docs.example.org
  script:
    - DIGEST="$(cat artifacts/website.digest)"
    - qbec apply default --root deploy/website --force:k8s-context __incluster__ --wait --yes --vm:ext-str digest="$DIGEST"

این به Gitlab اجازه می دهد تا کار را با آن مرتبط کند تولید کننده محیط و نمایش لینک صحیح به آن.

حالا بیایید دو کار دیگر اضافه کنیم:

deploy_website:
  extends: .deploy_qbec_app
  environment:
    name: prod
    url: https://docs.example.org
  script:
    - DIGEST="$(cat artifacts/website.digest)"
    - qbec apply default --root deploy/website --force:k8s-context __incluster__ --wait --yes --vm:ext-str digest="$DIGEST"

deploy_review:
  extends: .deploy_qbec_app
  environment:
    name: review/$CI_COMMIT_REF_NAME
    url: http://$CI_ENVIRONMENT_SLUG.docs.example.org
    on_stop: stop_review
  script:
    - DIGEST="$(cat artifacts/website.digest)"
    - qbec apply review --root deploy/website --force:k8s-context __incluster__ --wait --yes --vm:ext-str digest="$DIGEST" --vm:ext-str subdomain="$CI_ENVIRONMENT_SLUG" --app-tag "$CI_ENVIRONMENT_SLUG"
  only:
    refs:
    - branches
  except:
    refs:
      - master

stop_review:
  extends: .deploy_qbec_app
  environment:
    name: review/$CI_COMMIT_REF_NAME
    action: stop
  stage: deploy
  before_script:
    - git clone "$CI_REPOSITORY_URL" master
    - cd master
  script:
    - qbec delete review --root deploy/website --force:k8s-context __incluster__ --yes --vm:ext-str digest="$DIGEST" --vm:ext-str subdomain="$CI_ENVIRONMENT_SLUG" --app-tag "$CI_ENVIRONMENT_SLUG"
  variables:
    GIT_STRATEGY: none
  only:
    refs:
    - branches
  except:
    refs:
      - master
  when: manual

آنها با فشار دادن به هر شعبه ای به جز master راه اندازی می شوند و نسخه پیش نمایش سایت را مستقر می کنند.

ما یک گزینه جدید برای qbec می بینیم: --برنامه-برچسب - به شما امکان می دهد نسخه های مستقر شده برنامه را برچسب گذاری کنید و فقط در این تگ کار کنید؛ هنگام ایجاد و از بین بردن منابع در Kubernetes، qbec فقط با آنها کار می کند.
به این ترتیب ما نمی توانیم برای هر بررسی یک محیط مجزا ایجاد کنیم، بلکه به سادگی از همان محیط استفاده مجدد می کنیم.

در اینجا ما نیز استفاده می کنیم بررسی درخواست qbec، بجای qbec پیش فرض را اعمال کنید - این دقیقاً همان لحظه ای است که ما سعی خواهیم کرد تفاوت های محیط های خود را توصیف کنیم (بازبینی و پیش فرض):

اضافه کنیم این فایل نقد می نویسید: محیط در deploy/website/qbec.yaml

spec:
  environments:
    review:
      defaultNamespace: docs
      server: https://kubernetes.example.org:8443

سپس آن را در آن اعلام خواهیم کرد deploy/website/params.libsonnet:

local env = std.extVar('qbec.io/env');
local paramsMap = {
  _: import './environments/base.libsonnet',
  default: import './environments/default.libsonnet',
  review: import './environments/review.libsonnet',
};

if std.objectHas(paramsMap, env) then paramsMap[env] else error 'environment ' + env + ' not defined in ' + std.thisFile

و پارامترهای سفارشی آن را در آن یادداشت کنید deploy/website/environments/review.libsonnet:

// this file has the param overrides for the default environment
local base = import './base.libsonnet';
local slug = std.extVar('qbec.io/tag');
local subdomain = std.extVar('subdomain');

base {
  components+: {
    website+: {
      name: 'example-docs-' + slug,
      domain: subdomain + '.docs.example.org',
    },
  },
}

بیایید نگاهی دقیق تر به jobu بیندازیم stop_review، هنگامی که شعبه حذف شود فعال می شود و به طوری که gitlab سعی نمی کند پرداخت کند از آن استفاده می شود GIT_STRATEGY: هیچ، بعداً کلون می کنیم استاد-شاخه کنید و نقد را از طریق آن حذف کنید.
کمی گیج کننده است، اما من هنوز راهی زیباتر پیدا نکرده ام.
یک گزینه جایگزین، قرار دادن هر بررسی در یک فضای نام هتل است که همیشه می تواند به طور کامل تخریب شود.

فراموش نکنید که تغییرات ما را انجام دهید:

git add .
git commit -m "Enable automatic review"

فشار دادن, git checkout -b test, تست مبدا فشار git، بررسی:

اسکرین شات از محیط های ایجاد شده در Gitlab

آزمایش ابزارهای جدید برای ساخت و استقرار خودکار در Kubernetes

همه چیز کار می کند؟ - عالی، شاخه آزمایشی ما را حذف کنید: استاد git checkout, git push origin :test، بررسی می کنیم که کارهای حذف محیط بدون خطا کار می کنند.

در اینجا می خواهم بلافاصله توضیح دهم که هر توسعه دهنده در یک پروژه می تواند شعبه ایجاد کند، او همچنین می تواند تغییر کند .gitlab-ci.yml فایل و دسترسی به متغیرهای مخفی
بنابراین، اکیداً توصیه می شود که استفاده از آنها را فقط برای شاخه های محافظت شده مجاز کنید، به عنوان مثال در استاد، یا یک مجموعه جداگانه از متغیرها برای هر محیط ایجاد کنید.

13. برنامه ها را مرور کنید

مرور برنامه ها این یک ویژگی GitLab است که به شما امکان می دهد برای هر فایل در مخزن یک دکمه اضافه کنید تا به سرعت آن را در یک محیط مستقر مشاهده کنید.

برای اینکه این دکمه ها ظاهر شوند، باید یک فایل ایجاد کنید gitlab/route-map.yml و تمام تبدیل های مسیر را در آن توصیف کنید؛ در مورد ما بسیار ساده خواهد بود:

# Indices
- source: /content/(.+?)_index.(md|html)/ 
  public: '1'

# Pages
- source: /content/(.+?).(md|html)/ 
  public: '1/'

فراموش نکنید که تغییرات ما را انجام دهید:

git add .gitlab/
git commit -m "Enable review apps"

فشار دادن، و بررسی:

اسکرین شات از دکمه Review App

آزمایش ابزارهای جدید برای ساخت و استقرار خودکار در Kubernetes

کار تمام شد!

منابع پروژه:

ممنون از توجهتون امیدوارم خوشتون اومده باشه آزمایش ابزارهای جدید برای ساخت و استقرار خودکار در Kubernetes

منبع: www.habr.com

اضافه کردن نظر