ساخت تصاویر Docker بهینه شده برای یک برنامه Spring Boot

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

این مقاله روش‌های مختلفی را برای کانتینر کردن یک برنامه Spring Boot پوشش می‌دهد:

  • ساختن یک تصویر داکر با استفاده از فایل داکر،
  • ساختن یک تصویر OCI از منبع با استفاده از Cloud-Native Buildpack،
  • و بهینه سازی تصویر در زمان اجرا با جداسازی قطعات JAR به سطوح مختلف با استفاده از ابزارهای لایه ای.

 نمونه کد

این مقاله با یک مثال کد کار همراه است در GitHub .

اصطلاحات کانتینر

ما با اصطلاحات کانتینر مورد استفاده در سراسر مقاله شروع می کنیم:

  • تصویر ظرف: فایلی با فرمت خاص. ما با اجرای ابزار build برنامه خود را به یک تصویر ظرف تبدیل می کنیم.
  • ظرف: یک نمونه اجرایی از تصویر ظرف.
  • موتور کانتینری: فرآیند دیمون که وظیفه اجرای کانتینر را بر عهده دارد.
  • میزبان کانتینر: ماشین میزبان که موتور کانتینر روی آن کار می کند.
  • رجیستری کانتینر: مکان عمومی مورد استفاده برای انتشار و توزیع تصویر کانتینر.
  • استاندارد OCIطرح کانتینر باز (OCI) یک چارچوب مدیریتی سبک و متن باز است که توسط بنیاد لینوکس شکل گرفته است. مشخصات تصویر OCI استانداردهای صنعتی را برای قالب‌های تصویر کانتینر و زمان اجرا تعریف می‌کند تا اطمینان حاصل شود که همه موتورهای کانتینر می‌توانند تصاویر کانتینر ایجاد شده توسط هر ابزار ساخت را اجرا کنند.

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

نسخه 2.3 Spring Boot پلاگین هایی برای ساخت تصاویر OCI ارائه می دهد.

کارگر بارانداز رایج ترین پیاده سازی کانتینر است و ما در مثال های خود از Docker استفاده می کنیم، بنابراین تمام مراجع کانتینر بعدی در این مقاله به Docker اشاره می کنند.

ساخت تصویر ظرف به روش سنتی

ساخت تصاویر Docker برای برنامه های Spring Boot با افزودن چند دستورالعمل به Dockerfile بسیار آسان است.

ابتدا یک JAR اجرایی ایجاد می کنیم و به عنوان بخشی از دستورالعمل های Dockerfile، پس از اعمال سفارشی سازی های لازم، JAR اجرایی را در بالای تصویر پایه JRE کپی می کنیم.

بیایید برنامه Spring خود را ایجاد کنیم Spring Initializr با وابستگی ها weblombokи actuator. ما همچنین یک کنترلر استراحت برای ارائه یک API اضافه می کنیم GETروش.

ایجاد یک Dockerfile

سپس با افزودن این برنامه را در ظرفی قرار می دهیم Dockerfile:

FROM adoptopenjdk:11-jre-hotspot
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/application.jar"]

Dockerfile ما حاوی یک تصویر پایه، از adoptopenjdk، در بالای آن فایل JAR خود را کپی می کنیم و سپس پورت را باز می کنیم. 8080که به درخواست ها گوش می دهد.

مونتاژ برنامه

ابتدا باید با استفاده از Maven یا Gradle یک اپلیکیشن ایجاد کنید. در اینجا ما از Maven استفاده می کنیم:

mvn clean package

این یک فایل JAR قابل اجرا برای برنامه ایجاد می کند. ما باید این JAR اجرایی را به یک تصویر داکر تبدیل کنیم تا روی موتور داکر اجرا شود.

یک تصویر ظرف ایجاد کنید

سپس با اجرای دستور این فایل اجرایی JAR را در تصویر داکر قرار می دهیم docker buildاز دایرکتوری ریشه پروژه حاوی Dockerfile ایجاد شده قبلی:

