Help us understand the problem. What is going on with this article?

Go言語でKibana(Elastic Cloud)へのプロキシを作成する

この記事では App Engine Standard Environment(2nd gen) を用いて Kibana へのプロキシを作成する方法について紹介しています。

Elastic Cloud の一番安いプランで一つデプロイを持っているのですが、今回そこで可視化しているダッシュボードを不特定多数に公開したくなりました。
そこで困ったのが、Elastic Cloud には Anonymous ユーザ機能がない・・・。
ダッシュボードにアクセスさせるにはユーザにログインしてもらう必要があり、しかしパスワードとかは勝手に変更されないようにしなければならないので、みなさんどうやってるのか調べた結果、

  • nginx や Apache でリバースプロキシ
    • Bearer basic ヘッダーで自動でログイン
    • https://{ユーザ名}:{パスワード}@yoursubdomain.yourdomain/kibana では自動ログインできないので、ヘッダーで送る必要がある(ただ、ヘッダーの方がユーザパスワードを iframe の url に書かなくていいので安心できそう
    • パスワード変更のリクエストをピンポイントで弾く設定を入れる

として対応しているようでした。
この方針で問題ないのですが、自分の場合以下の問題が発生。

  • nginx 立てたくない
    • https なサイトに iframe でダッシュボードを表示したいので nginx を https 化することが必須
    • Let's encrypt とかで自動化したとしても、念のため定期的に証明書の確認しなきゃいけないのが嫌だった
    • nginx 立てるお金がない
    • f1-micro 等無料枠の仮想マシンに建てるにしても証明書管理したくなかった
    • Google App Engine Flexible なら Docker で http なリバプロ立てておけば Google が勝手に https な URL を作ってくれる(*.appspot.com)けど、Flexible Environment には無料枠がない:sob:

と言うわけで、App Engine Standard Environment で Go で適当なプロキシ作ることにしました。
特定のヘッダーにのみ気をつけて GET/POST プロキシさせるだけできれいに表示できた。
(けど、なんか他にいい方法ないですかね??)

main.go
package main

import (
    "context"
    "time"

    "cloud.google.com/go/profiler"
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()

    router.AppEngine = true

    // Stackdriver Profiler
    profiler.Start(profiler.Config{})

    // login/logout は触れさせない
    // router.Any("/login", proxy.Proxy)
    // router.Any("/logout", proxy.Proxy)
    router.Any("/bundles/*uri", proxy.Proxy)
    router.Any("/built_assets/*uri", proxy.Proxy)
    router.Any("/node_modules/*uri", proxy.Proxy)
    router.Any("/translations/*uri", proxy.Proxy)
    router.Any("/ui/*uri", proxy.Proxy)
    router.Any("/api/*uri", proxy.Proxy)
    router.Any("/s/yourspace/*uri", proxy.Proxy) // FIXME 「yourspace」と言う名前のスペース以外触れさせない

    router.Run()
}
proxy.go
package proxy

import (
    "encoding/base64"
    "net/http"
    "strings"
    "time"

    "github.com/gin-gonic/gin"
)

const KibanaURL = "https://yourkibana.us-west-2.aws.found.io:9243" // FIXME

var Authorization = "Basic " + base64.StdEncoding.EncodeToString([]byte("youruser:yourpassword")) // FIXME

func replaceDomain(url string) string {
    return strings.ReplaceAll(url, "https://yourgaeproject.appspot.com", KibanaURL) // FIXME
}

func Proxy(ctx *gin.Context) {
    method := ctx.Request.Method

    httpClient, err := httput.NewClient(ctx, time.Duration(20)*time.Second)
    if err != nil {
        ctx.Status(http.StatusInternalServerError)
        return
    }

    if strings.EqualFold(method, "GET") {
        ProxyGet(ctx, httpClient)
    } else if strings.EqualFold(method, "POST") {
        ProxyPost(ctx, httpClient)
    } else {
        ctx.Status(http.StatusMethodNotAllowed)
        return
    }
}

