開賽啦~開賽啦~這 30 天會帶大家從地面飛上雲端!呃不是,是從本機建立 Laravel 的 Docker image 開始,一步步透過 Gitlab Pipeline 建立將 image deploy 上 AWS 的 CD 流程,再利用 AWS 的 container 服務 ECS 來運行 Laravel 的 web application。
另外,作為工程師,懶是最大的美德,能電腦做的事情就不要自己做,能用程式跑的東西就不要自己手動按,所以我們也會使用 infrastructure as code 的工具 Terraform 來維護以及持續進化各項基礎建設。(重點是雲裡那麼多複雜的設定,沒有程式碼筆者根本記不起來)
這 30 天會使用 Linux 作為主要操作平台,各種指令與工具等等都以 Linux 為主。使用到的主要工具都是跨平台的,Windows 跟 MAC 原則上都能使用,只是文章會以 Linux 為主說明,用其他平台的話可能需要自己摸索工具如何安裝、設定檔在哪裡等等。
我們會從本機 build docker image、啟動 container 開始,一路設定 Gitlab、使用 AWS web console 以及 command line 工具建立各項基礎建設,再到使用 terraform 並進一步讓我們的雲端架構達到 high availability。因為起點是 Linux、Gitlab 跟 Docker,讀者需要對 Linux、Git 以及 Docker 有基本了解,像是 Docker 知道有 image、image 可以拿來跑 container 以及 Docker Hub 是什麼就夠了。
開始前的提醒:這 30 天內會使用 AWS 的服務,如果讀者沒有用過 AWS,可以申請帳號、利用一年的免費使用時間。如果讀者已經超過一年的免費使用期,請注意:照著本系列文章操作,是會有費用產生的,筆者在範例中盡可能用費用較低的資源,但是讀者須自行注意資源的使用情形與花費。(講到 AWS 會再提醒一次並說明如何使用 AWS Budgets 功能)
行前最後提醒:這個旅程……不會很順利,各位會看到 DevOps 充滿青春與汗水(??)的日常生活。
好!那就讓我們從在本機 build 出 Laravel 的 Docker image 開始吧~
我們這裡要使用的是 Ernest Chiang 的 nginx + php-fpm docker image 作為 base image(感謝大大!)這是一個把 nginx 跟 php-fpm 結合起來的 docker image,我們只要把 Laravel 的程式碼跟一些相關設定放進去,就是個可以運行的 image 了!
這個 image 是以 supervisor 來啟動及監控 nginx、php-fpm worker 等 process。supervisor 會維持設定的 process 數量,發現 process 被(各種原因)關掉的話會重新啟動它。這麼做才能確保我們的 container 一直有執行著需要的 process,不會因為 process 當掉之類的問題就整個無法正常提供服務。
有了 Web Server 跟 PHP 後,絕大多數情況都需要的還有 Database。為了讓 Laravel 可以順利執行,我們先隨便用 docker 準備個 MySQL database 給它連:
$ sudo docker run -d \
-e MYSQL_ALLOW_EMPTY_PASSWORD=true \
-e MYSQL_DATABASE=laravel \
--name dbserv \
mysql:8.0
這個指令翻成白話文:在背景啟動一個 MySQL 8.0 的 container 叫做 dbserv,並且以環境變數允許帳號的密碼是空的、預設 database 叫做 laravel。
允許空密碼、使用 default database 作為 laravel 的 database 只是為了實驗而簡化的作法,一般環境不建議這麼使用!(好孩子不要偷懶
啟動 container 後可以用 sudo
docker ps
看到執行中的 container 們:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f335009dde55 mysql:8.0 "docker-entrypoint.s…" 43 minutes ago Up 43 minutes 3306/tcp, 33060/tcp dbserv
為了設定 Laravel 的 database 連線資訊並且降低範例的麻煩複雜度,我們直接用 container 的 IP 來連線,所以用 sudo docker inspect dbserv
來查 dbserv 這個 container 的 IP address。
$ docker inspect dbserv
...
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "014262341ff329e4be2a7c0c87249540164cbb19b3c2a81770007cab657c261f",
"EndpointID": "a85ad67a24a8d24043ee1fb909139cf5faa406fe771fc213f31199915925faff",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.3",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:11:00:03",
"DriverOpts": null
}
...
在 Networks block 看到 IP address 是172.17.0.3
,所以我們在 .env
的 DATABASE 相關設定要設成:
DB_CONNECTION=mysql
DB_HOST=172.17.0.3
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
接下來看看 docker image 的靈魂:Dockerfile!每行指令的意思直接標注在 comment,完整程式碼在 Gitlab 。
# 使用 Ernest Chiang 的 nginx + php-fpm docker image 作為 base image
# 選擇 bullseye flavor 的 image 在使用上比較簡單,但產生出來的 image 可能比較大
FROM dwchiang/nginx-php-fpm:8.2.5-fpm-bullseye-nginx-1.24.0
# 工作目錄在 /var/www/html,也是 web server 預設的目錄
WORKDIR /var/www/html
# 告訴社會大眾(?)這個 image 會使用 port 80
EXPOSE 80
# 在 image 內以 apt 安裝各種需要的 package
RUN apt-get --allow-releaseinfo-change update && \
apt-get install --no-install-recommends --no-install-suggests -y \
cron \
ssh \
wget \
libzip-dev && \
docker-php-ext-install zip pdo_mysql && \
apt-get remove --purge --auto-remove -y && apt-get autoclean && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/partial/*
# 複製 nginx 設定
COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf
# 安裝 composer
RUN wget https://getcomposer.org/installer -O - -q | \
php -- --install-dir=/usr/local/bin --filename=composer --2 --quiet
# 設定 Laravel Schedule 需要的 cron job
RUN echo "* * * * * root /usr/local/bin/php /var/www/html/artisan schedule:run >> /var/log/cron.log 2>&1" >> /etc/crontab
# Laravel 相關
# 複製 host 上目前目錄所有檔案到 image 的 /var/www/html/ 底下
COPY ./ /var/www/html
# 我們要以 supervisor 執行 Laravel 的 worker,所以要複製 worker 的 supervisor 設定進 image
COPY docker/laravel-worker.conf /etc/supervisor/conf.d/laravel-worker.conf
# composer install 並且設定 Laravel 相關 directory 的權限
RUN composer install --no-dev && \
chown -R www-data:www-data storage/ && \
chown -R www-data:www-data bootstrap/cache
# container 跑起來時預設會執行的 script,把它複製進去並且加上執行權限
COPY docker/docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
# container 跑起來時預設執行的指令,會執行我們複製進去的 entry script
CMD /docker-entrypoint.sh
很多 docker image 後面會接像是 -buster
、-bullseye
、-slim
或 -alpine
的 suffix,這些 suffix 表示不同的 image flavor(口味?),也就是它們基於什麼 Linux distribution 而來或者有什麼特性。
常見的口味(?)有:
沒有 suffix 的 full official image:建立在最新的 stable Debian 上
buster/bullseye
等等 Debian codename:建立在某個版本的 Debian 上。
slim
:從 full image 瘦身而來的 image。
alpine
:以 Alpine Linux 為基礎的 image,優點是 image 小,但可能需要處理相容性問題。
docker/nginx/default.conf
是 nginx 的設定,使用 Laravel 官方設定 做兩處修改:
root /var/www/html/public;
改到 image 放 Laravel code 的路徑
fastcgi_pass 127.0.0.1:9000;
因為我們以 php-fpm 預設方式啟動,它會聽 localhost 的 port 9000 來收 request (ref),所以我們要在 nginx 把 request 傳過去。
server {
listen 80;
listen [::]:80;
server_name example.com;
root /var/www/html/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
前面說到 image 是用 supervisor 把各種 process 執行起來並且監控它們,所以我們也用 supervisor 把 Laravel worker 執行起來,以下是 worker 用的 supervisor 設定:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=8
redirect_stderr=true
stdout_logfile=/var/www/html/worker.log
stopwaitsecs=3600
大致來說是以 user www-data
來執行 8 個 process,會把 log 記錄在 /var/www/html/worker.log
,會自動重啟等等。
entry script 是從 source 複製來的,後面加入跟 Laravel 有關的啟動 crontab、執行 migration、做各種 cache 的指令。
#!/bin/sh
# vim:sw=4:ts=4:et
set -e
if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then
exec 3>&1
else
exec 3>/dev/null
fi
if [ "$1" = "nginx" -o "$1" = "nginx-debug" ]; then
if /usr/bin/find "/docker-entrypoint.d/" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then
echo >&3 "$0: /docker-entrypoint.d/ is not empty, will attempt to perform configuration"
echo >&3 "$0: Looking for shell scripts in /docker-entrypoint.d/"
find "/docker-entrypoint.d/" -follow -type f -print | sort -n | while read -r f; do
case "$f" in
*.sh)
if [ -x "$f" ]; then
echo >&3 "$0: Launching $f";
"$f"
else
# warn on shell scripts without exec bit
echo >&3 "$0: Ignoring $f, not executable";
fi
;;
*) echo >&3 "$0: Ignoring $f";;
esac
done
echo >&3 "$0: Configuration complete; ready for start up"
else
echo >&3 "$0: No files found in /docker-entrypoint.d/, skipping configuration"
fi
fi
# start cron
/etc/init.d/cron start
# app
php artisan migrate --force
php artisan cache:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
# exec "$@"
supervisord -n -c /etc/supervisord.conf
寫好 Dockerfile 就是要編 image 啦~我們以目前目錄作為 context 來 build image,context 可以想成建立 image 的環境,通常會是 Dockerfile 的所在地。用 -t
給 image 上 my-app
的 tag,之後可以用來在操作中指定 image (docker build 指令 ref):
$ docker build . -t my-app
build 好 image 後,終於要啟動 container 看看能不能成功執行 Laravel:
$ sudo docker run -it -p 8000:80 --name app my-app
這裡我們以 tag 為 my-app
的 docker image 建立並啟動 container,將 container 命名為 app
,並且讓我們可以透過 console 直接操作 container。另外,把本機的 port 8000 對應到 container 的 port 80,如此對應後存取本機 port 8000 等同存取 container 的 port 80,也就是一般 web server http 使用的 port。(docker run 指令 ref)
啟動 container 後會看到 entry script 執行的 output:
如果中間有任何錯誤也會顯示出來並且(大多數狀況下)container 會停止執行,最常遇到的是各種 Laravel 相關的錯誤,諸如連不上 database、沒有 app key 等等。我們的 container 看起來挺正常的,用瀏覽器連上 http://127.0.0.1:8000
看看吧!
是 Laravel 的歡迎畫面!成功啦!
執行完 container 要停止有幾個方法:
在上面 container 的 console 畫面直接 ctrl + c 結束它
使用指令 docker stop
app
這裡的 app
是 container 的名稱,也可以用 container id
已經關閉的 container 要再啟動執行可以用 docker start
[CONTAINER NAME or ID]
。如果想刪除一個 container,關閉(stop)它後用 docker rm
[CONTAINER NAME or ID]
。
要不要讓container來個美美的prompt呀XD
RUN echo 'export PS1="\[\e[0;33m\]\u@\h\[\e[m\]-\[\e[0;34m\]god-cjw\[\e[m\] \w\$ "' >> ~/.bashrc
不過我會需要是因為我切了好幾個container分別跑http server, php-fpm, php-worker
orz
開發用的 container 是蠻需要的XD
話說 nginx + php-fpm 很多都是切好幾個 container 來做的