CI/CD Laravel Application with Github Actions & Kubernetes Cluster

CI/CD Laravel Application with Github Actions & Kubernetes Cluster

Prerequisites

  1. Laravel Project.

  2. Kubernetes Cluster. (Minikube)

  3. Redis.

  4. Helm.

Artikel ini berasumsi bahwa Anda sudah memiliki proyek Laravel dan contoh penempatan ini dilakukan pada versi Laravel 8. Jika Anda menggunakan versi Laravel yang berbeda, Anda perlu memodifikasi dan mengubah dockerfile di bawah ini untuk bagian backend.

Contoh proyek ini menggunakan database MySQL, jika Anda menggunakan database yang berbeda, Anda perlu memodifikasi dockerfile untuk backend untuk menginstal paket-paket yang diperlukan untuk database Anda.

Kita akan menggunakan chart Helm untuk menginstal MySQL dan Redis di klaster k8s, yang merupakan cara yang paling direkomendasikan untuk mendeploy layanan-layanan ini.

Mengapa Anda memerlukan Redis?

Secara default, sesi Laravel disimpan di memori server, yang dapat berisi sesi kredensial pengguna jika aplikasi Laravel Anda menggunakan cara lama untuk mengautentikasi pengguna (Token Opaque) yang menggunakan file sesi untuk validasi. Metode autentikasi ini tidak kompatibel dengan arsitektur mikro layanan. Pendekatan Mikro Layanan menyarankan penggunaan token JWT untuk autentikasi atau menggunakan Redis untuk menyimpan sesi.

Cara terbaik adalah dengan menyimpan sesi dari aplikasi Laravel Anda di Redis.

Mengkonfigurasi Laravel untuk Menyimpan Sesi di Redis

Untuk mengkonfigurasi aplikasi Laravel Anda agar mulai menyimpan informasi sesi di Redis, dari file config/session.php ubah yang berikut ini:

'driver' => env('SESSION_DRIVER', 'file'),

menjadi

'driver' => env('SESSION_DRIVER', 'redis'),

Kemudian jalankan perintah composer berikut untuk menambahkan paket predis ke file composer.json.

composer require predis/predis

Task 1. Running Application On Local

git clone https://github.com/mdrdani/app-recordCounseling.git
cd app-recordCounseling/
cp .env.example .env
composer update

install composer link jika belum terinstall.

php artisan key:generate
php artisan migrate
php artisan storage:link
php artisan serve

Task 2. Create Container Image with Github Actions (CI)

sebelum membuat workflow container image, hal pertama yaitu kita perlu membuat konfigurasi docker aplikasi laravel nya, buat folder dengan nama docker didalam aplikasi laravel dan buatkan beberapa file YAML seperti ini.

nginx.Dockerfile.yml

FROM node:20.5.1-alpine AS assets-build
WORKDIR /var/www/html
COPY . /var/www/html/

RUN npm ci
RUN npm run build

FROM nginx:1.19-alpine AS nginx
COPY /docker/vhost.conf /etc/nginx/conf.d/default.conf
COPY --from=assets-build /var/www/html/public /var/www/html/

fpm.Dockerfile.yml

FROM php:8.1-fpm-alpine AS base
ENV EXT_APCU_VERSION=master
RUN curl -vvv https://github.com/krakjoe/apcu.git

RUN apk add --update zlib-dev libpng-dev libzip-dev $PHPIZE_DEPS

RUN docker-php-ext-install exif
RUN docker-php-ext-install gd
RUN docker-php-ext-install zip
RUN docker-php-ext-install pdo_mysql
# RUN pecl install apcu
RUN docker-php-source extract \
     && apk -Uu add git \
     && git clone --branch $EXT_APCU_VERSION --depth 1 https://github.com/krakjoe/apcu.git /usr/src/php/ext/apcu \
     && cd /usr/src/php/ext/apcu && git submodule update --init \
     && docker-php-ext-install apcu
RUN docker-php-ext-enable apcu

FROM base AS dev

COPY /composer.json composer.json
COPY /composer.lock composer.lock
COPY /app app
COPY /bootstrap bootstrap
COPY /config config
COPY /artisan artisan

FROM base AS build-fpm

WORKDIR /var/www/html

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY /artisan artisan
COPY . /var/www/html
# COPY /composer.json composer.json

RUN composer install --prefer-dist --no-ansi --no-dev

COPY /bootstrap bootstrap
COPY /app app
COPY /config config
COPY /routes routes


