فهرست مطالب

ساخت تصاویر داکر به صورت امن و سریع با Kaniko

مقدمه

کانیکو چیست؟

کانیکو یک سازنده تصویر داکر است. ممکن است از این معرفی راضی نباشید، اما این تمام چیزی است که کانیکو درباره‌اش است. تصاویر را ساخته و به یک رجیستری از جمله Dockerhub، ECR، ACR یا رجیستری شخصی خودتان ارسال کنید.

چگونه کار می‌کند؟

استفاده از آن فوق‌العاده آسان است. ابتدا سیستم فایل تصویر پایه را استخراج کنید. هر یک از دستورات (COPY, ADD, RUN) را در سیستم فایل تصویر پایه اجرا کنید. یک اسنپ‌شات از وضعیت فعلی سیستم فایل ایجاد می‌شود، فایل‌های جدید یا تغییر یافته به تصویر پایه اضافه می‌شوند و متادیتای تصویر به‌روزرسانی می‌شود. بسیار مفید است. نه؟ نه تا وقتی که یاد بگیرید چگونه از آن استفاده کنید.

موارد استفاده

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

  • اگر در حال ساخت تصاویر روی یک ماشین یا رانر مشترک هستید، ممکن است بتوانید خطرات نقض امنیتی را کاهش دهید. روش‌های مختلفی برای ساخت تصاویر وجود دارد. شاید بتوانید برخی از اسکریپت‌های bash، playbookهای Ansible یا نوعی اسکریپت را به ارائه‌دهنده CI/CD خود بنویسید تا چیزی شبیه به docker build -t blahblahblah:v0.0.1 -f SomeSortOfDockerfile انجام دهد. شما می‌توانید این دستور را از bash یا از یک کانتینر (Docker in Docker) اجرا کنید. هیچ یک از این‌ها برای محیط‌های مشترک مناسب نیست. اول، تصور کنید یک سناریو که یک کاربر/توسعه‌دهنده/پایپلاین دیگر به کد شما دسترسی پیدا می‌کند، یا شما نیاز دارید به برخی داده‌های حیاتی از یک پایگاه داده دسترسی پیدا کنید، یا شاید نیاز دارید برخی اعتبارنامه‌های حیاتی (یا کلیدها) را از منبع دیگری دریافت کنید. این می‌تواند به یک فاجعه منجر شود. کد شما می‌تواند توسط دیگران دسترسی پیدا کند، داده‌های پایگاه داده شما می‌تواند نشت کند، یا کسی می‌تواند کلیدهای شما را بدزدد. این بدترین کابوس برای تیم‌های امنیت و عملیات است.

  • کاهش زمان ساخت یکی دیگر از ویژگی‌هایی است که ممکن است به آن علاقه‌مند باشید. سرورها (رانرها) گاهی اوقات برای تیم‌های DevOps مشکل‌ساز می‌شوند به دلیل زمان طولانی اجرای فرآیند ساخت. ممکن است روز بدی داشته باشید اگر پایگاه کد شما بزرگ باشد و بسیاری وابستگی‌ها را از اینترنت دریافت کنید. هیچ‌کس (منظورم تیم‌های DevOps یا SRE است) نمی‌خواهد یک تیکت دیگر از تیم توسعه دریافت کند که “پایپلاین‌های ساخت ما کند هستند. ما در فورس هستیم و باید هات‌فیکس‌ها فوراً در تولید ادغام شوند”.

  • شاید بودجه محدودی برای زیرساخت داشته باشید، بنابراین کاملاً به کلاستر Kubernetes خود متکی هستید. از پایگاه داده تا محیط برای ساخت تصاویر داکر. (نکات حرفه‌ای: هیچ چیزی روی Kubernetes مستقر نکنید. در آینده، در مورد آن خواهم نوشت. پایگاه داده‌ها می‌توانند روی Kubernetes اجرا شوند، اما این کار را نکنید)

ما کانیکو را برای رفع این مشکلات داریم. اگرچه یک ابرقهرمان نیست، اما می‌تواند ما را به ابرقهرمان شرکت تبدیل کند.

مشکل

چیزی که داشتیم

من از این فرصت استفاده می‌کنم تا به اشتراک بگذارم چگونه زیرساخت CI/CD ما در Alibaba Travels مستقر شد و چه کارهایی انجام دادیم تا آن را قابل اعتمادتر کنیم. این یک زیرساخت قدیمی بود که نیاز به نوسازی یا بازسازی داشت. ما تازه چندین پروژه را به این ساختار جدید به محیط staging، توسعه و دیگر محیط‌ها به غیر از تولید منتقل کرده‌ایم، اما هنوز کارهای بسیاری باقی مانده که باید انجام شوند.

