20191224のdockerに関する記事は13件です。

Kaggle・機械学習実験用Docker環境構築入門

はじめに

初めましての方は初めまして,最近Volareという学生団体に所属することになったtattakaです.
普段は関西の大学院の修士課程でロボットビジョンの研究をしていて,趣味ではKaggleの主に画像コンペに参加しています.
この記事では機械学習の研究やKaggleで使いまわせる環境の構築について書いていこうと思います.
(この記事はVolare Advent Calendar 2019の24日目です.)

この記事で書くこと

  • Kaggleや機械学習系の研究室でDockerを使う利点
  • ディレクトリ構成について
  • DockerHubを使ったイメージの管理

書かないこと

  • NVIDIA Docker・Docker Composeの話
    • (このあたりは筆者が怠慢諸事情により最新バージョンを追えていないので......)

1. Kaggleや機械学習系の研究室でDockerを使う利点

具体的な環境構築の前に,Dockerを使ってKaggleや機械学習の環境を構築することの利点について述べていきます.

  • 環境構築するときの虚無の緩和
    ホスト環境に影響を与えないのでインストール方法などの試行錯誤がしやすいです.
    また,既存環境との衝突をあまり考えなくてもよくなります.

  • 環境汚染からの脱却
    同じイメージからは同じコンテナが生成されるので環境を汚してもコンテナを作り直せば良いので直で環境構築をするよりは環境が汚れにくいです.

  • 環境の共有が楽
    Dockerfileを共有すれば基本的に(GPUドライバ周りを合わせていれば)同じイメージを生成できます.また、後に説明するDockerHubを使えばイメージをビルドする時間の省略のほか,docker pullコマンドだけで違うマシンでも同じ環境を構築できるようになります.

2. ディレクトリ構成について

私はKaggleや研究では深層学習を主に用いるので以下のようなディレクトリ構成で運用しています.

ml_environment
├── projects
│   ├── project_dir1
│   ├── project_dir2
│   └── project_dir...
├── docker
│   ├── base
│   │   └── Dockerfile
│   ├── framework1
│   │   └── Dockerfile
│   ├── framework2
│   │   └── Dockerfile
│   ├── framework...
│   │   └── Dockerfile
│   ├── scripts
│   │   └──initialize-docker-container.sh
│   └── docker-compose.yml
└── RUN-DOCKER-CONTAINER.sh

それぞれのファイル・ディレクトリの説明をしていきます.

  • projects
    Kaggleや研究で用いるソースファイルを入れる場所です.ここに集めておくことによってJupyter Notebookなどをprojectsディレクトリで開いた時にプロジェクト間の行き来がしやすくなります.

  • docker
    Dockerfiledocker-compose.ymlなどを入れる場所です.
    フレームワークごとにDockerfileを分けたディレクトリを作っていて,イメージを再利用できるようにしています.

    • base
      ここにはフレームワークによらず共通で使うライブラリを固めるためのイメージを作成するDockerfileを置いています.
      筆者の場合こんな感じで使っています.
    • framework...
      ここにはフレームワーク別のDockerfileを置いています. 例えばPyTorchだとこのようなファイルになります.
    • scripts
      ここにはDocker起動時や内部で動作させるスクリプトを配置しています. 例えばinitialize-docker-container.shdbusの起動・exitをしてもバックグラウンドで動き続けさせるためのスクリプトです.
    • docker-compose.yml

      「はじめに」でも述べた通り,詳しくは書きませんが必須な設定とやっておくと便利な設定をいくつか述べます.

      • volumes: - ../:/root/ml_environment/
        ホスト上のプロジェクトディレクトリをDocker上にマウントします.
      • command: /root/ml_environment/docker/scripts/initialize-docker-container.sh
        上で設定したinitialize-docker-container.shを適用する設定です.
      • network_mode: host
        ネットワークデバイスをホストと共有する設定です.一般的にはポートマッピングを使った方が良いのですが,リモートでこのDocker環境を使うときSSHのポートフォワードだけでいろいろできるので便利です.
  • RUN-DOCKER-CONTAINER.sh
    上のdocker-compose.ymlを使ってコンテナを立ち上げるスクリプトです.こんな感じで書くといいと思います.

3. GitHubとDockerHubを使ったイメージの管理

なぜDockerHubを使った方が良いか

まず,DockerHubを使ってイメージを管理するメリットについて簡単に説明します.

  • イメージをビルドする時間の省略
    ローカルでイメージをビルドするのはマシンスペックによっては時間がかかってしまいます.また,ビルド時に大容量を要求するライブラリもありダウンロードするだけの方が時間短縮になる場合が多いです.
  • 違うマシンで同じイメージを使って環境構築できる
    お気づきの方がいるかもしれませんが,上で紹介したDockerfileの中のライブラリはバージョン指定をしていないものがあります.これは調べるのが面倒くさいからパッケージによってはaptのkeyが無効化されていてダウンロードできなくなっていたりunstableなバージョンだと消えていたりすることが考えられるからです.DockerHubを使うことでビルドする回数を最小限にして動作を担保したイメージを別のマシンに配布することが可能になります.

DockerHubとGitHubリポジトリの連携方法

0. 準備

GitHubのリポジトリを準備しましょう.
ここからは筆者のこのリポジトリ(tattaka/ml_environment)を使って説明していきます.
またDockerHubにリポジトリを作るためにはユーザ登録が必要なので登録しておきましょう.

1. リポジトリ作成