# COPY . /var/www/html

RUN composer dump-autoload -o

FROM build-fpm AS fpm

COPY --from=build-fpm /var/www/html /var/www/html

vhost.conf

server {
    listen  80 default_server;
    listen [::]:80 default_server;

    root /var/www/html;

    index /public/index.php;
    error_page 404 /public/index.php;

    location / {
        try_files $uri $uri/ /public/index.php;
    }

    location ~ \.php$ {
        fastcgi_pass localhost:9000;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param SCRIPT_NAME $fastcgi_script_name;
    }
}

Selanjutnya untuk push image ke DockerHub kita perlu menyimpan Username dan Token Dockerhub di Secrets Github, create secrets yang gunanya untuk menyimpan hal-hal sensitif seperti username dan token agar tidak terpublish di public.

ke Repo Github > Settings > Security > Secrets and variables > actions > New repository secret.

jika sudah selanjutnya membuat workflow.

Ke Repo Github > Actions > New Workflow, pilih Docker Image klik configure

untuk workflows yang pertama beri nama dengan backend-image.yml

name: Docker Image Backend CI

on:
  push:
    branches: [ "master" ]

jobs:

  build:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Login ke Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build docker image
        run: docker build . -f docker/fpm.Dockerfile -t cehamot/backend-appoinmentapp:${{ github.run_number }}

      - name: Push docker image
        run: docker push cehamot/backend-appoinmentapp:${{ github.run_number }}

dan kedua worflows di beri nama frontend-image.yml

name: Docker Image Frontend CI

on:
  push:
    branches: [ "master" ]

jobs:

  build:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Login ke Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build docker image
        run: docker build . -f docker/nginx.Dockerfile -t cehamot/frontend-appoinmentapp:${{ github.run_number }}

      - name: Push docker image
        run: docker push cehamot/frontend-appoinmentapp:${{ github.run_number }}

simpan dengan memilih Commit Changes.

check di Docker Hub apakah sudah ada container image Backend dan Frontend.

Oke Build Docker Container image (Continuous Integration) sudah selesai.

Task 3. Deploy To Kubernetes (CD)

Sebelum memulai membuat berkas manifest K8s, kita perlu menjalankan MySQL dan Redis di dalam kluster terlebih dahulu, jadi saya akan menggunakan chart Helm untuk itu.

Pastikan Anda telah menginstal Helm di mesin Anda, lalu jalankan perintah berikut untuk menambahkan repositori bitnami.

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

Mulai menginstal MySQL menggunakan chart Helm.

Pada saat perintah pemasangan, Anda akan memberikan nama basis data, pengguna basis data, dan kata sandi basis data. Informasi ini akan digunakan dalam konfigurasi dan rahasia (secret) dari penyebaran (deployment).

helm install mysql bitnami/mysql --set auth.database="application" --set auth.username="dani" --set auth.password="Secret123!!"

Menginstal Redis menggunakan chart Helm, pastikan untuk menyimpan kata sandi Redis yang diberikan pada perintah saat menginstal.

helm install redis bitnami/redis --set auth.password="Secret123!!"

Periksa status pod MySQL dan Redis, seharusnya keduanya sudah berjalan.

kubectl get pod

Jika keduanya sudah berjalan, Anda siap untuk mulai membuat berkas manifest Kubernetes.
Sebelumnya kita buat direktori Kubernetes di project.

mkdir ./k8s

Buat config file untuk deployment.

touch ./k8s/app_config.yaml

di bawah ini content app_config.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: application-config
data:
  APP_NAME: appointment-app
  APP_ENV: production
  APP_DEBUG: "false"
  APP_URL: http://localhost:8080
  LOG_CHANNEL: stack
  LOG_LEVEL: debug
  DB_CONNECTION: mysql
  DB_HOST: mysql
  DB_PORT: "3306"
  DB_DATABASE: application
  DB_USERNAME: dani
  BROADCAST_DRIVER: log
  CACHE_DRIVER: redis
  QUEUE_CONNECTION: sync
  SESSION_DRIVER: redis
  SESSION_LIFETIME: "120"
  PROMETHEUS_NAMESPACE: default
  PROMETHEUS_METRICS_ROUTE_ENABLED: "true"
  PROMETHEUS_METRICS_ROUTE_PATH: metrics
  PROMETHEUS_METRICS_ROUTE_MIDDLEWARE: "null"
  PROMETHEUS_STORAGE_ADAPTER: memory
  REDIS_HOST: redis-master
  REDIS_PORT: "6379"
  REDIS_CLIENT: "predis"
  PROMETHEUS_REDIS_PREFIX: PROMETHEUS_

