Cloud9 PRO終了のお知らせにカッとなって、自分用のCloud9を作った

まえおき

f:id:YusukeIwaki:20190713151336p:plain

Cloud9AWS Cloud9という似て非なるサービスに変わった。

自分はCloud9 PROというサービスをとても熱心に(課金して)使っていたので、「これからはAWS Cloud9を使ってね♪」って気軽に言われてもな、そうはいかないんだ。

  • AWSGitHubじゃログインできないし
  • Cloud9 PROだと Runボタンをポチッとおしたらhttpsで即座に外から見れた のが、AWS Cloud9はできないし
  • そもそも開発環境なのに従量課金なんてありえないだろ!!!!!!

ということで、ものすごくフラストレーションが溜まっていた。

Cloud9 PROが良かった理由

他のクラウドIDEは使えなさすぎた

Cloud9終了のお知らせを受けて、 Alternatives to Cloud9 とかでGoogle検索して色々探したよ。有名ドコロだと

などなど。

ただ、実際に使ってみて使い物にならなかった。Paiza Cloudは謎のGUIがとても邪魔くさかったし、CodeAnywhereはエディタの品質が悪すぎ&コンテナごとに画面が分けられていないのがしっくりこなかった。

あと、両者とも致命的にダメだったのが、コンソールからエディタを起動できないこと。

Cloud9は c9 ってコマンドを使うとコンソールからエディタが立ち上がるんだけど、

f:id:YusukeIwaki:20190713013551g:plain

それに相当する機能がPaiza CloudにもCodeAnywhereにもなかった。

これエンジニアだったら必須機能だと思うんだけど、PaizaやCodeAnywhereはユーザの使い込み試験やってないの?

料金体系がエンジニアフレンドリーだったCloud9 PRO

成果物をhttpsでササッと公開する、というだけならCloud9じゃなくても他のクラウドIDEでもできるんだけど、無料でそこまで太っ腹なことをやっているところは無かった。完全に蛇足だけど、いろいろ話題を読んでいるプログラミングスクール とかでも、昔は↑を最大限に活用したカリキュラムを組んでいた(と、人づてに聞いた)。今はどうしてるんだろう。

個人的には課金はしていたので、無料であることは求めていなかったんだけど、それでも

  • 「あ、これやってみたい」と思ったものは基本的に新規に作業用コンテナ作って始めたい
    • 作成可能なコンテナ数に上限があるとか論外
  • せめて1つくらいは常時(外部から)接続可能なコンテナはあってほしい
  • 料金は10ドル/月 くらいであってほしい

という、エンジニアの平均的な要望を満たしてくれるIDEはCloud9 PRO以外には無かったのだ。

無いなら作るしかない

Cloud9 はエディタとしては100% ではないんだけど、ちょうどいい具合に整っていた。Cloud9 PROが終了してもAWS Cloud9は使いたくないし、他のクラウドIDEも使いものにならないので、もう自分で作る以外の選択肢が現状では無い。

Cloud9は親切にも、SDKを公開してくれている。

github.com

あとは、クラウドワ ークス って会社で働いていたときに

qiita.com

こんな素晴らしいことをやってるメンバーがいて、これにCloud9 SDKを組み込むと理想的な使い捨て開発環境ができることはお試し済みだった。

なので、サーバーの月額費用だけなんとかなれば、自分用のCloud9 PROはできるという確信はあった。

ちょうどタイミングよく、今の会社でMicrosoft Azureの優待利用(1ヶ月あたり○○円までは無料でずっと使える、的なやつ)があったのでそれも相まって、作ることにした。

自分用のCloud9 PROっぽい環境の作る前に・・・

Qiita記事にするか迷ったけど、面倒なのでここに書く。

前述の通り、ほぼほぼ Docker swarmで作る社内heroku: yadockeri - Qiita のパクリである。

用意するもの

制限事項

自分専用なので

  • 認証は特にやらない
  • SSL化はいったんあきらめる

ということにする。

SSHのポートだけ開放して、SSHポートフォワーディングで使う想定。

自分用のCloud9 PROっぽい環境を作る

さて作るぞ。

とりあえずEC2のインスタンスが3台あると仮定してすすめる。

同じVPC(Azureだったらリソースグループ)上に配置して、プライベートIPで相互に通信できるようにしておく。

dockerを入れる

個々のサーバーにSSHで入り、

curl https://raw.githubusercontent.com/YusukeIwaki/cloud-pine/190e8454b1dbae0521f035f7f37ceb62f8f126f4/install-docker-ce.sh | bash

