PHP в Kubernetes локально

Сергей Буланов

Пишу Backend на PHP более 5 лет.
В ближайшее время планирую активно погружаться в Golang.

Люблю природу и катать на велике.
А еще это первый доклад на открытую публику =)

https://t.me/ave404

Мотивация

Кто переезжает в кубер - отладка проекта

Кто думает переезжать - проверить совместимость

Кого интересует кубер и новые инструменты

Итерационно посмотрим как реализовать джентльменский набор проекта:

  • Поднимем PHP
  • Подключим базу данных
  • Настроим миграции и очереди
  • Поработаем с кроном
  • Настроим доступ в кластер

Хардкор и много кода на yaml

cat cat

Что понадобится

  • docker
  • kind | minikube | k3s | k0s
  • kubectl
  • helm

Что имеем


						├── src
						│   ├── app
						│   ├── bootstrap
						│   ├── config
						│   ├── database
						│   ├── public
						│   ├── resources
						│   ├── routes
						│   ├── storage
						│   ├── tests
						│   ├── vendor
						│   ├── artisan
						│   ├── composer.json
						│   ├── composer.lock
						│   ├── .editorconfig
						│   ├── .env
						│   ├── .env.example
						│   ├── .gitattributes
						│   ├── .gitignore
						│   ├── package.json
						│   ├── phpunit.xml
						│   ├── README.md
						│   └── vite.config.js
						├── .gitignore
						└── README.md
					

Шаг 1. PHP

kind.yaml


						apiVersion: kind.x-k8s.io/v1alpha4
						kind: Cluster
						name: php-kubernetes-example

						nodes:
						  - role: control-plane
						    extraMounts:
						      - hostPath: ./
						        containerPath: /project
					

Makefile


						up:
							kind create cluster --config kind.yaml

						down:
							kind delete cluster --name php-kubernetes-example
					

helm create .helm/app


						.helm/app
						├── charts
						├── templates
						│   ├── tests
						│   │   └── test-connection.yaml
						│   ├── deployment.yaml
						│   ├── _helpers.tpl
						│   ├── hpa.yaml
						│   ├── ingress.yaml
						│   ├── NOTES.txt
						│   ├── serviceaccount.yaml
						│   └── service.yaml
						├── Chart.yaml
						└── values.yaml 
					

Оставляем нужное


						.helm/app
						├── templates
						│   ├── deployment.yaml
						│   └── _helpers.tpl
						├── Chart.yaml
						└── values.yaml 
					

.helm/app/Chart.yaml


						apiVersion: v2
						name: php-kubernetes-example-app
						version: 0.1.0
					

Абстракции

pod replicaset deployment pod job cronjob

.helm/app/templates/deployment.yaml


                        apiVersion: apps/v1
                        kind: Deployment

                        metadata:
                          name: {{ include ".helm.fullname" . }}
                          labels:
                            {{- include ".helm.labels" . | nindent 4 }}

                        spec:
                          replicas: 1
                          selector:
                            matchLabels:
                              {{- include ".helm.selectorLabels" . | nindent 6 }}
                          template:
                            metadata:
                              labels:
                                {{- include ".helm.selectorLabels" . | nindent 8 }}
                            spec:
                              containers:
                                - name: php
                                  image: php-kubernetes-example/php:latest
                                  imagePullPolicy: Never
                                  securityContext:
                                    runAsUser: 1000
                                    runAsGroup: 1000
                                  volumeMounts:
                                    - name: source-code
                                      mountPath: /var/www/html

                              volumes:
                                - name: source-code
                                  hostPath:
                                    path: /project/src
					

.docker/php/Dockerfile


						FROM php:8.2-fpm-alpine

						COPY --from=composer:2.7 /usr/bin/composer /usr/local/bin/composer
					

Makefile


						up:
							kind create cluster --config kind.yaml

						down:
							kind delete cluster --name php-kubernetes-example
					

Makefile


						up: down
							# Сборка контейнера
							docker build . \
								--file .docker/php/Dockerfile \
								--tag php-kubernetes-example/php:latest
							# Установка зависимостей
							docker run --rm -it \
								-v $$PWD/src:/var/www/html \
								php-kubernetes-example/php:latest \
								composer install
							# Создание кластера
							kind create cluster --config kind.yaml
							# Загрузка контейнера
							kind load \
								docker-image php-kubernetes-example/php:latest \
								--name php-kubernetes-example
							# Запуск приложения
							helm upgrade --install --wait \
								php-kubernetes-example-app .helm/app

						down:
							kind delete cluster --name php-kubernetes-example
					

