なにをした?
トークン認証が必要なAPIを簡単に利用するために、プロキシ用のエンドポイントをnginx+Luaで作成した。(実際に利用したのはOpenRestyですが)
といっても、それまでクライアントでやってた「トークン取得」と「トークンをつけたリクエスト」をサーバ上で肩代わりさせただけです。
なぜやろうとしたか?
以前に PhotonOSでk8sクラスタ作成 したとき、dockerのratelimitに引っかかったかどうか、以下のように確認していました。
$ TOKEN=$(curl -sSL "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token)
$ curl -i -sSL -v -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest 2>/dev/null | egrep "ratelimit-|rate limit"
ratelimit-limit: 100;w=21600
ratelimit-remaining: 65;w=21600
docker-ratelimit-source: x.x.x.x(← これはアクセス元のIPアドレスになる)
ただ、コマンドラインで2行分ペーストするのは面倒だし、戻ってくる情報もJSONじゃないし(そもそもJSONを返すオプションがあるのか?とかを調べるのが面倒でもあった)、なんかスマートじゃないなぁ。と思った。
で、どうなった?
最終的に、内部からのみアクセスできるWebサーバの特定のエンドポイントにアクセスすると、必要な情報だけ取得できるようになりました。
$ curl -s http://[自サーバ]/docker-limit/ | jq
{
"limit": "100;w=21600",
"remain": "61;w=21600",
"source": "x.x.x.x"
}
検証環境
- CentOS7( CentOS Linux release 7.9.2009 (Core) )
- OpenResty( nginx version: openresty/1.21.4.1 )
作業内容
OpenResty導入
sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
sudo yum install -y openresty
OpenResty設定
トークン取得して、プロキシ用エンドポイントへのリクエストがあった場合に転送する設定を書きます。
とりあえず動作する最低限の設定のみなので、これ1ファイルで全文です。
dockerじゃないサービスの場合は、以下の変更が必要です
- トークン取得用のlocationにある proxy_pass のプロキシ先
- ngx.req.set_header でセットするヘッダ名と、対象のトークンがレスポンスのどのキーに含まれているか
- APIプロキシ用のlocationにある proxy_pass のプロキシ先
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name _;
# トークン取得用の location
# (「このサーバから取得したトークン」が取得できてしまうので、internalで設定する)
location /get-docker-token/ {
internal;
proxy_pass https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull;
}
# APIプロキシ用の location
location ~ ^/docker-proxy/ {
access_by_lua_block {
-- json処理するためのモジュールを読み込み
local cjson = require "cjson"
-- DockerAPIにアクセスするためのトークンをリクエスト
local res = ngx.location.capture("/get-docker-token/")
-- トークンが取得できていたらリクエストヘッダにセット
if res.status == ngx.HTTP_OK then
body = cjson.decode(res.body)
-- 認証ヘッダは Authorization で、値はレスポンスJSONのtokenキーの値
ngx.req.set_header("Authorization", "Bearer "..body.token)
else
ngx.log(ngx.ERR, 'GET TOKEN REQUEST: FAILED')
ngx.exit(500)
end
}
# もとのリクエストパスをプロキシのパスにする
rewrite ^/docker-proxy/(.*)$ /$1 break;
# lua内で付与した Authorization をそのまま利用するため、proxy_pass_header する
proxy_pass_header Authorization;
# docker-proxyでアクセスさせるAPIドメインを設定
proxy_pass https://registry-1.docker.io;
}
}
}
アクセスしてみる
$ curl -i -s http://[自サーバ]/docker-proxy/v2/ratelimitpreview/test/manifests/latest | egrep "ratelimit-|rate limit"
ratelimit-limit: 100;w=21600
ratelimit-remaining: 66;w=21600
docker-ratelimit-source: x.x.x.x
汎用的なAPIプロキシが完成!
でも、スマートさに欠けるなぁ
整形したい
汎用的なAPIプロキシ用のエンドポイントを作成しても、結局使うのはratelimitの部分だけなので、ここに対するプロキシに限定して、レスポンスを整形してから返してあげることにした。
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name _;
# トークン取得用の location
# (「このサーバから取得したトークン」が取得できてしまうので、internalで設定する)
location /get-docker-token/ {
internal;
proxy_pass https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull;
}
# ratelimitを取得するための location
# lua内で付与した Authorization をそのまま利用するため、proxy_pass_header して使う
location /get-docker-limit/ {
proxy_pass_header Authorization;
proxy_pass https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest;
}
# ratelimit専用のAPIプロキシ用の location
location /docker-limit/ {
access_by_lua_block {
-- json処理するためのモジュールを読み込み
local cjson = require "cjson"
-- DockerAPIにアクセスするためのトークンをリクエスト
local res = ngx.location.capture("/get-docker-token/")
-- トークンが取得できていたらリクエストヘッダにセット
if res.status == ngx.HTTP_OK then
body = cjson.decode(res.body)
ngx.req.set_header("Authorization", "Bearer "..body.token)
else
ngx.log(ngx.ERR, 'GET TOKEN REQUEST: FAILED')
ngx.exit(500)
end
-- DockerAPIへratelimitを問い合わせ
res = ngx.location.capture("/get-docker-limit/")
-- 問い合わせ結果が返ってきたら、レスポンスヘッダのratelimit関連を整形して出力
if res.status == ngx.HTTP_OK then
--body = cjson.decode(res.body)
result = {
limit = res.header["ratelimit-limit"],
remain = res.header["ratelimit-remaining"],
source = res.header["docker-ratelimit-source"]
}
ngx.say(cjson.encode(result))
else
ngx.log(ngx.ERR, 'GET LIMIT REQUEST: FAILED')
ngx.exit(500)
end
}
}
}
}
(luaモジュールが入るとシンタックスがうまくいかないなぁ)
アクセスしてみる
$ curl -s http://[自サーバ]/docker-limit/ | jq
{
"limit": "100;w=21600",
"remain": "61;w=21600",
"source": "x.x.x.x"
}
やったぜ(今日はのこり61回分だ!)
さいごに
ngx.location.capture には直接URLを指定できないがためにlocationを設定しているだけなので、これらをinternalにするのはもちろんですが、プロキシ用エンドポイント自体も外部にさらさないようにしておく必要があります。そうしないと、外部の人が認証トークンを使ってアクセスできてしまうので、まじぃです。
プロキシ用のエンドポイントを利用するための認可をつけておくと良いですね。
参考