برمی‌گردیم به موضوع اصلی، ما پروژه‌هایی از استک‌های مختلف داریم. از Dotnet Core گرفته تا Nodejs و Python همه را استفاده می‌کنیم. قبلاً همه پروژه‌ها از یک رانر مشترک استفاده می‌کردند که بر روی Gitlab Continuous Integration اجرا می‌شد. چندین رانر shell و Docker روی آن سرور قوی نصب شده بودند (برای کاهش خطرات خرابی، هنوز در دسترس هستند اما برنامه داریم آن‌ها را مهاجرت دهیم). این‌ها انواع رانرهایی هستند که پروژه‌ها از آن‌ها استفاده می‌کنند. بدیهی است که پروژه‌هایی که فرآیند ساخت خود را بر روی Docker اجرا می‌کنند، به سوکت Docker دسترسی خواهند داشت، و در پایپلاین‌ها، همه افراد درون آن رانر می‌توانند یکدیگر را ببینند و حتی دسترسی shell به یک کانتینر دیگر بدهند.

منطقه خطر

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

یک قدم جلوتر

پس از ایجاد چندین رانر Kubernetes، همه چیز به خوبی پیش می‌رفت، اما یک مشکل دیگر وجود داشت. امکان مونت کردن سوکت Docker میزبان به کانتینر وجود ندارد. این خیلی خطرناک است. کانتینر به کانتینرهای میزبان دسترسی پیدا می‌کند. برخی از شما گفتید که این مشکلی ندارد، ما می‌توانیم ماشین‌های اختصاصی برای اجرای آن‌ها ایجاد کنیم (که یک راه‌حل پرهزینه است). آیا می‌توانید یک سوکت Docker را به چندین کانتینر مونت کنید؟ علاوه بر این، شما باید با کانتکست امنیتی سروکار داشته باشید که در سیستم حفره‌های بیشتری ایجاد می‌کند.

این یک فاجعه است.

راه حل

چیزی که نیاز داریم

کانیکو به ما کمک می‌کند تا این شکاف‌ها را ببندیم. پس از پیاده‌سازی کانیکو، ساخت یک تصویر داکر از یک اپلیکیشن DotNet به 21 دقیقه زمان نیاز داشت. این فرآیند بدون رانر Kubernetes سه و نیم دقیقه طول می‌کشید. برای یک اپلیکیشن Node.js، این زمان به 45 دقیقه افزایش یافت. در رانرهای مشترک ما، این کار 11 دقیقه زمان می‌برد.

کاری که ما انجام دادیم

ما می‌خواستیم این کار قابل اعتمادتر باشد، اما کاری که انجام دادیم زمان بیشتری برد.

کشینگ

کشینگ به من کمک کرد تا آن را اصلاح کنم. ابتدا باید کشینگ فعال شود (فلگ --cache=true). دو انتخاب برای کشینگ داریم.

  • کش کردن در یک دایرکتوری محلی (--cache-dir)
  • کش کردن در یک رجیستری مشخص (--cache-repo flag) از آنجایی که ما منابع زیادی در رجیستری خود داریم، تصمیم گرفتیم آن‌ها را کش کنیم.

فراموش نکنید که دستورهای COPY و RUN را می‌توان کش کرد زمانی که فلگ --cache-copy-layers=true را فعال کنید. نکته ۲: این دو روش کشینگ زمانی در دسترس خواهند بود که --cache=true تنظیم شده باشد.

Dockerfile

این Dockerfile را بررسی کنید. به نظر می‌رسد همه چیز خوب است و توسعه‌دهنده با آن خوشحال است.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
FROM mcr.microsoft.com/dotnet/sdk:6.0
RUN apt update && apt install libgnutls30 && apt install ca-certificates && update-ca-certificates
WORKDIR /app
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:80
COPY . ./
RUN dotnet restore   src/app/app.csproj --configfile src/app/nuget.config
RUN dotnet publish src/app/apptifier.csproj -c Release -o /app/out
FROM mcr.microsoft.com/dotnet/sdk:6.0
WORKDIR /app
COPY --from=build-env /app/out .
EXPOSE 80
ENTRYPOINT ["dotnet","app.dll"]

کاملاً عادی به نظر می‌رسد، اما فکر می‌کنم این یک فاجعه دیگر است. اجازه دهید توضیح دهم:

  • دستورهای COPY باید به بخش‌های کوچکتر تقسیم شوند. در این حالت من آن را تغییر دادم تا هر ماژول اپلیکیشن کپی شود.
  • امکان ادغام دو دستور RUN وجود دارد.
  • دستور apt بسته‌های کش شده را حذف نمی‌کند. این در این وضعیت اهمیت چندانی ندارد، اما ما همچنان آن را انجام خواهیم داد.