Проверяем работу


						$ kubectl get po

						NAME                                         READY   STATUS    RESTARTS   AGE
						php-kubernetes-example-app-b7b57d858-dpp8n   1/1     Running   0          25m

						$ kubectl exec -it php-kubernetes-example-app-b7b57d858-dpp8n -- sh

						$ php artisan inspire

						  “ Well begun is half done. ”
						  — Aristotle
					
kind kubernetes pod command

Шаг 2. Nginx + PHP

.helm/app/templates/deployment.yaml


						apiVersion: apps/v1
						kind: Deployment
						...
						spec:
						  ...
						    spec:
						      containers:
						        - name: nginx
						          image: nginxinc/nginx-unprivileged:1.25-alpine-slim
						          ports:
						            - containerPort: 8080
						          securityContext:
						            runAsUser: 101
						            runAsGroup: 101
						          volumeMounts:
						            - name: nginx-config-files
						              subPath: nginx-php.conf
						              mountPath: /etc/nginx/conf.d/default.conf
						        ...
						      volumes:
						        - name: nginx-config-files
						          configMap:
						            name: {{ include ".helm.fullname" . }}
						            items:
						              - key: nginx-php.conf
						                path: nginx-php.conf
						        ...
					

.helm/app/templates/configmap.yaml


                        apiVersion: v1
                        kind: ConfigMap

                        metadata:
                          name: {{ include ".helm.fullname" . }}
                          labels:
                            {{- include ".helm.labels" . | nindent 4 }}

                        data:
                          nginx-php.conf: |-
                            server {
                                listen 0.0.0.0:8080;

                                root /var/www/html/public;

                                location / {
                                  try_files $uri /index.php$is_args$args;
                                }

                                location ~ \.php$ {
                                  fastcgi_index        index.php;
                                  fastcgi_param        SCRIPT_FILENAME $document_root$fastcgi_script_name;
                                  fastcgi_pass         127.0.0.1:9000;
                                  include              fastcgi_params;
                                }
                            }
					

Проверяем работу


						$ kubectl get po

						NAME                                          READY   STATUS    RESTARTS   AGE
						php-kubernetes-example-app-79677db779-l9mkh   2/2     Running   0          25m

						$ kubectl port-forward pods/php-kubernetes-example-app-79677db779-l9mkh 8080:8080

						Forwarding from 127.0.0.1:8080 -> 8080
						Forwarding from [::1]:8080 -> 8080
						Handling connection for 8080
					

.env


						SESSION_DRIVER=file
						#SESSION_DRIVER=database
					

Проверяем работу

first run
php nginx configmap fail success

Шаг 3. PostgreSQL

.helm/db


						.helm/db
						├── templates
						│   ├── _helpers.tpl
						│   ├── service.yaml
						│   └── statefulset.yaml
						├── Chart.yaml
						└── values.yaml 
                    

.helm/db/Chart.yaml


						apiVersion: v2
						name: php-kubernetes-example-db
						version: 0.1.0
					

.helm/db/templates/statefulset.yaml


						apiVersion: apps/v1
						kind: StatefulSet

						metadata:
						  name: {{ include ".helm.fullname" . }}
						  labels:
						    {{- include ".helm.labels" . | nindent 4 }}

						spec:
						  serviceName: {{ include ".helm.fullname" . }}
						  replicas: 1
						  selector:
						    matchLabels:
						      {{- include ".helm.selectorLabels" . | nindent 6 }}
						  template:
						    metadata:
						      labels:
						        {{- include ".helm.selectorLabels" . | nindent 8 }}
						    spec:
						      initContainers:
						        - name: permission
						          image: busybox
						          command: [ "sh", "-c", "chown 70:70 /var/lib/postgresql/data" ]
						          volumeMounts:
						            - name: pgsql-data
						              mountPath: /var/lib/postgresql/data

						      containers:
						        - name: pgsql
						          image: postgres:16.2-alpine
						          ports:
						            - containerPort: 5432
						          securityContext:
						            runAsUser: 70
						            runAsGroup: 70
						          env:
						            - name: POSTGRES_PASSWORD
						              value: 'password'
						          volumeMounts:
						            - name: pgsql-data
						              mountPath: /var/lib/postgresql/data

						      volumes:
						        - name: pgsql-data
						          hostPath:
						            path: /project/.docker/pgsql/data
					

.helm/db/templates/service.yaml


						apiVersion: v1
						kind: Service

						metadata:
						  name: {{ include ".helm.fullname" . }}
						  labels:
						    {{- include ".helm.labels" . | nindent 4 }}

						spec:
						  selector:
						    {{- include ".helm.selectorLabels" . | nindent 4 }}
						  ports:
						    - port: 5432
						      targetPort: 5432
					

