Skip to content

Commit 1d4a2b4

Browse files
committed
v0.1.2: add Kubernetes resource requests/limits; refactor into HairpinProxyController class for readability
1 parent 29b04f6 commit 1d4a2b4

File tree

3 files changed

+66
-41
lines changed

3 files changed

+66
-41
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ If you've had problems with ingress-nginx, cert-manager, LetsEncrypt ACME HTTP01
77
## One-line install
88

99
```shell
10-
kubectl apply -f https://raw.githubusercontent.com/compumike/hairpin-proxy/v0.1.1/deploy.yml
10+
kubectl apply -f https://raw.githubusercontent.com/compumike/hairpin-proxy/v0.1.2/deploy.yml
1111
```
1212

1313
If you're using [ingress-nginx](https://kubernetes.github.io/ingress-nginx/) and [cert-manager](https://github.com/jetstack/cert-manager), it will work out of the box. See detailed installation and testing instructions below.
@@ -46,10 +46,10 @@ None of these are particularly easy without modifying upstream packages, and the
4646

4747
## The hairpin-proxy Solution
4848

49-
1. hairpin-proxy intercepts and modifies cluster-internal DNS lookups for hostnames that are served by your ingress controller, pointing them to the IP of an internal `hairpin-proxy-haproxy` service instead. (This is managed by `hairpin-proxy-controller`, which simply watches the Kubernetes API for new/modified Ingress resources, examines their `spec.tls.hosts`, and updates the CoreDNS ConfigMap when necessary.)
49+
1. hairpin-proxy intercepts and modifies cluster-internal DNS lookups for hostnames that are served by your ingress controller, pointing them to the IP of an internal `hairpin-proxy-haproxy` service instead. (This DNS redirection is managed by `hairpin-proxy-controller`, which simply polls the Kubernetes API for new/modified Ingress resources, examines their `spec.tls.hosts`, and updates the CoreDNS ConfigMap when necessary.)
5050
2. The internal `hairpin-proxy-haproxy` service runs a minimal HAProxy instance which is configured to append the PROXY line and forward the traffic on to the internal ingress controller.
5151

52-
As a result, when pod in your cluster (such as cert-manager) try to access http://your-site/, they resolve to the hairpin-proxy, which adds the PROXY line and sends it to your `ingress-nginx`. The NGINX parses the PROXY protocol just as it would if it had come from an external load balancer, so it sees a valid request and handles it identically to external requests.
52+
As a result, when pods in your cluster (such as cert-manager) try to access http://your-site/, they resolve to the hairpin-proxy, which adds the PROXY line and sends it to your `ingress-nginx`. The NGINX parses the PROXY protocol just as it would if it had come from an external load balancer, so it sees a valid request and handles it identically to external requests.
5353

5454
## Installation and Testing
5555

@@ -60,7 +60,7 @@ Let's suppose that `http://subdomain.example.com/` is served from your cluster,
6060
Get a shell within your cluster and try to access the site to confirm that it isn't working:
6161

6262
```shell
63-
k run my-test-container --image=alpine -it --rm -- /bin/sh
63+
kubectl run my-test-container --image=alpine -it --rm -- /bin/sh
6464
apk add bind-tools curl
6565
dig subdomain.example.com
6666
curl http://subdomain.example.com/
@@ -72,12 +72,12 @@ The `dig` should show the external load balancer IP address. The first `curl` sh
7272
### Step 1: Install hairpin-proxy in your Kubernetes cluster
7373

7474
```shell
75-
kubectl apply -f https://raw.githubusercontent.com/compumike/hairpin-proxy/v0.1.0/deploy.yml
75+
kubectl apply -f https://raw.githubusercontent.com/compumike/hairpin-proxy/v0.1.2/deploy.yml
7676
```
7777

7878
If you're using `ingress-nginx`, this will work as-is.
7979

80-
If you using an ingress controller other than `ingress-nginx`, you must change the `TARGET_SERVER` environment variable passed to the `hairpin-proxy-haproxy` container. It defaults to `ingress-nginx-controller.ingress-nginx.svc.cluster.local`, which specifies the `ingress-nginx-controller` Service within the `ingress-nginx` namespace. You can change this by editing the `hairpin-proxy-haproxy` Deployment and specifiying an environment variable:
80+
However, if you using an ingress controller other than `ingress-nginx`, you must change the `TARGET_SERVER` environment variable passed to the `hairpin-proxy-haproxy` container. It defaults to `ingress-nginx-controller.ingress-nginx.svc.cluster.local`, which specifies the `ingress-nginx-controller` Service within the `ingress-nginx` namespace. You can change this by editing the `hairpin-proxy-haproxy` Deployment and specifiying an environment variable:
8181

8282
```shell
8383
kubectl edit -n hairpin-proxy deployment hairpin-proxy-haproxy

deploy.yml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,15 @@ spec:
2323
app: hairpin-proxy-haproxy
2424
spec:
2525
containers:
26-
- image: compumike/hairpin-proxy-haproxy:0.1.1
26+
- image: compumike/hairpin-proxy-haproxy:0.1.2
2727
name: main
28+
resources:
29+
requests:
30+
memory: "100Mi"
31+
cpu: "10m"
32+
limits:
33+
memory: "200Mi"
34+
cpu: "50m"
2835

2936
---
3037

