{"id":16878862,"url":"https://github.com/peterj/square-service-gateway","last_synced_at":"2025-10-14T08:35:13.356Z","repository":{"id":145601244,"uuid":"258913270","full_name":"peterj/square-service-gateway","owner":"peterj","description":null,"archived":false,"fork":false,"pushed_at":"2020-04-26T01:42:08.000Z","size":92,"stargazers_count":11,"open_issues_count":0,"forks_count":4,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-25T08:01:44.300Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/peterj.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-04-26T01:40:55.000Z","updated_at":"2024-08-28T15:17:21.000Z","dependencies_parsed_at":null,"dependency_job_id":"53d6cd15-e207-47b3-8257-6dab710c5145","html_url":"https://github.com/peterj/square-service-gateway","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterj%2Fsquare-service-gateway","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterj%2Fsquare-service-gateway/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterj%2Fsquare-service-gateway/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterj%2Fsquare-service-gateway/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/peterj","download_url":"https://codeload.github.com/peterj/square-service-gateway/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248385419,"owners_count":21094884,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-13T15:51:52.564Z","updated_at":"2025-10-14T08:35:08.326Z","avatar_url":"https://github.com/peterj.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"This repo accompanies the [Beginners guide to gateways and proxies](https://learncloudnative.com/blog/2020-04-25-beginners-guide-to-gateways-proxies/) article.\n\n\n## Walkthrough\n\nLet'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.\n\nThe `./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.\n\n\u003eAnother option would be to create a separate Dockerfile that is based on the HAProxy image and then copy the proxy configuration.\n\nWe 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.\n\nLet's look at the contents of the `haproxy.cfg` file:\n\n```\nglobal\n  maxconn 4096\n  daemon\n\ndefaults\n    log     global\n    mode    http\n\n    timeout connect 10s\n    timeout client 30s\n    timeout server 30s\n\nfrontend api_gateway\n    bind 0.0.0.0:80\n    default_backend be_square \n\nbackend be_square\n    server s1 square-service:8080\n```\n\nWe 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.\n\nIn 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.\n\nLet's run this and test the behavior - from the `./gateway` folder, run;\n\n```\n$ docker-compose up\nCreating network \"gateway_default\" with the default driver\nCreating gateway_square-service_1 ... done\nCreating gateway_haproxy_1        ... done\nAttaching to gateway_square-service_1, gateway_haproxy_1\nsquare-service_1  | {\"level\":\"info\",\"msg\":\"Running on 8080\",\"time\":\"2019-11-02T00:56:07Z\"}\nhaproxy_1         | \u003c7\u003ehaproxy-systemd-wrapper: executing /usr/local/sbin/haproxy -p /run/haproxy.pid -db -f /usr/local/etc/haproxy/haproxy.cfg -Ds\n```\n\nThe 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:\n\n```\n$ curl localhost:5000/square/12\n144\n```\n\nYou will also notice the log that gets written when the service is called. \n\n## Enabling stats\n\nHAProxy 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:\n\n```\nlisten stats\n    bind *:8404\n    stats enable\n    stats uri /stats\n    stats refresh 5s\n```\n\nThe 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:\n\n```\n    ports:\n      - \"5000:80\"\n      - \"8404:8404\"\n```\n\nRestart the containers and see if you can get to the stats page (press CTRL+C if you still have docker-compose running):\n\n```\n$ docker-compose down\n...\n$ docker-compose up\n...\n```\n\nNext, 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.\n\n## Health checks\n\nThe 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:\n\n```\nserver s1 square-service:8080 check\n```\n\nThis instructs the HAProxy to do an active health check by periodically making a TCP request to the server.\n\nUpdate the `haproxy.cfg` by adding the `check` keyword as shown above and restart the containers.\n\nNext, 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.\n\n![HAProxy health check](./img/haproxy-healthcheck.png)\n\nWith 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].\n\nAlternatively, you use the name of the container (`gateway_square-service_1`) to kill it.\n\nWith 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.\n\n\u003eIf you hover over the column `LastChk`, you will also get a reason for the failing health check (Layer4 timeout)\n\n## Denying requests\n\nWe 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. \n\nTo do that, you can add the following line, right after the bind command in the frontend section:\n\n```\nhttp-request deny unless { req.hdr(api-key) -m found }\n```\n\nWith this line we are telling the proxy to deny the request unless a header called `api-key` is found.\n\nIf you try to make the exact same request as before, you will get the 403 response from the proxy:\n\n```\n$ curl localhost:5000/square/12\n\u003chtml\u003e\u003cbody\u003e\u003ch1\u003e403 Forbidden\u003c/h1\u003e\nRequest forbidden by administrative rules.\n\u003c/body\u003e\u003c/html\u003e\n```\n\nHowever, if you add a header value, the request will work just fine: \n\n```\n$ curl -H \"api-key: hello\" localhost:5000/square/12\n144\n```\n\n## Rate limiting\n\nIn addition to requiring the API keys, we also want to rate-limit users, so they aren't making too many requests and causing unnecessary strain to our service.\n\nTo do that, we need to define a couple of things - let's explain each line separately first, and then see how to put it together.\n\n1. We need a way to store/count the number of requests. For that purpose, you can use a stick table - it stores the number of requests and automatically expires after a certain period of time (`5m` in our case):\n\n```\nstick-table type string size 1m expire 5m store http_req_cnt\n```\n\n1. We also need to set a limit after which we will be denying request. You can do that using an ACL where we check if the number of requests with a specific `api-key` header value exceeds the limit - we set it to 10, so it's easier to test:\n\n```\nacl exceeds_limit req.hdr(api-key),table_http_req_cnt(api_gateway) gt 10\n```\n\n1. If the limit is not exceeded, let's track the current request:\n\n```\nhttp-request track-sc0 req.hdr(api-key) unless exceeds_limit\n```\n\n1. Finally, we need to deny the request if limit is exceeded:\n\n```\nhttp-request deny deny_status 429 if exceeds_limit\n```\n\nThe final frontend configuration should then look like this:\n\n```\nfrontend api_gateway\n    bind 0.0.0.0:80\n\n    # Deny the request unless the api-key header is present\n    http-request deny unless { req.hdr(api-key) -m found }\n\n    # Create a stick table to track request counts\n    # The values in the table expire in 5m\n    stick-table type string size 1m expire 5m store http_req_cnt\n\n    # Create an ACL that checks if we exceeded the value of 10 requests \n    acl exceeds_limit req.hdr(api-key),table_http_req_cnt(api_gateway) gt 10\n    \n\n    # Track the value of the `api-key` header unless the limit was exceeded \n    http-request track-sc0 req.hdr(api-key) unless exceeds_limit\n\n    # Deny the request with 429 if limit was exceeded\n    http-request deny deny_status 429 if exceeds_limit\n\n    default_backend be_square \n    ....\n```\n\n\nRestart the containers and try it out - make 10 requests and on the eleventh request, you will get the following response back:\n\n```\n$ curl -H \"api-key: hello\" localhost:5000/square/12\n\u003chtml\u003e\u003cbody\u003e\u003ch1\u003e429 Too Many Requests\u003c/h1\u003e\nYou have sent too many requests in a given amount of time.\n\u003c/body\u003e\u003c/html\u003e\n```\n\nYou can wait for 5 min to check that the table expires, or you can also try making requests using a different `api-key` header value, since we are tracking the value of it:\n\n```\n$ curl -H \"api-key: TEST\" localhost:5000/square/12\n144\n```\n\nFinally, let's check the stats again - specifically the Denied column in the `api_gateway` section. You should see the number of denied requests there.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeterj%2Fsquare-service-gateway","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpeterj%2Fsquare-service-gateway","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeterj%2Fsquare-service-gateway/lists"}