func ProxyGet(ctx *gin.Context, httpClient *httput.HTTPClient) {
    uri := ctx.Request.URL.String()

    if strings.Contains(uri, "/api/spaces/space") { // スペース一覧の表示を禁止
        ctx.Status(http.StatusForbidden)
        return
    }

    if strings.HasPrefix(uri, "/s/") && !strings.Contains(uri, "/yourspace/") { // FIXME yourspace スペース以外のスペースの表示を禁止
        ctx.Status(http.StatusForbidden)
        return
    }

    isStatic := strings.HasSuffix(uri, ".js") || strings.HasSuffix(uri, ".css") || strings.HasSuffix(uri, ".svg")
    if isStatic {
        staticContent, err := redisut.Get(ctx, uri).Result()
        if err == nil { // Redis に入ってればそれを使う
            if strings.HasSuffix(uri, ".js") {
                ctx.Writer.Header().Add("Content-Type", "text/javascript")
            } else if strings.HasSuffix(uri, ".css") {
                ctx.Writer.Header().Add("Content-Type", "text/css")
            } else if strings.HasSuffix(uri, ".svg") {
                ctx.Writer.Header().Add("Content-Type", "image/svg+xml")
            }

            ctx.String(http.StatusOK, staticContent)

            return
        }
    }

    headers := make(map[string]string)
    headers["Authorization"] = Authorization
    if ctx.Request.Header.Get("kbn-version") != "" {
        headers["kbn-version"] = ctx.Request.Header.Get("kbn-version")
    }
    if ctx.Request.Header.Get("User-Agent") != "" {
        headers["User-Agent"] = ctx.Request.Header.Get("User-Agent")
    }
    if ctx.Request.Header.Get("Origin") != "" {
        headers["Origin"] = replaceDomain(ctx.Request.Header.Get("Origin"))
    }
    if ctx.Request.Header.Get("Referer") != "" {
        headers["Referer"] = replaceDomain(ctx.Request.Header.Get("Referer"))
    }

    resp, err := httpClient.GetURLResponse(ctx, KibanaURL+uri, "GET", headers)
    if err != nil {
        ctx.Status(http.StatusInternalServerError)
        return
    }

    if resp.Header.Get("Location") != "" {
        ctx.Redirect(resp.StatusCode, resp.Header.Get("Location"))
        return
    }
    if resp.Header.Get("kbn-license-sig") != "" {
        ctx.Writer.Header().Add("kbn-license-sig", resp.Header.Get("kbn-license-sig"))
    }
    if resp.Header.Get("kbn-name") != "" {
        ctx.Writer.Header().Add("kbn-name", resp.Header.Get("kbn-name"))
    }
    if resp.Header.Get("kbn-xpack-sig") != "" {
        ctx.Writer.Header().Add("kbn-xpack-sig", resp.Header.Get("kbn-xpack-sig"))
    }
    if resp.Header.Get("Content-Type") != "" {
        ctx.Writer.Header().Add("Content-Type", resp.Header.Get("Content-Type"))
    }
    if resp.Header.Get("x-cloud-request-id") != "" {
        ctx.Writer.Header().Add("x-cloud-request-id", resp.Header.Get("x-cloud-request-id"))
    }
    if resp.Header.Get("x-found-handling-cluster") != "" {
        ctx.Writer.Header().Add("x-found-handling-cluster", resp.Header.Get("x-found-handling-cluster"))
    }
    if resp.Header.Get("x-found-handling-instance") != "" {
        ctx.Writer.Header().Add("x-found-handling-instance", resp.Header.Get("x-found-handling-instance"))
    }
    if resp.Header.Get("x-found-handling-server") != "" {
        ctx.Writer.Header().Add("x-found-handling-server", resp.Header.Get("x-found-handling-server"))
    }

    body, err := httput.GetBody(ctx, resp.Body, nil)
    if err != nil {
        ctx.Status(http.StatusInternalServerError)
        return
    }

    if isStatic {
        redisut.SetNX(ctx, uri, body, redisut.StaticFilesTTL)
    }

    ctx.String(resp.StatusCode, body)
}

