Kubernetes 部署 Laravel 應用的最佳實踐

jxlwqq發表於2020-03-13

實驗前提

  • 需要你有 macOS 開發環境,本文以此為例,其他型別的開發環境請自行搭建。
  • 需要你對 YAML 這一專門用來寫配置檔案的語言有所瞭解。
  • 需要你對 Docker 有一些基本的瞭解。
  • 需要你對 Kubernetes 中的 Node、Pod、ReplicaSet、Deployment、Service、Ingress、ConfigMap 等一些核心基礎概念有一定的瞭解。
  • 本文實驗以學習為主,僅適合本地開發環境,請勿使用在生產環境中。

YAML 配置檔案下載地址:

git clone https://github.com/jxlwqq/kubernetes-examples.git
cd deploying-laravel-7-with-mysql-and-redis

安裝 Docker for Mac

下載地址:https://hub.docker.com/editions/community/...

啟動並開啟 Kubernetes 功能,功能開啟過程中,Docker 將會自動拉取 Kubernetes 相關映象,所以全程需要科學上網。

為啥不使用 minikube?minikube + virtualbox + kubectl 安裝起來太繁瑣了,而且即使科學上網了你也不一定能搞定。當然阿里雲提供了一篇安裝教程可以參考。

本地埠準備

請確保本地 localhost 的 80 埠沒有被佔用,已在使用的請在實驗期間暫時關閉佔用 80 埠的服務。

切換叢集

如果你本地有多個 Kubernetes 的叢集配置,請先切換至名為 docker-desktop 的叢集:

kubectl config use-context docker-desktop

建立 MySQL 服務

為了提高 Pod 的啟動速度,我們首先準備好 MySQL 的映象:

docker pull mysql:5.7

部署 MySQL 服務:

kubectl apply -f mysql-deployment-and-service.yaml

注意:deployment 在生產場景中對 MySQL 這種有狀態的服務並不適合。

yaml 檔案解讀:

kind: Deployment # 物件型別
apiVersion: apps/v1 # api 版本
metadata: # 物件後設資料
  name: mysql-deployment # 物件名稱,加字尾 -deployment 主要是為了新手看的明白,也可以直接改為 name: mysql
  labels: # 物件標籤
    app: mysql # 標籤
spec: # 物件規約,注意這裡的物件指的是 Deployment 物件
  selector: # 選擇器,不太恰當的類比就相當於 CSS 選擇器
    matchLabels: # 匹配標籤
      app: mysql # Pod 標籤
  strategy: # 部署策略
    type: Recreate # Recreate 重新建立  RollingUpdate 滾動升級
  template: # 模版
    metadata: # 後設資料
      labels: # 標籤
        app: mysql # Pod 的標籤,這裡的值與上面的 selector matchLabels 的標籤對應
    spec: # 物件規約,注意這裡的物件指的是 Pod 物件
      containers: # 容器
        - name: mysql # 容器名稱
          image: mysql:5.7 # 容器映象
          env: # 環境變數
            - name: MYSQL_ALLOW_EMPTY_PASSWORD
              value: 'true'
          ports: # 埠
            - containerPort: 3306 # 容器埠
          volumeMounts: 
            - mountPath: /var/lib/mysql
              name: mysql-storage
      volumes:
        - name: mysql-storage
          persistentVolumeClaim: # PersistentVolume(PV)是一塊叢集裡由管理員手動提供,或 kubernetes 透過 StorageClass 動態建立的儲存。 PersistentVolumeClaim(PVC)是一個滿足對 PV 儲存需要的請求。
            claimName: mysql-persistentvolumeclaim
---
kind: PersistentVolumeClaim # 物件型別
apiVersion: v1 # api 版本
metadata: # 後設資料
  name: mysql-persistentvolumeclaim # 物件名稱
  labels: # 標籤
    app: mysql
spec: # 物件規約
  accessModes: # 訪問方式
    - ReadWriteOnce
  resources: # 資源
    requests: # 請求配置
      storage: 1Gi # 大小,這裡主要做了實驗,我們就設定小點的
---
kind: Service # 物件型別
apiVersion: v1 # api 版本
metadata: # 後設資料
  name: mysql-service # 物件名稱
  labels: # 標籤
    app: mysql
spec: # 物件規約
  selector: # 選擇器
    app: mysql # Pod 的標籤
  ports: # 埠
    - port: 3306 # 埠號
      targetPort: 3306 # 與 Pod  containerPort 埠號一致

進入 Pod:

kubectl get pods # 獲取 pods 列表
kubectl exec -it mysql-deployment-79cdbc594-rmhjk mysql # pod 名稱改成你自己

建立一個名為 laravel 的資料庫:

create database laravel;

資料庫 laravel 建立完成後,即可 exit。

建立 Redis 服務

為了提高 Pod 的啟動速度,我們首先準備好 Redis 的映象:

docker pull redis

部署 Redis 服務:

kubectl apply -f redis-deployment-and-service.yaml

yaml 檔案解讀:

kind: Deployment # 物件型別
apiVersion: apps/v1 # api 版本
metadata: # 後設資料
  name: redis-deployment # 物件名稱
  labels: # 物件標籤
    app: redis
