Dotenvx با داکر، راه بهتری برای مدیریت متغیرهای محیط پروژه با اسرار است

امروز میخواهیم سادهترین راه را برای ادغام Dotenvx در پروژهای که به شدت از خدمات Docker استفاده میکند، بحث کنیم. Dotenvx یک ابزار مدیریت متغیر محیطی منبع باز است که در مدیریت اسرار تخصص دارد و توسط توسعه دهنده ابزار محبوب Dotenv ایجاد شده است. Dotenvx به عنوان جانشین Dotenv عمل می کند.
شروع کنیم
هنگام استفاده از Dotenvx با Docker Compose دو چالش اصلی وجود دارد:
-
چگونه می توانم اسرار متغیر محیط رمزگشایی شده خود را به Docker Compose برسانم تا بتوانم خود ابزار Docker Compose را پیکربندی کنم و توانایی انتقال و دسترسی به متغیرهای محیط را از طریق آن حفظ کنم.
compose.yml
فایل؟ -
چگونه می توانم متغیرهای محیط رمزگشایی شده خود را در ظرف سرویس Docker Compose خود دریافت کنم تا در زمان اجرا در دسترس فرآیند سرویس و فایل سیستم کانتینر قرار گیرند؟
راه واضح برای انجام آن
تمایل اولیه استفاده از روش تبلیغ شده و اولیه تزریق محیط است که توسط ابزار Dotenvx و آن ارائه شده است. run
استدلال، به عنوان مثال:
dotenvx run -f .env.dev -- python webserver.py
اما از آنجایی که ما از Docker استفاده می کنیم، در نهایت به این نتیجه می رسیم:
dotenvx run -f .env.dev -- docker compose up --build
چالش یکی با موفقیت حل شد. ما موفق شده ایم متغیرهای محیط رمزگشایی شده را در Docker Compose وارد کنیم تا بتوانیم از آنها در داخل خود استفاده کنیم compose.yml
فایل مانند این:
# compose.yml
services:
postgres:
image: postgres:latest
environment:
# $POSTGRES_PASSWORD is stored as an encrypted password in .env.dev
# But dotenvx has decrypted it for us and injected it, so we can use it here
POSTGRES_PASSWORD: $POSTGRES_PASSWORD
همچنین در صورت نیاز به دسترسی به آنها در Dockerfile، میتوانیم env vars رمزگشایی شده را به مرحله ساخت تصویر خود منتقل کنیم، مانند:
# compose.yml
services:
custom_service:
build:
target: custom_service
args:
SUPER_SECRET_FROM_ENV: $SUPER_SECRET_FROM_ENV
اما چگونه env vars را در زمان اجرا به کانتینر سرویس تزریق کنیم و چالش دو را حل کنیم؟
درد و اندوه
این جایی است که رنج شروع می شود. به طور معمول، ما به اعتماد خود تکیه می کنیم env_file
دستورالعمل Docker Compose، اما این کار نمیکند، زیرا تنها زمانی میتوانیم فایل رمزگذاری شده خود را در ظرف بارگیری کنیم که بتوانیم آن را به فایلی روی دیسک نشان دهیم. در حال حاضر، ما در حال رمزگشایی env vars خود و تزریق آنها به ابزار Compose هستیم.
در اینجا روش استاندارد برخورد مردم با این موضوع آمده است: قرار دادن یک کپی از باینری Dotenvx در سیستم فایل کانتینر و رمزگشایی و تزریق برای بار دوم، به فرآیند کانتینر سرویس. چگونه این کار را انجام دهیم و آیا می توانیم بدون تبدیل پروژه خود به یک آشفتگی نفرت انگیز این کار را انجام دهیم؟ خوب… نه. قاطعانه نه. این چیزی است که در نهایت به آن خواهید رسید:
-
هر سرویس Docker که از اسرار استفاده می کند (که احتمالاً اکثر آنها، اگر نه همه آنها) استفاده می کند، اکنون به مرحله ساخت خود در Dockerfile و یک تصویر سفارشی ساخته شده از تصویر قبلی ما با افزودن باینری Dotenvx نیاز دارد. به عنوان مثال، نمیتوانید سرویس Postgres را مانند مثال اول بالا با استفاده از عبارت اعلام کنید
postgres:latest
تصویر اکنون باید باینری Dotenvx را دانلود و در آن فایل سیستم نصب کنیم. -
ما باید فایل env رمزگذاری شده خود را به mount متصل کنیم، یا آن را در تصویر کپی کنیم تا در زمان اجرا برای رمزگشایی در دسترس باشد. ما نمی توانیم استفاده کنیم
env_file
دستور العمل زیرا سرویس در حال اجرا (در مثال ما Postgres) فقط مقادیر رمزگذاری شده را می بیند. اگر بخواهیم نسخه کانتینر ما از Dotenvx محیط ما را رمزگشایی کند، باید بتواند به یک فایل فیزیکی اشاره کند. از آنجا که ما نمی خواهیم هر بار که تغییری در متغیرهای محیطی خود ایجاد می کنیم مجبور به بازسازی تصاویر خود باشیم، و چون دو منبع حقیقت را نمی خواهیم (فایل env در میزبان، در مقابل فایل env در کانتینر)، ما این گزینه را انتخاب می کنیم. الزام آور. -
ما باید به نحوی کلید خصوصی خود را به Docker Compose منتقل کنیم تا بتوان آن را به زمان اجرای کانتینر ما تزریق کرد، در حالت ایدهآل بدون نشت آن در گزارشهای تاریخچه پوسته یا نیاز به وارد کردن دستی آن در خط فرمان.
در اینجا به نظر می رسد:
# Dockerfile
FROM postgres:16.2-bookworm AS postgres
RUN apt-get update
&& apt-get install -y curl
&& curl -fsS https://dotenvx.sh/ | sh
RUN apt-get remove curl && apt-get autoremove
USER postgres
# compose.yml
services:
postgres:
build:
target: postgres
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
ports:
- ${POSTGRES_PORT}:${POSTGRES_PORT}
volumes:
- .env.dev:/app/.env.dev:ro
- dev_postgres:/var/lib/postgresql/data
command: ["env", "DOTENV_PRIVATE_KEY=$KEY", "dotenvx", "run", "-f", "/app/.env.dev", "--", "/usr/local/bin/docker-entrypoint.sh", "postgres"]
$: KEY=your_private_key dotenvx run -o -f .env.dev -- docker compose up postgres
شاید بپرسید چرا نقطه ورود عجیب است؟ زیرا، دلایل: https://github.com/dotenvx/dotenvx/issues/142
Redis همچنین هنگام دستکاری تصویر خود مشکلاتی برای هک کردن داشت، داستانی برای یک روز دیگر.
پس همین جا هستیم، کارمان تمام شد… اما من نمی توانم از آن متنفر نباشم.
دستورالعملهای فرمان زشت و متورم هستند، Dockerfile زشت و متورم است، من باید در مورد مسائل عجیب و غریب کار کنم که باینریها را در تصاویری که برای سفارشیسازی مجهز نیستند، حل کنم، من یک mount lame bind به فایل env خود دارم و دارم دو باینری از یک برنامه به طور همزمان در حال اجرا هستند که به عنوان دروازهبان فرآیندهای Docker Compose و container من عمل میکنند.
مطمئنا، ممکن است به نظر نرسد -که- اگر فقط به نمونهای از سرویس ONE نگاه میکنید، بد است، اما این را به پنج یا ده سرویس تبدیل کنید، و تمام ویژگیهای عجیب و غریب را که باعث میشود نقاط ورودی به خوبی بازی کنند، بیابید، و همه چیز خیلی سریع از کنترل خارج میشود.
هدف واقعی در اینجا چیست؟
من به سادگی میخواهم از مزایای ذخیره فایلهای env خود در کنترل منبع، بدون خطرات امنیتی مرتبط، با استفاده از جریان کاری که امکان افشای اسرار را برای توسعهدهندگان بعید میسازد.
ذخیره فایلهای env در کنترل منبع دارای مزایای زیر است:
- این به ویژه زمانی مفید است که چندین محیط برای پیکربندی وجود داشته باشد که هر کدام تنظیمات منحصر به فرد خود را دارند.
- اگر توسعه دهندگان از پیکربندی های یکسانی استفاده کنند، تکرارپذیری بسیار بالاتر است و به اشتراک گذاری تنظیمات، محیط های تکرارپذیر را ارتقا می دهد.
- نگه داشتن تاریخچه تغییرات پیکربندی، استدلال آنها و اینکه چه کسی مسئول آنها بوده است، بسیار خوب است.
- این اسناد را به نظرات درون خطی که به راحتی قابل دسترسی هستند که مستقیماً به متغیرها ارجاع می دهند متمرکز می کند و درک تفاوت های پیکربندی در بین محیط ها را آسان تر می کند.
- فرآیندهای ساخت را ساده می کند – نیازی به ساخت پویا فایل های env در CI یا تولید اسرار از یک فروشگاه مخفی مانند Github Actions Secrets یا AWS Secrets Manager نیست.
- اگر بعد از یک سال بدون کپی از فایلهای env قدیمی خود به پروژه بازگردید، بسیار خوشحال خواهید شد.
اجماع عمومی مدیریت فایل env این است که هرگز نباید آنها را در کنترل منبع ذخیره کنید. این فلسفه نتیجه این است که افراد اسرار خود را در متن ساده ذخیره می کنند و سپس به صورت هدفمند یا تصادفی آنها را مرتکب می شوند. این احساس دلایل خوبی دارد، اما این یک فلسفه بود که قبل از ایجاد این تکنیک شکل گرفته بود. من هیچ مشکلی با ذخیره فایلها ندارم تا زمانی که اسرار درون آنها رمزگذاری شده است، و گردش کار شامل این نمیشود که افراد دائماً کلیدهای خصوصی را دستی لمس کنند، که میتواند منجر به نشت تصادفی شود. رمزگذاری شده و دستی راهی برای رفتن است.
TL;DR: با کار در محدوده مجموعه ویژگی های Dotenvx و مجموعه ویژگی های Docker، هدف این است که یک جریان کاری رمزگذاری شده و دستی داشته باشیم که توانایی حفظ فایل های env را در کنترل منبع حفظ کند و در عین حال وابستگی های اضافه شده و نفخ کد را که ما ایجاد کرده ایم حذف کند.
چگونه می توانیم این را حل کنیم
در تولید، چرا استفاده کنید dotenvx run
اصلا؟ سرورهای تولید و ظروف آنها از قبل سخت شده اند و مجوزهای مدیر Secrets را دارند. ذخیره اسرار در محیط میزبان تولید، یا روی دیسک آنجا، یک نگرانی امنیتی نیست زیرا ما از طریق کانتینرهای Docker خود ایزوله شده ایم. به هر حال سرور برای عملکرد به این مقادیر رمزگشایی شده نیاز دارد. بنابراین تنها کاری که در اینجا باید انجام دهیم این است که به سادگی کلید خصوصی را روی سرور ذخیره کنیم (ترجیحاً در مدیر مخفی) و یک اسکریپت bash را کپی و رمزگشایی کنیم. .env.production
فایل به عنوان بخشی از جریان استقرار مداوم. سپس می توانیم از env_file
دستورالعملی برای بارگذاری فایل env رمزگشایی شده در ظروف تولیدی ما، بدون نیاز به فراخوانی باینری Dotenvx در هر نقطه.
اما چگونه می توانیم این کار را برای توسعه انجام دهیم، جایی که همه چیز کمی متفاوت است؟ در توسعه، ما نمی خواهیم توسعه دهندگان مجبور به رمزگشایی دستی باشند .env
فایل ها را بر روی دیسک بگذارید. آنها ممکن است به طور تصادفی آنها را در مخزن رها کنند و آنها را متعهد کنند، یا ممکن است آنها را در جایی بگذارند که شخص دیگری به آنها دسترسی داشته باشد. نه تنها این یک مشکل امنیتی است، بلکه آنها باید هر بار که فایلهای env خود را تغییر میدهند، فایلها را به صورت دستی رمزگشایی کنند و چندین فایل محیطی را مدیریت کنند (رمزگشایی شده در مقابل غیر).
راه حل بهتر
در اینجا راه حل بهتر من (به طور حکایتی) آمده است: یک ناظر فایل env که به صورت پویا فایل env شما را در هنگام تغییر فایل با استفاده از رمزگشایی می کند inotifywatch
و آن را در یک فایل سیستم امن، زودگذر و درون حافظه به نام ramfs کپی می کند تا بتوانیم داکر خود را نشان دهیم. env_file
دستورالعمل به این فایل رمزگشایی شده، و به روز رسانی فایل env در زمان واقعی از طریق آن منتشر شود. تمام کادرها را چک می کند.
#!/usr/bin/env bash
#
# ./watch.sh [env-files...]
#
function show_help() {
cat << EOF
Usage: $0 [options] [file...]
Options:
-h, --help Show this help message and exit.
Description:
This script monitors one or more environment files for changes.
When a change is detected, it will decrypt the monitored file using dotenvx,
and store the decrypted file in a ramfs memory-based filesystem mount.
If no file is specified, it defaults to watching '.env.dev'.
Examples:
1. Watch the default environment file:
$0
2. Watch a specific environment file:
$0 .env.dev
3. Watch multiple environment files:
$0 ~/.env.dev /path/to/.env.prod
This command allows you to specify custom environment files to monitor. If no arguments are provided,
it assumes the file '.env.dev'. Multiple files can be watched by providing each as an argument separated
by spaces.
EOF
}
_mount_ramfs() {
local mount_point=$1
# Create a 20mb ramfs mount if we don't already have one to use
if ! mountpoint -q "$mount_point"; then
sudo mkdir -p "$mount_point"
sudo mount -t ramfs -o size=20M,mode=1777 ramfs "$mount_point"
fi
}
_watcher_cleanup() {
local mount_point=$1
# Cleanup background inotifywatcher jobs
while read p; do
kill $p 2>/dev/null || true
done <"/tmp/env_watch_pids.txt"
rm "/tmp/env_watch_pids.txt" 2>/dev/null || true
echo ""
echo "Deleting decrypted env files from memory: $mount_point"
rm -rf "$mount_point" >/dev/null || true
rm /tmp/env_watch.lock >/dev/null || true
}
_setup_watcher() {
local file=$1
local mount_point=$2
# Initial run to get the decrypted file into the ramfs mount
_decrypt_and_save "$file" "$mount_point" true
# Setup watcher to decrypt on modification to env file
inotifywait -q -m -e close_write -e delete_self -e move_self "$file" > >(while read path action; do
if [[ "$action" == "DELETE_SELF" || "$action" == "MOVE_SELF" ]]; then
echo "Env file deleted or moved. Terminating watcher for $file..."
break
fi
_decrypt_and_save "$file" "$mount_point"
done) &
decrypted_file="${mount_point}/$(basename "${file}").decrypted"
echo "Env file watcher started: $file -> $decrypted_file"
echo $! >>/tmp/env_watch_pids.txt
}
_decrypt_and_save() {
local encrypted_file=$1
local mount_point=$2
local decrypted_file="${mount_point}/$(basename "${encrypted_file}").decrypted"
# Decrypt and convert JSON to .env format
dotenvx get -f "$encrypted_file" | jq -r 'to_entries | .[] | "\(.key)=\(.value)"' >"$decrypted_file"
if [ $? -eq 0 ]; then
if [ $# -eq 2 ]; then
echo "Detected modification in $encrypted_file, decrypting and updating $decrypted_file ..."
fi
else
echo "Failed to decrypt $encrypted_file"
return 1
fi
}
function env_watch {
set +e # disable exit on error to ensure cleanup doesn't get skipped
local sub_dir="${RAMFS_SUBDIR:-dotenvx}"
local mount_point="/mnt/ramfs/${sub_dir}"
# Check for another instance running
if [ -f /tmp/env_watch.lock ]; then
echo "Another instance of env:watch is already running. If this is not the case, please check running processes and remove lockfile: /tmp/env_watch.lock"
exit 1
fi
# Check if inotifywait and jq are installed
if ! command -v inotifywait >/dev/null || ! command -v jq >/dev/null || ! command -v dotenvx >/dev/null; then
echo "This script requires inotify-tools, jq, and dotenvx. Please install them first."
exit 1
fi
# Error if no args supplied to ./run env:watch
if [ $# -lt 1 ]; then
echo "Warning: No env file supplied. Defaulting to .env.dev, see --help for more info."
set -- ".env.dev"
fi
for env_file in $@; do
if [[ "$env_file" == *".env.prod"* || "$env_file" == *".env.production"* ]]; then
echo "Running on production env files is insecure. The env:watcher should only be used on dev."
exit 1
fi
if [ ! -f "$env_file" ]; then
echo "Error: '$env_file' does not exist."
exit 1
fi
done
touch /tmp/env_watch.lock
# Make sure we don't have a stale pids file
rm "/tmp/env_watch_pids.txt" 2>/dev/null || true
# Set up ramfs mount and exit cleanup
_mount_ramfs "/mnt/ramfs"
trap "_watcher_cleanup '/mnt/ramfs/$sub_dir'" EXIT
# Ensure subdirectory exists
mkdir -p "$mount_point"
# Main loop to setup watchers for each file
pids=()
for env_file in $@; do
_setup_watcher "$env_file" "$mount_point"
pids+=($!)
done
echo "Env file watchers running and waiting for file changes. Ctrl+C to quit..."
# If all watchers terminate, exit app
wait ${pids[@]}
}
# Main script logic
case "$1" in
-h|--help)
show_help
;;
*)
env_watch "$@"
;;
esac
و اکنون با اضافه شدن این به این موضوع بازگشته ایم env_file
بخشنامه:
# compose.yml
services:
postgres:
image: postgres:latest
env_file:
- /mnt/ramfs/dotenvx/.env.dev.decrypted
environment:
# $POSTGRES_PASSWORD is decrypted inside .env.dev.decrypted
POSTGRES_PASSWORD: $POSTGRES_PASSWORD
$: ./watch.sh .env.dev
Env file watcher started: .env.dev -> /mnt/ramfs/dotenvx/.env.dev.decrypted
Env file watchers running and waiting for file changes. Ctrl+C to quit...
Detected modification in .env.dev, decrypting and updating /mnt/ramfs/dotenvx/.env.dev.decrypted ...
^C
Deleting decrypted env files from memory: /mnt/ramfs/dotenvx
به هیچ وجه نیازی به درگیری با Dockerfile نیست. میتوانید جدیدترین نسخه اسکریپت را در مخزن Github من پیدا کنید: https://github.com/nullbio/dotenvx-watcher.