Symfony4 Kubernetes Local Development Environment #5 Postgresql, HTTPS, Xdebug

In part4 we configured Helm so that we can easily deploy a local configuration (which allows for local development) and a production one.

So far our example construct doesn’t have a database, so we will add a Postgresql one. Also we will configure Nginx to work with HTTPS and setup Xdebug. It’s gonna be a long one, so let’s begin!

How does this article work?

As always, use repo git@github.com:wuestkamp/kubernetes-local-dev.git and branch part4 then follow along these steps, or checkout branch part5 where this all has been done already.

1. PostgreSQL Database

Add Postgresql via Helm

We can install latest postgresql like this:

mkdir infrastructure/helm/sf/charts
helm fetch stable/postgresql -d infrastructure/helm/sf/charts

“stable” here is the default repo, see all with helm repo list. Now there should be a new file infrastructure/helm/sf/charts/postgresql-3.13.1.tgz which contains the postgresql helm chart. Now let’s add a new file infrastructure/helm/sf/requirements.yaml:

dependencies:
- name: postgresql
version: 3.13.1
repository: https://kubernetes-charts.storage.googleapis.com/
condition: postgresql.enabled

Configure Postgresql chart with values

Let’s add a few more parameters to our values.yaml so that it looks like this:

replicas: 1
environment: dev

php:
symfony:
appEnv:
dev
databaseUrl: pgsql://pg:abcdef@sf-postgresql:5432/symfony
image: localhost:5000/wuestkamp_php:latest
imagePullPolicy: Always

nginx:
image:
localhost:5000/wuestkamp_nginx:latest
imagePullPolicy: Always

postgresql:
enabled:
true
postgresqlUsername: pg
postgresqlPassword: abcdef
postgresqlDatabase: symfony
persistence:
enabled:
true

Configure Symfony to use Postgresql

In our sf-deployment.yaml we define the DATABASE_URL env variable with value from values.yaml:

# the php container
- image: {{ .Values.php.image }}
imagePullPolicy: {{ .Values.php.imagePullPolicy }}
name: php
env:
- name: APP_ENV
value: {{ .Values.php.symfony.appEnv }}
- name: DATABASE_URL
value: {{ .Values.php.symfony.databaseUrl }}

In symfony/config/packages/doctrine.yaml we need to switch to Postgresql:

doctrine:
dbal:
# configure these for your database server
driver: 'pdo_pgsql'
url: '%env(resolve:DATABASE_URL)%'

We need to “allow” the new env variable DATABASE_URL to be passed to our php fpm instance by adding it to infrastructure/php-fpm/symfony.pool.conf:

; Make specific Docker environment variables available to PHP
env[APP_ENV] = $APP_ENV
env[DATABASE_URL] = $DATABASE_URL ;I am the new line here!
env[PHP_POD_NAME] = $PHP_POD_NAME

Changes on PHP-FPM Dockerfile

I did change infrastructure/php-fpm/Dockerfile quite a bit:

Test entity

Now create a simple entity at symfony/src/Entity/Product.php.

<?php

declare

(strict_types=1);

namespace AppEntity;

use DoctrineORMMapping as ORM;

/**
*
@ORMEntity()
*/
class Product
{
/**
*
@ORMId
* @ORMGeneratedValue
* @ORMColumn(type="integer")
*/
private $id;

/**
*
@ORMColumn(type="string", length=255)
*/
private $name;
}

Let’s try it out:

./run/down.sh
./run/build.sh
./run/up.sh prod

We should now see our Postgresql Kubernetes objects:

Postgresql might take some time to be ready. Then let’s create the database schema and test successful database connection:

kubectl exec -it sf-deployment-0.1.0–7b68999859–7f6jf -c php bin/console doctrine:schema:validate
kubectl exec -it sf-deployment-0.1.0–7b68999859–7f6jf -c php bin/console doctrine:schema:create

Manually test Postgresql connection

If you have issues with Postgresql authentication from Symfony you can test it manually using pgsql:

kubectl exec -it sf-deployment-768b866c6c-7b6pq -c php -- psql -h HOSTNAME DBNAME USERNAME