کاهش تعداد خطوط در Dockerfile برای کانیکو کار نمی‌کند. از نوشتن خطوط بیشتر نترسید. این چیزی است که من آن را تغییر دادم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env

WORKDIR /app
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:80

COPY src/Module1 ./src/Module1
COPY src/Module2 ./src/Module2
COPY src/app ./src/app

COPY tests/app.Test ./tests/Iapp.Test.Test
COPY tests/app.integrity.Test ./tests/app.integrity.Test

COPY app.sln ./
RUN dotnet publish --configfile src/app/nuget.config src/app/app.csproj -c Release -o /app/out --nologo -verbosity:quiet

FROM mcr.microsoft.com/dotnet/sdk:6.0

WORKDIR /app
COPY --from=build-env /app/out .

EXPOSE 80

ENTRYPOINT ["dotnet","app.dll"]

من از همین روش برای پروژه‌های دیگر هم استفاده کردم.

اسنپ‌شات

در ابتدای پست خود اشاره کردم که اسنپ‌شات‌ها روشی برای ذخیره لایه‌های تصاویر و وضعیت‌های آن‌ها هستند. با --single-snapshot=true، شما فقط در پایان فرآیند یک اسنپ‌شات می‌گیرید، که بسیار سریع است اما برای کش مناسب نیست، یا می‌توانید از --snapshotMode استفاده کنید تا اطمینان حاصل کنید که روش صحیح اسنپ‌شات استفاده می‌شود. من فقط آرگومان‌های snapshot را از مستندات رسمی کپی و پیست کردم تا نحوه کار آن را نشان دهم.

حالتنتیجه
fullتمام محتویات و متادیتای فایل در هنگام اسنپ‌شات در نظر گرفته می‌شوند. این گزینه کم‌کارآمدترین است، اما همچنین پایدارترین است.
redoزمان فایل، اندازه، حالت، مالک uid و gid هنگام اسنپ‌شات در نظر گرفته می‌شوند. این ممکن است تا 50٪ سریعتر از “full” باشد، به خصوص اگر پروژه شما تعداد زیادی فایل داشته باشد.
timeفقط زمان فایل در هنگام اسنپ‌شات در نظر گرفته می‌شود.

به دلایلی من حالت redo را انتخاب کردم.

نتیجه

ابتدا، کمی بیشتر از دستور docker build اصلی طول کشید، که قابل درک است. کانیکو تلاش می‌کند تا کش‌ها را ایجاد کند. با این حال، پس از ایجاد چند تغییر در ماژول‌ها و فایل‌ها، نتایج چشمگیر بود. در اینجا نگاهی به جدول می‌اندازیم:

نوع پروژهزمان ساخت داکرکانیکو بدون کشکانیکو اولین بار با کشکانیکو بار دوم با کش
Dotnet core3:1014:253:400:27
NodeJS11:4545:2013:102:10

استفاده از کانیکو

چهار مورد نیاز دارید تا از کانیکو استفاده کنید:

  • یک Dockerfile و کد منبع به نام build context، به همراه یک تغییر جزئی در Dockerfile شما
  • یک رجیستری که تصاویر را به آن ارسال کنید
  • کانیکو
  • مستندات کانیکو

روش من

این دستور کامل است که من استفاده می‌کنم:

1
2
3
4
5
6
7
8
9
        /kaniko/executor
        --context "projectDirectory"
        --dockerfile "$projectDirectory/Dockerfile"
        --destination "imageTag:version"
        --cache-copy-layers=true
        --snapshotMode=time
        --use-new-run
        --cache=true
        --cache-repo="imageTag:cache"

نتیجه‌گیری

یادداشت‌ها

یادداشت‌های مربوط به تولید

این روش روی محیط‌های staging، توسعه و دیگر محیط‌های غیر از تولید اعمال شد. ممکن است تا شش ماه طول بکشد تا این روش و ابزارها به‌طور کامل پذیرش شوند.
بنابراین، اگر مایل به پیاده‌سازی این روش هستید، مطمئن شوید که همه چیز را آزمایش کرده‌اید و هیچ مشکلی مانند مسائل امنیتی به وجود نمی‌آید که به محیط تولید شما آسیب بزند.
این یک ابزار پایدار و عالی است، اما با مسئولیت خود از آن استفاده کنید.

سایر یادداشت‌ها

هدف این پست این است که به شما کمک کند عملکرد و امنیت پایپلاین‌های ساخت داکر خود را بهبود بخشید. لطفاً هرگونه نظری دارید آزادانه بیان کنید.
لطفاً اگر هرگونه تعارض یا مشکلی وجود دارد به من اطلاع دهید :)