docker build  -t usersignup:v1 .

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

docker images 

خروجی دستور بالا شامل تصویر ما می شود usersignupبه همراه تصویر پایه، adoptopenjdkدر Dockerfile ما مشخص شده است.

REPOSITORY          TAG                 SIZE
usersignup          v1                  249MB
adoptopenjdk        11-jre-hotspot      229MB

مشاهده لایه های داخل یک تصویر ظرف

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

dive usersignup:v1

در اینجا بخشی از خروجی دستور Dive آمده است: 

ساخت تصاویر Docker بهینه شده برای یک برنامه Spring Boot

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

ساخت تصویر ظرف با Buildpack

بسته های مونتاژ (Buildpacks) یک اصطلاح عمومی است که توسط پیشنهادات مختلف پلتفرم به عنوان سرویس (PAAS) برای ایجاد یک تصویر ظرف از کد منبع استفاده می شود. این توسط Heroku در سال 2011 راه اندازی شد و از آن زمان توسط Cloud Foundry، Google App Engine، Gitlab، Knative و چند نفر دیگر پذیرفته شده است.

ساخت تصاویر Docker بهینه شده برای یک برنامه Spring Boot

مزیت بسته های ساخت ابری

یکی از مزایای اصلی استفاده از Buildpack برای ساخت تصاویر این است تغییرات پیکربندی تصویر را می توان به صورت مرکزی مدیریت کرد (سازنده) و با استفاده از سازنده به همه برنامه ها انتشار داد.

بسته های بیلد به شدت به پلتفرم گره خورده بودند. Cloud-Native Buildpacks با پشتیبانی از فرمت تصویر OCI، استانداردسازی را در سراسر پلتفرم ها ارائه می کند، که تضمین می کند که تصویر می تواند توسط موتور داکر اجرا شود.

با استفاده از افزونه Spring Boot

افزونه Spring Boot تصاویر OCI را از منبع با استفاده از Buildpack می سازد. تصاویر با استفاده از bootBuildImageوظایف (Gradle) یا spring-boot:build-imageهدف (Maven) و نصب Docker محلی.

ما می‌توانیم نام تصویری را که باید به رجیستری Docker فشار دهیم، با تعیین نام در آن سفارشی کنیم. image tag:

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <image>
      <name>docker.io/pratikdas/${project.artifactId}:v1</name>
    </image>
  </configuration>
</plugin>

بیایید از Maven برای اجرا استفاده کنیم build-imageاهداف ایجاد یک برنامه کاربردی و ایجاد یک تصویر ظرف. ما در حال حاضر از هیچ Dockerfiles استفاده نمی کنیم.

mvn spring-boot:build-image

نتیجه چیزی شبیه به این خواهد بود:

[INFO] --- spring-boot-maven-plugin:2.3.3.RELEASE:build-image (default-cli) @ usersignup ---
[INFO] Building image 'docker.io/pratikdas/usersignup:v1'
[INFO] 
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 0%
.
.
.. [creator]     Adding label 'org.springframework.boot.version'
.. [creator]     *** Images (c311fe74ec73):
.. [creator]           docker.io/pratikdas/usersignup:v1
[INFO] 
[INFO] Successfully built image 'docker.io/pratikdas/usersignup:v1'

از خروجی می بینیم که paketo Cloud-Native buildpackبرای ایجاد یک تصویر OCI در حال کار استفاده می شود. مانند قبل، با اجرای دستور می‌توانیم تصویری را که به‌عنوان یک تصویر Docker فهرست شده است، ببینیم:

docker images 

نتیجه گیری:

REPOSITORY                             SIZE
paketobuildpacks/run                  84.3MB
gcr.io/paketo-buildpacks/builder      652MB
pratikdas/usersignup                  257MB

ایجاد یک تصویر کانتینر با Jib