Pastikan untuk menuliskan nilai yang benar untuk DB_HOST, DB_USERNAME, dan REDIS_HOST sesuai dengan yang diberikan saat menjalankan perintah helm install untuk MySQL dan Redis. Nilai-nilai ini akan digunakan dalam konfigurasi berkas manifest Kubernetes .

Buat config file untuk Secret.

touch ./k8s/app_secret.yaml

di bawah ini content app_secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: application-secret
type: Opaque
data:
  REDIS_PASSWORD: U2VjcmV0MTIz
  DB_PASSWORD: U2VjcmV0MTIz
  APP_KEY: YmFzZTY0OlYweEFXc3JjeUFQTVkxK2tneXNBV20ycHRWU0ExMStVQXU3OG8vTXFEakk9Cg==

Catat bahwa REDIS_PASSWORD dan DB_PASSWORD adalah nilai yang diberikan saat menginstal dengan chart Helm, dan nilainya dienkripsi dalam format base64.

Mengenai APP_KEY, kita bisa membuat nya kemudian mengenkripsinya dalam format base64 dan menggantinya dengan yang ada.

Buat config file untuk Deployment.

Berikut ini adalah contoh berkas deployment Kubernetes yang mencakup dua kontainer (backend dan frontend) serta initContainer untuk menjalankan migrasi PHP guna mempersiapkan database.

touch ./k8s/app_deployment.yaml

di bawah ini content app_deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-application
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web-application
  template:
    metadata:
      labels:
        app: web-application
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/path: /metrics
        prometheus.io/port: "80"
    spec:
      volumes:
        - name: logs
          emptyDir: {}
        - name: views
          emptyDir: {}
      securityContext:
        fsGroup: 82
      initContainers:
        - name: database-migrations
          image: cehamot/backend-appoinmentapp:2
          imagePullPolicy: IfNotPresent
          envFrom:
            - configMapRef:
                name: application-config
            - secretRef:
                name: application-secret
          command:
            - "php"
          args:
            - "artisan"
            - "migrate:fresh"
            - "--seed"
      containers:
        - name: nginx
          imagePullPolicy: IfNotPresent
          image: cehamot/frontend-appoinmentapp:5
          resources:
            {}
            # limits:
            #   cpu: 500m
            #   memory: 50M
          ports:
            - containerPort: 80
        - name: fpm
          imagePullPolicy: IfNotPresent
          envFrom:
            - configMapRef:
                name: application-config
            - secretRef:
                name: application-secret
          securityContext:
            runAsUser: 82
            readOnlyRootFilesystem: true
          volumeMounts:
            - name: logs
              mountPath: /var/www/html/storage/logs
            - name: views
              mountPath: /var/www/html/storage/framework/views
          resources: {}
          image: cehamot/backend-appoinmentapp:2
          ports:
            - containerPort: 9000

Pastikan nama image benar di dalam file deployment.

Buat config file untuk Service.

Berkas terakhir yang diperlukan adalah untuk membuat layanan (service) guna mengekspos aplikasi. Untuk praktik terbaik, disarankan untuk membuat layanan dan ingress. Namun, untuk contoh ini, saya akan menggunakan tipe layanan NodePort saja tanpa membuat ingress.

touch ./k8s/app_service.yaml

di bawah ini content app_service.yaml

apiVersion: v1
kind: Service
metadata:
  labels:
    app: web-application-svc
  name: web-application-svc
spec:
  type: NodePort
  selector:
    app: web-application
  ports:
  - name: targetport
    port: 80
    protocol: TCP
    targetPort: 80
    nodePort: 30007

Apply k8s manifest files.

kubectl apply -f ./k8s/app_config.yaml
kubectl apply -f ./k8s/app_secret.yaml
kubectl apply -f ./k8s/app_deployment.yaml
kubectl apply -f ./k8s/app_service.yaml

check status Pods

kubectl get pod

Jika terdapat error di pod atau pod tidak running, check log pod, di bawah ini contoh log command.

kubectl logs pod <pod_Name> -c database-migrations
kubectl logs pod <pod_Name> -c nginx
kubectl logs pod <pod_Name> -c fpm

atau bisa describe pod untuk melihat keseluruhan detil untuk troubleshoot.

kubectl describe pod <pod_Name>