VSCodeのリモートコンテナで「サーバ上のDocker」に接続するクラウド開発環境をつくってみた【GCP】

ローカルPC ＋ Dockerで開発作業をすると、PCには負担のようで、すぐに熱くなるのが気になっていました。

そこで

- GCPに低スペックサーバを立てて
- サーバ上にソースとDockerを置いて
- ローカルのVSCodeからリモートコンテナ接続

してみたところ、PCが熱くならない省エネ開発環境が完成しました。
おまけに、もしPCが壊れてもリソースはサーバ側にあるので、新しいPCにVSCode等を入れ直せば環境復活するのも安心。

使い勝手はとても良かったので、所感も含めて環境の作り方を共有したいと思います。

# 良い点・ダメな点

### 良い点：とにかく省エネでPCに優しい

サーバ上でDockerを立ち上げるので、PCに負荷が掛かりません。
バッテリー節約モードでもサクサク動きます。

また、下記も地味に魅力です。

- GCEの無料枠スペックで動くので、ほぼお金が掛からない
- もしPCが壊れても、他のPCにVSCodeさえ入れれば環境復活
- スナップショットにより、毎晩自動で作業中コードのバックアップを取れる
- 個人の開発環境ながら固定グローバルIPが使える

### ダメな点：レスポンスが遅い

GCEの無料枠に収めるため、アメリカに低スペックなサーバを立ててます。
そのせいか、1つ1つの動作がやや待たされます。
WEBシステムのテスト工程で、ブラウザ上で動作確認するときなんかのテンポが悪いです。

一方、VSCodeでコードを書くときは、ローカルとほぼ遜色なくサクサク打ち込めます。

# 必要なもの
まず前提として、ローカルPCでVSCode＋Dockerで開発作業を行っていて、GCPの「Compute Engine」を触れる方向けの記事になります。

### サーバ環境

- GCE（Google Compute Engine）
- ロケーション：us-west1（オレゴン州）
- マシンタイプ：e2-micro
- ディスク：標準永続ディスク 30GB
- OS：CentOS 7
- 外部IP：なし
- HTTPトラフィック：ON
- 必須ソフト：Docker

GCEの無料枠スペックになります。
ですがセキュリティの観点で「外部IP：なし」にしてまして、外部との通信には有料の「Cloud NAT」を使ってます。

### ローカルPC環境

- Windows 10 Home
- Docker Desktop
- Google Cloud SDK
- Visual Studio Code
- Remote-Containers（VSCode拡張機能）

WSLは不要です。

# 環境構築の流れ
1. サーバ構築
1. 外部IPなしのサーバにSSH接続できるようにする
1. ローカルPCからサーバ上のDockerに接続実行

# ステップ1：サーバ構築

まずは普通に「外部IP：あり（エフェメラル）」でGCEを立ち上げてSSH接続します。
接続したら、最低限Dockerのインストールが必要です。
yamlファイルを使っている場合は、docker-domposeも入れておきます。

```shell:Dokcer インストール
# 必要なパッケージインストール
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
# レポジトリ追加
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# yumのパッケージインデックスを更新
sudo yum makecache fast
# インストール（最新バージョン）
sudo yum -y install docker-ce
# 起動
sudo systemctl start docker
# 確認
sudo docker run hello-world
# いちいちsudoを書かなくてもようにする
sudo usermod -aG docker $USER
# 再ログイン後に確認
docker run hello-world
# OS起動時にDockerを自動起動に
sudo systemctl enable docker
# Dockerにログイン（一度やればOK）
docker login

# ほぼ https://qiita.com/inakadegaebal/items/be9fecce813cebec5986 から引用させていただきました
```

```shell:dokcer-compose インストール（必要に応じて）
sudo curl -L "https://github.com/docker/compose/releases/download/1.28.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

# 確認
docker-compose -v

# 環境変数を設定（バインドマウントで利用）
echo export USER_NAME=${USER} >> /home/${USER}/.bashrc
```

<br>
あと必須ではないですが、下記のように初期セットアップしておくと使いやすいかなと。