Jib یک افزونه نگارش تصویر از Google است که روشی جایگزین برای ایجاد یک تصویر ظرف از منبع ارائه می دهد.

پیکربندی jib-maven-pluginدر pom.xml:

      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>2.5.2</version>
      </plugin>

در مرحله بعد، پلاگین Jib را با استفاده از دستور Maven برای ساخت برنامه و ایجاد تصویر کانتینر اجرا می کنیم. مانند قبل، ما از هیچ Dockerfiles در اینجا استفاده نمی کنیم:

mvn compile jib:build -Dimage=<docker registry name>/usersignup:v1

پس از اجرای دستور Maven فوق، خروجی زیر را دریافت می کنیم:

[INFO] Containerizing application to pratikdas/usersignup:v1...
.
.
[INFO] Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*, io.pratik.users.UsersignupApplication]
[INFO] 
[INFO] Built and pushed image as pratikdas/usersignup:v1
[INFO] Executing tasks:
[INFO] [==============================] 100.0% complete

خروجی نشان می دهد که تصویر کانتینر ایجاد و در رجیستری قرار گرفته است.

انگیزه ها و روش های ایجاد تصاویر بهینه

ما دو دلیل اصلی برای بهینه سازی داریم:

  • کارایی: در یک سیستم ارکستراسیون کانتینر، یک تصویر ظرف از رجیستری تصویر به میزبانی که موتور کانتینر را اجرا می کند، کشیده می شود. این فرآیند برنامه ریزی نامیده می شود. کشیدن تصاویر بزرگ از رجیستری منجر به زمان‌بندی طولانی در سیستم‌های ارکستراسیون کانتینر و زمان ساخت طولانی در خطوط لوله CI می‌شود.
  • امنیت: تصاویر بزرگ همچنین دارای یک منطقه بزرگ برای آسیب پذیری هستند.

یک تصویر Docker از پشته‌ای از لایه‌ها تشکیل شده است که هر کدام بیانگر یک عبارت در Dockerfile ما هستند. هر لایه نشان دهنده دلتای تغییرات در لایه زیرین است. هنگامی که یک تصویر Docker را از رجیستری می‌کشیم، به صورت لایه‌ای کشیده شده و روی هاست ذخیره می‌شود.

استفاده از بوت بهار "Fat JAR" در به عنوان فرمت بسته بندی پیش فرض وقتی به یک JAR چاق نگاه می کنیم، می بینیم که برنامه، بخش بسیار کوچکی از کل JAR است. این قسمتی است که بیشتر تغییر می کند. بقیه شامل وابستگی های Spring Framework است.

فرمول بهینه سازی حول جداسازی برنامه در سطحی جداگانه از وابستگی های Spring Framework متمرکز شده است.

لایه وابستگی که بخش عمده ای از فایل ضخیم JAR را تشکیل می دهد تنها یک بار دانلود شده و در سیستم میزبان ذخیره می شود.

فقط یک لایه نازک از برنامه در طول به‌روزرسانی برنامه و زمان‌بندی کانتینر کشیده می‌شود، همانطور که در این نمودار نشان داده شده است:

ساخت تصاویر Docker بهینه شده برای یک برنامه Spring Boot

در بخش های بعدی، نحوه ایجاد این تصاویر بهینه شده برای یک برنامه Spring Boot را بررسی خواهیم کرد.

ساخت یک تصویر کانتینر بهینه شده برای یک برنامه Spring Boot با Buildpack

Spring Boot 2.3 از لایه بندی با استخراج بخش هایی از یک فایل JAR ضخیم در لایه های جداگانه پشتیبانی می کند. ویژگی لایه‌بندی به‌طور پیش‌فرض غیرفعال است و باید به‌صراحت با استفاده از افزونه Spring Boot Maven فعال شود:

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <layers>
      <enabled>true</enabled>
    </layers>
  </configuration> 
</plugin>