する。

Docker Swarmクラスタを組む

c9-masterで docker swarm init する。

swarm initすると、「ワーカーを追加するには docker swarm join ….. —token=xxxxxx を打ってね」みたいなのが出てくるので、そのコマンドをコピって、c9-slave1, c9-slave2 で実行する。

ちなみに最近のDockerはSSH経由でリモートのDockerホストに接続できる(参考: Docker CE 18.09からssh経由でリモートのdockerデーモンに接続できるようになるってよ - Qiita )ので、c9-masterから

DOCKER_HOST=ssh://c9-slave1 docker swarm join …….. —token=xxxxxx

のように実行すれば、いちいちc9-slave1やc9-slave2にSSHしにいかなくても済む。

c9-masterで docker node ls を打って

$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
eeshee1yai5Moh5saGu0Ueje *   c9-master   Ready               Active              Leader              18.09.7
ahm3iexaeth5co2eir0Hahno     c9-slave1   Ready               Active                                  18.09.7
eangie6ohDigh1Zu4epung2I     c9-slave2   Ready               Active                                  18.09.7

みたいな出力が得られたらOK。

ワークスペース用のコンテナをつくる

Cloud9 PROはUbubtu 14.04ベースで、 RubyPythonもNodeもgitもheroku-cliaws-cliも(バージョンは古いけど)入っている状態だった。

とりあえず今はそこまでは求めないので、Cloud9 SDKだけ入れたDebianにする。(本気で使い始めたら、RubyPythonくらいは入れようと思う) cloud-pine/Dockerfile at df0962b100241db3eb3fd40219d6cc22e4195ea8 · YusukeIwaki/cloud-pine · GitHub

docker buildして、DockerHubにプッシュする。 https://hub.docker.com/r/yusukeiwaki/cloud-pine-workspace

docker-composeを↓みたいに作り、 docker-compose pull で正常にワークスペースが取得できる状態になればOK。 cloud-pine/docker-compose.yml at 190e8454b1dbae0521f035f7f37ceb62f8f126f4 · YusukeIwaki/cloud-pine · GitHub

ワークスペースを試しにデプロイしてみる

c9-masterにSSHで入り

$ wget https://raw.githubusercontent.com/YusukeIwaki/cloud-pine/190e8454b1dbae0521f035f7f37ceb62f8f126f4/docker-compose.yml
$ docker stack deploy --compose-file docker-compose.yml playground1

をする。docker-compose.yml は1つ前の手順で作成したものを指定する。

Creating network playground1_default
Creating service playground1_cloud9

こんな感じで、オーバーレイネットワークとサービスが作られたらOK。

$ docker service ps playground1_cloud9
ID                  NAME                   IMAGE                                     NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
prviwbnxxu3l        playground1_cloud9.1   yusukeiwaki/cloud-pine-workspace:latest   c9-slave1   Running             Running 43 seconds ago       

みたいな感じで、無事にc9-slave1 に配置された。(空いているDockerノードにいい感じに配置されるので、c9-masterやc9-slave2に配置されることもある)

ただ、これ単体では外からアクセスはできない。

外からアクセスできるようにする

グローバルIPを持っているc9-masterにnginx(リバースプロキシ)サーバーを立てるわけだが、ネイティブにリバースプロキシサーバを立てると、

$ ping playground1_cloud9
ping: playground1_cloud9: 名前またはサービスが不明です

のように、playground1_cloud9コンテナへのルーティングがうまくできない。docker inspectを使ってIPアドレスを特定して、・・・とかをやるのは茨の道だ。

ということで、nginxはネイティブで立てるのではなくdockerサービスとして立てる。そんで、playground1_cloud9とかと同じオーバーレイネットワークに入れれば、

$ docker exec -it 4936af085963 ping playground1_cloud9
PING playground1_cloud9 (10.0.6.88): 56 data bytes
64 bytes from 10.0.6.88: seq=0 ttl=64 time=0.058 ms
64 bytes from 10.0.6.88: seq=1 ttl=64 time=0.125 ms
64 bytes from 10.0.6.88: seq=2 ttl=64 time=0.076 ms
64 bytes from 10.0.6.88: seq=3 ttl=64 time=0.067 ms

のようにnginxコンテナからplayground1_cloud9が見えるようになる。

この辺の仕組みは Docker swarmで作る社内heroku: yadockeri - Qiita あたりで言及されているやつ丸パクリです。

nginxコンテナをつくる

