อยากจัดการ web apps ใน development และ production ได้อย่างมีประสิทธิภาพ ต้องเข้าใจ environment variables
ไม่ได้เป็นแบบนี้ตลอดมา แค่ไม่กี่ปีก่อน ไม่ค่อยมีใคร config Rails apps ด้วย environment variables แล้ว Heroku เกิดขึ้น
Heroku introduce developers ไปสู่ 12-factor app approach ใน manifesto วาง best practices สร้าง apps ที่ deploy ง่าย ส่วน environment variables 有影响力มาก
The twelve-factor app stores config in environment variables (often shortened to env vars or env). Env vars are easy to change between deploys without changing any code; unlike config files, there is little chance of them being checked into the code repo accidentally; and unlike custom config files, or other config mechanisms such as Java System Properties, they are a language- and OS-agnostic standard.
Rubyists ใช้ environment variables เยอะกว่าเดิม แต่มัก cargo-culty ใช้โดยไม่เข้าใจจริงๆ ว่าทำงานยังไง
โพสต์นี้แสดงว่า environment variables ทำงานจริงยังไง - และสำคัญกว่า คือทำงานยังไงที่ ไม่ได้ เราจะสำรวจ common ways จัดการ env variables ใน Rails apps เริ่มกันเลย!
NOTE: อ่านเรื่อง securing environment variables ได้ที่นี่
ทุก Process มี Environment Variables ชุดของตัวเอง
ทุกโปรแกรมที่รันบน server มีอย่างน้อย 1 process process นั้นได้ env variables ชุดของตัวเอง ได้แล้ว ไม่มีอะไรนอก process เปลี่ยนได้
Beginner เข้าใจผิดง่ายคิดว่า env variables เป็น server-wide Services เช่น Heroku ทำให้ดูเหมือน set env variables = edit config file บน disk แต่มันไม่เหมือน config files เลย
ทุกโปรแกรมบน server ได้ env variables ชุดของตัวเอง ตอน launch
ทุก process มี environment ของตัวเอง
Environment Variables ตายพร้อม Process
เคย set env variable reboot แล้วหายไหม? เพราะ env variables 属于 processes process จบ env variable หาย
ลอง set env variable ใน IRB session หนึ่ง ปิด แล้วพยายาม access ใน IRB session ที่ 2
Process shutdown env variables หาย
หลักการเดียวกันทำให้หายตอน server reboot หรือ exit shell อยาก persist across sessions ต้องเก็บใน config file เช่น .bashrc
Process ได้ Environment Variables จาก Parent
ทุก process มี parent เพราะทุกโปรแกรมต้องถูกเริ่มโดยโปรแกรมอื่น
bash shell launch vim = vim parent = shell Rails app ใช้ imagemagick identify image = parent ของ identify = Rails app
Child processes inherit env vars จาก parent
ตัวอย่างตั้ง $MARCO ใน IRB process แล้ว back-ticks shell out echo ค่านั้น
IRB เป็น parent ของ shell ที่สร้าง ได้ copy ของ $MARCO
Environment variables ตั้งใน Ruby inherit ไป child processes
Parents Customize Environment Variables ส่งให้ Children ได้
Default child ได้ copy ทุก env variable ของ parent แต่ parent ควบคุมได้
Command line ใช้ env program Bash มี syntax พิเศษ set env vars ให้ child โดยไม่ set บน parent
ใช้ env command ตั้ง env variables ให้ child โดยไม่ตั้งบน parent
Shell out จาก Ruby ให้ custom env variables ให้ child โดยไม่มลพิษ ENV hash ใช้ syntax นี้กับ system method:
วิธีส่ง custom environment variables เข้า Ruby's system method
Children ตั้ง Parents' Environment Variables ไม่ได้
Children ได้แค่ copies ของ parents' env variables การเปลี่ยนแปลงของ child ไม่มีผลกับ parent
Environment variables "passed by value" ไม่ใช่ "by reference"
ใช้ back-tick syntax shell out พยายาม set env variable variable จะ set ให้ child แต่ค่าใหม่ไม่ bubble ขึ้น parent
Child processes เปลี่ยน parents env vars ไม่ได้
Changes to the Environment ไม่ Sync ระหว่าง Running Processes
ตัวอย่างรัน 2 IRB copies side by side เพิ่ม variable ใน environment ของ IRB หนึ่ง ไม่มีผลกับอีกอัน
เพิ่ม env variable ให้ process หนึ่ง ไม่เปลี่ยน process อื่น
Shell แค่ UI สำหรับ Environment Variable System
System เป็นส่วนของ OS kernel Shell ไม่มี magical power over env variables ต้องปฏิบัติกฎเหมือนทุกโปรแกรม
Environment Variables ไม่เท่ากับ Shell Variables
Misunderstanding ใหญ่เพราะ shells มี "local" shell variable systems เอง Syntax ใช้ local variables บ่อยเหมือน env variables Beginner สับสนสองอย่าง
แต่ local variables ไม่ถูก copy ไป children
Environment variables ไม่เท่ากับ shell variables
ตัวอย่าง ตั้ง local shell variable MARCO ไม่ถูก copy ไป child processes พยายาม print ผ่าน Ruby ไม่ได้
ต่อมา export command แปลง local variable เป็น environment variable ตอนนี้ copy ไปทุก process ใหม่ shell สร้าง ตอนนี้ env variable available ให้ Ruby
Local variables ไม่ available ให้ child processes Export แปลง local variable เป็น environment variable
Managing Environment Variables in Practice
จริงๆ ทำงานยังไง? ทำตัวอย่าง:
สมมติ 2 Rails apps บนเครื่องเดียว ใช้ Honeybadger monitor exceptions แต่เจอปัญหา
อยากเก็บ Honeybadger API key ใน $HONEYBADGER_API_KEY แต่ 2 apps มี 2 API keys แยกกัน
Env variable เดียวมี 2 ค่าได้ยังไง?
ตอนนี้ควรรู้คำตอบแล้ว เพราะ env vars per-process 2 rails apps รันใน processes ต่างกัน ไม่มีเหตุผลไม่ให้แต่ละอันมี $HONEYBADGER_API_KEY ตัวเอง
คำถามคือ setup ยังไง โชคดีมี gems ทำให้ง่าย
Figaro
ติดตั้ง Figaro gem ใน Rails app ค่าใน config/application.yml จะ load เข้า ruby ENV hash ตอน startup
ติดตั้ง gem:
# Gemfile gem "figaro"
เพิ่ม items ใน application.yml สำคัญมาก add file นี้ใน .gitignore กัน commit secrets ผิดพลาด
# config/application.yml HONEYBADGER_API_KEY: 12345
Dotenv
Dotenv gem คล้าย Figaro แต่ load env variables จาก .env และไม่ใช้ YAML
ติดตั้ง gem:
# Gemfile gem 'dotenv-rails'
เพิ่ม config values ใน .env - git ignore file กัน publish ผิดพลาดไป github
HONEYBADGER_API_KEY=12345
Access values ใน Ruby ENV hash
ENV["HONEYBADGER_API_KEY"]
รัน commands ใน shell ด้วย pre-defined env vars:
dotenv ./my_script.sh
Secrets.yml?
Secrets.yml แม้ cool แต่ไม่ set environment variables ไม่ใช่ replacement สำหรับ Figaro/dotenv
Plain old Linux
Maintain unique env variable sets per app ด้วย basic linux commands ได้ ทางหนึ่งแต่ละ app บน server owned by different user ใช้ user's .bashrc เก็บ application-specific values