ما از این پیکربندی برای ساختن تصویر کانتینر خود ابتدا با Buildpack و سپس با Docker در بخش های بعدی استفاده خواهیم کرد.

بریم بدویم build-imageهدف Maven برای ایجاد یک تصویر ظرف:

mvn spring-boot:build-image

اگر Dive را برای دیدن لایه‌ها در تصویر حاصل اجرا کنیم، می‌توانیم ببینیم که لایه برنامه (که به رنگ قرمز دایره شده است) در محدوده کیلوبایتی در مقایسه با آنچه که با استفاده از فرمت ضخیم JAR به دست آورده‌ایم، بسیار کوچک‌تر است:

ساخت تصاویر Docker بهینه شده برای یک برنامه Spring Boot

ساخت یک تصویر کانتینر بهینه شده برای یک برنامه Spring Boot با Docker

به جای استفاده از افزونه Maven یا Gradle، می توانیم یک تصویر لایه لایه Docker JAR با فایل Docker نیز ایجاد کنیم.

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

محتویات JAR به دست آمده پس از ساخت با Maven با لایه‌بندی فعال به این صورت خواهد بود:

META-INF/
.
BOOT-INF/lib/
.
BOOT-INF/lib/spring-boot-jarmode-layertools-2.3.3.RELEASE.jar
BOOT-INF/classpath.idx
BOOT-INF/layers.idx

خروجی یک JAR اضافی به نام را نشان می دهد spring-boot-jarmode-layertoolsи layersfle.idxفایل. این فایل JAR اضافی، همانطور که در بخش بعدی توضیح داده شد، قابلیت لایه بندی را فراهم می کند.

وابستگی ها را در لایه های جداگانه استخراج کنید

برای مشاهده و استخراج لایه ها از JAR لایه ای خود، از ویژگی system استفاده می کنیم -Djarmode=layertoolsبرای شروع spring-boot-jarmode-layertoolsJAR به جای برنامه:

java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar

اجرای این دستور یک خروجی حاوی گزینه های دستور موجود تولید می کند:

Usage:
  java -Djarmode=layertools -jar usersignup-0.0.1-SNAPSHOT.jar

Available commands:
  list     List layers from the jar that can be extracted
  extract  Extracts layers from the jar for image creation
  help     Help about any command

خروجی دستورات را نشان می دهد listextractи helpс helpپیش فرض باشد بیایید دستور را با listگزینه:

java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar list
dependencies
spring-boot-loader
snapshot-dependencies
application

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

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

نام لایه

مقدار

dependencies

هر وابستگی که نسخه آن حاوی SNAPSHOT نباشد

spring-boot-loader

کلاس های بارگذار JAR

snapshot-dependencies

هر وابستگی که نسخه آن حاوی SNAPSHOT باشد

application

کلاس ها و منابع کاربردی

لایه ها در تعریف شده اند layers.idxفایل را به ترتیبی که باید به تصویر داکر اضافه شوند. این لایه ها پس از اولین واکشی روی هاست ذخیره می شوند زیرا تغییر نمی کنند. فقط لایه برنامه به روز شده در هاست دانلود می شود که به دلیل کاهش حجم سریعتر است .

ساخت یک تصویر با وابستگی های استخراج شده در لایه های جداگانه

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

بیایید Dockerfile خود را برای ساخت چند مرحله ای تغییر دهیم:

# the first stage of our build will extract the layers
FROM adoptopenjdk:14-jre-hotspot as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

# the second stage of our build will copy the extracted layers
FROM adoptopenjdk:14-jre-hotspot
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

ما این پیکربندی را در یک فایل جداگانه ذخیره می کنیم - Dockerfile2.

ما تصویر Docker را با استفاده از دستور زیر می سازیم:

docker build -f Dockerfile2 -t usersignup:v1 .

پس از اجرای این دستور خروجی زیر را دریافت می کنیم:

Sending build context to Docker daemon  20.41MB
Step 1/12 : FROM adoptopenjdk:14-jre-hotspot as builder
14-jre-hotspot: Pulling from library/adoptopenjdk
.
.
Successfully built a9ebf6970841
Successfully tagged userssignup:v1

می بینیم که تصویر Docker با یک شناسه تصویر ایجاد شده و سپس برچسب گذاری شده است.

در نهایت، دستور Dive را مانند قبل اجرا می کنیم تا لایه های داخل تصویر داکر تولید شده را بررسی کنیم. ما می توانیم یک شناسه یا برچسب تصویر را به عنوان ورودی دستور Dive ارائه دهیم:

dive userssignup:v1

همانطور که از خروجی می بینید، لایه حاوی برنامه اکنون تنها 11 کیلوبایت است و وابستگی ها در لایه های جداگانه کش می شوند. 

ساخت تصاویر Docker بهینه شده برای یک برنامه Spring Boot

وابستگی های داخلی را در لایه های جداگانه استخراج کنید

ما می‌توانیم با استخراج هر یک از وابستگی‌های سفارشی خود در یک لایه جداگانه به جای بسته‌بندی آن‌ها با برنامه، اندازه لایه برنامه را کاهش دهیم. ymlفایل مشابه با نام layers.idx:

- "dependencies":
  - "BOOT-INF/lib/"
- "spring-boot-loader":
  - "org/"
- "snapshot-dependencies":
- "custom-dependencies":
  - "io/myorg/"
- "application":
  - "BOOT-INF/classes/"
  - "BOOT-INF/classpath.idx"
  - "BOOT-INF/layers.idx"
  - "META-INF/"

در این فایل layers.idxما یک وابستگی سفارشی به نام اضافه کرده ایم، io.myorgحاوی وابستگی های سازمانی بازیابی شده از مخزن مشترک.

نتیجه

در این مقاله، ما به استفاده از Cloud-Native Buildpacks برای ساختن تصویر کانتینر به طور مستقیم از منبع نگاه کردیم. این جایگزینی برای استفاده از Docker برای ایجاد یک تصویر کانتینر به روش معمول است: ابتدا یک فایل JAR اجرایی ضخیم ایجاد می‌شود و سپس با مشخص کردن دستورالعمل‌ها در Dockerfile در یک تصویر ظرف بسته‌بندی می‌شود.

ما همچنین به بهینه سازی کانتینر خود با گنجاندن یک ویژگی لایه‌بندی که وابستگی‌ها را به لایه‌های جداگانه استخراج می‌کند که روی میزبان ذخیره می‌شوند و یک لایه کاربردی نازک در زمان‌بندی زمان‌بندی در موتورهای اجرایی کانتینر بارگذاری می‌شود، بررسی کردیم.

شما می توانید تمام کد منبع استفاده شده در مقاله را در اینجا پیدا کنید گیتهاب .

مرجع فرمان

در اینجا خلاصه ای از دستوراتی است که ما در این مقاله استفاده کرده ایم تا یک مرجع سریع ارائه دهیم.

پاکسازی زمینه:

docker system prune -a

ساخت تصویر کانتینر با Dockerfile:

docker build -f <Docker file name> -t <tag> .

ساخت تصویر کانتینر از منبع (بدون Dockerfile):

mvn spring-boot:build-image

مشاهده لایه های وابستگی قبل از ساختن فایل jar برنامه، مطمئن شوید که ویژگی لایه بندی در افزونه Spring-boot-maven-Plugin فعال است:

java -Djarmode=layertools -jar application.jar list

استخراج لایه های وابستگی قبل از ساختن فایل jar برنامه، مطمئن شوید که ویژگی لایه بندی در افزونه Spring-boot-maven-Plugin فعال است:

 java -Djarmode=layertools -jar application.jar extract

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

docker images

نمای سمت چپ داخل تصویر ظرف (مطمئن شوید که ابزار غواصی نصب شده است):

dive <image ID or image tag>

منبع: www.habr.com