nginx.confを

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    server {
      listen             80;

      set_real_ip_from   '10.0.0.0/8';
      real_ip_header     X-Forwarded-For;
      proxy_set_header   X-Forwarded-Scheme $http_x_forwarded_proto;
      proxy_set_header   Host $host;
      resolver           127.0.0.11 valid=5s; # nginx:alpineコンテナの/etc/resolve.conf にべた書きされているnameserverの値をそのまま転記

      if ($host ~* ([a-z0-9][a-z0-9_-]*)\.ide\.mydomain\.com$) {
        set $subdomain $1;
        set $dst_port 8888;
      }

      if ($host ~* ([a-z0-9][a-z0-9_-]*)\.preview\.mydomain\.com$) {
        set $subdomain $1;
        set $dst_port 8080;
      }

      location / {
        proxy_pass http://${subdomain}_cloud9:${dst_port};
      }
    }
}

こんな感じ。 mydomain.net のところは自分のドメインにする。

あとは、c9-masterにSSHで入って

$ docker network create --driver overlay --internal c9-overlay-network
$ docker service create --constraint 'node.role==manager' --mount type=bind,source=$(pwd)/nginx.conf,destination=/etc/nginx/nginx.conf,readonly --name nginx --network c9-overlay-network -p 80:80 nginx:stable-alpine

のようにDockerサービスを立ち上げれば、リバースプロキシが立つ。

ドメインの向き先をc9-masterにする

AWS Route53側の設定。

*.preview.mydomain.com*.ide.mydomain.com のAレコードをc9-masterのグローバルIPにする。(ちなみにAWS EC2じゃなくてAzure VMだったら、AレコードでIP指定するのではなく、CNAMEレコードで hogehoge.japanease.cloudapp.azure.com を指定する)

nginxからCloud9を見えるようにする

先の手順までで、 http://playground1.preview.mydomain.com/ とか http://playground1.ide.mydomain.com/ とかを指定すると、nginxが502エラーを返す状態にまでなるはず。

f:id:YusukeIwaki:20190713145639p:plain

これはnginxまでは到達できているけど、その先(Cloud9コンテナ)にリクエストを転送できていない状態。

なんでエラーになるかと言うと、この時点では先に立ち上げたplayground1サービスのCloud9コンテナが c9-overlay-network ネットワークに参加していないので、nginxからplayground1_cloud9の名前解決ができないのだ。じゃあどうすればできるかというと、とてもシンプルで

$ docker service update --network-add c9-overlay-network playground1_cloud9
playground1_cloud9
overall progress: 1 out of 1 tasks 
1/1: running   [==================================================>] 
verify: Service converged 

のように、ネットワークに参加させたらよい。リロードしたら・・・

f:id:YusukeIwaki:20190713145951p:plain

キタ━━━━(゚∀゚)━━━━!!

f:id:YusukeIwaki:20190713150017p:plain

キタ━━━━━━━━━━(゚∀゚)━━━━━━━━━━!!

となるだろう。

Cloud9 PROもどきの運用方法

構築手順はいろいろあったが、一旦作ってしまえばあとは簡単で、

新規のワークスペース立ち上げたいなーって思ったら

c9-masterに入って

docker stack deploy --compose-file docker-compose.yml nanika_new_idea
docker service update --network-add c9-overlay-network nanika_new_idea_cloud9

nanika_new_idea のところは都度変える)

と2つコマンドを打てば、1分もたたないうちに

  • http://nanika_new_idea.ide.mydomain.com/ でCloud9 IDEにアクセスできる
  • Cloud9上で8080番ポートでHTTPサーバーを立ち上げれば http://nanika_new_idea.preview.mydomain.com/ で見れる

ようになる。

ワークスペースを捨てたいなーって思ったら

c9-masterに入って

$ docker service ps playground1_cloud9 | grep Running | awk '{print $4}'
c9-slave1

のように、Cloud9を実行しているノードを特定してから、

$ docker stack rm playground1
Removing service playground1_cloud9
Removing network playground1_default
$ DOCKER_HOST=ssh://c9-slave1 docker volume rm  playground1_workspace-data
playground1_workspace-data

って感じで、2つコマンドを打てば瞬時に消せる。

まとめ

ほぼほぼ Docker swarmで作る社内heroku: yadockeri - Qiita なので、何も新しいことはやっていない。

とりあえず自分用Cloud9ができたので、しばらくは楽しくエンジニアやっていけそう。

追記

この構成だとChromebookでサクサク使えないことがわかったので続き・・・

yusukeiwaki.hatenablog.com