Roll Your Own API Management Platform with nginx and Lua

Software

jon-moore
  1. 1. Roll Your Own API Management Platform with nginx and Lua Jon Moore Senior Fellow, Comcast Cable @jon_moore
  2. 2. access control
  3. 3. capacity management (“rate limiting”)
  4. 4. capacity management (“rate limiting”)
  5. 5. HTTP Proxy custom logic!
  6. 6. Lua
  7. 7. --- Validates the OAuth signature! -- @return Const.HTTP_UNAUTHORIZED if either the key or signature is invalid! -- this method is internal and should not be called directly! function _M.validate_signature(self)! local headers = self.req.get_oauth_params()! local key = headers[Const.OAUTH_CONSUMER_KEY]! local keyconf = self.conf.keys[key]! if keyconf == nil then! return {! code = Const.HTTP_UNAUTHORIZED! error = Const.ERROR_INVALID_CONSUMER_KEY! }! end! ! local sig = get_hmac_signature(self.req, keyconf.secret)! if sig ~= headers[Const.OAUTH_SIGNATURE] then! return {! code = Const.HTTP_UNAUTHORIZED,! error = Const.ERROR_INVALID_SIGNATURE! }! end! end!
  8. 8. Lua < 3k LOC
  9. 9. testing
  10. 10. function TestOAuth1:test_reject_request_when_signature_invalid()! local header = Header:new()! header[Const.OAUTH_SIGNATURE] = “invalid”! local req = Req:new({oauth_params = header })! local conf = Conf:new()! local oauth = OAuth1:new(conf, req)! ! local res = oauth:authorize()! assertEquals(res.code, Const.HTTP_UNAUTHORIZED)! assertEquals(res.error, Const.ERROR_INVALID_SIGNATURE)! end! ! lu = LuaUnit.new()! Lu:setOutputType(“tap”)! os.exit(lu:runSuite())!
  11. 11. ngx.log(ngx.ERR, “oops”)!
  12. 12. ngx.log(ngx.ERR, “oops”)!
  13. 13. function get_oauth_params_from_auth_header()!
  14. 14. function get_oauth_params_from_auth_header(env)! env = env or ngx! local auth_hdrs = env.req.get_headers()[“Authorization”]! ...!
  15. 15. function get_oauth_params_from_auth_header(env)! env = env or ngx! local auth_hdrs = env.req.get_headers()[“Authorization”]! ...!
  16. 16. function get_oauth_params_from_auth_header(env)! env = env or ngx! local auth_hdrs = env.req.get_headers()[“Authorization”]! ...!
  17. 17. function TestRequest:test_retrieve_oauth_params_from_header()! local header = [[Oauth realm=“example.com”, ]]! .. [[oauth_consumer_key=“mykey”,]]! .. [[oauth_version=“1.0”]]! local ngx = StubNgx:new({ Authorization = header })! local res = get_oauth_params_from_auth_header(ngx)! assertEquals(“1.0”, res.oauth_version)! ...!
  18. 18. function TestRequest:test_retrieve_oauth_params_from_header()! local header = [[Oauth realm=“example.com”, ]]! .. [[oauth_consumer_key=“mykey”,]]! .. [[oauth_version=“1.0”]]! local ngx = StubNgx:new({ Authorization = header })! local res = get_oauth_params_from_auth_header(ngx)! assertEquals(“1.0”, res.oauth_version)! ...!
  19. 19. function TestRequest:test_retrieve_oauth_params_from_header()! local header = [[Oauth realm=“example.com”, ]]! .. [[oauth_consumer_key=“mykey”,]]! .. [[oauth_version=“1.0”]]! local ngx = StubNgx:new({ Authorization = header })! local res = get_oauth_params_from_auth_header(ngx)! assertEquals(“1.0”, res.oauth_version)! ...!
  20. 20. function TestRequest:test_retrieve_oauth_params_from_header()! local header = [[Oauth realm=“example.com”, ]]! .. [[oauth_consumer_key=“mykey”,]]! .. [[oauth_version=“1.0”]]! local ngx = StubNgx:new({ Authorization = header })! local res = get_oauth_params_from_auth_header(ngx)! assertEquals(“1.0”, res.oauth_version)! ...!
  21. 21. function TestRequest:test_retrieve_oauth_params_from_header()! local header = [[Oauth realm=“example.com”, ]]! .. [[oauth_consumer_key=“mykey”,]]! .. [[oauth_version=“1.0”]]! local ngx = StubNgx:new({ Authorization = header })! local res = get_oauth_params_from_auth_header(ngx)! assertEquals(“1.0”, res.oauth_version)! ...!
  22. 22. test harness
  23. 23. nginx-oauth1.conf! test harness
  24. 24. nginx-oauth1.conf! test harness
  25. 25. nginx-oauth1.conf! test harness
  26. 26. nginx-oauth1.conf! test harness
  27. 27. def spec_using_valid_oauth_credentials(self, harness)! auth = OAuth1(“mykey”, “mysecret”)! body_data = “{‘function’: ‘tick’}”! harness.reset_data()! response = requests.post(root_url, data=body_data, auth=auth,! headers={‘Content-Type’:! ‘application/json’})! assert response.status_code == 200! assert “Authorization” in harness.forwarded_headers! assert “Oauth” in harness.forwarded_headers[“Authorization”]! assert harness.forwarded_body == body_data!
  28. 28. def spec_using_valid_oauth_credentials(self, harness)! auth = OAuth1(“mykey”, “mysecret”)! body_data = “{‘function’: ‘tick’}”! harness.reset_data()! response = requests.post(root_url, data=body_data, auth=auth,! headers={‘Content-Type’:! ‘application/json’})! assert response.status_code == 200! assert “Authorization” in harness.forwarded_headers! assert “Oauth” in harness.forwarded_headers[“Authorization”]! assert harness.forwarded_body == body_data!
  29. 29. def spec_using_valid_oauth_credentials(self, harness)! auth = OAuth1(“mykey”, “mysecret”)! body_data = “{‘function’: ‘tick’}”! harness.reset_data()! response = requests.post(root_url, data=body_data, auth=auth,! headers={‘Content-Type’:! ‘application/json’})! assert response.status_code == 200! assert “Authorization” in harness.forwarded_headers! assert “Oauth” in harness.forwarded_headers[“Authorization”]! assert harness.forwarded_body == body_data!
  30. 30. def spec_using_valid_oauth_credentials(self, harness)! auth = OAuth1(“mykey”, “mysecret”)! body_data = “{‘function’: ‘tick’}”! harness.reset_data()! response = requests.post(root_url, data=body_data, auth=auth,! headers={‘Content-Type’:! ‘application/json’})! assert response.status_code == 200! assert “Authorization” in harness.forwarded_headers! assert “Oauth” in harness.forwarded_headers[“Authorization”]! assert harness.forwarded_body == body_data!
  31. 31. def spec_using_valid_oauth_credentials(self, harness)! auth = OAuth1(“mykey”, “mysecret”)! body_data = “{‘function’: ‘tick’}”! harness.reset_data()! response = requests.post(root_url, data=body_data, auth=auth,! headers={‘Content-Type’:! ‘application/json’})! assert response.status_code == 200! assert “Authorization” in harness.forwarded_headers! assert “Oauth” in harness.forwarded_headers[“Authorization”]! assert harness.forwarded_body == body_data!
  32. 32. def spec_using_valid_oauth_credentials(self, harness)! auth = OAuth1(“mykey”, “mysecret”)! body_data = “{‘function’: ‘tick’}”! harness.reset_data()! response = requests.post(root_url, data=body_data, auth=auth,! headers={‘Content-Type’:! ‘application/json’})! assert response.status_code == 200! assert “Authorization” in harness.forwarded_headers! assert “Oauth” in harness.forwarded_headers[“Authorization”]! assert harness.forwarded_body == body_data!
  33. 33. def spec_using_valid_oauth_credentials(self, harness)! auth = OAuth1(“mykey”, “mysecret”)! body_data = “{‘function’: ‘tick’}”! harness.reset_data()! response = requests.post(root_url, data=body_data, auth=auth,! headers={‘Content-Type’:! ‘application/json’})! assert response.status_code == 200! assert “Authorization” in harness.forwarded_headers! assert “Oauth” in harness.forwarded_headers[“Authorization”]! assert harness.forwarded_body == body_data!
  34. 34. capacity management
  35. 35. N = XR # concurrent requests transaction rate response time
  36. 36. API Mgmt client origin 1s2 req/s
  37. 37. API Mgmt client origin 1s2 req/s N = XR = 2 req/s × 1s = 2 req
  38. 38. API Mgmt client origin 1s2 req/s N = XR = 2 req/s × 1s = 2 req
  39. 39. API Mgmt client origin 1s2 req/s N = XR = 2 req/s × 1s = 2 req
  40. 40. API Mgmt client origin 1s2 req/s N = XR = 2 req/s × 1s = 2 req
  41. 41. API Mgmt client origin 1s2 req/s N = XR = 2 req/s × 1s = 2 req
  42. 42. API Mgmt client origin 1s2 req/s
  43. 43. API Mgmt client origin 10s2 req/s N = XR = 2 req/s × 10s = 20 req
  44. 44. API Mgmt client origin 10s2 req/s N = XR = 2 req/s × 10s = 20 req
  45. 45. API Mgmt client origin 10s2 req/s N = XR = 2 req/s × 10s = 20 req
  46. 46. API Mgmt client origin 10s2 req/s N = XR = 2 req/s × 10s = 20 req
  47. 47. API Mgmt client origin 10s2 req/s N = XR = 2 req/s × 10s = 20 req
  48. 48. API Mgmt client origin 10s2 req/s N = XR = 2 req/s × 10s = 20 req
  49. 49. API Mgmt client origin 10s2 req/s ✗ N = XR = 2 req/s × 10s = 20 req
  50. 50. API Mgmt client origin 10s2 req/s ✗ ✗ N = XR = 2 req/s × 10s = 20 req
  51. 51. access_by_lua ...! log_by_lua ...! +1 -1
  52. 52. deployment
  53. 53. Version Control templates vault (keys) nginx.conf!ssh!
  54. 54. architecture
  55. 55. HAProxy HAProxy VIP . . . <API>
  56. 56. DC1 DC2 DC3 VIP VIP VIP entry-vip-dc1. A 10.1.0.1! <foo> <foo> <bar> <bar> foo-dc1. CNAME entry-vip-dc1.! foo. CNAME foo-dc1.! (GSLB) entry-vip-dc2. A 10.2.0.1! entry-vip-dc3. A 10.3.0.1!
  57. 57. DC1 DC2 DC3 VIP VIP VIP entry-vip-dc1. A 10.1.0.1! <foo> <foo> <bar> <bar> foo-dc1. CNAME entry-vip-dc1.! foo. CNAME foo-dc1.! (GSLB) entry-vip-dc2. A 10.2.0.1! entry-vip-dc3. A 10.3.0.1!
  58. 58. DC1 DC2 DC3 VIP VIP VIP entry-vip-dc1. A 10.1.0.1! <foo> <foo> <bar> <bar> foo-dc1. CNAME entry-vip-dc1.! foo. CNAME foo-dc1.! (GSLB) entry-vip-dc2. A 10.2.0.1! entry-vip-dc3. A 10.3.0.1!
  59. 59. DC1 DC2 DC3 VIP VIP VIP entry-vip-dc1. A 10.1.0.1! <foo> <foo> <bar> <bar> foo-dc1. CNAME entry-vip-dc1.! foo. CNAME foo-dc1.! (GSLB) entry-vip-dc2. A 10.2.0.1! entry-vip-dc3. A 10.3.0.1!
  60. 60. DC1 DC2 DC3 VIP VIP VIP entry-vip-dc1. A 10.1.0.1! <foo> <foo> <bar> <bar> foo-dc1. CNAME entry-vip-dc1.! foo. CNAME foo-dc1.! (GSLB) entry-vip-dc2. A 10.2.0.1! entry-vip-dc3. A 10.3.0.1!
  61. 61. DC1 DC2 DC3 VIP VIP VIP entry-vip-dc1. A 10.1.0.1! <foo> <foo> <bar> <bar> foo-dc1. CNAME entry-vip-dc1.! foo. CNAME foo-dc1.! (GSLB) entry-vip-dc2. A 10.2.0.1! entry-vip-dc3. A 10.3.0.1!
  62. 62. Roll Your Own API Management with nginx and Lua • nginx + Lua => great for HTTP middleware with a small amount of custom logic • Automated test and deployment pipeline with Vagrant, Python, and Ansible • Concurrent request limiting, not rate limiting • Network architecture with operational flexibility Presentation title (optional)67
