Google Cloud Workstationsの環境にリバースプロキシ経由でアクセスする

## はじめに

Goを使いちょっとしたリバースプロキシを書くことで、遠隔にあるGoogle Cloud Workstationsに立てたサーバーに対し、ローカルPCから`http://localhost:{port}`でアクセスできたので、その方法を紹介します。

## Google Cloud Workstations とは

Google Cloudが提供するフルマネージドな開発環境として使えるサービスです。

https://cloud.google.com/workstations?hl=ja

2023年5月に正式リリースされました。

https://www.publickey1.jp/blog/23/google_cloudcloud_workstationsbeyondcorp.html

特徴としては

* ローカルPCにソースコードを置くことなく、ブラウザやIDEからアクセスできるセキュアな開発環境を提供する。
* コンテナ定義として開発環境の設定を記述しておけるので、エンジニアは一貫した開発環境を利用できる。

などがあります。

## Google Cloud Workstations の制約

一方、フルマネージドになるがゆえの制約もあります。ポートへのアクセスの仕方がその一つです。

例えば、`http://localhost:3000`で待ち受けるサーバーを、Workstation上で立ち上げていた場合、Workstationの外からアクセスするには、次のURLでアクセスする必要があります。
`https://3000-workstation1.cluster-xxx.cloudworkstations.dev`

`https://{port}-{workstation}.{cluster}.cloudworkstations.dev`
というフォーマットです。

すでに`http://localhost:3000`で接続しにいくクライアントがある場合、この変換が必要になるのは使いづらいため、URLを変えずにすむ方法はないかと調査しました。


## Goでリバースプロキシを書く

リバースプロキシをローカルPC側に立てておき、そこがよしなに変換してくれれば対応できそうです。

自分が比較的よく書くGoでは、リバースプロキシを立てるための機能が標準パッケージで提供されています。
そのため、ローカルのポートで待ち受けて任意のURLに転送するプロキシを簡単に書けます。

https://pkg.go.dev/net/http/httputil#example-ReverseProxy

また、Google Cloud Workstationsのドキュメントに、WorkstationへHTTPリクエストを送る方法が書かれています。

https://cloud.google.com/workstations/docs/authentication?hl=ja#http

具体的には、以下のメソッドを使って、Workstationへのアクセストークンを取得し、それをAuthorizationヘッダーにセットします。
[`projects.locations.workstationClusters.workstationConfigs.workstations.generateAccessToken`](https://cloud.google.com/workstations/docs/reference/rest/v1/projects.locations.workstationClusters.workstationConfigs.workstations/generateAccessToken)
アクセストークンはブラウザ上の"Try this method"から取得することもできます。
![dfba74f8-1837-26f2-2726-9ef649ef5709-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30246/65503b1e-1ecf-13c6-0f80-a3471876f639.png)


以上の2点を組み合わせると、サンプルレベルではありますが50行程度で、`localhost:{port}`を待ち受けてWorkstation側へ転送するリバースプロキシを書くことができました。
転送するポートが複数ある場合でも、`listenProxy`をgoroutineで複数立てておけば対応できます。

```go
package main

import (
	"fmt"
	"net/http"
	"net/http/httputil"
	"net/url"
)

func main() {
	// 3000番ポートでリバースプロキシを起動
	if err := listenProxy(3000); err != nil {
		panic(err)
	}
}

func listenProxy(port uint) error {
	target, err := url.Parse(fmt.Sprintf(workstationURLFormat(), port))
	if err != nil {
		return err
	}
	bearer := fmt.Sprintf("Bearer %s", generateWorkstationToken())

	proxy := &httputil.ReverseProxy{
		Rewrite: func(r *httputil.ProxyRequest) {
			r.SetXForwarded()
			r.SetURL(target)
			r.Out.Header.Set("Authorization", bearer)
		},
	}

	return http.ListenAndServe(fmt.Sprintf(":%d", port), proxy)
}

func workstationURLFormat() string {
	// サンプルのため転送先のURLはハードコード
	return "https://%d-workstation1.cluster-xxxxxxx.cloudworkstations.dev/"
}

func generateWorkstationToken() string {
	// WorkstationのアクセストークンはTTLがあるので、実際は自動で生成したり更新する機構が必要。
	// see: https://cloud.google.com/workstations/docs/authentication?hl=ja#http
	return "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.xxxxxxxxxxxxxxxxxxxxxxxxx"
}
```

## まとめ

実運用に向けた検証はこれからですが、リバースプロキシを介すことでこれまでと変わらないアクセスを実現できそうなことはわかりました。
このようなユースケースが必要となる場面は少ないかもしれませんが、同じような課題に困っている方への参考になれば幸いです。

## 参考資料

* [Google Cloud、クラウドで開発環境を提供する「Cloud Workstations」正式リリース。ゼロトラストのBeyondCorpとの統合など新機能 － Publickey](https://www.publickey1.jp/blog/23/google_cloudcloud_workstationsbeyondcorp.html)
* [Cloud Workstations  |  Google Cloud](https://cloud.google.com/workstations?hl=ja)
* [Tilt | Kubernetes for Prod, Tilt for Dev](https://tilt.dev/)