shell2httpを使ってお手軽にCLIツールをHTTP API化する

この記事は、[富士通クラウドテクノロジーズ Advent Calendar 2020](https://qiita.com/advent-calendar/2020/fjct) の **11日目** の記事です。   
10日目は @nashikさんの [PowerPointで絶対にプレゼンの時間を遵守させるツールを作る](https://qiita.com/nashik/items/d56f5cdf19da4df66352) の記事でした。   
パワポVBAでこんなこともできるのですね。興味深い記事でした。（**ウサギ実行**という関数名が素敵です）

本日はIaaS APIの開発をしている私が、開発をする上で役立ちそうなツールについて紹介したいと思います。

## はじめに
最近API開発でとあるシミュレーター（ステートフルなMockサーバーのようなもの）を使っています。そのシミュレーターをテストコード内で呼び出すときに、起動と終了をテストケースごとに行えれば実装がシンプルになります。

事前処理や事後処理内でプロセスを直接呼びだしても良いのですが、そんなヘビーなことをテストコード内でやりたくありませんね・・

CLIを制御するようなHTTP API生えてきてくれないかな :angel_tone2: と思っていたら簡単に生やす方法がありそうなので見てみたいと思います。

## shell2http
[shell2http](https://github.com/msoap/shell2http) は名前の通りシェルコマンドをHTTP越しで実行するためのツールです。   
Go製でStar数は2020/12/09現在 **666** です。   
今回はこちらのツールを利用してAPI化してみたいと思います。

### インストール
[Install](https://github.com/msoap/shell2http#install) に書いてある手順のどれかでインストールできます。

Goが使えれば下記が楽かと思います。

```sh
go get -u github.com/msoap/shell2http
# set link to your PATH if needed:
ln -s $(go env GOPATH)/bin/shell2http ~/bin/shell2http
```

### 使ってみる
とりあえずHTTP経由でdateコマンドを実施したいと思います。
API公開は下記のコマンドを実施するだけです。Go製のツールだけあってさくっと一瞬で起動します。

- ターミナル1
   - サーバー起動
 
```sh
$ shell2http /date "date"
```

- ターミナル2
   - HTTPリクエスト

```sh
$  curl http://localhost:6000/date
Wed Dec  9 18:12:32 JST 2020
```

ターミナル1でlocalhostの `/hello` にechoコマンドがバインドされたサーバーが起動したため、ターミナル2でcurlで取得できるようになりました。

### 指定可能なオプションについて

`$ shell2http -help` で指定可能なオプションが見れます。   
 `-port` でのサーバーポート指定や `-form` でのリクエストパラメータ指定はよく使うかと思います。

```sh
$ shell2http -help
usage: shell2http [options] /path "shell command" /path2 "shell command2"
  -add-exit
        add /exit command
  -basic-auth string
        setup HTTP Basic Authentication ("user_name:password")
  -cache int
        caching command out (in seconds)
  -cert string
        SSL certificate path (if specified -cert/-key options - run https server)
  -cgi
        run scripts in CGI-mode
  -export-all-vars
        export all current environment vars
  -export-vars string
        export environment vars ("VAR1,VAR2,...")
  -form
        parse query into environment vars, handle uploaded files
  -host string
        host for http server
  -include-stderr
        include stderr to output (default is stdout only)
  -key string
        SSL private key path
  -log string
        log filename, default - STDOUT
  -no-index
        don\'t generate index page
  -no-log-timestamp
        log output without timestamps
  -one-thread
        run each shell command in one thread
  -port int
        port for http server (default 8080)
  -shell string
        custom shell or "" for execute without shell (default "sh")
  -show-errors
        show the standard output even if the command exits with a non-zero exit code
  -timeout int
        set timeout for execute shell command (in seconds)
  -version
        get version

```

## shell2httpでCLIツールの制御用コマンドをAPI公開する
先述した通り、CLIツールの起動、終了をAPI経由で叩けるようにしてみます。
サンプルでは[vcsim](https://github.com/vmware/govmomi/tree/master/vcsim)というvCenterのシミュレーターツールの起動・終了コマンドをAPI公開したいと思います。

### APIサーバー起動
vcsimを制御する下記のような3つのAPIを起動してみます。

- `/start`
    - vcsimを起動する(リクエストパラメータでオプションを指定可能)
- `/kill`
    - 起動中のvcsimプロセスを強制終了する
- `/status`
    - vcsimが動作していることを確認する

下記でサーバーを起動します。

```sh
$ shell2http -form -port 5000 \
/start "vcsim $v_options" \
/kill "killall vcsim" \
/status "env GOVC_URL=https://user:pass@127.0.0.1:8989/sdk govc find -k ."
```

### `/start` をHTTPリクエストしてvcsimを起動 :sunrise: 
オプションパラメータを指定しつつ起動します。

```sh
$ curl -m 1 http://localhost:5000/start --data-urlencode "options=-cluster 2"
```
※ フォアグラウンドで起動しますので `-m` オプションクライアント側からタイムアウトしています。
※ `options` として指定したリクエストパラメーターが `$v_options` の形でshell2http側で変換されます。

### `/status` をHTTPリクエストしてvcsimの状態を確認 :eyes: 
起動できたことを確認します。シミュレーターがVMっぽいものを起動していますね。

```sh
$ curl http://localhost:5000/status
/
/DC0
/DC0/vm
/DC0/vm/DC0_H0_VM0
/DC0/vm/DC0_H0_VM1
...
```

### `/kill` をHTTPリクエストしてvcsimを終了 :waxing_gibbous_moon: 
起動できましたので今度はシャットダウンしてみます。

```sh
$ curl http://localhost:5000/kill
```

### `/status` をHTTPリクエストしてvcsimの状態を再確認 :eyeglasses: 
終了しているかどうかを確認するため、もう一度状態を確認してみます。確認時にエラーが発生していることを確認できます。

```sh
$ curl http://localhost:5000/status
exec error: exit status 1% 
```

---

以上のようにすればコマンドの起動、終了、状態確認をHTTP経由で実施できます。
curlで行っているリクエストをそれぞれのプログラミング言語のHTTPクライアントで実装するだけで、簡単にテストが書けますね！

サンプルとしてはvcsimを用いていますが、他のツールでも同様の手順でAPI公開できるかと思います。

## (おまけ) HTTPメソッドについて
今回は簡単にすべてGETメソッドで実現しましたが、一般的に `/kill` や `/start` はPOSTメソッドで実現するかと思いますのでそんなときはshell2httpの起動コマンドを下記のようにパスの前にメソッドを指定すればよいです。こうしておくことで `/kill` がブラウザで補完されてうっかり消してしまうことはなくなるでしょう。


```sh
$ shell2http -form -port 5000 \
POST:/start "vcsim $v_options" \
POST:/kill "killall vcsim" \
GET:/status "env GOVC_URL=https://user:pass@127.0.0.1:8989/sdk govc find -k ."
```

## (おまけ) shell2httpをコンテナ化
テストコードで扱う上ではshell2http自体はコンテナを起動するだけで実行できるとさらに良いですね。
vcsimの場合例えば下記のようにDockerfileを書くことで同様の挙動を期待できます。

```dockerfile:Dockerfile
FROM nimmis/vcsim
EXPOSE 8989 443 80 8080

# shell2httpで制御用コマンドをAPI公開する
COPY --from=msoap/shell2http /app/shell2http /usr/local/bin/shell2http
EXPOSE 5000
ENTRYPOINT ["/usr/local/bin/shell2http", "-form", "-port", "5000","/spawn", "hostname | xargs -I {} vcsim -l {}:8989 $v_options", "/killall", "killall vcsim --wait -q", "/govc_find", "hostname | xargs -I {} env GOVC_URL=https://user:pass@{}:8989/sdk govc find -k ."]
```

## まとめ
シェルコマンドをHTTP API化する方法を紹介しました。   
今回はテスト用途での利用を想定しましたが、他にも活用の幅は広いツールかと思いますのでぜひご活用ください。

※ ちなみに本番環境で使うのであれば、リクエストパラメーターでのOSコマンドインジェクションの脆弱性がないか、認証は十分か等セキュリティ対策が十分に必要ですので、お手軽に使いたいのでしたらあくまで**検証、開発用のある程度閉じた環境で使うことをお勧め**します。

---

明日は @tily さんの2020年のFJCTエンジニアタスクフォースの活動報告記事です。    
今年はどのような活動をしたのでしょうか。ぜひご覧ください！

## 参考文献
- [https://github.com/ansible/vcenter-test-container](vcenter-test-container)
  - vcsimをコンテナ化しflaskでラッパーしたもの(本記事はこれをより汎用的に使う方法を考えた結果です) 
- [shell2httpでAWS FargateやSSHレスの運用を少しだけ楽にする](https://blog.s-tajima.work/post/2018/shell2http-on-fargate/)
- 類似ツール
  - [webhook](https://github.com/adnanh/webhook)
      - jsonでより細かく設定できる。どちらかというとパッケージ名通りwebhook的用途向き。
  - [websocketd](https://github.com/joewalnes/websocketd)
      - コマンドをwebsocketで公開できる。面白そう。