```shell:その他もろもろ設定（必須ではない）
sudo yum update -y
sudo yum clean all

# タイムゾーン設定
timedatectl status
sudo timedatectl set-timezone Asia/Tokyo
timedatectl status

# 日本語化
localectl
sudo localectl set-locale LANG=ja_JP.UTF-8
source /etc/locale.conf
localectl

# SELinux無効化
sudo sed -i.org s/SELINUX=enforcing/SELINUX=disabled/ /etc/sysconfig/selinux

#ジャーナルフォルダ作成
sudo mkdir /var/log/journal

# swap優先度調整をMAXに（VSCodeでのSSH接続が安定します）
sudo grep vm.swappiness /usr/lib/tuned/virtual-guest/tuned.conf
sudo sed -i.org -E 's/^.+swappiness.+$/vm.swappiness = 100/' /usr/lib/tuned/virtual-guest/tuned.conf
sudo grep vm.swappiness /usr/lib/tuned/virtual-guest/tuned.conf
sudo systemctl restart tuned
sudo sysctl vm.swappiness

# 基本ツールインストール
sudo yum -y install zip unzip wget make gcc htop iotop

# gitインストール
GITVERSION="2.24.1"
sudo yum -y remove git
sudo yum -y install curl-devel expat-devel gettext-devel openssl-devel zlib-devel perl-ExtUtils-MakeMaker
sudo wget https://www.kernel.org/pub/software/scm/git/git-${GITVERSION}.tar.gz
sudo tar -zxf git-${GITVERSION}.tar.gz
cd git-${GITVERSION}
sudo make prefix=/usr/local all
sudo make prefix=/usr/local install
git --version
cd ~
sudo rm -rf git-${GITVERSION}.tar.gz
```

<br>

また、GCEのスタートアップスクリプトに下記を登録しておくと安定します。

```shell:スタートアップに登録するスクリプト
# swap作成
# さすがに無料枠のメモリでは少なすぎるので、1GBくらい作成
sudo mkdir /swap
cd /swap
if [ -f /swap/swap ]; then
sudo swapon swap
else
sudo dd if=/dev/zero of=swap bs=1M count=1024
sudo chmod 600 swap
sudo mkswap swap
sudo swapon swap
fi
```


<br>

# ステップ2：外部IPなしのサーバにSSH接続できるようにする

つづいて、GCEの画面でサーバの外部IPを「なし」に変更します。
これは必須作業ではないですが、開発ソースだけでなくSSHキー（秘密鍵）もサーバ上に置きたいので、セキュリティを高めておきたいためです。
<br>

SSHキーをサーバに保存する理由は、Dockerコンテナ内で作業している状態で、SSH接続しているGitリポジトリに pull や push ができてスムーズになります。
便利な反面、仮にSSHキー（秘密鍵）が流出すると非常にマズイので、もし外部IP：あり の状態で使うのなら

- 第三者からのアクセス対策
- 開発環境のWEB画面へのアクセス制限

などが必要かなと思います。
<br>

それでは、外部IPなしでも接続できるようになる「Cloud IAP」と「Cloud NAT」の2つを設定していきます。

## Cloud IAP の設定

### Cloud IAP とは？

GCPのサービスの1つで、Googleアカウントを使った認証機能です。
ログインIDとパスワードを打ち込んでいたのを「Googleアカウントでログイン」でいけるようになるイメージです。

今回は、外部IPではなく「Googleでログイン」を使ってサーバにSSH接続するために利用します。

### Cloud IAP のセットアップ

