├── img └── haproxy-healthcheck.png ├── docker-compose.yaml ├── docker-compose-final.yaml ├── square ├── go.mod ├── Dockerfile ├── pkg │ └── server │ │ ├── middleware.go │ │ ├── server.go │ │ └── handlers.go ├── deploy │ ├── square.yaml │ ├── kibana.yaml │ ├── prometheus.yaml │ ├── fluentd.yaml │ └── elastic.yaml ├── main.go └── go.sum ├── haproxy.cfg ├── haproxy-final.cfg └── README.md /img/haproxy-healthcheck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterj/square-service-gateway/HEAD/img/haproxy-healthcheck.png -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | haproxy: 4 | image: haproxy:1.7 5 | volumes: 6 | - ./:/usr/local/etc/haproxy:ro 7 | ports: 8 | - "5000:80" 9 | links: 10 | - square-service 11 | 12 | square-service: 13 | image: learncloudnative/square:0.1.0 14 | -------------------------------------------------------------------------------- /docker-compose-final.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | haproxy: 4 | image: haproxy:1.7 5 | volumes: 6 | - ./:/usr/local/etc/haproxy:ro 7 | ports: 8 | - "5000:80" 9 | - "8404:8404" 10 | links: 11 | - square-service 12 | 13 | square-service: 14 | image: learncloudnative/square:0.1.0 15 | -------------------------------------------------------------------------------- /square/go.mod: -------------------------------------------------------------------------------- 1 | module square 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/gorilla/mux v1.7.3 7 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 8 | github.com/modern-go/reflect2 v1.0.1 // indirect 9 | github.com/prometheus/client_golang v1.2.1 10 | github.com/sirupsen/logrus v1.4.2 11 | ) 12 | -------------------------------------------------------------------------------- /haproxy.cfg: -------------------------------------------------------------------------------- 1 | global 2 | maxconn 4096 3 | daemon 4 | 5 | defaults 6 | log global 7 | mode http 8 | 9 | timeout connect 10s 10 | timeout client 30s 11 | timeout server 30s 12 | 13 | frontend api_gateway 14 | bind 0.0.0.0:80 15 | 16 | default_backend be_square 17 | 18 | # Backend is called `be_square` 19 | backend be_square 20 | # There's only one instance of the server and it 21 | # points to the `square-service:8080` (name is from the docker-compose) 22 | server s1 square-service:8080 -------------------------------------------------------------------------------- /square/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the Go binary in this first image 2 | FROM golang:1.12.4-alpine AS builder 3 | 4 | RUN apk add --no-cache ca-certificates git 5 | WORKDIR /src 6 | 7 | COPY ./go.mod ./go.sum ./ 8 | RUN go mod download 9 | 10 | COPY ./ ./ 11 | 12 | # Build the Go binary 13 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ 14 | -ldflags="-w -s" \ 15 | -o /app . 16 | 17 | # Final image with binary 18 | FROM scratch 19 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs 20 | 21 | # Copy the built binary from the previous step 22 | COPY --from=builder /app /app 23 | 24 | ENTRYPOINT ["./app"] -------------------------------------------------------------------------------- /square/pkg/server/middleware.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | const ( 11 | // UserAgentHeaderName is the User-Agent header name 12 | UserAgentHeaderName = "User-Agent" 13 | ) 14 | 15 | // WithLogging middleware logs all requests 16 | func WithLogging(next http.Handler) http.Handler { 17 | start := time.Now() 18 | 19 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 20 | logrus.Infof( 21 | "%s | %s | %s | %s", 22 | r.Method, 23 | r.RequestURI, 24 | r.Header.Get(UserAgentHeaderName), 25 | time.Since(start), 26 | ) 27 | next.ServeHTTP(w, r) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /square/pkg/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | "github.com/prometheus/client_golang/prometheus/promhttp" 9 | ) 10 | 11 | // Server wraps the Mux router 12 | type Server struct { 13 | Mux *mux.Router 14 | } 15 | 16 | // New creates a new instance of the Server 17 | func New(ctx context.Context) *Server { 18 | s := &Server{ 19 | Mux: mux.NewRouter(), 20 | } 21 | s.Mux.Use(WithLogging) 22 | 23 | s.Mux.Handle("/metrics", promhttp.Handler()) 24 | s.Mux.HandleFunc("/square/{number}", squareHandler).Methods("GET") 25 | return s 26 | } 27 | 28 | // ServeHTTP is an HTTP handler 29 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 30 | s.Mux.ServeHTTP(w, r) 31 | } 32 | -------------------------------------------------------------------------------- /square/deploy/square.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: square 5 | labels: 6 | app: square 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: square 12 | template: 13 | metadata: 14 | labels: 15 | app: square 16 | spec: 17 | containers: 18 | - image: learncloudnative/square:0.1.0 19 | imagePullPolicy: Always 20 | name: square 21 | ports: 22 | - containerPort: 8080 23 | --- 24 | kind: Service 25 | apiVersion: v1 26 | metadata: 27 | name: square 28 | labels: 29 | app: square 30 | spec: 31 | selector: 32 | app: square 33 | ports: 34 | - port: 80 35 | name: http 36 | targetPort: 8080 37 | type: LoadBalancer 38 | -------------------------------------------------------------------------------- /square/pkg/server/handlers.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/prometheus/client_golang/prometheus" 10 | "github.com/prometheus/client_golang/prometheus/promauto" 11 | ) 12 | 13 | var totalCalls = promauto.NewCounter(prometheus.CounterOpts{ 14 | Name: "square_endpoint_total_calls", 15 | Help: "The total number of times square endpoint was called", 16 | }) 17 | 18 | func squareHandler(w http.ResponseWriter, r *http.Request) { 19 | totalCalls.Inc() 20 | 21 | vars := mux.Vars(r) 22 | num, err := strconv.ParseInt(vars["number"], 10, 32) 23 | if err != nil { 24 | w.WriteHeader(http.StatusInternalServerError) 25 | return 26 | } 27 | w.Write([]byte(fmt.Sprintf("%d", num*num))) 28 | } 29 | -------------------------------------------------------------------------------- /square/deploy/kibana.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: kibana 5 | namespace: kube-system 6 | labels: 7 | app: kibana 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: kibana 13 | template: 14 | metadata: 15 | labels: 16 | app: kibana 17 | spec: 18 | containers: 19 | - name: kibana 20 | image: docker.elastic.co/kibana/kibana-oss:6.4.3 21 | resources: 22 | limits: 23 | cpu: 1000m 24 | requests: 25 | cpu: 100m 26 | env: 27 | - name: ELASTICSEARCH_URL 28 | value: http://elasticsearch:9200 29 | ports: 30 | - containerPort: 5601 31 | --- 32 | apiVersion: v1 33 | kind: Service 34 | metadata: 35 | name: kibana 36 | namespace: kube-system 37 | labels: 38 | app: kibana-logging 39 | spec: 40 | ports: 41 | - port: 5601 42 | selector: 43 | app: kibana 44 | -------------------------------------------------------------------------------- /haproxy-final.cfg: -------------------------------------------------------------------------------- 1 | global 2 | maxconn 4096 3 | daemon 4 | 5 | defaults 6 | log global 7 | mode http 8 | 9 | timeout connect 10s 10 | timeout client 30s 11 | timeout server 30s 12 | 13 | frontend api_gateway 14 | bind 0.0.0.0:80 15 | 16 | # Deny the request unless the api-key header is present 17 | http-request deny unless { req.hdr(api-key) -m found } 18 | 19 | # Create a stick table to track request counts 20 | # The values in the table expire in 5m 21 | stick-table type string size 1m expire 5m store http_req_cnt 22 | 23 | # Create an ACL that checks if we exceeded the value of 10 requests 24 | acl exceeds_limit req.hdr(api-key),table_http_req_cnt(api_gateway) gt 10 25 | 26 | # Track the value of the `api-key` header unless the limit was exceeded 27 | http-request track-sc0 req.hdr(api-key) unless exceeds_limit 28 | 29 | # Deny the request with 429 if limit was exceeded 30 | http-request deny deny_status 429 if exceeds_limit 31 | 32 | default_backend be_square 33 | 34 | # Backend is called `be_square` 35 | backend be_square 36 | # There's only one instance of the server and it 37 | # points to the `square-service:8080` (name is from the docker-compose) 38 | 39 | # `check` instructs HAProxy to do a health check by 40 | # making a TCP requests on an interval 41 | server s1 square-service:8080 check 42 | 43 | # Set up stats page as well (accessible at :8404/stats) 44 | listen stats 45 | bind *:8404 46 | stats enable 47 | stats uri /stats 48 | stats refresh 5s -------------------------------------------------------------------------------- /square/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | import ( 3 | "context" 4 | "fmt" 5 | "flag" 6 | "net/http" 7 | "time" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | 12 | "square/pkg/server" 13 | 14 | "github.com/sirupsen/logrus" 15 | ) 16 | const ( 17 | defaultPort = "8080" 18 | shutdownTimeout = 5 * time.Second 19 | ) 20 | 21 | func init() { 22 | logrus.SetFormatter(&logrus.JSONFormatter{}) 23 | logrus.SetLevel(logrus.DebugLevel) 24 | logrus.SetOutput(os.Stdout) 25 | flag.Parse() 26 | } 27 | 28 | func main() { 29 | port := os.Getenv("PORT") 30 | if port == "" { 31 | port = defaultPort 32 | } 33 | 34 | s := server.New(context.Background()) 35 | hs := &http.Server{ 36 | Handler: s, 37 | Addr: fmt.Sprintf(":%s", port), 38 | WriteTimeout: 15 * time.Second, 39 | ReadTimeout: 15 * time.Second, 40 | } 41 | 42 | go func() { 43 | logrus.Printf("Running on %s", defaultPort) 44 | if err := hs.ListenAndServe(); err != http.ErrServerClosed { 45 | logrus.Fatalf("failed to start the server %+v", err) 46 | } 47 | }() 48 | 49 | shutdown(hs, shutdownTimeout) 50 | } 51 | 52 | // shutdown gracefully shuts down the HTTP server 53 | func shutdown(h *http.Server, timeout time.Duration) { 54 | stop := make(chan os.Signal, 1) 55 | signal.Notify(stop, os.Interrupt, syscall.SIGTERM) 56 | <-stop 57 | 58 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 59 | defer cancel() 60 | 61 | logrus.Printf("shutting down with timeout %s", timeout) 62 | if err := h.Shutdown(ctx); err != nil { 63 | logrus.Fatalf("shutdown failed: %v", err) 64 | } else { 65 | logrus.Printf("shutdown completed") 66 | } 67 | } -------------------------------------------------------------------------------- /square/deploy/prometheus.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: prom-config 5 | labels: 6 | name: prom-config 7 | data: 8 | prometheus.yml: |- 9 | global: 10 | scrape_interval: 5s 11 | scrape_configs: 12 | - job_name: prometheus 13 | static_configs: 14 | - targets: ['square'] 15 | --- 16 | apiVersion: apps/v1 17 | kind: Deployment 18 | metadata: 19 | name: prometheus 20 | labels: 21 | app: prometheus 22 | spec: 23 | replicas: 1 24 | selector: 25 | matchLabels: 26 | app: prometheus 27 | template: 28 | metadata: 29 | labels: 30 | app: prometheus 31 | spec: 32 | containers: 33 | - image: prom/prometheus 34 | args: 35 | - "--config.file=/etc/prometheus/prometheus.yml" 36 | - "--storage.tsdb.path=/prometheus/" 37 | imagePullPolicy: Always 38 | name: prometheus 39 | ports: 40 | - containerPort: 9090 41 | volumeMounts: 42 | - name: prom-config-volume 43 | mountPath: /etc/prometheus 44 | - name: prom-storage-volume 45 | mountPath: /prometheus/ 46 | volumes: 47 | - name: prom-config-volume 48 | configMap: 49 | defaultMode: 420 50 | name: prom-config 51 | - name: prom-storage-volume 52 | emptyDir: {} 53 | --- 54 | kind: Service 55 | apiVersion: v1 56 | metadata: 57 | name: prometheus 58 | labels: 59 | app: prometheus 60 | spec: 61 | selector: 62 | app: prometheus 63 | ports: 64 | - port: 9090 65 | name: http 66 | targetPort: 9090 67 | -------------------------------------------------------------------------------- /square/deploy/fluentd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: fluentd 5 | namespace: kube-system 6 | labels: 7 | app: fluentd 8 | --- 9 | apiVersion: rbac.authorization.k8s.io/v1 10 | kind: ClusterRole 11 | metadata: 12 | name: fluentd 13 | labels: 14 | app: fluentd 15 | rules: 16 | - apiGroups: 17 | - "" 18 | resources: 19 | - pods 20 | - namespaces 21 | verbs: 22 | - get 23 | - list 24 | - watch 25 | --- 26 | kind: ClusterRoleBinding 27 | apiVersion: rbac.authorization.k8s.io/v1 28 | metadata: 29 | name: fluentd 30 | roleRef: 31 | kind: ClusterRole 32 | name: fluentd 33 | apiGroup: rbac.authorization.k8s.io 34 | subjects: 35 | - kind: ServiceAccount 36 | name: fluentd 37 | namespace: kube-system 38 | --- 39 | apiVersion: apps/v1 40 | kind: DaemonSet 41 | metadata: 42 | name: fluentd 43 | namespace: kube-system 44 | labels: 45 | app: fluentd 46 | spec: 47 | selector: 48 | matchLabels: 49 | app: fluentd 50 | template: 51 | metadata: 52 | labels: 53 | app: fluentd 54 | spec: 55 | serviceAccount: fluentd 56 | serviceAccountName: fluentd 57 | tolerations: 58 | - key: node-role.kubernetes.io/master 59 | effect: NoSchedule 60 | containers: 61 | - name: fluentd 62 | image: fluent/fluentd-kubernetes-daemonset:v0.12-debian-elasticsearch 63 | env: 64 | - name: FLUENT_ELASTICSEARCH_HOST 65 | value: "elasticsearch.kube-system.svc.cluster.local" 66 | - name: FLUENT_ELASTICSEARCH_PORT 67 | value: "9200" 68 | - name: FLUENT_ELASTICSEARCH_SCHEME 69 | value: "http" 70 | - name: FLUENT_UID 71 | value: "0" 72 | resources: 73 | limits: 74 | memory: 512Mi 75 | requests: 76 | cpu: 100m 77 | memory: 200Mi 78 | volumeMounts: 79 | - name: varlog 80 | mountPath: /var/log 81 | - name: varlibdockercontainers 82 | mountPath: /var/lib/docker/containers 83 | readOnly: true 84 | terminationGracePeriodSeconds: 30 85 | volumes: 86 | - name: varlog 87 | hostPath: 88 | path: /var/log 89 | - name: varlibdockercontainers 90 | hostPath: 91 | path: /var/lib/docker/containers 92 | -------------------------------------------------------------------------------- /square/deploy/elastic.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: es-cluster 5 | namespace: kube-system 6 | labels: 7 | app: elasticsearch 8 | spec: 9 | serviceName: elasticsearch 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: elasticsearch 14 | template: 15 | metadata: 16 | labels: 17 | app: elasticsearch 18 | spec: 19 | containers: 20 | - name: elasticsearch 21 | image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.3 22 | resources: 23 | limits: 24 | cpu: 500m 25 | memory: 2400Mi 26 | requests: 27 | cpu: 100m 28 | memory: 2350Mi 29 | ports: 30 | - containerPort: 9200 31 | name: rest 32 | protocol: TCP 33 | - containerPort: 9300 34 | name: transport 35 | protocol: TCP 36 | volumeMounts: 37 | - name: data 38 | mountPath: /usr/share/elasticsearch/data 39 | env: 40 | - name: "ELASTIC_PASSWORD" 41 | value: "elastic" 42 | - name: "NAMESPACE" 43 | valueFrom: 44 | fieldRef: 45 | fieldPath: metadata.namespace 46 | - name: cluster.name 47 | value: k8s-logs 48 | - name: node.name 49 | valueFrom: 50 | fieldRef: 51 | fieldPath: metadata.name 52 | # - name: MINIMUM_MASTER_NODES 53 | # value: "1" 54 | - name: discovery.zen.ping.unicast.hosts 55 | value: "es-cluster-0.elasticsearch,es-cluster-1.elasticsearch,es-cluster-2.elasticsearch" 56 | - name: discovery.zen.minimum_master_nodes 57 | value: "1" 58 | - name: ES_JAVA_OPTS 59 | value: "-Xms1024m -Xmx1024m" 60 | initContainers: 61 | - name: fix-permissions 62 | image: busybox 63 | command: 64 | ["sh", "-c", "chown -R 1000:1000 /usr/share/elasticsearch/data"] 65 | securityContext: 66 | privileged: true 67 | volumeMounts: 68 | - name: data 69 | mountPath: /usr/share/elasticsearch/data 70 | - name: increase-vm-max-map 71 | image: busybox 72 | command: ["sysctl", "-w", "vm.max_map_count=262144"] 73 | securityContext: 74 | privileged: true 75 | - name: increase-fd-ulimit 76 | image: busybox 77 | command: ["sh", "-c", "ulimit -n 65536"] 78 | securityContext: 79 | privileged: true 80 | volumes: 81 | - name: data 82 | emptyDir: {} 83 | --- 84 | apiVersion: v1 85 | kind: Service 86 | metadata: 87 | name: elasticsearch 88 | namespace: kube-system 89 | labels: 90 | app: elasticsearch 91 | spec: 92 | ports: 93 | - port: 9200 94 | name: rest 95 | - port: 9300 96 | name: transport 97 | selector: 98 | app: elasticsearch 99 | -------------------------------------------------------------------------------- /square/go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 2 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 4 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 5 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 6 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 7 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 8 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 9 | github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA= 10 | github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 14 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 15 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 16 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 17 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 18 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 19 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 20 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 21 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 22 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 23 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 24 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 25 | github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= 26 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 27 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 28 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 29 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 30 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 31 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 32 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 33 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 34 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 35 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 36 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 37 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 38 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 39 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 40 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 41 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 42 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 43 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 44 | github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI= 45 | github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U= 46 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 47 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 48 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= 49 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 50 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 51 | github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY= 52 | github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= 53 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 54 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 55 | github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8= 56 | github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= 57 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 58 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 59 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 60 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 61 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 62 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 63 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 64 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 65 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 66 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 67 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 68 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 69 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 70 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 71 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 72 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 73 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 74 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY= 75 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 77 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 78 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 79 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 80 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repo accompanies the [Beginners guide to gateways and proxies](https://learncloudnative.com/blog/2020-04-25-beginners-guide-to-gateways-proxies/) article. 2 | 3 | 4 | ## Walkthrough 5 | 6 | Let's start the example by running a simple proxy in-front of the `square` service. This will simply take the incoming requests and pass them to the instance of the square service. 7 | 8 | The `./gateway/docker-compose.yaml` defines two services - `haproxy` and `square-service`. The `haproxy` service also mounts a volume - this is so we can include the `haproxy.cfg` file in the container. 9 | 10 | >Another option would be to create a separate Dockerfile that is based on the HAProxy image and then copy the proxy configuration. 11 | 12 | We are also exposing port `8080` (on the host) to be directed to the port `80` inside the container - this is where the HAProxy instance will be listening on and it's defined in the `haproxy.cfg` file. 13 | 14 | Let's look at the contents of the `haproxy.cfg` file: 15 | 16 | ``` 17 | global 18 | maxconn 4096 19 | daemon 20 | 21 | defaults 22 | log global 23 | mode http 24 | 25 | timeout connect 10s 26 | timeout client 30s 27 | timeout server 30s 28 | 29 | frontend api_gateway 30 | bind 0.0.0.0:80 31 | default_backend be_square 32 | 33 | backend be_square 34 | server s1 square-service:8080 35 | ``` 36 | 37 | We are interested in two sections - **frontend** and **backend**. We are calling the frontend section `api_gateway` and this is where we define where the proxy will listen on as well as how to route the incoming traffic. We are simply setting a default_backend to the `be_square` backend that's defined right after the frontend section. 38 | 39 | In the backend section we are creating a single server called `s1` with an endpoint `square-service:8080` - this is the name that we defined for the square service in the `docker-compose.yaml` file. 40 | 41 | Let's run this and test the behavior - from the `./gateway` folder, run; 42 | 43 | ``` 44 | $ docker-compose up 45 | Creating network "gateway_default" with the default driver 46 | Creating gateway_square-service_1 ... done 47 | Creating gateway_haproxy_1 ... done 48 | Attaching to gateway_square-service_1, gateway_haproxy_1 49 | square-service_1 | {"level":"info","msg":"Running on 8080","time":"2019-11-02T00:56:07Z"} 50 | haproxy_1 | <7>haproxy-systemd-wrapper: executing /usr/local/sbin/haproxy -p /run/haproxy.pid -db -f /usr/local/etc/haproxy/haproxy.cfg -Ds 51 | ``` 52 | 53 | The docker-compose will do it's job, it will create a new network and two services. Run the `curl` command from a separate terminal window: 54 | 55 | ``` 56 | $ curl localhost:5000/square/12 57 | 144 58 | ``` 59 | 60 | You will also notice the log that gets written when the service is called. 61 | 62 | ## Enabling stats 63 | 64 | HAProxy is collecting a lot of stats on the requests, frontends, and backend servers. To enable the stats, add the following section to the end of the `haproxy.cfg` file: 65 | 66 | ``` 67 | listen stats 68 | bind *:8404 69 | stats enable 70 | stats uri /stats 71 | stats refresh 5s 72 | ``` 73 | 74 | The above section binds port `8404` to be available on the `/monitor` URL. Since we are exposing the stats on a different port, you also need to update the `docker-compose.yaml` file to expose that additional port. Add the line `"8404:8404"` under the ports key: 75 | 76 | ``` 77 | ports: 78 | - "5000:80" 79 | - "8404:8404" 80 | ``` 81 | 82 | Restart the containers and see if you can get to the stats page (press CTRL+C if you still have docker-compose running): 83 | 84 | ``` 85 | $ docker-compose down 86 | ... 87 | $ docker-compose up 88 | ... 89 | ``` 90 | 91 | Next, open `http://localhost:8404/stats` to see the stats for the frontend and backend. This page shows you the number of requests, sessions, and bunch of other stats. Make a couple of request to the `square` service and see how the stats page changes. 92 | 93 | ## Health checks 94 | 95 | The simplest way you can add a health check for your backend services is to add the word `check` on the same line your server backend is defined. Like this: 96 | 97 | ``` 98 | server s1 square-service:8080 check 99 | ``` 100 | 101 | This instructs the HAProxy to do an active health check by periodically making a TCP request to the server. 102 | 103 | Update the `haproxy.cfg` by adding the `check` keyword as shown above and restart the containers. 104 | 105 | Next, you can open the HAProxy stats page (`http://localhost:8404/stats`) again to see the health check in action. Notice the row in the backend table has turned green as shown in figure below. 106 | 107 |  108 | 109 | With the containers running, go back to the terminal and kill the container running the square service. First, run `docker ps` to get the container ID and then run `docker kill [container-id]. 110 | 111 | Alternatively, you use the name of the container (`gateway_square-service_1`) to kill it. 112 | 113 | With the container killed, go back to the stats page and you will notice that the row that was previously green, has turned yellow and eventually red. This means that the health check is failing. 114 | 115 | >If you hover over the column `LastChk`, you will also get a reason for the failing health check (Layer4 timeout) 116 | 117 | ## Denying requests 118 | 119 | We want the square API users to use an API key when accessing the functionality. As a first step we can do is to deny any requests that don't have an API key header set. 120 | 121 | To do that, you can add the following line, right after the bind command in the frontend section: 122 | 123 | ``` 124 | http-request deny unless { req.hdr(api-key) -m found } 125 | ``` 126 | 127 | With this line we are telling the proxy to deny the request unless a header called `api-key` is found. 128 | 129 | If you try to make the exact same request as before, you will get the 403 response from the proxy: 130 | 131 | ``` 132 | $ curl localhost:5000/square/12 133 |