spec: # 物件規約,這裡的物件指的是 Deployment 物件
  selector: #  選擇器
    matchLabels: # 匹配標籤
      app: redis 
  template: # 模版
    metadata: # 後設資料
      labels: # 標籤
        app: redis
    spec: # 物件規約,這裡的物件指的是 Pod 物件
      containers: # 容器 
        - name: redis # 容器名稱
          image: redis # 容器映象
          ports: # 埠
            - containerPort: 6379 # Redis 的服務埠
---
kind: Service # 物件型別
apiVersion: v1 # api 版本
metadata: # 後設資料
  name: redis-service # 物件名稱
spec: # 物件規約
  selector: # 選擇器
    app: redis # 標籤,選擇 標籤包含 app: redis 的 一組 Pod
  ports: # 埠
    - port: 6379 # 暴露的埠,如果 port 和 targetPort 一致,targetPort 可以不寫

建立 Laravel 服務

為了提高 Pod 的啟動速度,我們首先準備好 Laravel-demo 的映象:

docker pull jxlwqq/laravel-7-kubernetes-demo

映象地址:https://hub.docker.com/repository/docker/j...

原始碼地址:https://github.com/jxlwqq/laravel-7-kubern...

原始碼基於官方 Laravel v7 版本,做了以下修改:點選檢視compare

  • 增加了 Docker 映象構建相關的檔案:Dockerfile 和 .dockerignore
  • 增加了一個 config/apache2/sites-available/laravel.conf
  • 增加了一個 crontab 檔案,執行 php artisan schedule:run
  • 增加了一個 docker/entrypoint.sh 檔案:Docker 容器啟動時執行的命令

我這裡把核心的 Dockerfile 和 entrypoint.sh 的程式碼貼過來:

Dockerfile:

# First Stage
FROM node:alpine as frontend
COPY package.json package-lock.json /app/
RUN cd /app \
    && npm install
COPY webpack.mix.js /app/
COPY resources/js/ /app/resources/js/
COPY resources/sass/ /app/resources/sass/
RUN cd /app \
      && npm run production

# Second Stage
FROM composer as composer
COPY database/ /app/database/
COPY composer.json composer.lock /app/
RUN cd /app \
      && composer install \
           --optimize-autoloader \
           --ignore-platform-reqs \
           --prefer-dist \
           --no-interaction \
           --no-plugins \
           --no-scripts \
           --no-dev

# Third Stage
FROM php:7.4-apache-buster
RUN apt-get update \
    && apt-get install -y vim cron libmagickwand-dev imagemagick
RUN docker-php-ext-install intl pdo_mysql bcmath \
    && pecl install redis imagick \
    && docker-php-ext-enable opcache redis imagick \
    && cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
RUN apt-get clean \
    && apt-get autoclean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

ARG LARAVEL_PATH=/var/www/laravel
WORKDIR ${LARAVEL_PATH}

COPY . ${LARAVEL_PATH}
COPY --from=composer /app/vendor/ ${LARAVEL_PATH}/vendor/
COPY --from=frontend /app/public/js/ ${LARAVEL_PATH}/public/js/
COPY --from=frontend /app/public/css/ ${LARAVEL_PATH}/public/css/
COPY --from=frontend /app/mix-manifest.json ${LARAVEL_PATH}/mix-manifest.json

RUN cd ${LARAVEL_PATH} \
      && php artisan package:discover \
      && chown www-data:www-data bootstrap/cache \
      && chown -R www-data:www-data storage/

