خب توی این پست قراره یه داکرفایل خیییلی سبک رو بنویسیم که مثه ببر شنا میکنه و غرق نمیشه:)
احتمالا اینجا پیشنیازا رو میذارن ولی خب چون خوانندگان این پست قهرمان گو و داکر حداقل در منطقهشون هستن پس مستقیم میریم سر اصل مطلب...
مثلا شما خییلی راحت میتونید از ایمیج رسمی گولنگ استفاده کنید تا حجم ایمیجتون یه چیزی حول و حوش ۸۲۳ مگ برسه؛ یا این که بلابازی در بیاری و بگید چه کاریه و برید به جای debianبیسش از اون گوگلنگه که بر پایه alpine لینوکسه استفاده کنید؛ که اتفاقا خییلیم حرکت حرفیهایه ولی خب باید دقت کنید که دیگه git و gcc و bash این سوسولبازیارو نداره که بسته به استفادهتون شاید به دیپندنسی پرابلمها بخورید! یا شایدم نخورید:)! که البته اینجام حجمتون یه چیزی تقریبا ۲۳۰ مگ میشه!!!
که برای پرداکشن خب حجم زیادیه؛ پس کلا سمت اینایی که یه مرحله استیج به قول باکلاسا دارن و برنامه رو بیلد میکنن نمیریم حداقل توی این پست!!!
صرفهجویی در وقت(بسته به شخص متغیر)☕
خب حالا که سراغ اون کارای زشت نمیریم؛ پس بهتره هر چه زودتر دست به کار بشیم و سلام دنیای معروفمون رو در گولنگ بنویسیم البته این دفه صدامونو دارید از کف کانتیرا و اقیانوس;)
خب این تا اینجا...
حالا بریم داکر فایل خوشگلومونو برسی کنیم:
به صورت کلی ما اومدیم و از تکنیک مولتی استیجینگ توی داکر برای کم کردن محسوس حجم ایمیجمون و کش کردن ماژولهایی که دانلود میکنیم روی سیستم خودمون برای بالا بردن سرعت بیلد ایمیج استفاده کردیم.
یعنی در واقع توی این تکنیک میایم در دو مرحله یا stage کارمون رو انجام میدیم.
در مرحله اول برنامهمون رو با یه ایمیج alpine بیلد میکنیم به صورت استاتیک؛ یعنی هر ماژول یا خرت و پرتی که لازم داره رو به لینکر میگه همرامش بیاره و ما در مرحله دوم صرفا اون باینری رو میتونیم هر جایی ببریم و ران کنیم.
البته بعدا میبینم که هر جایی نمیبریم و برای کاهش حجم ایمیجمون میریم روی یه ایمیج اصطلاحا Distroless که وابسته به توزیع خاصی نیستش و صرفا، برای مثال یه یوزر nobody و گروهش رو با حداقل چیزهای مورد نیاز برامون فراهم میکنه تا با یوزر روت که کار خیلی بدیه کارامون رو انجام ندیم.
با استفاده از کشم که دیگه استادین میایم بهش میگیم که آقا/خانوم یا هر چیز دیگه داکر جان بیا قبل این که go mod download رو ران کنی، یه سر به مسیری که توی متغیر محیطی GOCACHE تعریف کردیم بزن ببین اونجا چیزی هست که به دردت بخوره!
خب حالا بریم یه مقدار با جزئیات بیشتر توضیح بدیم که دقیقا چی کار کردیم. از همون اول شروع میکنیم:
ARG BUILDER_IMAGE=golang:1.22-alpine ARG DISTROLESS_IMAGE=gcr.io/distroless/static
توی دو خط اول اومدیم و صرفا ایمیجامون رو توی دو تا متغیر ریختیم تا ادای چیزارو در بیاریم و مرتبتر بشه که اگر بعدا مثلا خواستیم ورژن ایمیج رو عوض کنیم فقط به متغیر مقدار جدید میدیم و در ادامه برنامه هیچ تغییری ایجاد نخواهد شد.
FROM ${BUILDER_IMAGE} as builder RUN update-ca-certificates WORKDIR /myapp
بعدش اومدیم و میگیم در مرحله اول از BUILDER_IMAGE شروع کن و مطمئن میشم سرتیفیکیتامون آپدیتن و دایرکتوریمون رو به myapp تغییر میدیم(همون طور که در جریانید دیگه خداروشکر اون GOPATH و اینا قدیمی شدن و از گولنگ ۱.۱۶ دیگه دیفالت زبون اینه که بگرده فایل go.mod توی روت همون پکیجاسپیسمونو پیدا کنه و دیپندنسیارو بر اساس اون هندل میکنه)
ENV GOCACHE=$HOME/.cache/go-build RUN --mount=type=cache,target=$GOCACHE
خب اینجا همون طور که قبلا توضیح دادیم متغیر محیطی GOCACHE رو ست میکنیم البته دیفالتشم معمولا همینه ولی خب برای این که مطمئن شیم ستسش میکنیم و در ادامه میگیم GOCACHE رو ببین و به عنوان کش ازش استفاده کن تا اگه چیزایی که توی go.mod هستن و ما داریمشون دوباره نریم با اینترنت خفنمون دانشون کنیم و ۴۰۳ بگیریم:)
COPY go.mod . RUN go mod download && go mod verify
توی این دو خطم که معلومه، میایم کپی میکنیم go.mod(جایی که اون وابستگیهایی که ایمپورت کردیم و توی برنامهمون ازش استفاده کردیم رو نگه میداره) و با go mod verify میتونیم یه چک اضافی با go.sum انجام بدیم که هش اون چیزایی که دان میشن یکسان باشن با اون مال خودمون.
COPY . . RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ -ldflags='-w -s -extldflags "-static"' -a \ -o /myapp/hello .
خب اینجا میرسیم به قسمت حساس ماجرا که به خاطرش من این پستو نوشتم. توی خط اول که میگه هر چی توی دایرکتوری فعلی هستو کپی کن همونجا(/myapp)که هستی؛ در واقع پروژهمونه که اینجا Hello,World.
ولی در ادامه ما برای این که حجم ایمجمون رو مینیممِ مینمم بکنیم میگیم که اولا ما از کد C توی برنامهمون استفاده نکردیم(همون طور که در جریانید میشه تو GO مستقیم کد C کال کرد) البته معلومه که قبلش از این که به لایبرری C کدمون وابسته نیست اطمینان حاصل کردیم.
بعدش میگم که صرفا بیا و برای سیستمعامل linux با معماری amd64(اینتل و اون یکی amd ۶۴ بیتی) بسازش و باینری معلومه که فقط روی اینا ران میشه...
بعد میایم با -a میگیم به زور از اول همه پکیجهارو بیلد کن و با -o اون مسیر و نام فایلو مشخص میکنیم و در انتها . که یعنی هر چی اینجا هستو. به عبارت دیگه هر فایل گو اینجا هستو برامون بیلد کن.
خب قسمت جالب ماجرا اینجاست که با ldflags میتونیم برای لینکر کامپلایرمون فلگها و آپشنای مختلف بفرستیم.
مثلا با -w میگیم اطلاعات دیباگ DWARF رو نمیخوایم؛ که همون جور که از اسمش مشخصه هم DWARF یه فرمت استاندارد برای اطلاعاتی در مورد دیباگ یه برنامه یا کتابخونه خاصه.(مثلا یه سری چیزا مثلِ اسم برنامه، خطوط برنامه، نام و نوع متغیرها و توابعمون و مکان و آدرسشون توی مموری و اینجور چیزا که توی دیباگ به درد میخوره و عملا توی پرداکشن زیاد جایی ندارن رو نگه میداره)
جهت اطلاعات بیشتر اگه خووووشتون اومده، میتونید یه سر به این لینک بزنید.
با -s میتونیم بگیم جدول نمادها رو در نظر نگیر و نمیخوایمش؛ حالا این جدول شامل چه چیزاییه؟ همون نام و نوع نمادها، متغیرها، توابع و آدرس و offsetشون توی مموری
همون طور که دیدین -s و -w خیلی اطلاعات شبیه به همی رو نگه میدارن و معمولا در کنار هم استفاده میشن.
دقت کنید که همون طور که گفته شد با این مدل بیلد دیگه دیباگو باید بیخیال بشید.
و در مورد آخر:
-extldflags "-static"
هم میگیم به سایر ldflags هم بگو که برنامهمون رو میخوایم به صورت استاتیک بسازیم. استاتیک در مقابل داینامیکه. اگه یادتون باشه گفتیم که C و لایبریهاشو کلا بیخیال شدیم. بیلد داینامیک این مدلیه که میگیم اگه libc و فَک و فامیلاش رو سیستم بودن دیگه تو نمیخواد مال خودتو بار سنگینی ببری و از همونا استفاده کن؛ که خب معلومه در نتیجه باینریمون سبکتر میشه و آخرین آپدیتارو توسط خود سیستم دریافت میکنه و ...
ولی خب در مقابل ما اومدیم اینجا از اَپرُچ استاتیک استفاده کردیم، اگه بیشترم دقت کنید، میبینید که در آینده ایمیج Distrolessمون هم بر اساس همین نیازه که libc و این جور خرت و پرتایی که همه دارنو نداره...
پس ما میایم توی فرآیند بیلد به لینکر میگیم آقا/خانوم یا هر چیز دیگه لینکر، هر چی که برنامه ما لازم داره رو بردار و با خودت بیار که بعد قراره از ایمیج Distroless استاتیک استفاده کنیم؛ نگی نگفتی....
خب از مرحله نفسگیر اول گذشتیم به سلامت گذشتیم، حالا یه باینری(فایل اجرایی در پلتفرم مورد نظر)داریم و یه جایی میخوایم که این فایلو روش ران کنیم. پس سعی میکنیم اون جاهه تا جایی که میتونیم مینیمال باشه.(من به شدت در زندگی طرفدار #مینیمالیسم هستم) البته اگه کرم داشته باشیم و بخاریم؛ میتونیم از ایمیج scratch استفاده کنیم و بعد خودمون براش یوزر بسازیم و پسشو گروهشو ست کنیم... که تهش یک مگ فرقش باشه!!!
ولی خب مثه آدم از همین ایمیجهای Distroless استفاده میکنیم (خودشون یوزر و گروه nobody رو از قبل همکارانمون در گوگل ساختن و ما با یوزر روت که کار بیتربیتیه، این کارارو انجام نمیدیم) و برنامهمون رو اون تو ران میکنیم.
پس میایم میگیم از:
FROM ${DISTROLESS_IMAGE}
ایمیج Distroless استاتیکی که همکارمون درست کرده بیا و استفاده کن و در ادامه با:
COPY --from=builder /myapp/hello /myapp/hello
هر چی که توی مسیر /myapp/hello اون کانتینر که اسمشو گذاشته بودیم builder مرحله اولمون بود که با بدبختی و خساست تمام بیلدش کردیم و الان اون تو باینریشو به صورت استاتیک داریم؛ اون همه چیزای لازم رو برای ران شدن همراه خودش داره؛ پس بیا کپیش کن توی /myapp/hello خودمون این ایمیج جدیده!
و در نهایت با:
ENTRYPOINT ["/myapp/hello"]
میگیم نقطه ورودت اینجا باشه و از اینجا برنامه رو ران کن!!!
خب تموم شد و ما از اون 823MB اول با این هنرایی که به خرج دادیم رسیدیم به 3.8MB و جا داره واقعا بر طبل شادانه مثل سیاوش تنها...
راستی راستی من و همکارم در این ریپو داکرفایلهای معمولی(بیس) و from scratch هم قرار دادیم و makefile و کلی چیز دیگه(خالی بستم دقیقا همنیان:))
خوشحال میشم اگه نظری استاری فورکی چیزی حمایتی خلاصه سال نوتون مبارک:)