ทดลองสร้าง API Gateway ด้วย OpenResty และ Lua

ในปัจจุบันนั้นการพัฒนา Application นั้นการออกแบบให้เป็น Modular ไม่ใช่เรื่องแปลกอีกต่อไปมันกลายเป็นเรื่องปกติไปเสียแล้ว โดยเฉพาะสำหรับ Mobile Application หรือ Web Application ใหม่ๆที่มักจะทำงานบน API ที่ออกแบบมาให้ทำงานร่วมกัน รวมไปถึงคนที่พัฒนาระบบงานด้วย Microservice Architecture ซึ่งเมื่อมี Microservice เกิดขึ้นจำนวนหนึ่ง มักจะเกิดความยุ่งยากในการใช้งาน จึงมีคนเอาแนวคิดของ API Gateway มาใช้ ซึ่งหน้าตาก็จะเป็นประมาณนี้

0__GSMUaHihirlIedB

ซึ่งเมื่อเราทำงานกับหลายๆ Service เรามักจะพบว่า API call ต่างๆเหล่านี้มักจะมี common property ที่เราต้องการให้มันมี และมักจะใช้งานร่วมกันกับหลายๆ Service ซะด้วยสิ สำหรับคนที่ทำ Java/C# เป็นหลักอย่างผม ก็ไม่ใช่เรื่องยาก เราสามารถเพิ่ม Abstraction Layer ใน Service นั้น แล้วก็ inject ตัว logic นั้นๆเข้าไปเป็น Library ให้ที่ทำงานด้วยกันได้ก็น่าจะจบ แต่เมื่อลองไปหาดูในตลาดก็พบว่า API Gateway นั้นมีความสามารถอีกอย่างหนึ่งที่น่าสนใจไม่แพ้กันนั้นก็คือ การทำตัวเป็น Middleware ระหว่าง API Client และตัว API เอง เช่นตัวอย่างในภาพด้านล่าง มีการประยุกต์เอา API Gateway มาเพื่อทำ Middleware สำหรับการทำ Authorization, API rate limit และ Logging

1_8XpS0w0raC8cDijTtaRC-Q

image from https://medium.com/descartestech/microservice-usage-logging-with-openresty-and-google-bigquery-5866aad99b66
สำหรับส่วนที่ผมสนใจนั้นก็คือเรื่องของการ process ในส่วนของ preprocess request เพื่อให้ข้อมูลในการที่ API จะนำไปใช้งานได้อย่างสะดวกและถูกต้อง และสามารถใช้งานร่วมกันได้กับทุกๆ Service ภายใต้ API Gateway ยกตัวอย่างของข้อมูลเช่น

  • GeoIP / Country of Origin ของผู้ใช้งาน
  • เป็นผู้ใช้งานจริงๆไหม Bot Checking
  • DataCenter ที่ผู้ใช้เข้ามาใช้งาน
  • UniqueID สำหรับการใช้งาน
  • ค่า random สำหรับใช้สร้าง initialize vector เป็นต้น

ซึ่งเราจะทำการ preprocess ข้อมูลของผู้ใช้และเพิ่มลงไปใน custom http header โดยในตอนเริ่มต้นเราก็สร้าง mock API server เพื่อให้สามารถเช็คดูค่าต่างๆเหล่านี้จาก Header ได้โดยสะดวก

var express    = require('express');  
var app        = express();           
var bodyParser = require('body-parser');

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

var port = process.env.PORT || 3000;  
var router = express.Router();  
router.get('/headers', function(req, res) {
    res.json({ 
        message: 'Backend API in NodeJS with Express!' , 
        headers: req.headers
    });   
});

app.use('/api', router);

app.listen(port);
console.log('API running on port ' + port);

ซึ่งเมื่อเราทำการรันโค้ดข้างต้นนี้ และทำการทดสอบด้วย Postman หรือ curl ตามแต่ศรัทธา เราก็น่าจะได้เห็น result ประมาณนี้

1_J_lJNxJoXOdeNf-T9wGfaA

API result without modification
จากนั้นผมก็ทดสอบง่ายๆโดยการติดตั้ง API Gateway โดยในที่นี้เราจะใช้ OpenResty ในการใช้งาน และจะใช้ Lua เป็น preprocessor engine นะครับ