.helm/app/templates/deployment.yaml


                        apiVersion: apps/v1
                        kind: Deployment
                        ...
                        spec:
                          ...
                            spec:
                              containers:
                                ...
                                - name: php
                                  ...
                                  env:
                                    - name: DB_CONNECTION
                                      value: 'pgsql'
                                    - name: DB_HOST
                                      value: 'php-kubernetes-example-db'
                                    - name: DB_DATABASE
                                      value: 'postgres'
                                    - name: DB_USERNAME
                                      value: 'postgres'
                                    - name: DB_PASSWORD
                                      value: 'password'
					

.docker/php/Dockerfile


						FROM php:8.2-fpm-alpine

						COPY --from=composer:2.7 /usr/bin/composer /usr/local/bin/composer

						RUN apk --no-cache add libpq-dev \
							&& docker-php-ext-install pdo_pgsql
					

.env


						#SESSION_DRIVER=file
						SESSION_DRIVER=database
					

Makefile


						up: down
							# Сборка контейнера
							docker build . \
								--file .docker/php/Dockerfile \
								--tag php-kubernetes-example/php:latest
							# Установка зависимостей
							docker run --rm -it \
								-v $$PWD/src:/var/www/html \
								php-kubernetes-example/php:latest \
								composer install
							# Создание кластера
							kind create cluster --config kind.yaml
							# Загрузка контейнера
							kind load \
								docker-image php-kubernetes-example/php:latest \
								--name php-kubernetes-example
							# Запуск DB
							helm upgrade --install --wait \
								php-kubernetes-example-db .helm/db
							# Запуск приложения
							helm upgrade --install --wait \
								php-kubernetes-example-app .helm/app

						down:
							kind delete cluster --name php-kubernetes-example
					

Проверяем работу


						$ kubectl get po

						NAME                                          READY   STATUS    RESTARTS   AGE
						php-kubernetes-example-app-7f76bc54cc-sv2tq   2/2     Running   0          13m
						php-kubernetes-example-db-0                   1/1     Running   0          14m

						$ kubectl exec -it php-kubernetes-example-app-7f76bc54cc-sv2tq -c php -- sh

						$ php artisan migrate

						   INFO  Preparing database.

						  Creating migration table ................ 15.23ms DONE

						   INFO  Running migrations.

						  0001_01_01_000000_create_users_table .... 36.57ms DONE
						  0001_01_01_000001_create_cache_table .... 21.20ms DONE
						  0001_01_01_000002_create_jobs_table ..... 36.21ms DONE
					
php postgresql service

Шаг 4. Jobs

.helm/app/templates/migrations.yaml


                        apiVersion: batch/v1
                        kind: Job

                        metadata:
                          name: {{ include ".helm.fullname" . }}-migrations
                          labels:
                            {{- include ".helm.labels" . | nindent 4 }}
                          annotations:
                            "helm.sh/hook": pre-install,pre-upgrade
                            "helm.sh/hook-weight": "1"
                            "helm.sh/hook-delete-policy": before-hook-creation

                        spec:
                          backoffLimit: 0
                          activeDeadlineSeconds: 120
                          template:
                            spec:
                              restartPolicy: Never
                              containers:
                                - name: php
                                  image: php-kubernetes-example/php:latest
                                  imagePullPolicy: Never
                                  securityContext:
                                    runAsUser: 1000
                                    runAsGroup: 1000
                                  volumeMounts:
                                    - name: source-code
                                      mountPath: /var/www/html
                                  env:
                                    - name: DB_CONNECTION
                                      value: 'pgsql'
                                    - name: DB_HOST
                                      value: 'php-kubernetes-example-db'
                                    - name: DB_DATABASE
                                      value: 'postgres'
                                    - name: DB_USERNAME
                                      value: 'postgres'
                                    - name: DB_PASSWORD
                                      value: 'password'
                                  command: [ "php", "artisan", "migrate" ]

                              volumes:
                                - name: source-code
                                  hostPath:
                                    path: /project/src
					

Проверяем работу


						$ kubectl get po

						NAME                                          READY   STATUS      RESTARTS   AGE
						php-kubernetes-example-app-6c8b44fc98-6m6np   2/2     Running     0          21s
						php-kubernetes-example-app-migrations-clx8d   0/1     Completed   0          25s
						php-kubernetes-example-db-0                   1/1     Running     0          51s

						$ kubectl logs php-kubernetes-example-app-migrations-clx8d

						   INFO  Preparing database.

						  Creating migration table ................ 21.81ms DONE

						   INFO  Running migrations.

						  0001_01_01_000000_create_users_table .... 34.75ms DONE
						  0001_01_01_000001_create_cache_table .... 15.22ms DONE
						  0001_01_01_000002_create_jobs_table ..... 25.33ms DONE
					

