Monday, December 30, 2019

Deploying Nginx + PHP + git-sync on Kubernetes

In my previous post, I explained how to setup a simple nginx instance that could be used to sync to a private Git repo. The only drawback is that this setup will only serve static pages. What if you wanted to be able to run a server with dynamic code like PHP? I'm glad you asked! In this post, we'll update our config to include a php-fpm instance to allow us to serve PHP pages.

I have planned these articles out so that they build on each other. With that in mind, I'm assuming you have followed my articles to date and therefore we'll be simply extended the current deployment.

If you're impatient like me, just scroll to the bottom and download the full files.

Setting Up The PHP-FPM Instance

First we need to get our PHP-FPM yaml setup. By default, php-fpm runs on port 9000. This means we need a service definition to expose this to the cluster. This will also need access to the git repo we created so we'll add in the git container spec. Instead of running the nginx image, we'll run the php-fpm image. In order to make life easy on ourselves, I'm going to use the webserver.yaml from my previous post as a template. I'm going to make the following changes to it:

  1. Replace any reference of "webserver" with "phpfpm".
  2. Change the following in the service definition
    1. change the port name from http to phpfpm
    2. change the port number from 80 to 9000
  3. Remove the ConfigMap
    1. Remove the definition of it from the top of the file
    2. Remove the references to it in the spec volumes and the container volumeMounts
  4. Change the image of the second container from nginx:latest to php:fpm
  5. Change the containerPort from 80 to 9000
If we've done this all correctly, we should have a yaml that looks similar to the below:

apiVersion: v1

kind: Service
metadata:
  name: phpfpm
  labels:
    tier: backend
spec:
  selector:
    app: phpfpm
    tier: backend
  ports:  
  - name: phpfpm
    port: 9000
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: phpfpm
  labels:
    tier: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: phpfpm
      tier: backend
  template:
    metadata:
      labels:
        app: phpfpm
        tier: backend
    spec:
      securityContext:
        fsGroup: 65533 # to make SSH key readable
      volumes:
      - name: dir
        emptyDir: {}
      - name: git-secret
        secret:
          secretName: github-creds
          defaultMode: 288
      containers:
      - env:
        - name: GIT_SYNC_REPO
          value: git@github.com:<some user>/mysamplerepo.git
        - name: GIT_SYNC_BRANCH
          value: master
        - name: GIT_SYNC_SSH
          value: "true"
        - name: GIT_SYNC_PERMISSIONS
          value: "0777"
        - name: GIT_SYNC_DEST
          value: www
        - name: GIT_SYNC_ROOT
          value: /git
        name: git-sync
        image: k8s.gcr.io/git-sync:v3.1.1
        securityContext:
          runAsUser: 65533 # git-sync user
        volumeMounts:
        - name: git-secret
          mountPath: /etc/git-secret
        - name: dir
          mountPath: /git
      - name: phpfpm
        image: php:fpm
        ports:
        - containerPort: 9000
        volumeMounts:
        - name: dir
          mountPath: /usr/share/nginx

We can now save this yaml and apply it to our cluster:

# kubectl apply -f phpfpm.yaml 
service/phpfpm unchanged

deployment.apps/phpfpm configured

Assuming all went well, we should now have our webserver and phpfpm containers up and running:

# kubectl get pod
NAME                         READY   STATUS    RESTARTS   AGE
phpfpm-b46969c5f-zzh6d       2/2     Running   0          103s

webserver-8fb84dc86-7xw4w    2/2     Running   0          10s

That's just lovely but what next?

Configuring Nginx for PHP

At this point, we basically have two unassociated containers that are living independently in the same cluster. The only common bond is that they have the same set of files synched from the Git Repo. Next, we need to tell nginx to handle PHP requests and where to send them. This will require us to update our Nginx configMap. We do this by adding a location statement to handle php files like so:


      location ~ .php$ {
          try_files $uri =404;
          fastcgi_split_path_info ^(.+.php)(/.+)$;
          fastcgi_pass phpfpm:9000;
          fastcgi_index index.php;
          include fastcgi_params;
          fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
          fastcgi_param PATH_INFO $fastcgi_path_info;
      }