func ProxyPost(ctx *gin.Context, httpClient *httput.HTTPClient) {
    uri := ctx.Request.URL.String()

    if strings.Contains(uri, "/internal/security/") { // パスワード等プロフィールの変更を禁止
        ctx.Status(http.StatusForbidden)
        return
    }

    if strings.HasPrefix(uri, "/s/") && !strings.Contains(uri, "/yourspace/") { // FIXME yourspace スペース以外の表示を禁止
        ctx.Status(http.StatusForbidden)
        return
    }

    headers := make(map[string]string)
    headers["Authorization"] = Authorization
    if ctx.Request.Header.Get("kbn-version") != "" {
        headers["kbn-version"] = ctx.Request.Header.Get("kbn-version")
    }
    if ctx.Request.Header.Get("User-Agent") != "" {
        headers["User-Agent"] = ctx.Request.Header.Get("User-Agent")
    }
    if ctx.Request.Header.Get("Content-Length") != "" {
        headers["Content-Length"] = ctx.Request.Header.Get("Content-Length")
    }
    if ctx.Request.Header.Get("Content-Type") != "" {
        headers["Content-Type"] = ctx.Request.Header.Get("Content-Type")
    }
    if ctx.Request.Header.Get("Origin") != "" {
        headers["Origin"] = replaceDomain(ctx.Request.Header.Get("Origin"))
    }
    if ctx.Request.Header.Get("Referer") != "" {
        headers["Referer"] = replaceDomain(ctx.Request.Header.Get("Referer"))
    }

    resp, err := httpClient.PostBodyURLResponse(ctx, KibanaURL+uri, ctx.Request.Body, headers)
    if err != nil {
        ctx.Status(http.StatusInternalServerError)
        return
    }

    if resp.Header.Get("Location") != "" {
        ctx.Redirect(resp.StatusCode, resp.Header.Get("Location"))
        return
    }
    if resp.Header.Get("kbn-license-sig") != "" {
        ctx.Writer.Header().Add("kbn-license-sig", resp.Header.Get("kbn-license-sig"))
    }
    if resp.Header.Get("kbn-name") != "" {
        ctx.Writer.Header().Add("kbn-name", resp.Header.Get("kbn-name"))
    }
    if resp.Header.Get("kbn-xpack-sig") != "" {
        ctx.Writer.Header().Add("kbn-xpack-sig", resp.Header.Get("kbn-xpack-sig"))
    }
    if resp.Header.Get("Content-Type") != "" {
        ctx.Writer.Header().Add("Content-Type", resp.Header.Get("Content-Type"))
    }
    if resp.Header.Get("x-cloud-request-id") != "" {
        ctx.Writer.Header().Add("x-cloud-request-id", resp.Header.Get("x-cloud-request-id"))
    }
    if resp.Header.Get("x-found-handling-cluster") != "" {
        ctx.Writer.Header().Add("x-found-handling-cluster", resp.Header.Get("x-found-handling-cluster"))
    }
    if resp.Header.Get("x-found-handling-instance") != "" {
        ctx.Writer.Header().Add("x-found-handling-instance", resp.Header.Get("x-found-handling-instance"))
    }
    if resp.Header.Get("x-found-handling-server") != "" {
        ctx.Writer.Header().Add("x-found-handling-server", resp.Header.Get("x-found-handling-server"))
    }

    body, err := httput.GetBody(ctx, resp.Body, nil)
    if err != nil {
        ctx.Status(http.StatusInternalServerError)
        return
    }

    ctx.String(resp.StatusCode, body)
}

(httput は GoでGET/POST を参照してください)

もちろん nginx とか立てた方が性能はいいと思います。
redis へのキャッシュも実際にファイルの更新あるかどうか関係なく時間決め打ちで適当なのでとりあえず動けばいい人向きです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away