Weaveworks 2022.03 release featuring Magalix PaC | Learn more
Balance innovation and agility with security and compliance
risks using a 3-step process across all cloud infrastructure.
Step up business agility without compromising
security or compliance
Everything you need to become a Kubernetes expert.
Always for free!
Everything you need to know about Magalix
culture and much more
Laravel is a free, open-source PHP framework, created by Taylor Otwell and intended for the development of web applications following the model–view–controller (MVC) architectural pattern.
In this article, we’ll be building a Laravel app, dockerizing it, and deploying it on top of Kubernetes. But first, you may be wondering why we’re using Kubernetes and Docker for the deployment of our Laravel application. Kubernetes allows us to derive maximum utility from containers and build cloud-native applications that can run anywhere, independent of cloud-specific requirements. With Kubernetes, we can automatically deploy, scale, and manage our application so that we can pay more attention to building efficient applications. We will go through the steps required in building a To-do app in Laravel and automating the deployment of the app to a server. We'll learn how Laravel handles database migration as well as the Laravel's Eloquent models. Of course, we'll see the Routing and View Layouts. All basic stuff, but fundamental elements of Laravel.
The first thing you have to do is to install composer from https://getcomposer.org/ and then install Laravel, check Laravel’s documentation for that https://laravel.com/docs. At this point, we’ll need to pay close attention to the codeblocks that we are going to use for the building of our Laravel application because, without the correct codeblocks, we will have no dockerizing nor deployment of our app.
To create our new laravel app, run this:
$ laravel new todo
Create a database in your preferred database app, then set-up your project-dir/.env file like:
DB_HOST=localhost
DB_CONNECTION=mysql
DB_DATABASE=tasks
DB_USERNAME=root
DB_PASSWORD=""
Create a database migration for ‘tasks’ :
$ php artisan make:migration create_tasks_table --create=tasks
The migration will be placed in the database/migrations directory of our project. Add a string column to hold the name of the tasks by adding $table->string('name'); to create_task_table.php file like this :
public function up()
{
Schema::create('tasks', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->timestamps();
});
Then run:
$ php artisan migrate
Next, define a Task model that corresponds to our tasks database table. Run :
$ php artisan make:model Task
All Laravel routes are defined in the app/Http/routes.php.
Our app needs three routes:
Put all of these routes in app/Http/routes.php file:
'web'], function () {
/**
* Show Task Dashboard
*/
Route::get('/', function () {
$tasks = Task::orderBy('created_at', 'asc')->get();
return view('tasks', [
'tasks' => $tasks
]);
});
/**
* Add New Task
*/
Route::post('/task', function (Request $request) {
$validator = Validator::make($request->all(), [
'name' => 'required|max:255',
]);
if ($validator->fails()) {
return redirect('/')
->withInput()
->withErrors($validator);
Define a new layout view in resources/views/layouts/app.blade.php. The .blade.php extension instructs the framework to use the Blade templating engine to render the view.
The new app.blade.php view would look like this:
<!DOCTYPE html>
<html lang="en”>
<head>
<title>Laravel Quickstart - Basic</title>
<!-- CSS And JavaScript -->
</head>
<body>
<div class="container">
<nav class="navbar navbar-default">
<!-- Navbar Contents -->
</nav>
</div>
@yield('content')
</body>
</html>
Define a view (resources/views/tasks.blade.php) that contains a form to create a new task as well as a table that lists all existing tasks.
Define this view in resources/views/tasks.blade.php like this:
@extends('layouts.app')
@section('content')
<!-- Bootstrap Boilerplate... -->
<div class="panel-body">
<!-- Display Validation Errors -->
@include('common.errors')
<!-- New Task Form -->
<form action="" method="POST" class="form-horizontal">
{!! csrf_field() !!}
<!-- Task Name -->
<div class="form-group">
<label for="task" class="col-sm-3 control-label">Task</label>
<div class="col-sm-6">
<input type="text" name="name" id="task-name" class="form-control">
</div>
</div>
<!-- Add Task Button -->
<div class="form-group">
<div class="col-sm-offset-3 col-sm-6">
<button type="submit" class="btn btn-default">
<i class="fa fa-plus"></i> Add Task
</button>
</div>
</div>
</form>
</div>
<!-- TODO: Current Tasks -->
@endsection
Define the contents of the view resources/views/common/errors.blade.php. It should look like this:
@if (count($errors) > 0)
<!-- Form Error List -->
<div class="alert alert-danger">
<strong>Whoops! Something went wrong!</strong>
<br><br>
<ul>
@foreach ($errors->all() as $error)
<li></li>
@endforeach
</ul>
</div>
@endif
@extends('layouts.app')
@section('content')
<!-- Bootstrap Boilerplate... -->
<div class="panel-body">
<!-- Display Validation Errors -->
@include('common.errors')
<!-- New Task Form -->
<form action="" method="POST" class="form-horizontal">
{!! csrf_field() !!}
<!-- Task Name -->
<div class="form-group">
<label for="task" class="col-sm-3 control-label">Task</label>
<div class="col-sm-6">
<input type="text" name="name" id="task-name" class="form-control">
</div>
</div>
<!-- Add Task Button -->
<div class="form-group">
<div class="col-sm-offset-3 col-sm-6">
<button type="submit" class="btn btn-default">
<i class="fa fa-plus"></i> Add Task
</button>
</div>
</div>
</form>
</div>
<!-- Current Tasks -->
@if (count($tasks) > 0)
<div class="panel panel-default">
<div class="panel-heading">
Current Tasks
</div>
<div class="panel-body">
<table class="table table-striped task-table">
<!-- Table Headings -->
<thead>
<th>Task</th>
<th> </th>
</thead>
<!-- Table Body -->
<tbody>
@foreach ($tasks as $task)
<tr>
<!-- Task Name -->
<td class="table-text">
<div></div>
</td>
<!-- Delete Button -->
<td>
<form action="" method="POST">
{!! csrf_field() !!}
{!! method_field('DELETE') !!}
<button>Delete Task</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif
@endsection
After successfully building our app, we’ll be running the app locally using the following commands:
$ php artisan serve
There’s a number of ways to dockerize your Laravel project. You may use the official Nginx and PHP images from Dockerhub, but we found it’s a bit troublesome to set them up.
So instead of messing around with all the different kinds of docker images, we discovered docker-images-php, a set of production-ready docker images.
In building your project, your Dockerfile should look like this:
ARG PHP_EXTENSIONS="apcu bcmath opcache pcntl pdo_mysql redis zip sockets imagick gd exif"
FROM thecodingmachine/php:7.3-v2-slim-apache as php_base
ENV TEMPLATE_PHP_INI=production
COPY --chown=docker:docker . /var/www/html
RUN composer install --quiet --optimize-autoloader --no-dev
FROM node:10 as node_dependencies
WORKDIR /var/www/html
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=false
COPY --from=php_base /var/www/html /var/www/html
RUN npm set progress=false && \
npm config set depth 0 && \
npm install && \
npm run prod && \
rm -rf node_modules
FROM php_base
COPY --from=node_dependencies --chown=docker:docker /var/www/html /var/www/html
There are a couple of yaml files required to set up the deployment of the Laravel project on Kubernetes:
The ConfigMap is the section of your code that stores the configuration settings for your application to work, or for a Kubernetes pod to work. Now while writing your configmap, you have to consider your App_DEBUG input on line 6 “APP_DEBUG: "false"” making sure your application is not debugged and inputting your APP_NAME on line 10 APP_NAME: "Laravel-Todo" correctly so as not to send your deployment on a wild goose chase.
apiVersion: v1
kind: ConfigMap
metadata:
name: laravel-todo
data:
APP_DEBUG: "false"
APP_ENV: production
APP_KEY: changeme
APP_LOG_LEVEL: debug
APP_NAME: "Laravel-Todo"
APP_URL: https://www.laravel.com
BROADCAST_DRIVER: pusher
CACHE_DRIVER: redis
QUEUE_DRIVER: redis
SESSION_DRIVER: redis
DB_CONNECTION: mysql
DB_DATABASE: task
DB_HOST: host
DB_PASSWORD: password
DB_PORT: "3306"
DB_USERNAME: username
REDIS_HOST: redis
REDIS_PORT: "6379"
A Deployment resource uses a ReplicaSet to manage the pods. However, it handles updating them in a controlled way. You can dig deeper into Deployment Controllers and patterns Here.
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: laravel-todo
name: laravel-todo
spec:
minReadySeconds: 5
replicas: 3
revisionHistoryLimit: 1
selector:
matchLabels:
app: laravel-todo
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 50%
type: RollingUpdate
template:
metadata:
labels:
app: laravel-todo
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- backend
topologyKey: kubernetes.io/hostname
weight: 100
initContainers:
- args:
- /bin/bash
- -c
- (php artisan migrate || true) && (php artisan config:cache || true) && (php
artisan route:cache || true) && (cp -rp /var/www/html /codebase)
envFrom:
- configMapRef:
name: laravel-todo-config
image: changeme
imagePullPolicy: Always
name: artisan
volumeMounts:
- mountPath: /codebase
name: codebase
containers:
- name: app
envFrom:
- configMapRef:
name: laravel-todo-config
image: changeme
imagePullPolicy: Always
livenessProbe:
initialDelaySeconds: 10
periodSeconds: 15
tcpSocket:
port: 80
timeoutSeconds: 30
ports:
- containerPort: 80
readinessProbe:
initialDelaySeconds: 10
periodSeconds: 10
tcpSocket:
port: 80
resources:
limits:
cpu: 200m
memory: 400M
requests:
cpu: 100m
memory: 200M
volumeMounts:
- mountPath: /var/www
name: codebase
volumes:
- emptyDir: {}
name: codebase
Ingress brings in, and exposes, your HTTP and HTTPS routes that are outside the cluster into services that are inside the cluster.
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/from-to-www-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: 100m
labels:
app: laravel-todo
name: laravel-todo
spec:
rules:
- host: yourdomain.com
http:
paths:
- laravel-todo:
serviceName: laravel-todo
servicePort: 80
path: /
# tls:
# - hosts:
# - yourdomain.com
# secretName: yourdomain-com-tls
# ---
# apiVersion: certmanager.k8s.io/v1alpha1
# kind: Certificate
# metadata:
# name: yourdomain.com
# spec:
# acme:
# config:
# - dns01:
# provider: cf-dns
# domains:
# - yourdomain.com
# commonName: yourdomain.com
# dnsNames:
# - yourdomain.com
# issuerRef:
# kind: ClusterIssuer
# name: letsencrypt
# secretName: yourdomain-com-tls
Service does only one thing, and that’s exposing your running applications on a specified set of pods as a network.
apiVersion: v1
kind: Service
metadata:
labels:
app: laravel-todo
name: laravel-todo
spec:
ports:
- name: http
port: 80
protocol: TCP
selector:
app: laravel-todo
type: ClusterIP
Now that we have our Laravel To-do app, Dockerfile, and Kubernetes configurations ready, go ahead and set up Gitlab Continuous Integration to automate our deployment making use of Kubernetes token authentication.
Within Repo goto Settings then CI / CD, we need to store our Kubernetes Cluster credentials into Gitlab CI’s environment variables.
Move the Dockerfile to your project’s root directory, then create a folder called k8 and store all the Kubernetes yaml files inside. Create a file called .gitlab-ci.yml that contains the following:
variables:
DOCKER_DRIVER: "overlay2"
REPOSITORY_URL: "url_name:latest"
stages:
- build
- deploy
services:
- docker:dind
build-app:
stage: build
only:
- master
script:
- mkdir -p $HOME/.docker
- apk add --no-cache curl jq python py-pip
- pip install awscli
- $(aws ecr get-login --no-include-email --region ap-southeast-1)
- docker info
- docker build -t ${REPOSITORY_URL} .
- docker push ${REPOSITORY_URL}
deploy-app:
stage: deploy
image: alpine
only:
- master
script:
- apk add --no-cache curl
- curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
- chmod +x ./kubectl
- mv ./kubectl /usr/local/bin/kubectl
- kubectl config set-cluster k8s --server="$KUBE_URL" --insecure-skip-tls-verify=true
- kubectl config set-credentials admin --token="$KUBE_TOKEN"
- kubectl config set-context default --cluster=k8s --user=admin
- kubectl config use-context default
- cd k8s && kubectl apply -f -
- kubectl patch deployment laravel-todo -p "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"`date +'%s'`\"}}}}}"
The final step is to push your code to the repo and wait for it to be deployed automatically and without any downtime.
After pushing your Laravel code to the repository, it’s automatically deployed. Navigate to your URL on a browser to check your application. It should look like this:
You can then begin to add and delete tasks (as needed) on your deployed application.
Self-service developer platform is all about creating a frictionless development process, boosting developer velocity, and increasing developer autonomy. Learn more about self-service platforms and why it’s important.
Explore how you can get started with GitOps using Weave GitOps products: Weave GitOps Core and Weave GitOps Enterprise. Read more.
More and more businesses are adopting GitOps. Learn about the 5 reasons why GitOps is important for businesses.
Implement the proper governance and operational excellence in your Kubernetes clusters.
Comments and Responses