ブラウザからワンクリックでサーバ上にdockerコンテナを立ち上げる

この記事は[ニフクラ](https://www.nifcloud.com/) 等を提供している、[富士通クラウドテクノロジーズ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/fjct) の15日目の記事です。

前日は @earth429さんの[SpotifyのNowPlayingを投稿しステータスに設定もしてくれるSlackbotを作ってみた](https://qiita.com/earth429/items/b3aad4c2fc439dc49a52)でした。 
弊社にはSlackに現在聞いている曲を共有するチャンネルがあるのですが、そのチャンネルでめちゃくちゃ活躍してくれそうだなと感じました。
~~AppleMusic版もほしいな、、~~

# はじめに
本記事では、ブラウザから「コンテナ作成」ボタンを押すだけでサーバ上にDockerコンテナを作成する方法について記述します。

最低限の構成を紹介するので、必要に応じて増やしたり減らしたりしてみてください。
やってることは簡単なことの組み合わせだと思います。（方法2は少し難しいかも）

方法1と方法2を記述するので、好きな方を選んでください。
おすすめは方法2です。

# ゴール
* ブラウザ（chromeやfirefoxなど）から「コンテナ作成」のボタンを押すと、裏で動いているサーバ上にDockerコンテナが作成されること
* ブラウザから作成したコンテナにアクセスできること（今回はApacheコンテナ）

# 方法1,2の違い
方法1
* APIサーバからAPPサーバのDockerを操作する(`docker -H`)
* `docker -H`が公式から推奨されていない
* 実装内容の理解は簡単かも
* UIサーバ、APIサーバ、APPサーバで合計3台のサーバが必要

方法2
* APPサーバ上でAPPサーバのDockerを操作する
* DockerAPIを直接叩くので実装内容理解がちょっと大変かも
* Docker公式非推奨の方法を避けることができる
* （この記事ではAPIサーバを用意したが）APIサーバを省いてUIサーバとAPPサーバの2台で構成できる


# 構成図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2295232/039fac6d-aa60-000a-ff96-8aba3b7d7613.png)


# 環境
* UIサーバ
  * CentOS7.4.1708
  * Apache/2.4.6
  * PHP7.4.30
* APIサーバ
  * CentOS7.4.1708
  * Apache/2.4.6
  * Docker20.10.17
  * PHP7.4.30
* APPサーバ
  * CentOS7.4.1708
  * Apache/2.4.6
  * Docker20.10.17

# 実装の流れ
方法1,2共通
1. UIサーバ,APIサーバ,APPサーバを作成する
2. UIサーバにApache、PHPをインストール
3. APIサーバにApache、Docker、PHPをインストール
4. APPサーバにApache、Dockerをインストール
5. FWの設定

方法1
6. APIサーバからAPPサーバのDockerを操作できるように設定
7. UIサーバ上にHTML,PHPのファイルを配置
8. APIサーバ上にPHPのファイルを配置
9. APPサーバ上に使いたいコンテナイメージを入れる
10. 動作確認

方法2
6. APPサーバのDockerをDockerAPIで直接叩けるように設定
7. UIサーバ上にHTML,PHPのファイルを配置
8. APIサーバ上にPHPのファイルを配置
9. APPサーバ上にPHPのファイルを配置
10. APPサーバ上に使いたいコンテナイメージを入れる
11. 動作確認


以下、実装の流れに沿って解説していきます。

# 方法1,2共通
## UIサーバ,APIサーバ,APPサーバを作成する
好きなサービス、もしくは自前でUIサーバ,APIサーバ,APPサーバを作成してください。
今回はCentOS7.4の想定で進めます。

## UIサーバにApache、PHPをインストール
UIサーバにApache、PHPをインストールします。
~~~
yum -y install httpd

yum -y install http://rpms.remirepo.net/enterprise/remi-release-7.rpm
yum -y install --enablerepo=remi,remi-php74 php php-mbstring php-xml
~~~
参考
[CentOS7へPHP7.4をインストール](https://kcfran.com/2022/02/25/php74-centos7-install/)

## APIサーバにApache、Docker、PHPをインストール
APIサーバにApache、Docker（方法1のみ)、PHPをインストールします。
~~~
yum -y install httpd

## 方法1のみ
yum -y install yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum -y install docker-ce

yum -y install http://rpms.remirepo.net/enterprise/remi-release-7.rpm
yum -y install --enablerepo=remi,remi-php74 php php-mbstring php-xml
~~~
参考
[Docker CEをCentOS 7にyumインストールする手順](https://weblabo.oscasierra.net/docker-ce-install-centos7/)
[CentOS7へPHP7.4をインストール](https://kcfran.com/2022/02/25/php74-centos7-install/)

## APPサーバにApache、Docker、PHPをインストール
APPサーバにApache（方法2のみ）、Docker、PHP（方法2のみ）をインストールします。
~~~
## 方法2のみ
yum -y install httpd

yum -y install yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum -y install docker-ce

## 方法2のみ
yum -y install http://rpms.remirepo.net/enterprise/remi-release-7.rpm
yum -y install --enablerepo=remi,remi-php74 php php-mbstring php-xml
~~~
参考
[Docker CEをCentOS 7にyumインストールする手順](https://weblabo.oscasierra.net/docker-ce-install-centos7/)

## FWの設定
それぞれのサーバ間で通信できるように＆UIサーバのコンテンツをブラウザから見られるようにFWのINルールを追加します。
UIサーバはコンテナ作成ボタンを配置するためのサーバなので、`/var/www/html/`配下がブラウザから見れる必要があります。

* ブラウザ→UIサーバ...80番ポート(http用）
* ブラウザ→APPサーバ...8080番ポート（作成したコンテナアクセス用）
* UIサーバ→APIサーバ...80番ポート（http用）
* APIサーバ→APPサーバ...2375番ポート（Docker用）※方法1の場合のみ
* APIサーバ→APPサーバ...80番ポート(http用）※方法2の場合のみ


# 方法1
## APIサーバからAPPサーバのDockerを操作できるように設定
APIサーバからAPPサーバのDockerを操作できるように`docker -H`のオプションを使えるようにします。

:::note warn
警告
この方法は[Docker公式サイト](https://docs.docker.jp/engine/userguide/basics.html)で非推奨とされています。使用する場合は注意してください。
:::

APPサーバに`/etc/docker/daemon.json`を作成、追記します。
~~~:/etc/docker/daemon.json
{"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"]}
~~~

APPサーバに`/etc/systemd/system/docker.service.d/docker.conf`を作成、追記します。
~~~:/etc/systemd/system/docker.service.d/docker.conf
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd                    
~~~

設定を変更したのでAPPサーバのDockerを再起動します。
~~~
systemctl restart docker
~~~

:::note warn
警告
>-H オプションを使うと、Docker デーモンは指定した IP アドレスとポートをリッスンします（ポートを開きます）。標準では、unix:///var/run/docker.sock をリッスンし、ローカルの root ユーザのみ接続できます。これを 0.0.0.0:2375 や特定のホスト IP を指定することで、誰でもアクセス可能にできましたが、推奨されていません。理由は、デーモンが稼働しているホスト上の root アクセスを誰もが簡単に得られるためです。
:::

参考
[Docker デーモンの設定とトラブルシュート](https://matsuand.github.io/docs.docker.jp.onthefly/config/daemon/)


## UIサーバ上にHTML,PHPのファイルを配置
UIサーバ上にコンテナ作成用のボタンを配置します。
（最低限の機能実装なので、必要に応じて追記してください。）

UIサーバに`/var/www/html/index.html`を作成、追記
~~~html:/var/www/html/index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>APIにpostリクエストを送信する</title>
    </head>
    <body>
        <form action="post.php" method="post">
            <button type="submit" name="create">作成</button><br>
        </form>
    </body>
</html>
~~~

ブラウザから見た画面↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2295232/de88171c-0f3e-18d1-931c-000bb8286bd3.png)

UIサーバに`/var/www/html/post.php`を作成、追記します。
~~~php:/var/www/html/post.php
<?php

$url = 'http://${APIサーバのIP}/api.php';

$data = array(
    'name' => 'test'
    'image' => 'httpd',
);

$context = array(
    'http' => array(
        'method'  => 'POST',
        'header'  => implode("\r\n", array('Content-Type: application/x-www-form-urlencoded',)),
        'content' => http_build_query($data),
    )   
);

$html = file_get_contents($url, false, stream_context_create($context));

echo $html . "<br>";
echo "コンテナを作成しました";
~~~

参考
[PHPでPOST送信まとめ](https://qiita.com/okdyy75/items/d21eb95f01b28f945cc6)

## APIサーバ上にPHPのファイルを配置
UIサーバで「作成」ボタンを押すと送信されるPOSTリクエストを処理するためのプログラムを作成、配置します。

APIサーバに`/var/www/html/api.php`を作成、追記します。

~~~php:/var/www/html/api.php
<?php

$cmd = "docker -H ${APPサーバのIP} run -d -p 8080:80 --name " . escapeshellarg($_POST['name']) . ' ' . escapeshellarg($_POST['image']) . ":latest";
echo exec($cmd) . "\n";
~~~

このプログラムが動くことでAPPサーバ上にコンテナが立ち上がります。（今回はApacheのコンテナ）


## APPサーバ上に使いたいコンテナイメージを入れる
今回はApacheのコンテナを動かしたいので事前にhttpdのイメージを入れておきます。

~~~
docker pull httpd
~~~

これですべて準備が完了したはずです。

## 動作確認
ブラウザからUIサーバにアクセスします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2295232/850582f8-90a3-5c78-7c15-03cd6b96a695.png)

「作成」ボタンが表示されます。

そのボタンをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2295232/6ad9743d-b690-599e-2bc2-12de2e6f9028.png)

コンテナが作成されます。

作成されたコンテナにアクセスしてみます。
APPサーバ上に作成されているのでAPPサーバの8080番ポートにブラウザからアクセスします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2295232/09a66a7b-2bb9-7668-bd38-f1a08958fd9e.png)

Apacheが動いていることが確認できました。

一応APPサーバの裏側でもちゃんと動いているか確認します。
~~~
[root@app ~]# docker ps
CONTAINER ID   IMAGE          COMMAND              CREATED         STATUS         PORTS                                   NAMES
6387a999e61f   httpd:latest   "httpd-foreground"   6 minutes ago   Up 6 minutes   0.0.0.0:8080->80/tcp, :::8080->80/tcp   test
~~~

ちゃんと動いていることが確認できました。

# 方法2
## APPサーバのDockerをDockerAPIで直接叩けるように設定
APPサーバのDockerをDockerAPIで直接叩けるように設定します。

APPサーバに`/etc/docker/daemon.json`を作成、追記します。
~~~:/etc/docker/daemon.json
{"hosts": ["unix:///var/run/docker.sock", "tcp://127.0.0.1:2375"]}
~~~

APPサーバに`/etc/systemd/system/docker.service.d/docker.conf`を作成、追記します。
~~~:/etc/systemd/system/docker.service.d/docker.conf
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd                    
~~~

設定を変更したのでAPPサーバのDockerを再起動します。
~~~
systemctl restart docker
~~~

## UIサーバ上にHTML,PHPのファイルを配置

UIサーバ上にコンテナ作成用のボタンを配置します。
（最低限の機能実装なので、必要に応じて追記してください。）

UIサーバに/var/www/html/index.htmlを作成、追記

~~~html:/var/www/html/index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>APIにpostリクエストを送信する</title>
    </head>
    <body>
        <form action="post.php" method="post">
            <button type="submit" name="create">作成</button><br>
        </form>
    </body>
</html>
~~~

ブラウザから見た画面↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2295232/de88171c-0f3e-18d1-931c-000bb8286bd3.png)

UIサーバに`/var/www/html/post.php`を作成、追記します。
~~~php:/var/www/html/post.php
<?php

$url = 'http://${APIサーバのIP}/api_post.php';

$data = array(
    'name' => 'test',
    'image' => 'httpd',
);

$context = array(
    'http' => array(
        'method'  => 'POST',
        'header'  => implode("\r\n", array('Content-Type: application/x-www-form-urlencoded',)),
        'content' => http_build_query($data),
    )   
);

$html = file_get_contents($url, false, stream_context_create($context));

echo $html . "<br>";
echo "コンテナを作成しました";
~~~

参考
[PHPでPOST送信まとめ](https://qiita.com/okdyy75/items/d21eb95f01b28f945cc6)

## APIサーバ上にPHPのファイルを配置
APIサーバ上にUIサーバから来たPOSTをAPPサーバにPOSTするPHPファイルを配置します。
（APIサーバを飛ばして直接UIサーバからAPPサーバにPOSTを送りつけても大丈夫です。）

APIサーバに`/var/www/html/api_post.php`を作成、追記します。

~~~php:/var/www/html/api_post.php 
<?php

$url = 'http://${APPサーバのIP}/run.php';

$data = array(
    'name' => "{$_POST['name']}",
    'image' => "{$_POST['image']}",
);

$context = array(
    'http' => array(
        'method'  => 'POST',
        'header'  => implode("\r\n", array('Content-Type: application/x-www-form-urlencoded',)),
        'content' => http_build_query($data),
    )   
);

$html = file_get_contents($url, false, stream_context_create($context));

echo $html;
~~~

参考
[PHPでPOST送信まとめ](https://qiita.com/okdyy75/items/d21eb95f01b28f945cc6)

## APPサーバ上にPHPのファイルを配置
APPサーバ上にAPIサーバから来たPOSTを受け取り、Dockerコンテナを起動するためのPHPファイルを配置します。

APPサーバに`/var/www/html/run.php`を作成、追記します。

~~~php:/var/www/html/run.php
<?php

$data = array(
    'name' => $_POST['name'],
    'image' => $_POST['image'],
);

// create
$url = sprintf('http://127.0.0.1:2375/v1.41/containers/create?name=%s',
    urlencode($data['name'])
);

$context = array(
    'http' => array(
        'method'  => 'POST',
        'header'  => implode("\r\n", array('Content-Type: application/json',)),
        'content' => json_encode(array(
            'Image' => $data['image'],
            'HostConfig' => array(
                'PortBindings' => array(
                    '80/tcp' => array(
                        array('HostPort' => '10080')
                    )
                )
            )
        ))
    )   
);

$res = file_get_contents($url, false, stream_context_create($context));

echo $res;

// start
$url = sprintf('http://127.0.0.1:2375/v1.41/containers/%s/start',
    urlencode($data['name'])
);

$context = array(
    'http' => array(
        'method'  => 'POST'
    )   
);

$res = file_get_contents($url, false, stream_context_create($context));

echo $res;
~~~

このプログラムが動くことでDockerコンテナが起動します。

少し説明します。
DockerはDockerコマンドを叩く以外にも、DockerAPIで叩くこともできます。（詳細は参考サイトを読んで下さい。）
`// create`の部分ではコンテナの作成コマンドを叩いています。いわゆる`docker create`ですね。
`// start`の部分ではコンテナ起動のコマンドを叩いています。いわゆる`docker start`です。

わざわざDockerコマンドではなく、DockerAPIで叩いているのは、Dockerコマンドを直接叩こうとするとroot権限が必要になるため、スクリプトでは叩けません。
なのでDockerAPIで直接叩いています。

参考
[Docker Engine API について](https://qiita.com/doz13189/items/9ee8fda335605567b832)
[Docker Engine SDK および Docker API の利用例](https://matsuand.github.io/docs.docker.jp.onthefly/engine/api/sdk/examples/)

## APPサーバ上に使いたいコンテナイメージを入れる
今回はApacheのコンテナを動かしたいので事前にhttpdのイメージを入れておきます。

~~~
docker pull httpd
~~~

これですべて準備が完了したはずです。

## 動作確認
ブラウザからUIサーバにアクセスします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2295232/850582f8-90a3-5c78-7c15-03cd6b96a695.png)

「作成」ボタンが表示されます。

そのボタンをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2295232/bbe9f6cd-418d-d658-cfb5-9fa229cdb350.png)

コンテナが作成されます。

作成されたコンテナにアクセスしてみます。
APPサーバ上に作成されているのでAPPサーバの8080番ポートにブラウザからアクセスします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2295232/30547c54-5c2b-894f-be87-7b62e49684e6.png)


Apacheが動いていることが確認できました。

一応APPサーバの裏側でもちゃんと動いているか確認します。
~~~
[root@app html]# docker ps
CONTAINER ID   IMAGE     COMMAND              CREATED         STATUS         PORTS                                   NAMES
cd441a666d14   httpd     "httpd-foreground"   6 seconds ago   Up 5 seconds   0.0.0.0:8080->80/tcp, :::8080->80/tcp   test
~~~

ちゃんと動いていることが確認できました。

# まとめ
* ブラウザ（chromeやfirefoxなど）から「コンテナ作成」のボタンを押すと、裏で動いているサーバ上にDockerコンテナが作成された
* ブラウザから作成したコンテナにアクセスできた

# おわりに
この記事は[富士通クラウドテクノロジーズ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/fjct) の15日目の記事でした。

明日は@ktakaakiさんの[ITエンジニアがレザークラフトを真面目にやってみた](https://qiita.com/ktakaaki/items/7dd3268cd1b16bea2771)です。
洋服とか靴とか財布とか、革製品がマイブームなのでとても楽しみです！

この記事は必要最低限の動作をするシステムですので、必要に応じて改良してください。
誰かの役に立てば幸いです。
最後まで読んでいただきありがとうございました。