golangを使って、1クリックでGCP上のWindowsにリモートデスクトップ接続する

# TL;DR
ダブルクリックで指定したGCP上のwindowsインスタンスにリモートデスクトップ接続を開始するツールを作りました。

[mpppk/connect\-to\-gce\-win: Tool for connect to Windows on Google Compute Engine via Remote Desktop](https://github.com/mpppk/connect-to-gce-win)

![connect-to-gce-win2-compressor.gif](https://qiita-image-store.s3.amazonaws.com/0/28464/4bd409b5-9768-51ed-99e6-17c13707390c.gif)

クリックすると以下の処理が自動で行われます。

1. 最初にwindowsインスタンスの一覧を取得し、どのインスタンスに接続するかを表示します。
    * (上のgifの例では、設定ファイルで接続するインスタンスを指定しているので、チェックなしで接続を開始します)
2. 指定されたインスタンスがTERMINATEDであれば、起動します。
3. インスタンスのパスワードがリセットされ、新しいパスワードがクリップボードに保存されます。
3. インスタンスに接続するためのRDPファイルを生成し、OSでファイルに紐づけられているRDPクライアントを自動で開きます。
4. RDPクライアントがパスワードを聞いてきたら、クリップボードに保存されたパスワードをペーストするだけでログインできます。

(本当はRDPファイルにパスワードを含めることもできるっぽいのですが、めんどくさそうだったのでやってません)

# 背景
私はmacしか持っていないのですが、たまにwindowsでしかプレイできないゲームをやりたくなることがあります。
とはいえそんなに長時間やるわけではないので、そのためだけにそこそこハイスペックなwindows機を買うのも気が引けます。
で、よく考えると、動きの少ないカードゲーム等であれば、リモートでも十分プレイ可能では?ということで、やりたくなった時だけGCP上のwindowsインスタンスを立ち上げることにしました。
お値段も東京リージョン/8CPU/10GBメモリで$0.379/hourと、新しくWindows PCを買うことを考えれば大変お安いです。
(これは異常にCPUリソースを消費する[Magic Arena](https://magic.wizards.com/en/mtgarena)というゲームでの例なので8CPUという豪華な構成ですが、他のほとんどのゲームであればもっと安く済むと思います。n1-standard-2(2CPU/7.5GBメモリ)なら$0.1220/hですね。)

## GCP上のwindowsへ接続するまでの手順
通常、GCPのwindowsを立ち上げるには以下のステップを踏みます。
(インスタンスの作成とRDPクライアントのインストールは済んでいるものとします)

1. GCEのダッシュボードを開き、起動したいインスタンスの右側の「起動」を選択

<img width="871" alt="gce-dashboard.png" src="https://qiita-image-store.s3.amazonaws.com/0/28464/94329442-9353-06fb-d666-eebcdec6f51a.png">

3. 起動したらパスワードをリセットして、新しいパスワードをコピーしておく
    * ローカルにパスワードを保存しておくより、毎回リセットした方が安全なので

<img width="983" alt="gce-reset-windows-password.png" src="https://qiita-image-store.s3.amazonaws.com/0/28464/9f166fd4-2c90-068d-fd62-be761754d03d.png">

4. 同じメニューからRDPファイルをダウンロード
5. RDPクライアントで先ほどダウンロードしたRDPファイルを開く
6. パスワードを聞かれたら、先ほどコピーしたパスワードを入力
7. ゲームが終わったらインスタンスを落とす

本当は静的IPアドレスを振っておけば同じRDPファイルを使い回せるのですが、インスタンスを落としている間もお金がかかってしまうので、毎回新しくRDPファイルをダウンロードしています。
まあこれでも十分お手軽なので、ゲームをプレイする前に毎回上記の手順を行なっていたのですが、怠惰な人間なのでもっと楽に接続したくなってきました。

# golangで一連の処理を行う
GCPの起動やパスワードのリセットは全てAPI経由でも操作可能ですので、自動化できそうです。言語は何でも良いですが、今回はgolangで書いていくことにします。
パッと調べた感じ、GCEのgolang SDKは以下の2種類が使えそうです。

* [googleapis/google\-cloud\-go: Google Cloud Client Libraries for Go\.](https://github.com/googleapis/google-cloud-go)
    * google APIの定義から自動生成されたライブラリ
* [Daisy \| compute\-image\-tools](https://googlecloudplatform.github.io/compute-image-tools/daisy.html)
    * GCEに関するワークフローをDSLで定義できるっぽい?(今回はその機能は使ってません)

それぞれでしかできないことがあったりするので、今回は2つを併用しています。

## クライアントを生成

daisyは以下のようにimportしている前提です。

```golang
import daisyCompute "github.com/GoogleCloudPlatform/compute-image-tools/daisy/compute"
```

```golang
ctx := context.Background()

// google apiのクライアント生成
googleDefaultClient, err := google.DefaultClient(ctx)
gceClient, err := compute.New(googleDefaultClient)

// daisyのクライアント生成
daisyClient, err := daisyCompute.NewClient(ctx)
```

## 指定したインスタンスを取得
`daisyClient.GetInstance`で指定した名前のインスタンスの情報を取得できます。

```golang
instance := daisyClient.GetInstance(c.project, c.zone, instanceName)
fmt.Println(instance) // => e.g. "TERMINATED"
```

## インスタンスの一覧を取得
`gceCompute.ListInstance`で指定した名前のインスタンスの情報を取得できます。

```golang
instances, err := gceClient.ListInstances()
```

## インスタンスを起動
`daisyClient.StartInstance`で指定した名前のインスタンスを起動し、STATUSが"RUNNING"になるまでブロックします。
`gceClient`でも起動はできるのですが、起動完了を知るためにGetInstanceを定期的に叩いてSTATUSをチェックしなければならないので、daisyの方が楽です。

```golang
err := daisyClient.StartInstance(instance.Name)
```

## パスワードをリセット
パスワードのリセットは特にAPIが用意されているわけではなく、`Metadata`の`windows-keys`という項目へ暗号化した新たなパスワードを追加します。
オフィシャルドキュメントで詳しい方法が解説されているので、こちらをご覧ください。
[Windows パスワード生成の自動化  \|  Compute Engine ドキュメント  \|  Google Cloud](https://cloud.google.com/compute/docs/instances/windows/automate-pw-generation?hl=ja)

## RDPファイルを生成

RDPファイルは単に以下のフォーマットで書かれたテキストファイルです。

```txt
full address:s:[target_ip_address]
username:s:[your_username]
```

というわけで、インスタンスからIPアドレスを取得して、ファイルを生成します。

```golang
func extractNatIpFromInstance(instance *compute.Instance) (string, error) {
	if len(instance.NetworkInterfaces) == 0 {
		return "", errors.New("no NetworkInterfaces found")
	}
	accessConfigs := instance.NetworkInterfaces[0].AccessConfigs
	if len(accessConfigs) == 0 {
		return "", errors.New("no AccessConfigs found")
	}
	return accessConfigs[0].NatIP, nil
}

func generateRDPFile(filePath, ip, userName string) error {
	contents := "full address:s:" + ip + "\n" + "username:s:" + userName
	return ioutil.WriteFile(filePath, []byte(contents), 0644)
}

natIP, err := extractNatIpFromInstance(instance)
err = generateRDPFile("test.rdp", natIP, "your_username")
```

## RDPファイルを開く
[skratchdot/open-golang](https://github.com/skratchdot/open-golang)でRDPファイルを開けば、OSで紐づけられたクライアントが起動します。

```
err := open.Run("test.rdp")
```

# 実際の実装
これらの処理を実際に書いている部分が、[connect-to-gce-win/cmd/root.go](https://github.com/mpppk/connect-to-gce-win/blob/master/cmd/root.go)です。
実際に動作するコードを確認したい方はこちらをご覧ください。

これで、GCP上のWindowsに一瞬で接続できるようになりました。
Windows PC持ってない方はぜひご検討ください٩(●˙▿˙●)۶