Шаг 5. CronJobs & Daemons

Проблема дублирования

.helm/app/templates/_php.tpl


                        {{- define ".helm.php" }}
                        image: php-kubernetes-example/php:latest
                        imagePullPolicy: Never
                        securityContext:
                          runAsUser: 1000
                          runAsGroup: 1000
                        volumeMounts:
                          - name: source-code
                            mountPath: /var/www/html
                        env:
                          - name: DB_CONNECTION
                            value: 'pgsql'
                          - name: DB_HOST
                            value: 'php-kubernetes-example-db'
                          - name: DB_DATABASE
                            value: 'postgres'
                          - name: DB_USERNAME
                            value: 'postgres'
                          - name: DB_PASSWORD
                            value: 'password'
                        {{- end }}

                        {{- define ".helm.php_mounts" }}
                        - name: source-code
                          hostPath:
                            path: /project/src
                        {{- end }}
					

.helm/app/templates/cronjobs.yaml


                        apiVersion: batch/v1
                        kind: CronJob

                        metadata:
                          name: {{ include ".helm.fullname" . }}-inspire
                          labels:
                            {{- include ".helm.labels" . | nindent 4 }}

                        spec:
                          schedule: "*/1 * * * *"
                          concurrencyPolicy: Forbid
                          jobTemplate:
                            spec:
                              template:
                                spec:
                                  restartPolicy: Never
                                  containers:
                                    - name: php
                                      {{- include ".helm.php" . | indent 14 }}
                                      command: [ "php", "artisan", "inspire" ]

                                  volumes:
                                    {{- include ".helm.php_mounts" . | indent 12 }}
					

.helm/app/templates/deployment.yaml


                         ---

                         apiVersion: apps/v1
                         kind: Deployment

                         metadata:
                           name: {{ include ".helm.fullname" . }}-queue
                           labels:
                             {{- include ".helm.labels" . | nindent 4 }}

                         spec:
                           replicas: 1
                           selector:
                             matchLabels:
                               {{- include ".helm.selectorLabels" . | nindent 6 }}
                           template:
                             metadata:
                               labels:
                                 {{- include ".helm.selectorLabels" . | nindent 8 }}
                             spec:
                               containers:
                                 - name: php
                                   {{- include ".helm.php" . | indent 10 }}
                                   command: [ "php", "artisan", "queue:work" ]

                               volumes:
                                 {{- include ".helm.php_mounts" . | indent 8 }}
					

Проверяем работу


                        $ kubectl get po

                        NAME                                                READY STATUS    RESTARTS AGE
                        php-kubernetes-example-app-6c8b44fc98-q8glq         2/2   Running   0        104s
                        php-kubernetes-example-app-inspire-28585090-4qvsx   0/1   Completed 0        66s
                        php-kubernetes-example-app-inspire-28585091-6x82d   0/1   Completed 0        6s
                        php-kubernetes-example-app-queue-84595dc8dd-q24hx   1/1   Running   0        104s
                        php-kubernetes-example-db-0                         1/1   Running   0        2m15s

                        $ kubectl logs php-kubernetes-example-app-queue-84595dc8dd-q24hx
                          2024-05-07 18:10:50 App\Jobs\Hello ............ RUNNING
                        RnD PHP #7  2024-05-07 18:10:50 App\Jobs\Hello .. 4.73ms DONE
                          2024-05-07 18:10:50 App\Jobs\Hello ............ RUNNING
                        RnD PHP #7  2024-05-07 18:10:50 App\Jobs\Hello .. 1.91ms DONE
                          2024-05-07 18:10:50 App\Jobs\Hello ............ RUNNING
                        RnD PHP #7  2024-05-07 18:10:50 App\Jobs\Hello .. 1.67ms DONE

                        $ kubectl logs php-kubernetes-example-app-inspire-28585091-6x82d

                          “ Smile, breathe, and go slowly. ”
                          — Thich Nhat Hanh
					

Шаг 6. Ingress

Корректный доступ

ingress

