Kubernetes 部署 Laravel 应用的最佳实践

实验前提

  • 需要你有 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 协议》,转载必须注明作者和本文链接
本帖由 Summer 于 4年前 加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 11

:+1:厉害,多多学习学习

4年前 评论
mouyong

在 windows 下,成功开启了 svc ,但是通过 netstat 却查不到端口的监听,也无法通过浏览器访问服务。谷歌后没找到原因,想问下博主知道问题吗?

4年前 评论
mouyong (作者) 4年前
jxlwqq (楼主) 4年前
jxlwqq (楼主) 4年前
jxlwqq (楼主) 4年前
mouyong (作者) 4年前
jxlwqq (楼主) 4年前
mouyong (作者) 4年前
jxlwqq (楼主) 4年前
mouyong (作者) 4年前
jxlwqq (楼主) 4年前
mouyong (作者) 4年前
jxlwqq (楼主) 4年前
jxlwqq (楼主) 4年前
mouyong (作者) 4年前
jxlwqq (楼主) 4年前
mouyong (作者) 4年前
mouyong

@jxlwqq 2 个?

file

file

4年前 评论
mouyong

helm.exe install --dry-run --debug --name-template test ./cblink-printer

install.go:149: [debug] Original chart version: ""
install.go:166: [debug] CHART PATH: Z:\code\php\cblink-printer-service\deploy\k8s\helm\cblink-printer

NAME: test
LAST DEPLOYED: Thu Mar 19 08:45:41 2020
NAMESPACE: default
STATUS: pending-install
REVISION: 1
TEST SUITE: None
USER-SUPPLIED VALUES:
{}

COMPUTED VALUES:
affinity: {}
fullnameOverride: cblink-printer
image:
  pullPolicy: IfNotPresent
  repository: cblink-printer
imagePullSecrets: []
ingress:
  annotations: {}
  enabled: true
  hosts:
  - host: printer.cblink.test
    paths:
    - /
  tls: []
nameOverride: ""
nodeSelector: {}
podSecurityContext: {}
replicaCount: 1
resources: {}
securityContext: {}
service:
  port: 80
  type: NodePort
serviceAccount:
  annotations: {}
  create: false
  name: null
tolerations: []

HOOKS:
MANIFEST:
---
# Source: cblink-printer/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: laravel-env
data:
  APP_ENV: production
  APP_KEY: base64:U0JPo/TH3QP/2STt/+WhzRza2DQDLoLJXMkny4e+jp0=
  APP_DEBUG: "true"
  DB_HOST: 10.0.75.1
  DB_USERNAME: root
  DB_PASSWORD: root
  BROADCAST_DRIVER: log

  REDIS_HOST: 10.0.75.1

  CACHE_DRIVER: redis
  QUEUE_CONNECTION: redis
  SESSION_DRIVER: redis
  SESSION_LIFETIME: "120"
  JWT_SECRET: pY83ysdXTQ0lbM0E6xKBa9uDjx5c7Arb4w8gp7kDRTNF0zz1tDyzxj3hfwyMcMEB
---
# Source: cblink-printer/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: cblink-printer
  labels:
    helm.sh/chart: cblink-printer-latest
    app.kubernetes.io/name: cblink-printer
    app.kubernetes.io/instance: test
    app.kubernetes.io/version: "latest"
    app.kubernetes.io/managed-by: Helm
spec:
  type: NodePort
  ports:
    - port: 80
      nodePort: 30000
      targetPort: http
      protocol: TCP
      name: http
  selector:
    app.kubernetes.io/name: cblink-printer
    app.kubernetes.io/instance: test
---
# Source: cblink-printer/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql
spec:
  type: ExternalName
  externalName: ecs.iwnweb.com
  ports:
    - port: 3306
---
# Source: cblink-printer/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cblink-printer
  labels:
    helm.sh/chart: cblink-printer-latest
    app.kubernetes.io/name: cblink-printer
    app.kubernetes.io/instance: test
    app.kubernetes.io/version: "latest"
    app.kubernetes.io/managed-by: Helm
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: cblink-printer
      app.kubernetes.io/instance: test
  template:
    metadata:
      labels:
        app.kubernetes.io/name: cblink-printer
        app.kubernetes.io/instance: test
    spec:
      serviceAccountName: default
      securityContext:
        {}
      containers:
        - name: cblink-printer
          securityContext:
            {}
          image: "cblink-printer:latest"
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          envFrom:
            - configMapRef:
                name: laravel-env
          livenessProbe:
            httpGet:
              path: /
              port: http
          readinessProbe:
            httpGet:
              path: /
              port: http
          resources:
            {}
---
# Source: cblink-printer/templates/ingress.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: cblink-printer
  labels:
    helm.sh/chart: cblink-printer-latest
    app.kubernetes.io/name: cblink-printer
    app.kubernetes.io/instance: test
    app.kubernetes.io/version: "latest"
    app.kubernetes.io/managed-by: Helm
spec:
  rules:
    - host: "printer.cblink.test"
      http:
        paths:
          - path: /
            backend:
              serviceName: cblink-printer
              servicePort: 80

NOTES:
1. Get the application URL by running these commands:
  http://printer.cblink.test/
4年前 评论

部署laravel的cron, 我也会单独开个deployment, 在这个pod里不启动php-fpm, 用php-cli运行就可以。我一直很疑惑mysql集群,redis集群,有没有必要部署到k8s中?单机版mysql, redis我直接部署在docker中。

4年前 评论
jxlwqq (楼主) 4年前
jxlwqq (楼主) 4年前
DianWang

数据库和持久化的数据存储,不适合放docker,最好标注下

4年前 评论
jxlwqq (楼主) 4年前
sanders

请问楼主,laravel 中队列消费者 php artisan queue:work 这类进程是单独启用容器还是和 laravel 服务放一起?

3年前 评论
jxlwqq (楼主) 3年前
jxlwqq (楼主) 3年前
sanders (作者) 3年前
jxlwqq (楼主) 3年前
sanders (作者) 3年前
jxlwqq (楼主) 3年前
jxlwqq (楼主) 3年前
sanders (作者) 3年前

数据库真的不适合放在容器中?是不是云厂商想卖云数据库,还是什么原因?

3年前 评论
jxlwqq (楼主) 3年前
jxlwqq (楼主) 3年前
sanders

我这样理解,为 cron 和 queue 创建的 pod 应该就不用 service 了吧?service 仅仅是需要提供网络访问需要被创建。但是这类 pod 应该怎么设置探针哪?

3年前 评论
jxlwqq (楼主) 3年前
sanders (作者) 3年前

为啥要用apache不是nginx

2年前 评论

@Gundy 可以用 nginx,pod里会有 2 个容器,难度增加了一点点,具体参考:github.com/jxlwqq/kubernetes-examp...

2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!