ตัว OpenResty นั้นก็คือ Nginx ดีๆนี่เอง แต่ว่าเป็น distribution ที่มีการ embed lua JIT และ Nginx module อื่นๆมาด้วยนั่นเอง ในตอนนี้เราจะมา focus กันที่การ process data และ set header ด้วย Lua กัน

ปกตินั้นเราสามารถ set header ของ HTTP request ด้วย Nginx script อยู่แล้วเช่น ถ้าผมต้องการให้ Nginx ทำงานเป็น reverse proxy สำหรับ API ที่ทำงานบน http://localhost:3000 และผมจะเพิ่ม header บางตัวลงไปตรงๆ เช่น

#user  nobody;
worker_processes  1;

events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       8080;
        server_name  localhost;

        location /v1/ {
            set $dc "THA";
            proxy_pass                      http://localhost:3000/;
            proxy_set_header                X-Host example.com;
            proxy_set_header                X-DC $dc;
            proxy_pass_request_headers      on;
        }
    }

}

ใน configuration ชุดนี้ ผมได้ทำการ map URL /v1/ สำหรับ backend API ทั้งหมดใน http://localhost:3000/ และได้ทำการกำหนดตัวแปรชื่อ $dc ด้วย directive set และนำไปใส่เป็น HTTP header ให้กับ API Request ทั้งหมด ด้วย directive proxy_set_header ซึ่งส่วนใหญ่ก็มักจะเป็นการ extract ค่าออกจาก URL ด้วย regular expression แล้วทำการ rewrite ไปให้กับ backend

แต่ในกรณีที่เราต้องการนั้น เราอยากเอาข้อมูลไปทำการประมวลบางอย่าง(Preprocess) แล้วจึงสรุปผลและส่งไปให้กับ HTTP header ของ API Call อีกที ดังใน snippet ชุดนี้

-- do some process here
  return "BANGKOK"
  
#user  nobody;
worker_processes  1;

events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       8080;
        server_name  localhost;


        location /v1/ {
            set $dc "THA";
            set_by_lua_block $random { 
                math.randomseed(os.time()) 
                return math.random() 
            }
            set_by_lua_block $botname { return 123 }
            set_by_lua_file $origin '/scripts/geolocation.lua';
            set_by_lua_file $random_id '/scripts/random_id.lua';

            proxy_pass                      http://localhost:3000/;
            proxy_set_header                X-Host example.com;
            proxy_set_header                X-DC $dc;
            proxy_set_header                X-ORIGIN $origin;
            proxy_set_header                X-UID $random_id;
            proxy_set_header                X-RANDOM $random;
            proxy_set_header                X-BOT-NAME $botname;
            proxy_pass_request_headers      on;
        }
    }

}
local charset = {}

-- qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890
for i = 48,  57 do table.insert(charset, string.char(i)) end
for i = 65,  90 do table.insert(charset, string.char(i)) end
for i = 97, 122 do table.insert(charset, string.char(i)) end

function string.random(length)
  math.randomseed(os.time())

  if length > 0 then
    return string.random(length - 1) .. charset[math.random(1, #charset)]
  else
    return ""
  end
end

return string.random(16)

เมื่อลองทดสอบดูอีกครั้งก็พบว่ามี Additional HTTP Header มาแล้ว อาห์ ฟิน

1_R3kqd-KmHgSNXLTfsEC-Dg

API behind OpenResty
จะเห็นได้ว่าใน nginx เราสามารถทำการประมวลผล และส่งต่อข้อมูลไปยัง Backend API ได้ด้วย Lua Script ทันที โดยใน Lua Module ของ OpenResty นั้นยังสามารถที่จะใช้ในการทำงานร่วมกับ couchbase ด้วย lua-resty-couchbase เพื่อใช้ในการดำเนินการกับ couchbase และนำมาประมวลอีกครั้ง

สำหรับ OpenResty และ Lua นั้นยังเป็น Area ที่ผมยังไม่ค่อยเข้าใจมากนัก แต่จากที่ได้ลองในช่วงสุดสัปดาห์นี้พบว่ามันเยี่ยมสุดๆไปเลย คิดว่าน่าจะเอามาทำอะไรได้อีกเยอะ Happy Coding ครับ