There's lots going on here in this file but some important items to note. Nginx acts like a reverse proxy when handling PHP files. It simply takes the request and sends to php-fpm. The php-fpm service finds the request file locally, executes PHP on it, and sends the resulting processed output from PHP back to Nginx. Here is the full updated configMap:


apiVersion: v1
kind: ConfigMap
metadata:
  name: webserver-config
  labels:
    tier: backend
data:
  config :
    server {
        listen       80;
        server_name  localhost;

        location / {
            root   /usr/share/nginx/www/html;
            index  index.html index.htm;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   /usr/share/nginx/www/html;
        }

      location ~ .php$ {
          root /usr/share/nginx/www/html;
          try_files $uri =404;
          fastcgi_split_path_info ^(.+.php)(/.+)$;
          fastcgi_pass phpfpm:9000;
          fastcgi_index index.php;
          include fastcgi_params;
          fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
          fastcgi_param PATH_INFO $fastcgi_path_info;
      }

    }

Let's apply this to our cluster:


# kubectl apply -f configmap.yaml 
configmap/webserver-config configured

With the new configuration running, we'll need Nginx to reload it. There's a number of different ways we could do this but I'm going to use a hack that will allow us to test the config and then restart. First step, I want to make sure the new config will work for us:


# kubectl exec -it webserver-8fb84dc86-7xw4w -c webserver -- /usr/sbin/nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

It looks like the configuration is acceptable so let's reload Nginx.


# kubectl exec -it webserver-8fb84dc86-7xw4w -c webserver -- /usr/sbin/nginx -s reload
2019/12/28 14:01:32 [notice] 2804#2804: signal process started

We should now be ready to commit a PHP file to our repo and test.

Testing Our Configuration

Let's create a simple PHP file in the html directory of our. 


We'll jump onto the web server, install curl and test:


# kubectl exec -it webserver-8fb84dc86-7xw4w -c webserver -- /bin/bash
root@webserver-8fb84dc86-7xw4w:/# apt update
Hit:1 http://deb.debian.org/debian buster InRelease
Hit:2 http://deb.debian.org/debian buster-updates InRelease
Hit:3 http://security-cdn.debian.org/debian-security buster/updates InRelease
Reading package lists... Done
Building dependency tree       
Reading state information... Done
All packages are up to date.
root@webserver-8fb84dc86-7xw4w:/# apt install curl
Reading package lists... Done
Building dependency tree       
Reading state information... Done
curl is already the newest version (7.64.0-4).
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
root@webserver-8fb84dc86-7xw4w:/# curl localhost/index.php
hello world from php

A great question to ask is how does php-fpm know which file and where that file exists? Like I said, great question.

This is handled by the fastcgi_param SCRIPT_FILENAME entry. This means that Nginx is going to tell php-fpm that it should try to load the $document_root$fastcgi_script_name file for the request. If you look at our configMap, we define document root as /usr/share/nginx/www/html. Assuming a request comes for index.php into Nginx, Nginx will tell php-fpm to also load /usr/share/www/html/index.php. In an environment where Nginx + PHP live on the same host, this doesn't appear to be a problem because that file will exist for sure. In our configuration, we running two separate hosts aka containers. So we need to make sure the file exists on both servers in the same location. That's the easy part! It does! Reason being, we're using gitsynch on both containers and mounting that synched directory to the same location!

Full Working Configs

In case you want to just cheat and load the configurations, feel free to download them and play around:

No comments:

Post a Comment

Automatically Rebuild Image on Docker Hub

This post focuses on me being lazy. In the previous post , I talked about building a custom image and posting it to the Docker Hub. I have a...