Please download to view
67
All materials on our website are shared by users. If you have any questions about copyright issues, please report us to resolve them. We are always happy to assist you.
Description
Text
  1. 1. Roll Your Own API Management Platform with nginx and Lua Jon Moore Senior Fellow, Comcast Cable @jon_moore
  2. 2. access control
  3. 3. capacity management (“rate limiting”)
  4. 4. capacity management (“rate limiting”)
  5. 5. HTTP Proxy custom logic!
  6. 6. Lua
  7. 7. --- Validates the OAuth signature! -- @return Const.HTTP_UNAUTHORIZED if either the key or signature is invalid! -- this method is internal and should not be called directly! function _M.validate_signature(self)! local headers = self.req.get_oauth_params()! local key = headers[Const.OAUTH_CONSUMER_KEY]! local keyconf = self.conf.keys[key]! if keyconf == nil then! return {! code = Const.HTTP_UNAUTHORIZED! error = Const.ERROR_INVALID_CONSUMER_KEY! }! end! ! local sig = get_hmac_signature(self.req, keyconf.secret)! if sig ~= headers[Const.OAUTH_SIGNATURE] then! return {! code = Const.HTTP_UNAUTHORIZED,! error = Const.ERROR_INVALID_SIGNATURE! }! end! end!
  8. 8. Lua < 3k LOC
  9. 9. testing
  10. 10. function TestOAuth1:test_reject_request_when_signature_invalid()! local header = Header:new()! header[Const.OAUTH_SIGNATURE] = “invalid”! local req = Req:new({oauth_params = header })! local conf = Conf:new()! local oauth = OAuth1:new(conf, req)! ! local res = oauth:authorize()! assertEquals(res.code, Const.HTTP_UNAUTHORIZED)! assertEquals(res.error, Const.ERROR_INVALID_SIGNATURE)! end! ! lu = LuaUnit.new()! Lu:setOutputType(“tap”)! os.exit(lu:runSuite())!
  11. 11. ngx.log(ngx.ERR, “oops”)!
  12. 12. ngx.log(ngx.ERR, “oops”)!
  13. 13. function get_oauth_params_from_auth_header()!
  14. 14. function get_oauth_params_from_auth_header(env)! env = env or ngx! local auth_hdrs = env.req.get_headers()[“Authorization”]! ...!
  15. 15. function get_oauth_params_from_auth_header(env)! env = env or ngx! local auth_hdrs = env.req.get_headers()[“Authorization”]! ...!
  16. 16. function get_oauth_params_from_auth_header(env)! env = env or ngx! local auth_hdrs = env.req.get_headers()[“Authorization”]! ...!
  17. 17. function TestRequest:test_retrieve_oauth_params_from_header()! local header = [[Oauth realm=“example.com”, ]]! .. [[oauth_consumer_key=“mykey”,]]! .. [[oauth_version=“1.0”]]! local ngx = StubNgx:new({ Authorization = header })! local res = get_oauth_params_from_auth_header(ngx)! assertEquals(“1.0”, res.oauth_version)! ...!
  18. 18. function TestRequest:test_retrieve_oauth_params_from_header()! local header = [[Oauth realm=“example.com”, ]]! .. [[oauth_consumer_key=“mykey”,]]! .. [[oauth_version=“1.0”]]! local ngx = StubNgx:new({ Authorization = header })! local res = get_oauth_params_from_auth_header(ngx)! assertEquals(“1.0”, res.oauth_version)! ...!
  19. 19. function TestRequest:test_retrieve_oauth_params_from_header()! local header = [[Oauth realm=“example.com”, ]]! .. [[oauth_consumer_key=“mykey”,]]! .. [[oauth_version=“1.0”]]! local ngx = StubNgx:new({ Authorization = header })! local res = get_oauth_params_from_auth_header(ngx)! assertEquals(“1.0”, res.oauth_version)! ...!
  20. 20. function TestRequest:test_retrieve_oauth_params_from_header()! local header = [[Oauth realm=“example.com”, ]]! .. [[oauth_consumer_key=“mykey”,]]! .. [[oauth_version=“1.0”]]! local ngx = StubNgx:new({ Authorization = header })! local res = get_oauth_params_from_auth_header(ngx)! assertEquals(“1.0”, res.oauth_version)! ...!
  21. 21. function TestRequest:test_retrieve_oauth_params_from_header()! local header = [[Oauth realm=“example.com”, ]]! .. [[oauth_consumer_key=“mykey”,]]! .. [[oauth_version=“1.0”]]! local ngx = StubNgx:new({ Authorization = header })! local res = get_oauth_params_from_auth_header(ngx)! assertEquals(“1.0”, res.oauth_version)! ...!
  22. 22. test harness
  23. 23. nginx-oauth1.conf! test harness
  24. 24. nginx-oauth1.conf! test harness
  25. 25. nginx-oauth1.conf! test harness
  26. 26. nginx-oauth1.conf! test harness
  27. 27. def spec_using_valid_oauth_credentials(self, harness)! auth = OAuth1(“mykey”, “mysecret”)! body_data = “{‘function’: ‘tick’}”! harness.reset_data()! response = requests.post(root_url, data=body_data, auth=auth,! headers={‘Content-Type’:! ‘application/json’})! assert response.status_code == 200! assert “Authorization” in harness.forwarded_headers! assert “Oauth” in harness.forwarded_headers[“Authorization”]! assert harness.forwarded_body == body_data!
  28. 28. def spec_using_valid_oauth_credentials(self, harness)! auth = OAuth1(“mykey”, “mysecret”)! body_data = “{‘function’: ‘tick’}”! harness.reset_data()! response = requests.post(root_url, data=body_data, auth=auth,! headers={‘Content-Type’:! ‘application/json’})! assert response.status_code == 200! assert “Authorization” in harness.forwarded_headers! assert “Oauth” in harness.forwarded_headers[“Authorization”]! assert harness.forwarded_body == body_data!
  29. 29. def spec_using_valid_oauth_credentials(self, harness)! auth = OAuth1(“mykey”, “mysecret”)! body_data = “{‘function’: ‘tick’}”! harness.reset_data()! response = requests.post(root_url, data=body_data, auth=auth,! headers={‘Content-Type’:! ‘application/json’})! assert response.status_code == 200! assert “Authorization” in harness.forwarded_headers! assert “Oauth” in harness.forwarded_headers[“Authorization”]! assert harness.forwarded_body == body_data!
  30. 30. def spec_using_valid_oauth_credentials(self, harness)! auth = OAuth1(“mykey”, “mysecret”)! body_data = “{‘function’: ‘tick’}”! harness.reset_data()! response = requests.post(root_url, data=body_data, auth=auth,! headers={‘Content-Type’:! ‘application/json’})! assert response.status_code == 200! assert “Authorization” in harness.forwarded_headers! assert “Oauth” in harness.forwarded_headers[“Authorization”]! assert harness.forwarded_body == body_data!
  31. 31. def spec_using_valid_oauth_credentials(self, harness)! auth = OAuth1(“mykey”, “mysecret”)! body_data = “{‘function’: ‘tick’}”! harness.reset_data()! response = requests.post(root_url, data=body_data, auth=auth,! headers={‘Content-Type’:! ‘application/json’})! assert response.status_code == 200! assert “Authorization” in harness.forwarded_headers! assert “Oauth” in harness.forwarded_headers[“Authorization”]! assert harness.forwarded_body == body_data!
  32. 32. def spec_using_valid_oauth_credentials(self, harness)! auth = OAuth1(“mykey”, “mysecret”)! body_data = “{‘function’: ‘tick’}”! harness.reset_data()! response = requests.post(root_url, data=body_data, auth=auth,! headers={‘Content-Type’:! ‘application/json’})! assert response.status_code == 200! assert “Authorization” in harness.forwarded_headers! assert “Oauth” in harness.forwarded_headers[“Authorization”]! assert harness.forwarded_body == body_data!
  33. 33. def spec_using_valid_oauth_credentials(self, harness)! auth = OAuth1(“mykey”, “mysecret”)! body_data = “{‘function’: ‘tick’}”! harness.reset_data()! response = requests.post(root_url, data=body_data, auth=auth,! headers={‘Content-Type’:! ‘application/json’})! assert response.status_code == 200! assert “Authorization” in harness.forwarded_headers! assert “Oauth” in harness.forwarded_headers[“Authorization”]! assert harness.forwarded_body == body_data!
  34. 34. capacity management
  35. 35. N = XR # concurrent requests transaction rate response time
  36. 36. API Mgmt client origin 1s2 req/s
  37. 37. API Mgmt client origin 1s2 req/s N = XR = 2 req/s × 1s = 2 req
  38. 38. API Mgmt client origin 1s2 req/s N = XR = 2 req/s × 1s = 2 req
  39. 39. API Mgmt client origin 1s2 req/s N = XR = 2 req/s × 1s = 2 req
  40. 40. API Mgmt client origin 1s2 req/s N = XR = 2 req/s × 1s = 2 req
  41. 41. API Mgmt client origin 1s2 req/s N = XR = 2 req/s × 1s = 2 req
  42. 42. API Mgmt client origin 1s2 req/s
  43. 43. API Mgmt client origin 10s2 req/s N = XR = 2 req/s × 10s = 20 req
  44. 44. API Mgmt client origin 10s2 req/s N = XR = 2 req/s × 10s = 20 req
  45. 45. API Mgmt client origin 10s2 req/s N = XR = 2 req/s × 10s = 20 req
  46. 46. API Mgmt client origin 10s2 req/s N = XR = 2 req/s × 10s = 20 req
  47. 47. API Mgmt client origin 10s2 req/s N = XR = 2 req/s × 10s = 20 req
  48. 48. API Mgmt client origin 10s2 req/s N = XR = 2 req/s × 10s = 20 req
  49. 49. API Mgmt client origin 10s2 req/s ✗ N = XR = 2 req/s × 10s = 20 req
  50. 50. API Mgmt client origin 10s2 req/s ✗ ✗ N = XR = 2 req/s × 10s = 20 req
  51. 51. access_by_lua ...! log_by_lua ...! +1 -1
  52. 52. deployment
  53. 53. Version Control templates vault (keys) nginx.conf!ssh!
  54. 54. architecture
  55. 55. HAProxy HAProxy VIP . . . <API>
  56. 56. DC1 DC2 DC3 VIP VIP VIP entry-vip-dc1. A 10.1.0.1! <foo> <foo> <bar> <bar> foo-dc1. CNAME entry-vip-dc1.! foo. CNAME foo-dc1.! (GSLB) entry-vip-dc2. A 10.2.0.1! entry-vip-dc3. A 10.3.0.1!
  57. 57. DC1 DC2 DC3 VIP VIP VIP entry-vip-dc1. A 10.1.0.1! <foo> <foo> <bar> <bar> foo-dc1. CNAME entry-vip-dc1.! foo. CNAME foo-dc1.! (GSLB) entry-vip-dc2. A 10.2.0.1! entry-vip-dc3. A 10.3.0.1!
  58. 58. DC1 DC2 DC3 VIP VIP VIP entry-vip-dc1. A 10.1.0.1! <foo> <foo> <bar> <bar> foo-dc1. CNAME entry-vip-dc1.! foo. CNAME foo-dc1.! (GSLB) entry-vip-dc2. A 10.2.0.1! entry-vip-dc3. A 10.3.0.1!
  59. 59. DC1 DC2 DC3 VIP VIP VIP entry-vip-dc1. A 10.1.0.1! <foo> <foo> <bar> <bar> foo-dc1. CNAME entry-vip-dc1.! foo. CNAME foo-dc1.! (GSLB) entry-vip-dc2. A 10.2.0.1! entry-vip-dc3. A 10.3.0.1!
  60. 60. DC1 DC2 DC3 VIP VIP VIP entry-vip-dc1. A 10.1.0.1! <foo> <foo> <bar> <bar> foo-dc1. CNAME entry-vip-dc1.! foo. CNAME foo-dc1.! (GSLB) entry-vip-dc2. A 10.2.0.1! entry-vip-dc3. A 10.3.0.1!
  61. 61. DC1 DC2 DC3 VIP VIP VIP entry-vip-dc1. A 10.1.0.1! <foo> <foo> <bar> <bar> foo-dc1. CNAME entry-vip-dc1.! foo. CNAME foo-dc1.! (GSLB) entry-vip-dc2. A 10.2.0.1! entry-vip-dc3. A 10.3.0.1!
  62. 62. Roll Your Own API Management with nginx and Lua • nginx + Lua => great for HTTP middleware with a small amount of custom logic • Automated test and deployment pipeline with Vagrant, Python, and Ansible • Concurrent request limiting, not rate limiting • Network architecture with operational flexibility Presentation title (optional)67
Comments
Top