@@ -144,5 +151,12 @@ spec:
144151
runAsUser: 405
145152
runAsGroup: 65533
146153
containers:
147-
- image: compumike/hairpin-proxy-controller:0.1.1
154+
- image: compumike/hairpin-proxy-controller:0.1.2
148155
name: main
156+
resources:
157+
requests:
158+
memory: "50Mi"
159+
cpu: "10m"
160+
limits:
161+
memory: "100Mi"
162+
cpu: "50m"
Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,65 @@
11
#!/usr/bin/env ruby
22
# frozen_string_literal: true
33

4-
STDOUT.sync = true
5-
64
require "k8s-client"
5+
require "logger"
76

8-
def ingress_hosts(k8s)
9-
all_ingresses = k8s.api("extensions/v1beta1").resource("ingresses").list
10-
11-
all_tls_blocks = all_ingresses.map { |r| r.spec.tls }.flatten.compact
7+
class HairpinProxyController
8+
COREDNS_CONFIGMAP_LINE_SUFFIX = "# Added by hairpin-proxy"
9+
DNS_REWRITE_DESTINATION = "hairpin-proxy.hairpin-proxy.svc.cluster.local"
10+
POLL_INTERVAL = ENV.fetch("POLL_INTERVAL", "15").to_i.clamp(1..)
1211

13-
all_tls_blocks.map(&:hosts).flatten.compact.sort.uniq
14-
end
12+
def initialize
13+
@k8s = K8s::Client.in_cluster_config
1514

16-
def rewrite_coredns_corefile(cf, hosts)
17-
cflines = cf.strip.split("\n").reject { |line| line.strip.end_with?("# Added by hairpin-proxy") }
15+
STDOUT.sync = true
16+
@log = Logger.new(STDOUT)
17+
end
1818

19-
main_server_line = cflines.index { |line| line.strip.start_with?(".:53 {") }
20-
raise "Can't find main server line! '.:53 {' in Corefile" if main_server_line.nil?
19+
def fetch_ingress_hosts
20+
# Return a sorted Array of all unique hostnames mentioned in Ingress spec.tls.hosts blocks, in all namespaces.
21+
all_ingresses = @k8s.api("extensions/v1beta1").resource("ingresses").list
22+
all_tls_blocks = all_ingresses.map { |r| r.spec.tls }.flatten.compact
23+
all_tls_blocks.map(&:hosts).flatten.compact.sort.uniq
24+
end
2125

22-
rewrite_lines = hosts.map { |host| " rewrite name #{host} hairpin-proxy.hairpin-proxy.svc.cluster.local # Added by hairpin-proxy" }
26+
def coredns_corefile_with_rewrite_rules(original_corefile, hosts)
27+
# Return a String representing the original CoreDNS Corefile, modified to include rewrite rules for each of *hosts.
28+
# This is an idempotent transformation because our rewrites are labeled with COREDNS_CONFIGMAP_LINE_SUFFIX.
2329

24-
cflines.insert(main_server_line + 1, *rewrite_lines)
30+
# Extract base configuration, without our hairpin-proxy rewrites
31+
cflines = original_corefile.strip.split("\n").reject { |line| line.strip.end_with?(COREDNS_CONFIGMAP_LINE_SUFFIX) }
2532

26-
cflines.join("\n")
27-
end
33+
# Create rewrite rules
34+
rewrite_lines = hosts.map { |host| " rewrite name #{host} #{DNS_REWRITE_DESTINATION} #{COREDNS_CONFIGMAP_LINE_SUFFIX}" }
2835

29-
def main
30-
client = K8s::Client.in_cluster_config
36+
# Inject at the start of the main ".:53 { ... }" configuration block
37+
main_server_line = cflines.index { |line| line.strip.start_with?(".:53 {") }
38+
raise "Can't find main server line! '.:53 {' in Corefile" if main_server_line.nil?
39+
cflines.insert(main_server_line + 1, *rewrite_lines)
3140

32-
loop do
33-
puts "#{Time.now}: Fetching..."
41+
cflines.join("\n")
42+
end
3443

35-
hosts = ingress_hosts(client)
36-
cm = client.api.resource("configmaps", namespace: "kube-system").get("coredns")
44+
def main_loop
45+
@log.info("Starting main_loop with #{POLL_INTERVAL}s polling interval.")
46+
loop do
47+
@log.info("Polling all Ingress resources and CoreDNS configuration...")
48+
hosts = fetch_ingress_hosts
49+
cm = @k8s.api.resource("configmaps", namespace: "kube-system").get("coredns")
3750

38-
old_corefile = cm.data.Corefile
39-
new_corefile = rewrite_coredns_corefile(old_corefile, hosts)
51+
old_corefile = cm.data.Corefile
52+
new_corefile = coredns_corefile_with_rewrite_rules(old_corefile, hosts)
4053

41-
if old_corefile.strip != new_corefile.strip
42-
puts "#{Time.now}: Corefile changed!"
43-
puts new_corefile
54+
if old_corefile.strip != new_corefile.strip
55+
@log.info("Corefile has changed! New contents:\n#{new_corefile}\nSending updated ConfigMap to Kubernetes API server...")
56+
cm.data.Corefile = new_corefile
57+
@k8s.api.resource("configmaps", namespace: "kube-system").update_resource(cm)
58+
end
4459

45-
puts "#{Time.now}: Updating ConfigMap."
46-
cm.data.Corefile = new_corefile
47-
client.api.resource("configmaps", namespace: "kube-system").update_resource(cm)
60+
sleep(POLL_INTERVAL)
4861
end
49-
50-
sleep(15)
5162
end
5263
end
5364

54-
main
65+
HairpinProxyController.new.main_loop if $PROGRAM_NAME == __FILE__

0 commit comments

Comments
 (0)