kind.yaml


                        apiVersion: kind.x-k8s.io/v1alpha4
                        kind: Cluster
                        name: php-kubernetes-example

                        nodes:
                          - role: control-plane
                            extraMounts:
                              - hostPath: ./
                                containerPath: /project
                            kubeadmConfigPatches:
                              - |
                                kind: InitConfiguration
                                nodeRegistration:
                                  kubeletExtraArgs:
                                    node-labels: "ingress-ready=true"
                            extraPortMappings:
                              - containerPort: 80
                                hostPort: 80
                                protocol: TCP
					

.helm/app/templates/ingress.yaml


                        apiVersion: networking.k8s.io/v1
                        kind: Ingress

                        metadata:
                          name: {{ include ".helm.fullname" . }}
                          labels:
                            {{- include ".helm.labels" . | nindent 4 }}

                        spec:
                          rules:
                            - http:
                                paths:
                                  - path: /
                                    pathType: Prefix
                                    backend:
                                      service:
                                        name: {{ include ".helm.fullname" . }}
                                        port:
                                          number: 8080
					

.helm/app/templates/service.yaml


                        apiVersion: v1
                        kind: Service

                        metadata:
                          name: {{ include ".helm.fullname" . }}
                          labels:
                            {{- include ".helm.labels" . | nindent 4 }}

                        spec:
                          selector:
                            {{- include ".helm.selectorLabels" . | nindent 4 }}
                          ports:
                            - port: 8080
                              targetPort: 8080
					

.helm/app/templates/deployment.yaml


                        ...
                        spec:
                          replicas: {{ .Values.replicaCount }}
                        ...
					

.helm/app/values.yaml


						replicaCount: 2
					

Makefile


						up: down
							# Сборка контейнера
							docker build . \
								--file .docker/php/Dockerfile \
								--tag php-kubernetes-example/php:latest
							# Установка зависимостей
							docker run --rm -it \
								-v $$PWD/src:/var/www/html \
								php-kubernetes-example/php:latest \
								composer install
							# Создание кластера
							kind create cluster --config kind.yaml
							# Загрузка контейнера
							kind load \
								docker-image php-kubernetes-example/php:latest \
								--name php-kubernetes-example
							# Добавляем ingress
							kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
							# Запуск DB
							helm upgrade --install --wait \
								php-kubernetes-example-db .helm/db
							# Запуск приложения
							helm upgrade --install --wait \
								php-kubernetes-example-app .helm/app

						down:
							kind delete cluster --name php-kubernetes-example
					

Проверяем работу


						$ kubectl get po

						NAME                                                READY STATUS    RESTARTS AGE
						php-kubernetes-example-app-6c8b44fc98-lq2dz         2/2   Running   0        73s
						php-kubernetes-example-app-6c8b44fc98-t2x78         2/2   Running   0        73s
						php-kubernetes-example-app-inspire-28585136-kc2cb   0/1   Completed 0        46s
						php-kubernetes-example-app-queue-84595dc8dd-4wqbt   1/1   Running   0        73s
						php-kubernetes-example-db-0                         1/1   Running   0        119s

						$ curl 127.0.0.1/api/info
						{"hostname":"php-kubernetes-example-app-6c8b44fc98-lq2dz"}
						$ curl 127.0.0.1/api/info
						{"hostname":"php-kubernetes-example-app-6c8b44fc98-lq2dz"}
						$ curl 127.0.0.1/api/info
						{"hostname":"php-kubernetes-example-app-6c8b44fc98-t2x78"}
					

Что в итоге

replicaset job cronjob deployment user pods

Что в итоге


                        ├── .helm
                        │   ├── app
                        │   │   ├── templates
                        │   │   │   ├── _helpers.tpl
                        │   │   │   ├── _php.tpl
                        │   │   │   ├── configmap.yaml
                        │   │   │   ├── cronjobs.yaml
                        │   │   │   ├── deployment.yaml
                        │   │   │   ├── ingress.yaml
                        │   │   │   ├── migrations.yaml
                        │   │   │   └── service.yaml
                        │   │   ├── Chart.yaml
                        │   │   └── values.yaml
                        │   └── db
                        │       ├── templates
                        │       │   ├── _helpers.tpl
                        │       │   ├── service.yaml
                        │       │   └── statefulset.yaml
                        │       ├── Chart.yaml
                        │       └── values.yaml
                        ├── kind.yaml
                        └── Makefile
					

Что дальше

  • шаблонизация
  • адаптация под проект
  • отладка

Что может понадобится

  • kubectl logs
  • kubectl describe
  • kubectl events

Спасибо за внимание!

Сергей Буланов @ave404

source code
Проект

github.com/ave404/php-kubernetes-example
presentation
Презентация

github.com/ave404/php-kubernetes-presentation