Skip to content

Commit

Permalink
Merge pull request #666 from jcmoraisjr/jm-external-haproxy
Browse files Browse the repository at this point in the history
WIP: Add option to run an external haproxy instance
  • Loading branch information
jcmoraisjr authored Sep 27, 2020
2 parents a0b9476 + ed1c798 commit 7a9a4db
Show file tree
Hide file tree
Showing 25 changed files with 964 additions and 46 deletions.
24 changes: 24 additions & 0 deletions docs/content/en/docs/configuration/command-line.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ The following command-line options are supported:
| [`--ignore-ingress-without-class`](#ignore-ingress-without-class)| [true\|false] | `false` | v0.10 |
| [`--ingress-class`](#ingress-class) | name | `haproxy` | |
| [`--kubeconfig`](#kubeconfig) | /path/to/kubeconfig | in cluster config | |
| [`--master-socket`](#master-socket) | socket path | use embedded haproxy | v0.12 |
| [`--max-old-config-files`](#max-old-config-files) | num of files | `0` | |
| [`--profiling`](#stats) | [true\|false] | `true` | |
| [`--publish-service`](#publish-service) | namespace/servicename | | |
Expand Down Expand Up @@ -155,6 +156,29 @@ is deployed outside of the Kubernetes cluster.

---

## --master-socket

Since v0.12

Configures HAProxy Ingress to use an external haproxy deployment in master-worker mode. This option
receives the unix socket of the master CLI. The default value is an empty string, which will
instruct the controller to start and manage the embedded haproxy instead of an external instance.

The following conditions should be satisfied in order to an external haproxy work properly:

1. The following paths should be shared between HAProxy Ingress and the external haproxy: `/etc/haproxy`, `/var/lib/haproxy`, `/var/run/haproxy`. HAProxy Ingress must have write access to all of them, external haproxy should have write access to `/var/run/haproxy`. This can be made using a sidecar container and k8s' emptyDir, or a remote file system provided that it updates synchronously and supports unix sockets
1. Start the external haproxy with:
* `-S /var/run/haproxy/master.sock,mode,600`. `mode 600` isn't mandatory but recommended;
* `-f /etc/haproxy`
1. HAProxy Ingress image has a `--init` command-line option which creates an initial valid configuration file, this allows the external haproxy to bootstraps successfully. This option can be used as an init container.

See also:

* [example]({{% relref "../examples/external-haproxy" %}}) page.
* [External]({{% relref "keys#external" %}}) and [Master-worker]({{% relref "keys#master-worker" %}}) configuration keys

---

## --max-old-config-files

Everytime a configuration change need to update HAProxy, a configuration file is rewritten even if
Expand Down
69 changes: 66 additions & 3 deletions docs/content/en/docs/configuration/keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,10 @@ The table below describes all supported configuration keys.
| [`drain-support`](#drain-support) | [true\|false] | Global | `false` |
| [`drain-support-redispatch`](#drain-support) | [true\|false] | Global | `true` |
| [`dynamic-scaling`](#dynamic-scaling) | [true\|false] | Backend | `true` |
| [`external-has-lua`](#external) | [true\|false] | Global | `false` |
| [`forwardfor`](#forwardfor) | [add\|ignore\|ifmissing] | Global | `add` |
| [`fronting-proxy-port`](#fronting-proxy-port) | port number | Global | 0 (do not listen) |
| [`groupname`](#security) | haproxy group name | Global | `haproxy` |
| [`headers`](#headers) | multiline header:value pair | Backend | |
| [`health-check-addr`](#health-check) | address for health checks | Backend | |
| [`health-check-fall-count`](#health-check) | number of failures | Backend | |
Expand All @@ -171,6 +173,7 @@ The table below describes all supported configuration keys.
| [`limit-rps`](#limit) | rate per second | Backend | |
| [`limit-whitelist`](#limit) | cidr list | Backend | |
| [`load-server-state`](#load-server-state) (experimental) |[true\|false] | Global | `false` |
| [`master-exit-on-failure`](#master-worker) | [true\|false] | Global | `true` |
| [`max-connections`](#connection) | number | Global | `2000` |
| [`maxconn-server`](#connection) | qty | Backend | |
| [`maxqueue-server`](#connection) | qty | Backend | |
Expand Down Expand Up @@ -249,6 +252,7 @@ The table below describes all supported configuration keys.
| [`use-htx`](#use-htx) | [true\|false] | Global | `false` |
| [`use-proxy-protocol`](#proxy-protocol) | [true\|false] | Global | `false` |
| [`use-resolver`](#dns-resolvers) | resolver name | Backend | |
| [`username`](#security) | haproxy user name | Global | `haproxy` |
| [`var-namespace`](#var-namespace) | [true\|false] | Host | `false` |
| [`waf`](#waf) | "modsecurity" | Backend | |
| [`waf-mode`](#waf) | [deny\|detect] | Backend | `deny` (if waf is set) |
Expand Down Expand Up @@ -941,6 +945,27 @@ See also:

---

## External

| Configuration key | Scope | Default | Since |
|--------------------|----------|---------|-------|
| `external-has-lua` | `Global` | `false` | v0.12 |

Defines features that can be found in the external haproxy deployment, if an
external deployment is used. These options have no effect if using the embedded
haproxy.

* `external-has-lua`: Define as true if the external haproxy has Lua libraries
installed in the operating system. Currently only [OAuth](#oauth) needs Lua
socket installed and will not work if `external-has-lua` is not enabled.

See also:

* [OAuth](#oauth) configuration keys.
* [master-socket]({{% relref "command-line#master-socket" %}}) command-line option

---

## Forwardfor

| Configuration key | Scope | Default | Since |
Expand Down Expand Up @@ -1180,6 +1205,24 @@ See also:

---

## Master-worker

| Configuration key | Scope | Default | Since |
|--------------------------|----------|---------|-------|
| `master-exit-on-failure` | `Global` | `true` | v0.12 |

Configures master-worker related options.

* `master-exit-on-failure`: If `true`, kill all the remaining workers and exit
from master in the case of an unexpected failure of a worker, eg a segfault.

See also:

* https://cbonte.github.io/haproxy-dconv/2.0/configuration.html#3.1-master-worker
* [master-socket]({{% relref "command-line#master-socket" %}}) command-line option

---

## Modsecurity

| Configuration key | Scope | Default | Since |
Expand Down Expand Up @@ -1299,8 +1342,13 @@ on an ingress resource without oauth annotations. In other words, if two ingress
the same domain but only one has oauth annotations - the one that has at least the `oauth2_proxy`
service - all paths from that domain will be protected.

{{% alert title="Note" %}}
OAuth2 needs [`external-has-lua`](#external) enabled if running on an external haproxy deployment.
{{% /alert %}}

See also:

* [`external-has-lua`](#external) configuration key.
* [example](https://github.com/jcmoraisjr/haproxy-ingress/tree/master/examples/auth/oauth) page.

---
Expand Down Expand Up @@ -1432,15 +1480,26 @@ See also:

| Configuration key | Scope | Default | Since |
|--------------------|----------|---------|-------|
| `groupname` | `Global` | | v0.12 |
| `use-chroot` | `Global` | `false` | v0.9 |
| `use-haproxy-user` | `Global` | `false` | v0.9 |
| `username` | `Global` | | v0.12 |

Change security options.

* `use-chroot`: If `true`, configures haproxy to perform a `chroot()` in the empty and non-writable directory `/var/empty` during the startup process, just before it drops its own privileges. Only root can perform a `chroot()`, so HAProxy Ingress container should start as UID `0` if this option is configured as `true`. See the note below about `use-chroot` option limitations.
* `use-haproxy-user`: Defines if the haproxy's process should be changed to `haproxy`, UID `1001`. The default value `false` leaves haproxy running as root. Note that even running as root, haproxy always drops its own privileges before start its event loop.
* `username` and `groupname`: Changes the user and group names used to run haproxy as non root. The default value is an empty string, which means leave haproxy running as root. Note that even running as root, haproxy always drops its own privileges before start its event loop. Both options should be declared to the configuration take effect. Note that this configuration means "running haproxy as non root", it's only useful when the haproxy container starts as root.
* `use-chroot`: If `true`, configures haproxy to perform a `chroot()` in the empty and non-writable directory `/var/empty` during the startup process, just before it drops its own privileges. Only root can perform a `chroot()`, so HAProxy Ingress container should start as UID `0` if this option is configured as `true`. See **Using chroot()** section below.
* `use-haproxy-user`: If `true`, configures `username` and `groupname` configuration keys as `haproxy`. See `username` and `groupname` above. Note that this user and group exists in the embedded haproxy, and should exist in the external haproxy if used. In the case of a conflict, `username` and `groupname` declaration will have priority and `use-haproxy-user` will be ignored. If `false`, the default value, user and group names will not be changed.

**Starting as non root**

In the default configuration HAProxy Ingress container starts as root. Since v0.9 it's also possible to configure the container to start as `haproxy` user, UID `1001`.

If using the embedded haproxy, read the [Security considerations](http://cbonte.github.io/haproxy-dconv/2.0/management.html#13) from HAProxy doc before change the starting user.

In the default configuration, HAProxy Ingress container starts as root. Since v0.9 it's also possible to configure the container to start as `haproxy` user, UID `1001`. Read the [Security considerations](http://cbonte.github.io/haproxy-dconv/1.9/management.html#13) from HAProxy doc before change the starting user. The starting user can be changed in the deployment or daemonset's pod template using the following configuration:
If using an external haproxy, configures the pod's securityContext (instead of the container's one) which will make Kubernetes create the shared file system with write access, so the controller can create and update configuration, maps and certificate files.

The starting user can be changed in the deployment or daemonset's pod template using the following configuration:

```yaml
...
Expand All @@ -1452,6 +1511,10 @@ In the default configuration, HAProxy Ingress container starts as root. Since v0

Note that ports below 1024 cannot be bound if the container starts as non-root.

**Using chroot()**

Beware of some chroot limitations:

{{% alert title="Note" %}}
HAProxy does not have access to the file system after configure a `chroot()`. Unix sockets located outside the chroot directory are used in the following conditions:

Expand Down
135 changes: 135 additions & 0 deletions docs/content/en/docs/examples/external-haproxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
---
title: "External haproxy"
linkTitle: "External haproxy"
weight: 20
description: >
Demonstrate how to configure HAProxy Ingress to use an external haproxy deployment.
---

{{% pageinfo %}}
This is a `v0.12` example and need HAProxy Ingress `v0.12-snapshot.1` or above
{{% /pageinfo %}}

This example demonstrates how to configure HAProxy Ingress to manage an external
haproxy instance deployed as a sidecar container. This approach decouple the
controller and the running haproxy version, allowing the sysadmin to update any
of them independently of the other.

## Prerequisites

This document has the following prerequisite:

* A Kubernetes cluster with a running HAProxy Ingress controller v0.12 or above.
Follow The five minutes deployment in the [getting started]({{% relref "/docs/getting-started" %}}) guide.
* A running and exposed application in the Kubernetes cluster, this getting started
[deployment]({{% relref "/docs/getting-started#try-it-out" %}}) does the job.

## Update the deployment

The following instruction patches the current HAProxy Ingress daemonset (this will also revert the command-line arguments to the default value):

```
$ kubectl --namespace ingress-controller patch daemonset haproxy-ingress \
-p "$(curl -sSL https://haproxy-ingress.github.io/v0.12/docs/examples/external-haproxy/daemonset-patch.yaml)"
```

Check if the controller restarts without any problem:

```
$ kubectl --namespace ingress-controller get pod -w
```

## What was changed?

The sections below have details of what changed in the deployment.

### Sidecar

This example configures 2 (two) new containers in the controllers' pod:

* `haproxy` is the external haproxy deployment with two mandatory arguments: `-S` with the master CLI unix socket, and `-f` with the configuration files path
* `init`, a Kubernetes' [initContainer](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/) used to create an initial and valid haproxy configuration.

The `haproxy` container references the official Alpine based image `haproxy:alpine`,
but can be any other customized image. The only requisite is to be 2.0 or newer due
to some new keywords used by HAProxy Ingress.

The `init` container just copy a minimum and valid `haproxy.cfg`. This file is used
to properly starts haproxy and configures its master CLI that HAProxy Ingress uses
to manage the instance.

A new command-line `--master-socket` was also added to the HAProxy Ingress container.
This option enables an external haproxy instance, pointing to the unix socket path
of its master CLI.

### Shared filesystem

HAProxy Ingress sends configuration files to the haproxy instance using a shared
filesystem. A Kubernetes' [`emptyDir`](https://kubernetes.io/docs/concepts/storage/volumes/#emptydir)
works well.

The following directories must be shared:

* `/etc/haproxy`: configuration and map files - `init` and `haproxy-ingress` need write access, `haproxy` need read access.
* `/var/lib/haproxy`: mostly ssl related files - `haproxy-ingress` need write access, `haproxy` need read access.
* `/var/run/haproxy`: unix sockets - `haproxy-ingress` and `haproxy` need write access.

### Liveness probe

Default HAProxy Ingress deployment has a liveness probe to an haproxy's health
check URI. The patch of this example moves the liveness probe from the HAProxy
Ingress container to the haproxy one.

## Test

Open two distinct terminals to follow `haproxy-ingress` and `haproxy` logs:

```
$ kubectl --namespace ingress-controller get pod
NAME READY STATUS RESTARTS AGE
haproxy-ingress-6bsvz 3/3 Running 0 17m
$ kubectl --namespace ingress-controller logs -f haproxy-ingress-6bsvz -c haproxy-ingress
```

and

```
$ kubectl --namespace ingress-controller logs -f haproxy-ingress-6bsvz -c haproxy
```

Update syslog configuration using another terminal, this will make haproxy use stdout:

```
$ kubectl --namespace ingress-controller patch configmap haproxy-ingress -p '{"data":{"syslog-endpoint":"stdout","syslog-format":"raw"}}'
```

Do some `curl` to an exposed application deployed in the cluster:

```
$ kubectl get ing
NAME HOSTS ADDRESS PORTS AGE
nginx nginx.192.168.1.11.nip.io 80, 443 21m
$ curl nginx.192.168.1.11.nip.io
```

During the ConfigMap update and the endpoint calls, HAProxy Ingress and the external
haproxy should be logging its own events:

`haproxy-ingress` container:

```
I0921 16:06:12.699201 6 controller.go:314] starting haproxy update id=8
I0921 16:06:12.710723 6 instance.go:322] updating 1 host(s): [*.sub.t002.app.domain]
I0921 16:06:12.710752 6 instance.go:339] updating 1 backend(s): [0_echoserver_8080]
I0921 16:06:12.726794 6 instance.go:387] updated main cfg and 1 backend file(s): [002]
I0921 16:06:12.788496 6 instance.go:301] haproxy successfully reloaded (external)
I0921 16:06:12.790696 6 controller.go:346] finish haproxy update id=8: parse_ingress=1.323253ms write_maps=10.101055ms write_config=16.320063ms reload_haproxy=63.587362ms total=91.331733ms
```

`haproxy` container:

```
192.168.100.1:58167 [21/Sep/2020:16:06:15.003] _front_https~ t015_echoserver_8080/srv001 0/0/2/0/2 200 485 - - ---- 1/1/0/0/0 0/0 "GET https://t004.app.domain/ HTTP/2.0"
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
spec:
template:
spec:
containers:
- name: haproxy-ingress
image: quay.io/jcmoraisjr/haproxy-ingress:v0.12-snapshot.1
args:
- --configmap=ingress-controller/haproxy-ingress
- --master-socket=/var/run/haproxy/master.sock
livenessProbe: null
volumeMounts:
- mountPath: /etc/haproxy
name: etc
- mountPath: /var/lib/haproxy
name: lib
- mountPath: /var/run/haproxy
name: run
- name: haproxy
image: haproxy:alpine
args:
- -W
- -S
- /var/run/haproxy/master.sock,mode,600
- -f
- /etc/haproxy
livenessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10253
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
volumeMounts:
- mountPath: /etc/haproxy
name: etc
- mountPath: /var/lib/haproxy
name: lib
- mountPath: /var/run/haproxy
name: run
initContainers:
- name: init
image: quay.io/jcmoraisjr/haproxy-ingress:v0.12-snapshot.1
args:
- --init
volumeMounts:
- mountPath: /etc/haproxy
name: etc
volumes:
- emptyDir: {}
name: etc
- emptyDir: {}
name: lib
- emptyDir: {}
name: run
3 changes: 2 additions & 1 deletion pkg/common/ingress/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ type GenericController struct {

// Configuration contains all the settings required by an Ingress controller
type Configuration struct {
Client clientset.Interface
Client clientset.Interface
MasterSocket string

RateLimitUpdate float32
ResyncPeriod time.Duration
Expand Down
5 changes: 5 additions & 0 deletions pkg/common/ingress/controller/launch.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ func NewIngressController(backend ingress.Controller) *GenericController {
ingressClass = flags.String("ingress-class", "",
`Name of the ingress class to route through this controller.`)

masterSocket = flags.String("master-socket", "",
`Defines the master CLI unix socket of an external HAProxy running in master-worker mode.
Defaults to use the embedded HAProxy if not declared.`)

configMap = flags.String("configmap", "",
`Name of the ConfigMap that contains the custom configuration to use`)

Expand Down Expand Up @@ -296,6 +300,7 @@ func NewIngressController(backend ingress.Controller) *GenericController {
UpdateStatus: *updateStatus,
ElectionID: *electionID,
Client: kubeClient,
MasterSocket: *masterSocket,
AcmeServer: *acmeServer,
AcmeCheckPeriod: *acmeCheckPeriod,
AcmeElectionID: *acmeElectionID,
Expand Down
Loading

0 comments on commit 7a9a4db

Please sign in to comment.