次にこのページ(https://hub.docker.com/repositories)に飛んでCreate Repository+ボタンを押しましょう.
ここでリポジトリの各種設定ができるので必須項目を順に埋めていきましょう.

  1. Name: リポジトリ名を入力しましょう.筆者はml_environmentはもう使っているのでml_environment2という名前を使います.
  2. Visibility: リポジトリを公開するかどうかの設定です.無料アカウントだと非公開リポジトリは1つしか作れないみたいです......
  3. Build Settings: 外部のWebサービスと連携するかどうかの設定です.GitHubとBitbucketが選べますが今はおいておいて後で設定しましょう.

こんな画面になったでしょうか?

2. GitHub連携の設定

次にGitHub連携の設定をしていきます.
上の画面のBuildsタブをクリックして,この画面に遷移します.

ここでLink to GitHubを選択します.初めてリポジトリを作る場合は下のようなユーザ設定画面でGitHub連携の初期設定をする必要があります.2回目以降はGitHubの連携の初期設定は不要です.
スクリーンショット 2019-12-24 22.50.01.png
ここでGitHubの行のConnectボタンをクリックします.
GitHubに遷移するのでAuthorize dockerを選択します.
スクリーンショット 2019-12-24 23.00.25.png
GitHubのパスワードが求められるので入力してConfirm passwordをクリックします.
これでDockerHubアカウントとGitHubアカウントの連携ができました.

3. Automation Buildの設定

後もう一息です!
次にリポジトリのBuildsタグに戻り,もう一度Link to GitHubをクリックします.
ビルドの設定画面になるのでSelect Organizationに自分のユーザ名を入れ、Select Repositoryで使いたいリポジトリを選択します.

最後に上の画面下部のBUILD RULESを追加していきます.
今回は上で説明したbaseディレクトリのDockerイメージがgit push origin masterをすると自動でビルドされるように設定していきます.今は行いませんがBranchではなくTagと紐付けして自動ビルドを行うこともできます.
今回git push origin masterした時にビルドが走ってほしいのでSourceはそのままで大丈夫です.
Docker Tagはこの場合baseと名付けましょう.
Dockerfile locationはリポジトリの1番上から見てDockerfileがどこにあるかを設定します.
今回はdocker/base/Dockerfileとします.入力したら保存しましょう.
保存が完了するとBuildsタブに戻り設定が完了していることが確認できます.

git push origin masterすることでもビルドが始まりますが,手動でビルドしたい場合,下部に先ほど設定した自動ビルドの項目があるのでTrigger ▶︎をクリックするとビルドが始まります.
これで設定は完了です.お疲れ様でした!

今設定してビルドしたDockerイメージはdocker pull tattaka/ml_environment2:baseでダウンロードすることができます.ぜひ試して見てください.

最後に

本記事では筆者の思う使いやすい環境構築のTipsについて説明してきました.
Dockerの本来の使い方や思想とはあっていない(であろう)使い方のため,「もっとここをこうした方が良いよ」「ここおかしいんじゃない?」という意見があると思います.そのような場合はコメント欄に意見を送ってもらえるととてもありがたいです.
最後に、ここまで読んでいただき本当にありがとうございました.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Apache Ambari を Docker でインストール

Hadoop クラスタの環境を個人で実サーバで持つのはコストがかかるため、多少低スペックでも構わないので、個人のローカルPC上で出来る範囲で構築したいところです。
今回は以下の記事を元に Dockerfile を書いて Docker コンテナ上で Ambari を起動するところまでやってみます。
Apache Ambari Installation

Dockerfile を書く

  1. centos7 を使ってみます。
  2. いくつか使いそうなコマンドをインストールします。
  3. centos7 で service コマンドを使えるようにするため、initscripts をインストールします。
  4. Ambari repository をダウンロードしておきます。
  5. Ambari Server をインストールします。
Dockerfile
# 1
FROM centos:centos7
LABEL maintainer "blueskyarea"

USER root

# 2. Install commands
RUN yum install -y curl tar rsync wget

# 3. Install initscripts for service command
RUN yum -y install initscripts && yum clean all

# 4. Download Ambari repository
RUN wget -nv http://public-repo-1.hortonworks.com/ambari/centos7/2.x/updates/2.6.2.0/ambari.repo -O /etc/yum.repos.d/ambari.repo

# 5. Install Ambari server
RUN yum install -y ambari-server

Docker イメージを作る

今回はイメージ名として、"ambari262" と指定しておきます。

$ docker build -t ambari262 .
double free or corruption (out)
SIGABRT: abort
PC=0x7fe1348bde97 m=0 sigcode=18446744073709551610
signal arrived during cgo execution
(中略)
Complete!
Removing intermediate container 7b98ea9a218a
 ---> 7003da2b64b7
Successfully built 7003da2b64b7
Successfully tagged ambari262:latest

Docker イメージが作成されました。

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ambari262           latest              7003da2b64b7        4 minutes ago       1.27GB

コンテナを起動する

Ambari server では、デフォルトで組み込みの postgres を使用するようです。
postgres を起動するためには、systemctl コマンドが必要になりそうですが、centos7では、/sbin/init を実行する必要があるようです。

$ docker run -d --privileged ambari262 /sbin/init
84c36c37ff468532962121011002ecb324f3df8edf3f16ae0e7016ac1dcd3033

そして、そのコンテナに対して、bash を起動します。

$ docker exec -it 84c36c37ff46 /bin/bash

Ambari server の setup

マニュアルに沿ってセットアップしていきます。
注意しておきたいのは、"Enter advanced database configuration y/n? "の質問に対して、No(n) で回答するところです。
Yes(y) を選択して、使用するDBを手動で選択した場合、自分でDBを起動し、スキーマの作成など行う必要があります。
※私はそれで詰まってしまったため、コンテナの再作成からやり直ししました

[root@84c36c37ff46 /]# ambari-server setup
Using python  /usr/bin/python
Setup ambari-server
Checking SELinux...
WARNING: Could not run /usr/sbin/sestatus: OK
Customize user account for ambari-server daemon [y/n] (n)? 
Adjusting ambari-server permissions and ownership...
Checking firewall status...
Checking JDK...
[1] Oracle JDK 1.8 + Java Cryptography Extension (JCE) Policy Files 8
[2] Oracle JDK 1.7 + Java Cryptography Extension (JCE) Policy Files 7
[3] Custom JDK
==============================================================================
Enter choice (1): 
To download the Oracle JDK and the Java Cryptography Extension (JCE) Policy Files you must accept the license terms found at http://www.oracle.com/technetwork/java/javase/terms/license/index.html and not accepting will cancel the Ambari Server setup and you must install the JDK and JCE files manually.
Do you accept the Oracle Binary Code License Agreement [y/n] (y)? 
Downloading JDK from http://public-repo-1.hortonworks.com/ARTIFACTS/jdk-8u112-linux-x64.tar.gz to /var/lib/ambari-server/resources/jdk-8u112-linux-x64.tar.gz
jdk-8u112-linux-x64.tar.gz... 100% (174.7 MB of 174.7 MB)
Successfully downloaded JDK distribution to /var/lib/ambari-server/resources/jdk-8u112-linux-x64.tar.gz
Installing JDK to /usr/jdk64/
Successfully installed JDK to /usr/jdk64/
Downloading JCE Policy archive from http://public-repo-1.hortonworks.com/ARTIFACTS/jce_policy-8.zip to /var/lib/ambari-server/resources/jce_policy-8.zip

Successfully downloaded JCE Policy archive to /var/lib/ambari-server/resources/jce_policy-8.zip
Installing JCE policy...
Checking GPL software agreement...
GPL License for LZO: https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html
Enable Ambari Server to download and install GPL Licensed LZO packages [y/n] (n)? y
Completing setup...
Configuring database...
Enter advanced database configuration [y/n] (n)? 
Configuring database...
Default properties detected. Using built-in database.
Configuring ambari database...
Checking PostgreSQL...
Running initdb: This may take up to a minute.
Initializing database ... OK

About to start PostgreSQL
Configuring local database...
Configuring PostgreSQL...
Restarting PostgreSQL
Creating schema and user...
done.
Creating tables...
done.
Extracting system views...
.........ambari-admin-2.6.2.0.155.jar
..
Adjusting ambari-server permissions and ownership...
Ambari Server 'setup' completed successfully.

Ambari server の起動

[root@84c36c37ff46 /]# ambari-server start
Using python  /usr/bin/python
Starting ambari-server
Ambari Server running with administrator privileges.
Organizing resource files at /var/lib/ambari-server/resources...
Ambari database consistency check started...
Server PID at: /var/run/ambari-server/ambari-server.pid
Server out at: /var/log/ambari-server/ambari-server.out
Server log at: /var/log/ambari-server/ambari-server.log
(中略)

コンテナのIPアドレスを調べて、8080 ポートでアクセスしてみます。

$ docker inspect --format '{{.NetworkSettings.IPAddress}}' 84c36c37ff46
172.17.0.2

ユーザ(admin)/パスワード(admin) でログインします。
ambari-login.PNG

ログイン出来ました。
とりあえず、目的は達成できたので、ここからクラスタを作って色々と試していきたいと思います。
ambari-dashboard.PNG

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerなLaravel6でHMR(Hot Module Replacement)

はじめに

Laravelでvue.jsを使った開発をするときは
laravel-mix付属のホットリロード機能(HMR)が便利です。

ただ、ホストで動かすことが想定されており、dockerベースでHMRな開発環境を作ろうとして戸惑ったので情報を整理します。

概要

Laravel6 + vue.js on Docker な環境でHMRをする設定を説明します。
Laradockは使いません。

ホスト側にバージョン管理が必要なツールはあまり入れたくないため、すべてDockerコンテナ内で完結させる方針です。
(yarnの実行はコンテナ内で実行すると遅いのですが、Ubuntu 上ではそれほど違いが感じられなかったため、コンテナ内で行う場合について記載しております。)

環境

  • Ubuntu 18.04
  • Docker 19.03.5
  • docker-compose 1.25.0
  • Laravel 6

結論

  • WebサーバーはLaravelのビルドインサーバーを使う
  • PHPとNode.jsは同じコンテナに設定
  • Webpackのプロキシ機能で8080のアクセスを8000ポート(ビルドインサーバー)に転送

ソースはこちら
https://github.com/odaryo/laravel6_vuejs_HMR

環境構築について

ディレクトリ構成

┬- docker (Dockerの設定)
│    └- app
│        ├- conf.d  (php設定ファイル)
│        │   ├- php_settings.ini
│        │   └- xdebug.ini
│        └- Dockerfile
├- src  (Laravelディレクトリ)
└- docker-compose.yml

Docker環境

docker-compoe

appコンテナにはLaravelのビルドインサーバーを起動する設定を記載しています

docker-compose.yml
version: '3'

services:
  # laravel
  app:
    build:
      context: ./
      dockerfile: ./docker/app/Dockerfile
    depends_on:
      - db
    links:
      - db
    volumes:
      - ./app:/app:cached
      - ./docker/app/conf.d/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini
      - ./docker/app/conf.d/php_settings.ini:/usr/local/etc/php/conf.d/php_settings.ini
    command: bash -c "php artisan serve --host 0.0.0.0" # 起動時にビルドインサーバーを起動
    ports:
      - "8000:8000"  # php artisan serve 用のポート
      - "8080:8080"  # HMR用のポート

  # DB (mysql)
  db:
    image: mysql:8
    environment:
      - MYSQL_DATABASE=testdb
      - MYSQL_USER=testuser
      - MYSQL_PASSWORD=password
      - MYSQL_ROOT_PASSWORD=root
      - TZ=Asia/Tokyo
    restart: always
    volumes:
      - dev_mysql_data:/var/lib/mysql
    ports:
      - "33306:3306"

volumes:
  dev_mysql_data:
    driver: local

Dockerfile

PHPコンテナとNode.jsコンテナに分けるとうまく行かなかったため、マルチステージビルドの形でPHPコンテナにNode.jsを追加する

Dockerfile
# Nodeイメージ
FROM node:13-alpine as node

# PHPイメージ
FROM php:7.3-alpine

# Laravel環境に必要なパッケージをインストール
RUN apk update \
    && apk upgrade \
    && apk add --no-cache \
        bash \
        git \
        unzip \
        libpng \
        libpng-dev \
        libjpeg \
        icu \
        icu-dev \
        icu-libs \
        libxml2 \
        libxml2-dev \
        openssl \
        openssl-dev \
    && docker-php-ext-install \
        pdo_mysql \
        mysqli \
        gd \
        mbstring \
        intl \
        xml \
        opcache \
    && docker-php-ext-enable intl mbstring \
    && apk --update --no-cache add autoconf g++ make \
    # xdebugインストール
    && pecl install -f xdebug \
    && docker-php-ext-enable xdebug \
    && apk del --purge autoconf g++ make

# Composerのインストール
RUN curl -sS https://getcomposer.org/installer | php ;mv composer.phar /usr/local/bin/composer;
RUN composer global require hirak/prestissimo \
    && composer global require phpunit/phpunit

# Nodeコンテナからyarnとnodeをコピー
COPY --from=node /opt/yarn-v* /opt/yarn
COPY --from=node /usr/local/bin/node /usr/local/bin/

# 使いやすいようにシンボリックリンク作成
RUN ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
    && ln -s /opt/yarn/bin/yarnpkg /usr/local/bin/yarnpkg

# ホスト側とuser_id, group_idを合わせる
ARG USER_ID
ARG GROUP_ID

RUN addgroup -g ${GROUP_ID} -S app-user && \
    adduser -u ${USER_ID} -S app-user -G app-user

USER app-user:app-user

# Setup working directory
WORKDIR /app

参考

構築手順

docker起動

$ docker-compose build
$ docker-compose up -d

laravelインストール

appコンテナに入って実行します。
※laravelインストールまではコンテナが起動しないため、runコマンドで実行する必要があります。

$ docker-compose run app /bin/bash
$ composer create-project --prefer-dist laravel/laravel .

vue.jsを使う設定

laravel 6.0からデフォルトではインストールされないため、laravel/uiからインストールします。

$ composer require laravel/ui --dev
$ php artisan ui vue
$ exit

ここでdockerを再起動しておく

$ docker-compose up -d

http://localhost:8000にアクセスして、スタート画面が表示されたらOK
image.png

node_moduleのインストール

$ docker-compose exec app yarn

HMRの設定

ホットリロードの確認用に、welcome.blade.phpのbody内にVue.jsのComponentを追加しておく
Componentはインストール時に作成されるサンプルを使用

welcome.blade.php
<div id="app">
    <example-component></example-component>
</div>
<script src="{{ mix('js/app.js') }}"></script>

webpack.mix.jsにHMRの設定を追加

8080へのアクセスではエラーとなりCannot GET /が表示されてしまうため、ビルドインサーバーの8000ポートへプロキシしてやります

webpack.mix.js
mix.webpackConfig({
    devServer: {
        host: '0.0.0.0',
        port: 8080,
        proxy: {
            '*': 'http://0.0.0.0:8000'
        },
        // Windows(Docker for windows)の場合は下記を追加する
        // watchOptions:{
        //     aggregateTimeout:200,
        //     poll:5000
        // },
    }
});

ホットリロードの実行

$ docker-compose exec app yarn hot

http://localhost:8080へアクセスして、コンポーネントの内容が表示されれば完了です。
コンポーネントを修正して、変更が反映されることを確認しましょう。

image.png

参考

終わりに

ホットリロードでフロントエンド開発が捗ります。

注意点としては、HMRで監視するファイルはJavascriptやCSSなので、blade自体を更新した場合はブラウザの更新が必要となります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Cloud RunでnltkライブラリがインストールされたDockerコンテナをデプロイする方法

Cloud RunでnltkライブラリがインストールされたDockerコンテナをデプロイしようとしてハマったのでメモ。

事象

以下のように、Dockerfilenltkライブラリをインストールするよう記述し、Cloud RunでDockerコンテナをデプロイしようとするとエラーが発生します。

Dockerfile
RUN pip install nltk
RUN nltk.download('punkt')
error
2019-12-23 14:02:28.321 PDT Resource [93mpunkt[0m not found.
2019-12-23 14:02:28.321 PDT Please use the NLTK Downloader to obtain the resource:
2019-12-23 14:02:28.321 PDT
2019-12-23 14:02:28.321 PDT [31m>>> import nltk
2019-12-23 14:02:28.321 PDT >>> nltk.download('punkt')
2019-12-23 14:02:28.321 PDT [0m
2019-12-23 14:02:28.321 PDT For more information see: https://www.nltk.org/data.html
2019-12-23 14:02:28.321 PDT
2019-12-23 14:02:28.321 PDT Attempted to load [93mtokenizers/punkt/PY3/english.pickle[0m
2019-12-23 14:02:28.321 PDT
2019-12-23 14:02:28.321 PDT Searched in:
2019-12-23 14:02:28.321 PDT - '/home/nltk_data'
2019-12-23 14:02:28.321 PDT - '/usr/local/nltk_data'
2019-12-23 14:02:28.321 PDT - '/usr/local/share/nltk_data'
2019-12-23 14:02:28.321 PDT - '/usr/local/lib/nltk_data'
2019-12-23 14:02:28.321 PDT - '/usr/share/nltk_data'
2019-12-23 14:02:28.321 PDT - '/usr/local/share/nltk_data'
2019-12-23 14:02:28.321 PDT - '/usr/lib/nltk_data'
2019-12-23 14:02:28.321 PDT - '/usr/local/lib/nltk_data'
2019-12-23 14:02:28.321 PDT - ''

errorメッセージを読むと、「nltkライブラリのダウンロード先を探してみたけど見つからない!」と言っているようです。
Dockerfilenltkライブラリをインストールしているにも関わらず不思議なエラーです。

原因

以下でヒントが述べられています。
https://stackoverflow.com/questions/58654672/after-adding-nltk-downloadwords-google-cloud-run
どうやら、nltkのインストール方法には以下の3つが存在していて、App EngineやCloud Runで動作させるためには、Manual installationが必要とのこと。

  • Interactive installer
  • Command line installation
  • Manual installation

nltk.download()はInteractive installer(GUIを使う)なのでApp EngineやCloud Runで動作しなかったようです。

解決策

Dockerfileに以下を追記することでnltkライブラリを使えるようになりました。

# for install wget
RUN apt-get update && apt-get install -y wget

# for NLTK manual download
WORKDIR /usr/local/nltk_data/tokenizers
RUN wget "https://raw.githubusercontent.com/nltk/nltk_data/gh-pages/packages/tokenizers/punkt.zip" -O punkt.zip
RUN unzip punkt.zip
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker Compose 入門

Docker Compose とは

Docker Composeは、複数のコンテナで構成されるアプリケーションについて、Dockerイメージのビルドや各コンテナの起動・停止などをより簡単に行えるようにするツールです。

これまでの連載の中でも、複数のコンテナを起動させる場面がありました。その際、各コンテナを起動するために、それぞれ起動コマンドを実行する必要がありました。また、コンテナを起動する際に、いろいろなオプションを利用していました。コマンドや手順が複雑になると、他の環境で使う/使ってもらう場合に、ミスが発生しやすくなります。他の環境でも同じ構成(同じDockerイメージ)で動かせるというDockerのメリットを生かすには、起動手順なども簡単であってほしいですよね。

そこで活躍するのが、Docker Composeです。

Docker Composeでは、Dockerビルドやコンテナ起動のオプションなどを含め、複数のコンテナの定義をymlファイルに書き、それを利用してDockerビルドやコンテナ起動をすることができます。一つの簡単なコマンドで複数のコンテナを管理できるようになります。

引用: https://knowledge.sakura.ad.jp/16862/

要は 複数のコンテナを連携させたいときに楽しようよ っていうものです。

Docker Compose 導入

$ sudo curl -L "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
$ sudo gpasswd -a $USER docker
$ docker-compose -v
docker-compose version X.XX.X, build XXXXXXXX

動かしてみよう

拙作の docker-compose.yml を実際に動かしてみたいと思います。

$ git clone https://github.com/iedred7584/DockerWordpressPlayground.git
$ cd DockerWordpressPlayground/
$ docker-compose up -d
Creating mysql_playground     ... done
Creating wordpress_playground ... done
Creating pma_playground       ... done

docker-compose コマンドを使います。
updocker-compose.yml をもとに作成します。
-d で作成が完了して起動した際に自動でデタッチしてターミナルを返すようにします。

Creating mysql_playground     ... done
Creating wordpress_playground ... done
Creating pma_playground       ... done

と出力されれば成功です。

詳細な解説

docker-compose.yml
version: "3"
services:
  mysql:
    image: mysql:5.7
    container_name: mysql_playground
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_PASSWORD: docker
      MYSQL_DATABASE: playground
      MYSQL_USER: docker
      MYSQL_PASSWORD: docker
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_bin

  wordpress:
    image: wordpress:latest
    container_name: wordpress_playground
    volumes:
      - ./wordpress/wwwroot:/var/www/html
      - ./wordpress/backup:/tmp/backup
      - ./wordpress/log:/tmp/log
    ports:
      - 50080:80
    depends_on:
      - mysql
    environment:
      WORDPRESS_DB_NAME: playground
      WORDPRESS_DB_USER: docker
      WORDPRESS_DB_PASSWORD: docker
    links:
      - mysql:mysql

  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    container_name: pma_playground
    ports:
      - 50081:80
    depends_on:
      - mysql
    environment:
      PMA_HOST: mysql
      PMA_PORT: 3306
      PMA_USER: docker
      PMA_PASSWORD: docker
    links:
      - mysql:db

versiondocker-compose.yml がどのバージョンから対応させているのかを指定します。投稿日時点では 3.7 が最新です。(https://docs.docker.com/compose/compose-file/)
services はその docker-compose.yml で動かすものの指定です。 services 以下に mysql wordpress phpmyadmin がありますが、これは

$ docker run mysql
$ docker run wordpress
$ docker run phpmyadmin

と意味合いでは同じものです。
services 以下の
image は使用するイメージ
container_name は作成したコンテナの名称を指定します。
ports は作成したコンテナで使用するポートを指定します。
depends_on はここで指定したコンテナが起動するのを待ってからこのコンテナを作成する指示します。
environment は環境変数の宣言します。
links はコンテナ間連携で連携させるコンテナ名を指定します。

Docker Compose と Dockerfile の連携

docker-compose.yml
version: "3"
services:
  sample:
    build: .

buildDockerfile を指定します。これは docker-compose.yml から見た Dockerfile の場所を指定します。

まとめ

$ docker run --env xxxx=xxxx --env xxxx=xxxx mysql
$ docker run --env xxxx=xxxx --env xxxx=xxxx wordpress

とかつらい思いをせずに Docker Compose を使いましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AlpineLinux on Docker × Laravelが遅い話

開発環境だけアプリがめちゃくちゃ重い

新しいチームに配属されて最初の感想です。

本番ではめちゃめちゃ早いのに開発環境がむちゃくちゃに遅い。

DXが最悪だったので原因を調査したところAlpineからCentOSに乗り換えたら3倍ぐらい早くなったという話です。

image.png


image.png

ベンチマーク

長々した説明は置いといて、結論から言うと「よくわからないがdockerで動かしているphpが異常に重い」ということだけがわかった。

これアプリケーションコードが重いというわけではなく、Alpine Linux on Docker で動いているLaravelが全体的に処理が遅いという体感があったので実際に計測してみた。

以下のPHPコードをAlpineとCentOSで動かして比較してみる。CentOSのDockerfileはPHP入れる処理だけなので省略。

test.php
<?php
ini_set('memory_limit', '-1');
function benchmark($i) {

    if($i ==! 0) {
        benchmark($i - 1);
    }
    function() { return sha1("test${i}");};
    function() { return md5("test${i}");};
}

$time_start = microtime(true);
foreach (range(1, 10000) as $i) {
    benchmark($i);
}

$time = microtime(true) - $time_start;
echo "{$time} sec";

コードの内容は至って簡単で、メモリ上限を無くし再帰的処理でハッシュを取得していくだけ。

 /t/test  docker run --rm -v /tmp/test/test.php:/test.php centos-php php /test.php
8.2858350276947 sec
/t/test  docker run --rm -v /tmp/test/test.php:/test.php centos-php php /test.php
8.2832388877869 sec
/t/test  docker run --rm -v /tmp/test/test.php:/test.php centos-php php /test.php
8.2994618415833 sec

/t/test  docker run --rm -v /tmp/test/test.php:/test.php php:7.4.1-fpm-alpine php /test.php
8.1350269317627 sec
/t/test  docker run --rm -v /tmp/test/test.php:/test.php php:7.4.1-fpm-alpine php /test.php
8.0924451351166 sec
/t/test  docker run --rm -v /tmp/test/test.php:/test.php php:7.4.1-fpm-alpine php /test.php
8.0950059890747 sec

これは特に問題なさそう(むしろAlpineの方が早い

Laravelでのベンチマーク

実際にLaravelで測定した時の結果を貼っていく

用意が面倒だったので開発環境での弊社サービスのLPページを測定対象とする

https://album.8122.jp/

alpineでのdockerfileは以下の通り

FROM php:7.3-fpm-alpine
ENV LD_PRELOAD /usr/lib/preloadable_libiconv.so php

RUN apk --no-cache update \
&& apk add --no-cache $PHPIZE_DEPS postgresql-dev libpng-dev libjpeg-turbo-dev icu-dev \
&& docker-php-ext-configure gd --with-jpeg-dir=/usr/include/ --with-png-dir=/usr/include/ \
&& docker-php-ext-configure intl --enable-intl \
&& docker-php-ext-install exif pdo_pgsql gd intl opcache pcntl
RUN pecl install xdebug \
&& docker-php-ext-enable xdebug

WORKDIR /var/www/html

対してCentOSベースのDockerfileは以下の通り

FROM centos:7.5.1804

#locale 追加
RUN sed -i -e '/override_install_langs/s/$/,ja_JP.utf8/g' /etc/yum.conf

RUN curl -sL https://rpm.nodesource.com/setup_10.x | bash - \
  && yum install -y http://rpms.famillecollet.com/enterprise/remi-release-7.rpm \
  https://s3-ap-northeast-1.amazonaws.com/sen-infra/rpms/centos/7/x86_64/pgdg-centos10-10-2.noarch.rpm \
  && yum install -y postgresql10 \
  nodejs \
  zlib-devel \
  glibc-common \
  make \
  libpng-devel \
  cronie \
  && yum install -y --enablerepo=remi,remi-php73 \
  php \
  php-opcache \
  php-mbstring \
  php-pdo \
  php-pecl-memcache \
  php-pecl-memcached \
  php-pecl-redis \
  php-pecl-imagick \
  php-mcrypt \
  php-mysqlnd \
  php-xml \
  php-gd \
  php-devel \
  php-pgsql \
  php-pecl-ssh2 \
  php-process \
  php-intl \
  php-pear \
  php-pecl-apcu \
  php-pecl-apcu-bc \
  php-pecl-zip \
  php-fpm \
  && rm -rf /var/cache/yum/* \
  && yum clean all


COPY ./php-fpm.conf /etc/php-fpm.conf

RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin \
  && mv /usr/local/bin/composer.phar  /usr/local/bin/composer \
  && composer global require hirak/prestissimo

WORKDIR /var/www/html

CMD ["/usr/sbin/php-fpm","-F","-y","/etc/php-fpm.conf"]

LPページなのでミドルウェアもプロパイダーも挟んではいない(はず

まずはAlpineから

yoshiken@yskn:~/Git/sen/album$ time curl localhost
real    0m0.392s
user    0m0.013s
sys 0m0.004s
yoshiken@yskn:~/Git/sen/album$ time curl localhost
real    0m0.392s
user    0m0.013s
sys 0m0.000s
yoshiken@yskn:~/Git/sen/album$ time curl localhost
real    0m0.409s
user    0m0.012s
sys 0m0.004s

平均して約0.4sec

対してCentOSベースでは

yoshiken@yskn:~/Git/sen/album$ time curl localhost
real    0m0.064s
user    0m0.007s
sys 0m0.007s
yoshiken@yskn:~/Git/sen/album$ time curl localhost
real    0m0.062s
user    0m0.003s
sys 0m0.012s
yoshiken@yskn:~/Git/sen/album$ time curl localhost
real    0m0.052s
user    0m0.014s
sys 0m0.000s

平均して0.06sec前後といったところ。

よくわからん

ここまで書いていてなんだが、何故PHPとしてのパフォーマンスが上のAlpineがLaravelを介すると急激に遅くなるかわかっていない。

調べた感じAlpineだと遅いという点では色々困ってる人が見当たってるが、具体的な解決策、根本原因は未だ解明されていない。

[5.2] Slow response times running within a php-7 Docker container · Issue #12228 · laravel/framework

php - PHP7 + Laravel +Nginx is terribly slow on Docker - Stack Overflow

AlpineでPythonが遅くなるのはAlpineの独自パッケージによる差分が原因とされているが、PHPでは最初のベンチマークの時点では大きな乖離がなかった。
https://superuser.com/questions/1219609/why-is-the-alpine-docker-image-over-50-slower-than-the-ubuntu-image

言いたいこと

いろんな記事で「DockerとAlpine使って爆速で環境構築!!!」という文章を見かけるが、一旦待ってほしい。

ローカル環境では急速なスケールをする必要もないのでイメージが大きかろうが特に問題は無いわけであって、重要なのは使うフレームワークや言語に最適化された環境である。

もし、本番環境でコンテナ運用して開発と同じイメージ(特にAlpineLinux)を使用しているという状況であれば、まず一旦そのイメージの妥当性を確認すべきなのかなと。
他のディストリビューションをベースにした場合との速度計測をして正しい技術選定を行った方がよいのではと思う次第。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerfile の Multi Stage Build 入門

Dockerfile おさらい

FROM nginx:latest
ADD . /usr/share/nginx/html

こういうやつです。

今回のサンプル

この記事で実際に動かすサンプルは Golang で書いた HTTP サーバーです。

main.go
package main

import (
    "encoding/json"
    "net/http"
    "time"
)

func echo(w http.ResponseWriter, r *http.Request) {
    type Response struct {
        Status  int       `json:"status`
        Time    time.Time `json:"time`
        Message string    `json:"message`
    }

    w.Header().Set("Content-Type", "application/json")

    res := &Response{
        Status:  http.StatusOK,
        Time:    time.Now(),
        Message: "Json API Server by Golang on Docker",
    }

    json.NewEncoder(w).Encode(res)
}

func main() {
    http.HandleFunc("/", echo)
    http.ListenAndServe(":8888", nil)
}

今回の作業ディレクトリは ~/docker_msb/ とします。
~/docker_msb/main.go に上記のコードをコピーしてください。

Dockerfile を書く

Dockerfile は ~/docker_msb/dockerfile とします。

MultiStageBuild を使わない場合

# Golang 開発環境のイメージを使用
FROM golang:alpine
# 作業ディレクトリを /sample に指定(存在しない場合は勝手に作成される)
WORKDIR /sample
# main.go を作業ディレクトリにコピー
ADD main.go main.go
# コンテナの8888番ポートを開放
EXPOSE 8888
# コンテナが起動する際に実行されるコマンド # go run main.go 
CMD ["go", "run", "main.go"]

この Dockerfile を作成します。

$ docker build -t docker_s .

このコマンドでイメージを作成します。
今回は比較のためにイメージを作成しておきます。

MultiStageBuild を使う場合

# Golang の開発環境のイメージを build という名前をつけて使用
FROM golang:latest as build
# 作業ディレクトリを /build に指定(存在しない場合は勝手に作成される)
WORKDIR /build
# main.go を作業ディレクトリにコピー
ADD main.go main.go
# Go で main.go をビルド
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o webapp -ldflags "-s -w" main.go

# 空のイメージを使用
FROM scratch as release
# 上の build から /build/webapp を /webapp にコピー
COPY --from=build /build/webapp /webapp
# コンテナの8888番ポートを開放
EXPOSE 8888
# コンテナが起動する際に実行されるコマンド # /webapp
ENTRYPOINT ["/webapp"]

この Dockerfile を作成します。

$ docker build -t docker_m .

このコマンドでイメージを作成します。
今回は比較のためにイメージも作成しておきます。

比較

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
docker_s            latest              dbfcaa92a174        9 seconds ago       359MB # 使用しない場合
docker_m            latest              d2a160b3620f        49 seconds ago      5.57MB # 使用した場合

size を見ると一目瞭然ですね。
MultiStageBuild を使用しない場合のサンプルでは golang:alpine を使用しているため360MBですが、 golang:latest は800MBもあります。 MultiStageBuild を使用したほうがストレージの容量を圧迫せずに済みます。

まとめ

MultiStageBuild が使えるときは使おう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker-compose 停止 + イメージ削除 + ボリューム削除

docker-composeで起動

docker-compose up --build -d

http://docs.docker.jp/compose/reference/up.html

docker-composeで停止 + イメージ削除 + ボリューム削除

docker-compose down --rmi all -v

オプションの意味は公式サイト参照
http://docs.docker.jp/compose/reference/down.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

macOSでDockerを使ったGoのアプリケーション開発を爆速にするホットリローダを作った

はじめに

メリークリスマス!!

みなさんは Go のアプリケーション開発をどのような環境で行っていますか?
弊社ではゲームのアプリケーションサーバに Go を採用しており、開発は macOS で Docker for Mac を利用しています。開発当初はこの構成による不満は特に感じていませんでしたが、1年半ほど経ってプロジェクトの規模が大きくなったことで、無視できないレベルで開発スピードを低下させる要因となってしまいました。
弊社ではアプリケーション開発にソースコードの自動生成を多用しており、その影響もあってかコードベースの Go のコードは 150万行を超える規模になっています。 加えて、ビルドする際は cgo 経由で利用している C++ のコードもそれなりの量絡んでくることもあり、 Docker for Mac を使った Docker コンテナ上でのビルドに要する時間は、 メモリ8GB, 6CPUを割り当てたコンテナにも関わらず 5分を超える時間がかかっていました。 ( それでもまだ良い方で、他の方のマシンスペックでは 10分程かかる場合もあったようです )

実際はビルドキャッシュが効くので毎回 5 ~ 10 分かかるわけではありませんが、パッケージの依存関係によっては数珠つなぎ的に再ビルドが必要になってしまうケースもあるので、一文字編集したら 10 分待つという状況も起こり得ます。

このままではとても開発していられないということで、ビルドを爆速にするツールを開発してみました。
この記事では、開発したツールの紹介と、ちょっとトリッキーな実装をしているので、どうやって実現したかという話にも触れたいと思います。

ホットリローディング

ウェブアプリケーションなどを開発する際、ファイルの追加・更新・削除といったイベントを契機に自動で再ビルド・実行する仕組みを利用することが多いと思います。これらはホットリローディングやライブリローディングなどと呼ばれたりしますが、もちろん Go にも存在します。
有名どころだと https://github.com/gravityblast/freshhttps://github.com/oxequa/realize が挙げられますが、どちらも現在メンテされてはおりません...。

弊社では、上で挙げた https://github.com/gravityblast/fresh を利用していました。
また、ホットリローディングを Dockerコンテナ 上で動作するアプリケーションで行うため、以下のような設定を行っていました。

docker-compose.yml

version: '2'
services:
  app:
    image: golang:1.13.5
    container_name: app
    volumes:
      - '.:/go/src/app'
    working_dir: /go/src/app
    environment:
      GO111MODULE: "on"
    command: |
      go get -u github.com/pilu/fresh && fresh

つまり、 volumes でビルドに必要なソースコードが置かれているディレクトリをまるっとコンテナにマウントし、ホットリローダ ( fresh ) をコンテナ上にインストールしてファイル監視を始めます。

これによって、ローカル上のファイルを編集した場合でも、その変更がコンテナにも伝わり、
コンテナ上で動作しているホットリローダがそれを検知してアプリケーションを再ビルドし、無事ビルドできたら現在動いているアプリケーションと入れ替えます(リスタート)。

仕組み自体はとてもシンプルなものなので、再実装も難しくはありません。
ただ今回改善したいのは ビルド時間 なので、コンテナの上でビルドしているうちは改善できません。
そこで次のようなツールを開発しました

rebirth

rebirth という Go のための ホットリローダを開発しました。
既存のホットリローダと大きく異なるのは、 Docker コンテナ上でのビルドを避けるために
ホスト上でクロスコンパイルしつつDockerコンテナで動くアプリケーションをホットリロードできる 機能を持っている点です。
これによって、 Docker for Mac に依存せずにホストマシンの力を使い切ってビルドできるようになります。
( ホスト上でビルドするようにした結果 5分かかっていたビルド時間が 30 秒ほどに減り、目に見えて高速化しました )

どのように使うかというと

.
├── docker-compose.yml
├── main.go
└── rebirth.yml

このような構成のワークスペースがあったとして、
docker-compose.yml が以下のような内容だとします。 ( 先に挙げた docker-compose.yml 中の fresh の部分が tail -f /dev/null になっているだけです )

docker-compose.yml

version: '2'
services:
  app:
    image: golang:1.13.5
    container_name: app
    volumes:
      - '.:/go/src/app'
    working_dir: /go/src/app
    environment:
      GO111MODULE: "on"
    command: tail -f /dev/null

ここで docker-compose up -d とすると、 app という名前のコンテナが立ち上がると思います。
ここで、 rebirth.yml を記述します。

rebirth.yml

host:
  docker: app

host.docker にホットリロードしたいアプリケーションのあるコンテナの名前を書きます。

次に、以下を実行して rebirth という CLI をインストールします。

$ GO111MODULE=on go get -u github.com/goccy/rebirth/cmd/rebirth

これで準備完了です。 macOS上で rebirth を実行します

$ rebirth
# ホットリローダが立ち上がる。
# ファイルを編集すると、 app コンテナ上のアプリケーションがビルド後のものに入れ替わる

以上になります。

...ここで

!?

と思っていただけたら嬉しいのですが

コンテナ上に何もインストールしていないのに、 macOSにインストールしたバイナリのみでコンテナ上のアプリケーションのホットリロードを実現する というのがこのツールを作った時のこだわりポイントでした。これによって、Docker を使わない場合と使い方を変えることなく利用することができるようになっています ( rebirth.yml の書き方を変えるだけ )

続いて、これをどう実現しているかについて触れていきます

実装

流れが少し複雑なので、図を使って説明していきます。
はじめに、下の図を見てください。

一番外の大きい枠が macOS 上だということを表現しています。
その上にあるグレーの linux と書かれている部分は、
Docker for Mac を使って動作している linux コンテナを表しています。

破線で囲われている中は、 volumes でマウントされていることを表しています。
( つまり、 workspace が ~/work/app という状況で docker-compose.ymlvolumes.:/go/src/app と書かれている状態になります )

1. rebirth をインストールする

まず、 GO111MODULE=on go get -u github.com/goccy/rebirth/cmd/rebirthrebirth CLI をインストールします

2. rebirth 自身のクロスコンパイル

rebirth を実行した際にはじめに行うのは、ターゲットとなる Dockerコンテナのアーキテクチャ向けに自分自身をクロスコンパイルし、 __rebirth という名前で ~/work/app 直下の .rebirth ディレクトリ配下に置きます

コンパイル対象のアーキテクチャを知るため、 https://godoc.org/github.com/docker/docker/client を利用して docker remote API 経由で go env GOOSgo env GOARCH を実行しています。

3. コンテナ上にクロスコンパイルした rebirth バイナリを配置する

.rebirth があるディレクトリは ~/work/app 直下なので、コンテナ上にマウントされています。
このため、自動でコンテナ上の /go/src/app/.rebirth 配下にコンテナ上で実行可能な __rebirth バイナリが配置されます。

( このあたりは、マウントを利用せずともバイナリを直接コンテナ上にコピーすれば同じことができますが、大抵の場合はマウントを前提としても問題ないと思っているので、処理を簡単にするためにこのようにしています )

4. アプリケーションのファイルを監視し始める

図では main.go のファイルイベントを監視し始めることを表しています
( 実装には fsnotify を使っています )

5. アプリケーションコードをクロスコンパイルする

rebirth 自身をクロスコンパイルしたときと同じ要領で、コンテナのアーキテクチャ向けにクロスコンパイルします。
少し違うのは、アプリケーションコードが cgo を利用したものであったとしてもコンパイルできるようにしなければらない点です。

このため GOOSGOARCH の指定に加え、 CGO_ENABLED=1 を有効にします。
さらに、 C/C++ コードを macOS 上で linux 向けにコンパイルできるよう、クロスコンパイラを作らなければいけません。 https://github.com/FiloSottile/homebrew-musl-cross にあるように

$ brew install FiloSottile/musl-cross/musl-cross

でインストールをお願いします。 ( ビルドに30分程度かかります )

( 参考 : https://qiita.com/keijidosha/items/5f4a68a3341a44a25ab9 )

また、今までコンテナ上のパスとして設定されていた GOPATH が、ホスト上で指定されたものに変わるため、
振る舞いを揃えるために、ワークスペースに作った .rebirth ディレクトリを GOPATH の起点として扱い、 go.mod のモジュール名を見ながら、アプリケーションコードを .rebirth 配下に配置し直してビルドを行っています。

例えば、 go.modmodule github.com/company/webapp と書かれていたとすると、

  1. .rebirth/src/github.com/company というディレクトリを掘る
  2. ワークスペース ( webapp ディレクトリ ) への symlink を 1 で作ったディレクトリの配下に作る
  3. .rebirth/src/github.com/company/webapp へ移動する
  4. GOPATH=/path/to/.rebirth go build ... のように GOPATH を変更してビルドする

というようなことを行います。これによって、依存モジュールなどをアプリケーションワイドにインストールすることが可能なので、
ローカルの GOPATH と混ざったりすることはなくなります。

上記をまとめると、以下のような環境変数やオプションをつけて go build を実行しています

$ GOPATH=.rebirth GOOS=linux GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-linux-musl-cc CXX=x86_64-linux-musl-c++ go build --ldflags '-linkmode external -extldflags "-static"'

( ※ 実際には クロスコンパイラへのパスを通すために PATH に追加したり、 GOPATH も絶対パスで表現したりしています )

6. コンテナ上にクロスコンパイルしたアプリケーションバイナリを配置する

ビルドした結果は、 .rebirth 配下に program という名前で配置しています。
__rebirth のときと同様に、マウント先のコンテナ上に自動的に配置されます。

7. __rebirth の実行

コンテナ上で __rebirth バイナリを実行します。このとき、動作しているのが Dockerコンテナ上であり、
かつ rebirth.ymlhost.docker の指定がある場合には、ファイル監視を行わない専用のモードで起動します。
起動時に、自身の PID を記録したファイルを .rebirth 配下に書き出します。

( 自身がDockerコンテナ上で起動しているかどうかは、 /.dockerenv が存在するかで判定することができます )

8. コンテナ上でアプリケーションを起動する

__rebirth から .rebirth/program を実行します

9. ファイルの変更

main.go をホスト上で編集します

10. ファイルの変更を検知

ホスト上の rebirth プロセスが main.go の更新を検知します

11. アプリケーションの再ビルド

5 で説明したことをもう一度行います

12. アプリケーションの配置

6 で説明したことをもう一度行います

13. アプリケーションの再起動要請

ホスト上の rebirth から コンテナ上の __rebirth へアプリケーションの再起動要請を行います。
実装には、事前に書き出しておいた __rebirth プロセスの PID をもとに、 SIGHUP を送ることで実現しています。

本当は PID をファイルを経由せずに取得したかったのですが、 docker remote API を経由して知ろうとすると、ホスト上のPID名前空間で表現された値しかとれないため実現できませんでした ( コンテナ上で ps したときとは別の PID が返ってくる )

14. アプリケーションの再起動

SIGHUP を受け取った __rebirth プロセスが、起動中のプロセスを停止して新しく配置された program を実行すれば再起動の完了です

おまけ

cgo を利用しているコードから、他の C ライブラリを参照している場合

例えば弊社では、 cgo で記述されたコードから、 zlib を利用していました。
こういった場合は、別途 libz.a をクロスコンパイルする必要があります。

ビルドした libz.azlib のヘッダファイルを参照可能な場所に移して ( たとえば ワークスペース配下 )
rebirth.yml に以下のように書けばそれを用いてシンボル解決してくれるようになります

host:
  docker: app
build:
  env:
    CGO_LDFLAGS: ./lib/libz.a # lib に置いたクロスコンパイル済みの libz.a を参照する
    CGO_CXXFLAGS: -I./include # include に置いた `zlib.h` などを参照する

( 相対パスは、適宜ツール内部で絶対パスに置き換えて参照します )

ホットリロード以外の機能

ホットリロードをクロスコンパイルで行うようになると、今までコンテナ上で行っていたテストやスクリプトの実行などなどを同じ手段で行いたくなると思います。

そこで rebirth では go build , go test, go run をクロスコンパイルしつつ実行してくれるコマンドを用意しています。それぞれ rebirth build , rebirth test , rebirth run をホスト上で実行していただければ、クロスコンパイルしつつ、必要であればその結果をコンテナ上で走らせてくれます。ぜひご活用ください

おわりに

macOS 上で Docker を利用しているときのビルドを高速化したい!というニーズから実装したツールですが、
普通にホットリローダとして使う場合においても fresh より使いやすくなっていると思いますので、ぜひお試しいただければ幸いです。

引き続き改善しながら弊社で使っていこうと思っていますので、
なにか要望やバグを見つけた場合も気軽に報告いただければと思います。

それではよいクリスマスをお過ごしください!!

https://github.com/goccy/rebirth

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactとNginxでリロードしても404しないSPAを作る

はじめに

いなたつアドカレの二十四日目の記事です。
Dockerを使ってReactで作ったアプリケーションをnginx上にポイ投げするためのあれですね。

DockerってなにとかnginxってなにReactってなにってのはスルーで行きます。
今回の記事はcreate-react-app(以下CRA)を使用する前提となっています。

つかうもの

  • react
  • react-router
  • nginx
  • docker

完成系

react-router使ってnginx上でリンクに#がつかないかつリロードしても404にならない構成の作成

ディレクトリ構成

  • app
    • docker
      • react
        • Dockerfile
      • nginx
        • default.conf
    • web
    • docker-compose.yml

こんなかんじです

dockerディレクトリにdockerで使用するファイルを格納しています。
webディレクトリにCRAで作成したアプリが入りますね。

DockerでReact

docker-compose.yml
version: '3'

services:
  react_app:
    container_name: react_app
    build: ./docker/react
    command: npm start
    volumes:
      - ./web:/app
    ports:
      - 3000:3000

つづいて
docker/react/Dockerfile

FROM node

WORKDIR /app

ここは基本的になんでも構いません(めんどくさかった)
作成するアプリケーションに合わせてpackage.jsonなどを用意してあげてください。
今回は めんどくさいので これでいきます。

Dockerで んぎっくす

はい、Nginxいきます

docker-compose.yml
nginx:
    image: nginx
    container_name: nginx
    ports:
      - 8080:80
    volumes:
      - ./web/build:/var/www
      - ./docker/nginx/:/etc/nginx/conf.d/
    depends_on:
      - react_app

web(CRAのディレクトリ)の中のbuildをnginxコンテナにマウントしています。
docker/nginxには設定ファイルですね。

default.conf
server {
    listen       80;

    location / {
        root   /var/www;
        index  index.html index.htm;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

buildをマウントしてそのマウントしたファイルのindex.htmlを表示するぜ〜って感じですね。

Reactのコンポーネント構成

App.js
import React from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom'
import Hello from './Pages/Hello'
import Changed from './Pages/Changed'




function App() {
  return (
    <Router>
        <Route exact path='/' component={Hello} />
        <Route exact path='/changed' component={Changed} />
    </Router>

  );
}


export default App;

react-routerで2つのページを遷移できるようにします。

/Pages/Hello,js
import React  from 'react';
import {Link} from 'react-router-dom'

const Hello = () => {

    return (
        <div>
            <div>Hello</div>
            <Link to='/changed'>ぺーじせんい</Link>
        </div>
    )
}

export default Hello;
/Pages/Changed,js
import React from 'react';

const Changed = () => {

    return (
        <div>
            <div>Changed</div>
        </div>
    )
}
export default Changed;

ページ遷移するだけですね。

うごかしてみる

とりあえずreactのプロジェクトをbuildしましょう。

localhost:8080にアクセスします。

スクリーンショット 2019-12-24 14.05.23.png

こんな感じですね。

ページ遷移してみます。

スクリーンショット 2019-12-24 14.05.29.png

遷移できましたね。おっけーです。

ちょっとリロードしたくなってきたわ

スクリーンショット 2019-12-24 14.05.34.png

あっあっあっ

だめですね。んぎっくすさんに怒られちゃいました。

ハッシュルーターとかダサいじゃん?

URLに「#」とかつくHashルーターをつかうことでこれを解決することはできます。

localhost:8080/#/changedだっっっっっっっっっさあああああい

やだよ。

解決しよう

nginxの設定を少し変えましょう
```default.conf
server {
listen 80;

location / {
    root   /var/www;
    index  index.html index.htm;
    try_files $uri /index.html;
}
error_page   500 502 503 504  /50x.html;
location = /50x.html {
    root   /usr/share/nginx/html;
}

}
```

try_files $uri /index.html;この一行を書くだけですね。

ほら、リロードしてみてくださいよ、怒られますか?怒られませんよね。

これで解決ですね。

なぜ解決できたのか

とりま解決でけたらえーねんって人はみなくてよきですえ

nginxにはtry_files ディレクティブというものが存在し、これは、引数を前から順番にファイルが存在するかをtryしまくって行ってくれます。そして見つからなかった場合はindex.htmlを返却しましょうねっていう感じです。

何も見つからなかったら404を返すよ〜って実装をする場合もありますね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel + PassportでAPIを作成する

Making an API with Laravel + Passport

以前の投稿を読んでいる場合は、Laravelプロジェクトのセットアップ方法を既に知っているので、それから続けて、パスポートをインストールしてJWT APIを作成します

If you have followed my previous posts, you already know how to setup a Laravel project, continuing from that, we will install passport to create an JWT Api

必要条件 / Requisites:

Laravel Project

Docker

Laravel/Passport

Laravel-Shovel

laravelプロジェクトフォルダー内に、一時的なdockerコンテナーを作成して「composer」を使用し、パスポートをインストールします

Inside the laravel project folder, we create a temporary docker container to use composerand install passport

docker run --rm -v $(pwd):/app composer require laravel/passport

この命令にはしばらく時間がかかります...完了したら、 docker-composeを開始できます

This instruction will take a while ...once done, we can start the docker-compose

sudo docker-compose up --build

実行されると、「http:// localhost /」に移動して確認できます

Once its running, we can verify by going to http://localhost/

新しい移行を実行し、パスポートをインストールします

We run the new migrations and Install passport

sudo docker-compose exec app-server php artisan migrate
sudo docker-compose exec app-server php artisan passport:install

これで、お気に入りのエディター(私の場合はPHPStorm)を使用してプロジェクトを開き、 Userモデルを編集してHasApiTokens特性を追加できます。

We can now open the project using our favorite editor, in my case PHPStorm, and edit the User model to add the HasApiTokens trait.

<?php

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

次に、 passport:routesAuthServiceProviderに追加します

Next we add the passport:routes to the AuthServiceProvider

<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Laravel\Passport\Passport;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        // 'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();
        Passport::routes();
        //
    }
}

config \ auth.phpでApi認証プロバイダーをパスポートに変更します

We change the Api Auth provider to passport in the config\auth.php

'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
            'hash' => false,
        ],
    ],

次に、 AuthControllerを作成します

Now we create our AuthController

docker-compose exec app-server php artisan make:controller API\\AuthController

この新しいコントローラーはdockerによって作成されたため、ファイルの書き込み許可を変更する必要がある場合があります

Since this new controller was made by docker, you might need to change the write permission of the file

sudo chown -R myUser:myUser mylaravelproject/

エディターで新しいAuthControllerを開くことができます。関数については、itsolutionstuff.com

ただし、機能に若干の変更が加えられています

Now we can open the new AuthController in our editor, as for the functions, I took the example from the itsolutionstuff.com

But with some slight changes in the functions

<?php

namespace App\Http\Controllers\API;

use App\Http\Controllers\Controller;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;

class AuthController extends Controller
{
    /**
     * Register api
     *
     * @return \Illuminate\Http\Response
     */
    public function register(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required',
            'email' => 'required|email',
            'password' => 'required|confirmed',
            'password_confirmation' => 'required|same:password',
        ]);

        if($validator->fails()){
            return response()->json(["error"=>$validator->errors()],422);
        }

        $input = $request->all();
        $input['password'] = bcrypt($input['password']);
        $user = User::create($input);
        $success['token'] =  $user->createToken('MyApp')->accessToken;
        $success['name'] =  $user->name;

        return response()->json($success);
    }

    /**
     * Login api
     *
     * @return \Illuminate\Http\Response
     */
    public function login(Request $request)
    {
        if(Auth::attempt(['email' => $request->email, 'password' => $request->password])){
            $user = Auth::user();
            $success['token'] =  $user->createToken('MyApp')-> accessToken;
            $success['name'] =  $user->name;

            return  response()->json([$success]);
        }
        else{
            return response()->json(["error"=>"Unauthorized"],422);
        }
    }
}

routes \ api.php内にルートを追加します

Add the routes inside routes\api.php

Route::post('register', 'API\AuthController@register');
Route::post('login', 'API\AuthController@login');

お気に入りのRESTクライアントを使用して、登録とログインをテストできます。「不眠症」を使用しています

You can test the register and login, using your favorite REST client, I'm using insomnia

http://localhost/api/register
{
    "name":"My Name",
    "email":"secremeail@email.com",
    "password":"securepassword",
    "password_confirmation":"securepassword"
}
http://localhost/api/login
{
    "email":"secremeail@email.com",
    "password":"securepassword"
}

これで、基本的なAPIを実装しましたが、より良い応答をするために、以下を使用します。

With this, we have implemented our basic API, but to make a better response, we use:

laravel-shovel

より良いAPI応答を行うためのライブラリです。

Is a library to make better API responses.

インストールする前に、 docker-composeを停止し、一時的なcomposerコンテナを実行します。

Before installing it, we stop docker-compose and run a temporary composer container.

docker run --rm -v $(pwd):/app composer require stephenlake/laravel-shovel

ミドルウェア内のルートをグループ化します

We group the routes inside the middleware

Route::group(['middleware' => ['ApiRequest',"ApiResponse"]],function (){
    Route::post('register', 'API\AuthController@register');
    Route::post('login', 'API\AuthController@login');
});

すべてのAuthController応答を通常の応答に変更する必要があります

We have to change all the AuthController responses to normal resposes

<?php

namespace App\Http\Controllers\API;

use App\Http\Controllers\Controller;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;

class AuthController extends Controller
{

    public function register(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required',
            'email' => 'required|email',
            'password' => 'required|confirmed',
            'password_confirmation' => 'required|same:password',
        ]);

        if($validator->fails()){
           return response()->json(["messages"=>$validator->errors()],422)
               ->withMeta("message","Validation error");
        }

        $input = $request->all();
        $input['password'] = bcrypt($input['password']);
        $user = User::create($input);
        $success['token'] =  $user->createToken('MyApp')->accessToken;
        $success['name'] =  $user->name;

        return response($success);

    }


    public function login(Request $request)
    {
        $credentials = [
            'email' => $request->email,
            'password' => $request->password
        ];

        if (auth()->attempt($credentials)) {
            $token = auth()->user()->createToken('MyApp')->accessToken;
            return response(["token"=>$token]);
        } else {
            throw new \Exception("Invalid Credentials",422);
        }
    }
}

また、「app / Exceptions / handler.php」を処理する例外にjson応答を追加します

Also add a json response in the expection handled app/Exepctions/handler.php

public function render($request, Exception $exception)
    {
        if($request->acceptsJson())
        {
            return response()
                ->json(["messages"=>$exception->getMessage()],500)
                ->withMeta("message","internal server error");
        }
        return parent::render($request, $exception);
    }

このライブラリは、メタヘッダーを追加するすべてのAPI応答を標準化します

This library will standarize all api responses adding meta headers

{
  "meta": {
    "code": 200,
    "status": "success",
    "message": "OK"
  },
  "data": {
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiMTMzYzU3NjVlZmJjYTdmODQ0NDdlMTE4ZWUyZDc1YWI5YzY0MmQ3NTE2MjIzNWM1Y2FjNDNlNjI5ZDIyMzU1MzMzMzY1M2U2Yjc2ZTJhNzIiLCJpYXQiOjE1NzcxNTQwMzcsIm5iZiI6MTU3NzE1NDAzNywiZXhwIjoxNjA4Nzc2NDM3LCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.KIoArr6X69eRLDluWWgEOS5gAlLmLxtYKhMURgzsmgmLooVV6EJDHGx11gnQ7_WmagtbredLabHXeSks6DQ2A8tGeqFyrVxnCRddAySxHDxhzF0VF2wF9rn_1OduDcC3xOVdrXPj-VkxToHLyW3e6A714XSTxHgzynEKBh2JtDIRN3lCt13_1F8iD9ocGHPLBrW-XFhV4Iw2atSyL8N5qQH29wsopwWZCoTqqAN2whgfylyCTlXFAcQWe0AOJEzc39jpTudkiSXAKKFuS1hCjLYYdiuae-NJGOTutDD3CzFjYrO-Kvq3-QBX7go4uNftOVwuARsvlBuyCfPbpzCM8FVfuZCEHMv7YODCrCs2s305PvsGAsIIcKB_9_dpx0nO-lZy9_Hsn6HKAztCkLBNQponLAM10pah36xaq5c_mOwRMIltRArfDi-QuteIP4XUYXJXDV96bw2BQGNlybbOU0z7x7ocLmlP7xh4NBZFjUs5eM02U6eJykewxr8UpIkoyi6N3-ZxKKhdIWfeW6jRFe7IHlKQ2QTR1Hp33zGPlwTIW8dTe8UYo_FXdQojsFV7uxc9GUUDFimrJT3cem6JvcUEWsQtbxRv3tyyMST4P5gZpr-bANS2z6DiseuQRjHU9ZNYX9rm62GS8Wo_sNXxbfayTrFk6mBsuNn33kY1T8o"
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CesiumでSRTMの地形データを読み込ませてみる

概要

SENSYN Robotics(センシンロボティクス)の深見です。
主にWebアプリを中心に開発担当しており、最近はCesiumを利用したWebGL系の開発に関わっております。
本日は、その過程で得た、地形データをCesium上に反映させる方法につきまして紹介いたします。
Cesiumについてはこちら

Cesiumに反映する地形データの取得

  • Cesium上で地形データを反映させるためにはいくつか手段はありますが、ここではCesium World Terrain と同じくquantized meshで配信する事例とします。
  • 今回利用する地形データはNasa SRTM(height map)を利用します。
    • 地形データをダウンロード時には認証が求めらるので、こちらでログインIDを取得する必要があります。
    • また、データ量が多いため、今回の対象は関東エリア周辺(N35E139.hgt)のみといたします。

Cesium Terrain Builder

地形データの作成

  • cesium-terrain-builderを利用するため、docker imageを取得します。
$docker pull tumgis/ctb-quantized-mesh
  • 事前にダウンロードしたheight map(N35E139.hgt)を配置します。
$ mkdir ~/terrain
$ mv ~/Downloads/N35E139.hgt ~/terrain
  • dockerコンテナを起動します。
$ docker run -it --name ctb -v ~/terrain/:/data  tumgis/ctb-quantized-mesh
  • gdalbuilderで仮想的なレイヤを定義するXMLファイル(vrt)を作成します。また、Cesium上では地形データをquantized meshで展開するため、ファイルを作成します。
$ gdalbuildvrt tiles.vrt *.hgt
$ ctb-tile -f Mesh -C -N -o terrain tiles.vrt
  • Cesiumが地形データを認識するための設定ファイルとして、layer.jsonを作成します。
$ ctb-tile -f Mesh -C -N -o -l terrain tiles.vrt
  • 地形データとlayer.jsonが作成されているか下記コマンドで確認します。
$ tree -v -C -L 1 ~/terrain
terrain/
|-- 0
|-- 1
|-- 2
|-- 3
|-- 4
|-- 5
|-- 6
|-- 7
|-- 8
|-- 9
|-- 10
|-- 11
|-- 12
`-- layer.json

以上で地形データの作成は完了です。

地形データの配信

  • webアクセスできる環境を用意します。配信する際にresponseヘッダーの確認が必要です。配信するサーバの環境に合わせ確認してください。 image.png
    • Access-Control-Allow-Origin
      • corsで許可して配信する場合には必要、上記の例は特別な理由がないのでAccess-Control-Allow-Origin: *としています。
    • Content-Encording
      • ctb-tileのコマンドで作成された地形データはgzip形式で圧縮されているため、配信する場合はContent-Encording: gzipを指定します。
    • Content-Type
      • application/octet-streamを指定します。cesium-terrain-builderで作成されたファイルが.terrainの拡張子ファイルであり、一致するMIMEtypeが存在しないので、Content-Type: application/octet-streamとします。
  • 今回はpythonのSimpleHTTPServerを利用してweb配信します。
$ python SimpleHTTPServer.py
Serving HTTP on 0.0.0.0 port 8000 ...

Cesiumに地形データを読み込ませる

  • CesiumのterrainProvidorにurlを設定します。
  const viewer = new Cesium.Viewer('cesium', {
    terrainProvider : new Cesium.CesiumTerrainProvider({
      url : 'http://localhost:8000/terrain'
    }),
  });

ブラウザで確認

  • chromeで地データが反映されているか確認します。丹沢のあたりですが、地形データは上手く読み込めてそうです。

    image.png

  • 丹沢付近から御殿場方面にかけて確認してみると、地形データが関東エリア周辺に絞ったため、未反映の部分が目立ちます。
    image.png

以上になります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

code-server オンライン環境篇 (5) Docker 上で、code-server を立ち上げる

これは、2019年 code-server に Advent Calender の 第15日目の記事です。

前回に続き、EC2 Instance を 立ち上げたいと思います。

目次
ローカル環境篇 1日目
オンライン環境篇 1日目 作業環境を整備する
オンライン環境篇 2日目 仮想ネットワークを作成する
オンライン環境篇 3日目 Boto3 で EC2 インスタンスを立ち上げる
オンライン環境篇 4日目 Code-Serverをクラウドで動かしてみる
オンライン環境篇 5日目 Docker 上で、code-server を立ち上げる
オンライン篇 6日目 自動化してみよう
オンライン篇 7日目 簡単な起動アプリを作成してみよう(オンライン上に)
...
オンライン篇 .. Coomposeファイルで構築
オンライン篇 .. K8Sを試してみる
...
魔改造篇

はじめに

前回までで、boto3 x python で EC2 Instance を 立ち上げました。
そして、Code-Server を動かしました。

今回は、Docker を利用して、Code-Serverを立ち上げてみましょう。

EC2 Instance を作る

前回のつづきから

$ git clone https://github.com/kyorohiro/advent-2019-code-server.git
$ cd advent-2019-code-server/remote_cs04/
$ docker-compose build
$ docker-compose up -d

ブラウザで、http://127.0.0.1:8443/ を開く。

Screen Shot 2019-12-24 at 0.39.23.png

Terminal 上で

Terminal
$ pip install -r requirements.txt
$ aws configure 
..
..

EC2Instance を 作成

$ python main.py --create

EC2 情報を取得

$ python main.py --get
>>>> i-0d1e7775a07bbb326
>>>> 
>>>> 3.112.18.33
>>>> ip-10-1-0-228.ap-northeast-1.compute.internal
>>>> 10.1.0.228
>>>> {'Code': 16, 'Name': 'running'}

SSHで中に入る

$ chmod 600 advent-code-server.pem
$ ssh -i advent-code-server.pem ubuntu@3.112.18.33

Docker を install

Docker 環境を作成していきます

EC2上で

$ sudo apt-get update
$ sudo apt-get install -y docker.io

Docker の Hello World

$ sudo docker run hello-world
atest: Pulling from library/hello-world
1b930d010525: Pull complete 
Digest: sha256:4fe721ccc2e8dc7362278a29dc660d833570ec2682f4e4194f4ee23e415e1064
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

Code-Server を 起動してみよう

$ mkdir -p  ${HOME}/.local/share/code-server/extensions
$ sudo docker run -it -p 0.0.0.0:8080:8080 -p0.0.0.0:8443:8443  codercom/code-server:v2 --cert
info  Server listening on https://0.0.0.0:8080
info    - Password is 86821ed9f02ef11d83e980da
info      - To use your own password, set the PASSWORD environment variable
info      - To disable use `--auth none`
info    - Using generated certificate and key for HTTPS

Screen Shot 2019-12-24 at 1.11.08.png

Screen Shot 2019-12-24 at 1.06.50.png

Screen Shot 2019-12-24 at 1.12.23.png

できました!!

削除しよう

# ec2 instance から logout
$ exit

# local の code-server 上で
$ python main.py --delete

なんども使い回したいならば、 ec2 instance を停止するようにしてください

※ 次回か次次回

次回

手動で行っていた作業を、自動化してあげましょう!!

コード

https://github.com/kyorohiro/advent-2019-code-server/tree/master/remote_cs03

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む