RUN rm /etc/apache2/sites-enabled/*
COPY config/apache2 /etc/apache2/
RUN a2enmod rewrite headers \
    && a2ensite laravel

COPY docker/entrypoint.sh /usr/local/bin/entrypoint
RUN chmod +x /usr/local/bin/entrypoint
ENTRYPOINT ["/usr/local/bin/entrypoint"]

entrypoint.sh:

#!/usr/bin/env bash

set -e

cd /var/www/laravel
rm -f public/storage

echo 'migrate'
php artisan migrate --force

echo 'publish'
# php artisan vendor:publish --tag=laravel-pagination

echo 'cache'
php artisan config:cache
php artisan view:cache
php artisan route:cache
php artisan event:cache

echo "cron"
mkdir -p /var/spool/cron/crontabs/
cp crontab /var/spool/cron/crontabs/root
chmod 0644 /var/spool/cron/crontabs/root
crontab /var/spool/cron/crontabs/root
cron -f &

echo "queue"
php artisan queue:work --queue={default} --verbose --tries=3 --timeout=90 &

echo 'http'
exec apache2-foreground

部署 Laravel 服務:

kubectl apply -f configmap.yaml # 將 env 環境變數配置在了 ConfigMap 物件裡
kubectl apply -f laravel-deployment-and-service.yaml # 部署 Deployment 和 Service
kubectl apply -f ingress.yaml # 部署 ingress 路由規則

configmap.yaml 檔案解讀:

kind: ConfigMap # 物件型別:配置
apiVersion: v1 # api 版本
metadata: # 原資料
  name: laravel-env # 物件名稱
data: # 變數資料,跟 laravel env 檔案類似(不是太恰當的比方)
  APP_KEY: base64:zC8wVldUZfZJaGaZ7+CPh+5FzaXYmShm7G/Qh6GdRl8=
  APP_ENV: production
  DB_DATABASE: laravel
  DB_USERNAME: root

laravel-deployment-and-service.yaml 檔案解讀:

kind: Deployment # 物件型別
apiVersion: apps/v1 # api 版本
metadata: # 後設資料
  name: laravel-deployment # 物件名稱
  labels: # 物件標籤
    app: laravel
spec: # 物件規約
  selector: # 選擇器
    matchLabels: # 標籤匹配
      app: laravel
  replicas: 1 # 副本數量
  strategy: # 部署策略
    type: RollingUpdate # 滾動升級
    rollingUpdate: # 滾動升級的引數
      maxSurge: 1 # 在更新過程中最大同時建立 Pods 的數量
      maxUnavailable: 0 # 在更新過程中最大不可用的 Pods 的數量
  template: # 模版
    metadata: # 後設資料
      labels: # 標籤
        app: laravel 
    spec: # 物件規約
      containers: # 容器
        - name: laravel # # 容器名稱
          image: jxlwqq/laravel-7-kubernetes-demo # 映象
          ports: # 埠
            - containerPort: 80 # http web 服務預設埠 80
          env: # 環境變數,直接賦值
            - name: DB_HOST
              value : mysql-service # !注意!這裡的值,跟 MySQl Service 物件的 metadata name 一致
            - name: REDIS_HOST
              value: redis-service # !注意!這裡的值,跟 Redis Service 物件的 metadata name 一致
          envFrom: # 環境變數從哪裡獲得
            - configMapRef:
                name: laravel-env # 從名稱為 laravel-env 的 configMap 物件中獲得
          readinessProbe: # 就緒探針,或者叫做就緒探測器,叢集透過它可以知道容器什麼時候準備好了並可以開始接受請求流量
            httpGet: # 判斷專案首頁是不是返回 20X 狀態
              port: 80
              path: /
              scheme: HTTP
          livenessProbe: # 存活探針,或者叫做存活探測器,叢集透過它來知道什麼時候需要重啟容器
            httpGet: # 判斷專案首頁是不是返回 20X 狀態
              port: 80
              path: /
              scheme: HTTP
---
kind: Service # 物件型別
apiVersion: v1 # api 版本
metadata: # 物件後設資料
  name: laravel-service # 物件名稱
  labels: # 標籤
    app: laravel
spec: # 物件規約
  selector: # 選擇器
    app: laravel # 選擇那些包含 app: laravel 標籤的一組 Pods
  ports: # 埠
    - port: 80 # 暴露的埠
      targetPort: 80 # 與 上面的 Pod containerPort 一致

ingress.yaml 檔案解讀:

kind: Ingress # 物件型別
apiVersion: networking.k8s.io/v1beta1 # api 版本,這個資源物件還處於 beta 版本,但是已經很穩定了,k8s 社群還是比較謹慎的
metadata: # 後設資料
  name: laravel-ingress # 物件名稱
  labels: # 標籤
    app: laravel
spec: # 物件規約
  rules:
    - http:
        paths:
          - backend:
              serviceName: laravel-service # 與 Laravel Service 物件的 metadata name 一致
              servicePort: 80 # Service 埠,與 Laravel Service 物件的 port 一致

建立 Ingress-nginx 控制器

有了 Ingress 物件還不夠,還需要 Ingress-nginx 控制器。這裡又有一個不太好的比方了,Ingress 物件類似 Nginx 的 nginx.conf 檔案,單單有配置檔案是萬萬不行的,我們需要 Nginx 服務(軟體)本身。

為了讓 Ingress 資源工作,叢集必須有一個正在執行的 Ingress 控制器。 Kubernetes 官方目前支援和維護 GCE 和 nginx 控制器。

這裡我們選擇 Ingress-nginx 控制器:

cd ../ingress-nginx # 切換到 ingress-nginx 目錄
kubectl apply -f ingress-nginx-deployment-and-other-resources-mandatory.yaml
kubectl apply -f ingress-nginx-service.yaml

注:

詳細操作說明見:https://github.com/kubernetes/ingress-ngin...

訪問

curl http://localhost

撒花,結束。

最後的話

現在的 Demo 將 web 服務、定時任務還有佇列監聽都放在了一個 Pod 中,無法對其進行擴容(因為定時任務和佇列監聽會重複)。如果需要對 Laravel 應用進行 HPA 擴容的話,還需要對 Laravel 專案的 docker/entrypint.sh 進行一些改造。將上述的 laravel-deploment 拆分成 3 個 Deploment,將容器分為三個角色,分為是 web、cron、queue。分別提供 web 服務、定時任務以及佇列監聽。最後對提供 web 服務的 Deploment 設定 HPA,根據 cpu 或者 記憶體佔用率進行自動擴容。

另外,定時任務也可以使用 Kubernetes 的 CronJob 物件來實現。

這是後話,下期再細談。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章