Connect to Postgresql from localhost

To use tools like SQLPro for Prostgres you simply need to run port forwarding with kubectl:

kubectl port-forward sf-postgresql-0 5432:5432

Then you can connect to localhost:5432 to reach the Postgres server. I simply created yet another script ./run/pg_tunnel.sh which executes this command.

2. Nginx & SSL

We will now configure Nginx to work with HTTP and HTTPS.

Generate Certificates

We will generate test certificates. But we could also use existing ones. Create directory infrastructure/certs/nginx. In that directory run:

openssl req -x509 -new -newkey rsa:2048 -nodes -keyout server.key -out server.csr

We make sure that entry /infrastructure/certs/nginx/* is in .gitignore and we create a .gitkeep in that folder as well:

Create Kubernetes secret

Then we can create a Kubernetes secret:

kubectl create secret tls nginxsecret --key infrastructure/certs/nginx/server.key --cert infrastructure/certs/nginx/server.csr

We can also adjust our script ./run/up.sh to create that secret:

#!/usr/bin/env bash
kubectl create secret tls nginxsecret --key infrastructure/certs/nginx/server.key --cert infrastructure/certs/nginx/server.csr
helm install --name sf infrastructure/helm/sf --set environment=${1:-dev} --set php.symfony.appEnv=${1:-dev}

Nginx container should mount certificates

We need to change the sf-deployment.yaml to:

  1. open container port 443
  2. create a secret volume for the pod for secret nginxsecret
  3. mount that secret volume in nginx container to /etc/nginx/ssl

The result could look like this:

...
volumes:
- name: secret-volume
secret:
secretName:
nginxsecret
...

containers:
# the nginx container, it mounts the shared volume to provide assets directly via http response
- image: {{ .Values.nginx.image }}
imagePullPolicy: {{ .Values.nginx.imagePullPolicy }}
name: nginx
ports:
- containerPort: 80
- containerPort: 443
volumeMounts:
- mountPath: /etc/nginx/ssl
name: secret-volume
readOnly: true
...

See full file here. This means we will always require SSL certificates even when in development environment.

Configure Nginx virtual host config

We edit symfony_prod.conf to this:

server {
server_name _;
listen 80;
listen [::]:80;
listen 443 default_server ssl;
listen [::]:443 default_server ssl;

ssl_certificate /etc/nginx/ssl/tls.crt;
ssl_certificate_key /etc/nginx/ssl/tls.key;

root /var/www/symfony/public;

location / {
try_files $uri @rewriteapp;
}

location @rewriteapp {
rewrite ^(.*)$ /index.php/$1 last;
}

location ~ ^/index.php(/|$) {
fastcgi_pass 127.0.0.1:9000;
fastcgi_split_path_info ^(.+.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTPS off;
}

error_log /var/log/nginx/symfony_error.log;
access_log /var/log/nginx/symfony_access.log;
}

I decided to have one virtual host for http/https and then configure http->https redirect later on domain level if needed.

Adjust service

We want to add port 443 to the service in sf-service.yaml:

apiVersion: v1
kind: Service
metadata:
name:
sf-service
spec:
ports:
- name: "80"
port: 80
targetPort: 80
- name: "443"
port: 443
targetPort: 443
selector:
id:
{{ template "pod.id" . }} # label of our pod
type: LoadBalancer

Test this!

We run

./run/down.sh
./run/build.sh
./run/up.sh prod

Call http://localhost in browser which should work like before.

Now call https://localhost which should work after allowing the self signed certificate.

3. Xdebug

We will configure Xdebug for remote debugging our Symfony code.

Test connection from Kubernetes container to your local machine

Start your xdebug listener in your IDE, get your local IP with ifconfig and then run:

kubectl exec -it sf-deployment-0.1.0–5c96866464-rjwtn -c php — nc 192.168.0.234 9000 -vvv

I configured my PhpStorm debugger instance to listen on port 9000 for remote connections. On OSX I had no issues so I continued.

Setup xdebug config

We do already have the file infrastructure/php-fpm/xdebug.ini, change it to:

zend_extension=xdebug.so

[Xdebug]
xdebug.remote_enable=true
xdebug.remote_port=9000
xdebug.remote_host=192.168.0.234 ;replace your IP

Now run

./run/down.sh
./run/build.sh
./run/up.sh prod

Test xdebug

I use Xdebug Helper for Chrome to enable debugging for requests. Now we set a breakpoint in file symfony/public/index.php, enable the Xdebug helper and reload the page.

Awesome! But make sure to define correct mapping of remote and local filesystems:

Enable Xdebug via Helm value

We might not want to enable Xdebug in production as it would slow things down. To achieve this we will pass the xdebug.ini to our container using Kubernetes. For this we change comment the xdebug line in infrastructure/php-fpm/Dockerfile:

ADD $build_path/symfony.ini /etc/php7/conf.d/
ADD
$build_path/symfony.ini /etc/php7/cli/conf.d/
#ADD $build_path/xdebug.ini /etc/php7/conf.d/

RUN rm /etc/php7/php-fpm.d/www.conf
ADD $build_path/symfony.pool.conf /etc/php7/php-fpm.d/

Next we create a new Kubernetes ConfigMap which contains the xdebug.ini:

kubectl create configmap php-xdebug-config --from-file infrastructure/php-fpm/xdebug.ini

We add this command also in the ./run/up.sh.

Now we add a new line to the values.yaml:

...
php:
...
xdebug: true

Next we add a new volume and volume mount to the sf-deployment.yaml, but just when in dev environment:

volumes:
- name: secret-volume
secret:
secretName:
nginxsecret
{{ if ne .Values.environment "dev" }}
- name: sf-public
emptyDir: {}
{{ end }}
{{ if eq .Values.php.xdebug true }}
- name: php-xdebug-config
configMap:
name:
php-xdebug-config
{{ end }}
...
containers:
...
# the php container
- image: {{ .Values.php.image }}
...
volumeMounts:
{{ if ne .Values.environment "dev" }}
- mountPath: /var/www/symfony/public
name: sf-public
{{ end }}
{{ if eq .Values.php.xdebug true }}
- mountPath: /etc/php7/conf.d/xdebug.ini
name: php-xdebug-config
subPath: xdebug.ini
{{ end }}

Check the whole file here. When doing these changes its nice to test the rendered template by Helm with:

helm install --dry-run --debug infrastructure/helm/sf --set environment=prod

This allows us to mount a single file from a volume to a single file of the local container path.

No hard coded IP

Currently our xdebug.ini contains the hard coded IP which is not cool. We want to control this via Helm as well. For this we can use Kubernetes spec.hostAliases. For this we add another line in values.yaml:

replicas: 1
environment: dev
localhost: 192.168.0.234

In xdebug.ini we change the hard coded IP to a name:

zend_extension=xdebug.so

[Xdebug]
xdebug.remote_enable=true
xdebug.remote_port=9000
xdebug.remote_host=docker-host.localhost

Now we set the name docker-host.localhost to our ip in sf-deployment.yaml:

...
spec:
restartPolicy:
Always
hostAliases:
- ip: {{ .Values.localhost }}
hostnames:
- "docker-host.localhost"
...

Now we need to delete the existing ConfigMap with kubectl delete configmap php-xdebug-config. Next call of ./run/up.sh will create it again.

Test it all

Now Xdebug is enabled in dev but disabled in prod via Helm values. To test we can call ./run/up.sh or ./run/up.sh prod and the run:

kubectl exec -it sf-deployment-0.1.0–5c96866464-kx6mr -c php — cat /etc/php7/conf.d/xdebug.ini

We should see that in prod the content of xdebug.ini is simply:

; Uncomment to enable this extension.
;zend_extension=xdebug.so

Let’s test the whole development workflow:

./run/down.sh
./run/build.sh
./run/up.sh
./run/ksync.sh

We are now able to edit and debug code like working just locally.

Recap

We configured Postgresql,HTTPS for Nginx and Xdebug. To note though is that with current configuration it’s now mandatory to configure at least self signed ssl certificates to work, else the pod will throw an error if there is no secret to mount. But I guess its good to force people to ssl O:)