[GCPの画面](https://console.cloud.google.com/security/iap/)で「APIを有効にする」をクリックします。


<br>
これで Cloud IAP を使う準備は完了です。


## Cloud NAT の設定

### Cloud NAT とは？

名前の通りで、GCPのNATサービスです。
今回は、外部IPを持たないサーバがインターネットと通信するために利用します。

ちなみにサーバは無料枠で使えますが、この Cloud NAT はすこしお金がかかります。
サーバの稼働時間 と 通信量 で課金されるので、その日の仕事が終わったらサーバを落とすと節約できます。

自分の場合は月に150円くらい課金されてます。

### Cloud NAT のセットアップ

[GCPの画面](https://console.cloud.google.com/net-services/nat/)でサービスを有効にしたら、NATゲートウェイを作成します。
基本的にはデフォルトを選んで作成すればOKです。

固定のグローバルIPが欲しい場合は、NAT IPアドレスを手動にします。
<br>
▼NATゲートウェイ設定例

![NATゲートウェイ設定例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2279507/41c34a32-b93f-1be1-f57d-ae25f852db47.png)

## ローカルPCから外部IPなしサーバにSSH接続してみる

試しに外部IPを持たないサーバにSSH接続してみましょう。

まずはローカルPC内に接続先情報を設定します。

```shell:C：\Users\ユーザー名\\.ssh\config
Host dev # 名前は任意
    HostName 127.0.0.1
    User サーバ上のユーザー名
    IdentityFile ~/.ssh/SSH秘密鍵 #GCE接続用のSSHキー
    Port 22
    IdentitiesOnly yes
    ServerAliveInterval 60
```

つづいて、GCEに接続するためのトンネルをつくります。
（GCEをオレゴンリージョン・ゾーンaに作った例。場所が違う場合は zone を書き換えてください）

```shell:コマンドプロンプト
gcloud compute start-iap-tunnel GCEのサーバ名 22 --local-host-port=localhost:22 --zone us-west1-a
```

上記とは別のコマンドプロンプトを立ち上げて、次のように打つとssh接続できます。

```shell:コマンドプロンプト
ssh dev
```

<br>

# ステップ3：ローカルPCからサーバ上のDockerに接続実行

それではいよいよサーバ上のDockerに接続してみます。

Step2で登場した「SSH接続用のトンネル」をつくります。
（Step2のコマンドプロンプトが残っているなら不要）

```shell:コマンドプロンプト
gcloud compute start-iap-tunnel GCEのサーバ名 22 --local-host-port=localhost:22 --zone us-west1-a
```

もうひとつ、下記コマンドでDockerの接続先を「サーバ上のDocker」に向き先を変えます。

```shell:コマンドプロンプト
ssh -fNL localhost:23750:/var/run/docker.sock サーバ上のユーザー名@dev
```

<br>

上記2つのコマンドプロンプトを立ち上げた状態で、VSCodeのDocker拡張機能を開くと、サーバ上のDocker情報が表示されます。

▼Step1でDocker起動テストしたコンテナが表示されている
![サーバ上のDockerに接続](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2279507/4760dac4-147c-e38f-fe25-173a02958c5b.png)

ここで、サーバ上にソースファイルを置いて
docker-compose up -d
などでコンテナを起動します。

コンテナを起動させてから右クリックすると「Attach Visual Studio Code」というメニューが出るのでクリックします。

![2022-03-13_11h06_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2279507/c1292a02-2074-af7e-80e9-1bbe1a98fc85.png)

すると、サーバのDocker上にあるコンテナにVSCodeで入ることができます。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f323237393530372f65313038656331352d653439312d313039642d363939392d6361663933383938653965642e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2279507/371fe215-7a47-f2de-e76e-a515e23d2ff5.png)


あとはローカルPC＋Dockerで開発していたノリで、サーバ上のDockerで開発作業ができます！
WEBアプリも「http：//localhost」でアクセスすれば動作確認できますし、ブレークポイントを打ってデバッグもできます。




<br>

# おまけ：オススメ環境設定

## 1クリックでトンネル生成

PC起動のたびにコマンドプロンプを立ち上げてトンネル作るのも面倒なので、スクリプト化しておくと便利です。

```shell:22.bat
gcloud compute start-iap-tunnel GCEのサーバ名 22 --local-host-port=localhost:22 --zone us-west1-a
```

```shell:80.bat
gcloud compute start-iap-tunnel GCEのサーバ名 80 --local-host-port=localhost:22 --zone us-west1-a
```

```shell:DockerPortChange.bat
ssh -fNL localhost:23750:/var/run/docker.sock サーバ上のユーザー名@dev
```

上記3ファイルを作成して、これらをバックグラウンドでまとめて起動するVBSファイルを作ります。

```shell:execute.vbs
CreateObject("WScript.Shell").Run "22.bat",0
CreateObject("WScript.Shell").Run "80.bat",0

WScript.Sleep 5000   ’上記の接続が完了してからDocker接続先を変えないとエラーになるので、5秒ほど待機

CreateObject("WScript.Shell").Run "DockerPortChange.bat",0
```

これで execute.vbs をクリックするだけでトンネル作成などの一連の準備が完了します。
うまく接続できない場合は、Sleepの時間を伸ばしてみてください。


## 1クリックでGCEを起動・停止

GCEを起動している時間だけ Cloud NAT の料金がかかるので、こまめにサーバを落とすと節約できます。
ただ、いちいちGCEの画面を開くのも面倒なので、これもスクリプト化しちゃいましょう。

```shell:GCE起動.bat
gcloud compute instances start サーバ名
```

```shell:GCE停止.bat
gcloud compute instances stop サーバ名
```


## サーバ上のコンテナから git push できるようにする

VSCodeでサーバ上のコンテナに接続したら、そのまま git push や git pull できるとスムーズです。
下記のようにサーバ上にSSHファイル等を置いて、コンテナから読み込むことで可能になります。

```shell:コマンドプロンプト
# ローカルPCのSSH・git 設定ファイルをサーバに転送
scp -i C:\Users\ユーザー名\.ssh\SSHキー -r C:\Users\ユーザー名\.ssh ユーザー名@127.0.0.1:/home/ユーザー名/
scp -i C:\Users\ユーザー名\.ssh\SSHキー C:\Users\ユーザー名\.gitconfig ユーザー名@127.0.0.1:/home/ユーザー名
```

```shell:サーバ上で実行
# SSH初期設定
chmod 700 ~/.ssh
chmod 600 ~/.ssh/*
chmod 644 ~/.ssh/authorized_keys
```

```yaml:docker-compose.yml
version: '3'
services:
  app:
    build:
      context: .
      dockerfile: "Dockerfile"
    container_name: app
    ports:
      - "80:80"
    volumes:
      - /home/${USER_NAME}/src/app:/var/www/html/wp
      - /home/${USER_NAME}/.ssh:/root/.ssh.org #sshキー
      - /home/${USER_NAME}/.gitconfig:/root/.gitconfig #gitの設定
    working_dir: /var/www/html/wp
```

```json:devcontainer.json（最後の行でsshファイルを読み込んでいる）
{
	"name": "app",
	"service": "app",
	"dockerComposeFile": ["docker-compose.yml"],
	"workspaceFolder": "/var/www/html/wp",
	"postCreateCommand": "cp -ap ~/.ssh.org/* ~/.ssh/ && chown -R root:root ~/.ssh"
}
```

なお、devcontainer.json はサーバ上のdevcontainerフォルダではなく、ローカルPC内に作成されるようです。
C:\Users\ユーザー名\AppData\Roaming\Code\User\globalStorage\ms-vscode-remote.remote-containers\imageConfigs


<br>

# 困ったときは

## 使ってるとだんだんレスポンスが激遅に

はじめはよかったのに、接続して開発してるとだんだん反応が重くなり、最後は切断されてしまう。
コンテナやサーバを再起動すれば一時的に回復するものの、また使ってると次第に重くなる。

こういうときは、次の3点を試してみてください。

1つ目は、最近追加したVSCodeの拡張機能は無いか。
拡張機能が悪さをしている場合があるので、いったん無効にして解消するか試してください。

2つ目は、apacheがメモリを使い過ぎていないか。
コンテナ内でapacheを起動している場合は、WEBアクセスで急に使用メモリが増えてスワップが起き、ディスクが渋滞して反応が帰ってこないことがあります。
Dockerfileに下記のように書くとapacheのメモリ使用を抑えられ安定します。

```shell:Dockerfile
RUN echo '<IfModule mpm_prefork_module>' >> /etc/httpd/conf/httpd.conf &&\
	echo 'StartServers             1' >> /etc/httpd/conf/httpd.conf &&\
	echo 'MinSpareServers          1' >> /etc/httpd/conf/httpd.conf &&\
	echo 'MaxSpareServers          2' >> /etc/httpd/conf/httpd.conf &&\
	echo 'ServerLimit              2' >> /etc/httpd/conf/httpd.conf &&\
	echo 'MaxClients               2' >> /etc/httpd/conf/httpd.conf &&\
	echo 'MaxConnectionsPerChild 400' >> /etc/httpd/conf/httpd.conf &&\
	echo '</IfModule>' >> /etc/httpd/conf/httpd.conf
```

<br>

3つ目は、VSCodeをダウングレードするとどうか。
VSCodeの更新で相性が悪くなる？場合があるので、[VSCodeの更新履歴](https://code.visualstudio.com/updates/)から1つ前のバージョンとかを落としてインストールしてみてください。
（VSCodeの自動更新がONになっているとまた最新に戻ってしまうので、自動更新：OFFにしてから試してください）



<br>

# 参考文献

- [外部IPを持たないGCEインスタンスでweb開発](https://www.rhoboro.com/2019/09/06/gce-non-external-ip.html)
- [自宅のインターネット速度が遅すぎて開発が捗らない件をGoogle Comupte Engineで解決する](https://cloudandbuild.jp/blog/article-1)
- [ローカルのVSCodeから、ローカルに置いたソースコードをリモートのDocker上で動かす方法](https://amaya382.hatenablog.jp/entry/2019/10/17/193323)
- [リモートサーバーの中のDockerにローカルから接続する](https://www.yasunaga-lab.bio.kyutech.ac.jp/EosJ/index.php/%E3%83%AA%E3%83%A2%E3%83%BC%E3%83%88%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%AE%E4%B8%AD%E3%81%AEDocker%E3%81%AB%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%81%8B%E3%82%89%E6%8E%A5%E7%B6%9A%E3%81%99%E3%82%8B)
- [VSCodeのリモートDockerコンテナーに接続するための依存関係？](https://www.webdevqa.jp.net/ja/docker/vscode%E3%81%AE%E3%83%AA%E3%83%A2%E3%83%BC%E3%83%88docker%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%83%BC%E3%81%AB%E6%8E%A5%E7%B6%9A%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E4%BE%9D%E5%AD%98%E9%96%A2%E4%BF%82%EF%BC%9F/812875045/)
- [Docker Composeのvolumes使用時に出会うpermission deniedに対応する一つの方法](https://qiita.com/cheekykorkind/items/ba912b62d1f59ea1b41e)
- [続: Docker コンテナ内で Docker ホストと同じユーザを使う](https://blog.amedama.jp/entry/docker-container-host-same-user-bis)
- [dockerでvolumeをマウントしたときのファイルのowner問題](https://qiita.com/yohm/items/047b2e68d008ebb0f001)
- [Dockerのvolumeでpermission deniedが発生した場合の解決法](https://tech-blog.rakus.co.jp/entry/20200826/docker)
- [Docker で環境変数をホストからコンテナに渡す方法（ホスト OS 側からゲスト OS に渡す方法各種）](https://qiita.com/KEINOS/items/518610bc2fdf5999acf2)
- [Dockerfileの"ENV"と"ARG"と"環境変数"について](https://techblog.recochoku.jp/1979)
- [docker-composeで環境変数を設定する](https://qiita.com/reflet/items/a96752b4cd3838ea6932)
- [Visual Studio Code Remote Container を利用してもしなくても (Vim とかと docker-compose コマンド) 最強のローカル環境とデプロイ用のコンテナ化環境を作る方法、 Vue.js を例に添えて。](https://oki2a24.com/2020/11/16/develop-env-with-visual-studio-code-remote-container-or-vim-and-docker-compose-command/)
- [Add a non-root user to a container](https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user)
- [Dockerコンテナ内のユーザとホストのユーザとの関係について](https://unskilled.site/docker%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E5%86%85%E3%81%AE%E3%83%A6%E3%83%BC%E3%82%B6%E3%81%A8%E3%83%9B%E3%82%B9%E3%83%88%E3%81%AE%E3%83%A6%E3%83%BC%E3%82%B6%E3%81%A8%E3%81%AE%E9%96%A2%E4%BF%82/)
- [Docker: マウントしたVolumeにApacheが書き込めないとき](https://qiita.com/suin/items/3a0361102af83d0b69aa)