20200113のdockerに関する記事は20件です。

GKEのコンテナネイティブ負荷分散が想像以上に効果的だった話

背景

GKEのオートスケールは、便利ですがいくつか不満もありました。例えば、ノードによってトラフィックの割合が均一化されないと言う問題です。オートスケーラによってPodが増えた場合、新しいPodへのトラフィックはすぐに均一化される訳ではなく、段階的に上昇していきます。これはk8sのデフォルトでの負荷分散の仕組みに起因するものだと思うのですが、新しく作成されたPodが直ちに有効活用されません。この影響で、急なスパイクには対応しにくいですし、安全に運用するためにはオートスケールの条件を緩める必要がありました。

以下のチャートは、Podごとに負荷の推移を観察したものです。新しく作成されたPodの負荷が、既存のPodと同じ水準の負荷に増えるまで、2~3時間かかってるのがわかると思います。

スクリーンショット 2020-01-12 12.28.56.png

コンテナネイティブ負荷分散を使ってみる

去年の10月、GKEでコンテナネイティブ負荷分散が正式リリースされました。
https://cloud.google.com/kubernetes-engine/docs/how-to/container-native-load-balancing?hl=ja

既存の負荷分散だと、LB -> インスタンスグループ -> ノード -> Pod と言う流れでトラフィックが送られます。
コンテナネイティブ負荷分散だと、LB -> Network Endpoint Group -> Pod と言う流れになり、ノードを介さずに分散されます(Network Endpoint Groupに関しての詳細はドキュメントを見ていただけると)。

コンテナネイティブ負荷分散を既存のサービスで有効にする

留意点はいくつかありますが、コンテナネイティブ負荷分散を使用するために行うことは非常に簡単です。serviceリソースのmetadataに以下のannotationを追記するだけです。

apiVersion: v1
kind: Service
metadata:
  name: service名
  annotations:
    cloud.google.com/neg: '{"ingress": true}' # これを追記する

反映されるまでに4~5分かかります。稼働中のサービスで行う場合、一時的にトラフィックが途切れるので注意してください。

目に見えてトラフィックが均一化された!

コンテナネイティブ負荷分散を有効にした結果、Podへのトラフィックが均一化され、Podごとの負荷も均一になりました。

以下のチャートは、コンテナネイティブ負荷分散を有効にする前と後で、同じ時間帯(4時〜12時)におけるPodごとの負荷を観察したものです。

Before

スクリーンショット 2020-01-12 12.44.11.png

After

スクリーンショット 2020-01-12 12.43.21.png

有効にする前と比較すると、明らかにPodごとの負荷が均一になっていますね。またそれだけではなく、オートスケーラーによって新しく作成されたPodの負荷も、直ちに既存のPodと同じ水準まで増加しています。

まとめ

と言うわけで、コンテナネイティブ負荷分散をGKEで使ってみたわけですが・・・これはGKEでオートスケールを利用する場合はほぼ必須と言ってもいいくらい効果があると感じました。実際にこれを利用することで、安定運用に必要なノードの台数を削減することが出来ました。

今回扱ったコンテナネイティブ負荷分散はGCPのネットワークエンドポイントを利用するものですが、k8s側で同様の機能が提供されればAWS等他のプロバイダでも使えるので、今後はその辺にも期待したいです。

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

Dockerでnginx等を使う時はまずLinuxの知識をだな......(戒め)

DockerとかVagrantでnginx等を使う時はまずLinuxの知識をだな......(戒め)

1. 背景

フロント学んできて、そろそろバックエンドもまなんでみようと思い、せっかくなので流行りのDokcerを使用しPHPでサーバーに画像をアップロードするコードを書いた。
そしてハマった

2. 環境

・windows10<VirtualBox(vagrant)<CentOS7<Docker

起動構成

docker-compose.yml
version: '3'
services:
  web:
#ver 1.17.7
    build: ./nginx
    ports:
      - '8080:80'
    links:
      - php-fpm
    volumes:
      - ./data/public:/var/www/html/public
    depends_on:
      - php-fpm
  php-fpm:
#ver 7.3
    build: ./php-fpm
    links:
      - db
    volumes:
      - ./data:/var/www/html

3. ハマったところ

このPHPの一行からなるエラーで無限に悩んだ

upload.php
move_uploaded_file($_FILES['image']['tmp_name'], $savePath);

SnapCrab_NoName_2020-1-13_17-51-41_No-00.jpg

Permisson denied ......

4.1 まずやったこと

とりあえず浅いLinuxの知識を用いて画像保存フォルダの権限を
$sudo chmod 777 images
としてコードが動くのを確認。
もちろんこんなセキュリティガバガバな権限は例えローカルサーバーでも許せなかったので却下

4.2 解決策

phpinfoでユーザーを確認してみる。
SnapCrab_NoName_2020-1-13_18-30-53_No-00.jpg

www-dataがUSERだと判明。
じゃあwww-dataに権限付与してあげればよさそう

vagrant環境下の権限はこう

$ls -l
~
drwxrwxr-x. 2 vagrant vagrant 4096 Jan 13 08:32 images
~
$ sudo chown www-data images
chown: invalid user: ‘www-data’

有効ではないと告げられたのでユーザー一覧を確認してみます

$cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
~
vagrant:x:1000:1000:vagrant:/home/vagrant:/bin/bash
apache:x:48:48:Apache:/usr/share/httpd:/sbin/nologin

これ以外にも色々出てきたけどwww-dataユーザーなんて存在しない。

どうすればいいのか

コンテナを立ち上げて
docker exec -it [id] bash
でphp-fpmコンテナに入ってみる。
その中のファイル権限は

drwxrwxr-x. 2 1000 1000 4096 Jan 13 08:32 images

1000 1000

調べてみたらvagrantユーザーのuidが1000らしい。

これも$cat /etc/passwdでユーザー一覧を確認すると

vagrant:x:1000:1000:vagrant:/home/vagrant:/bin/bash

vagrntのuidに1000番が付けられてるのが分かる

VMという仮想環境の中にさらに仮想コンテナがあってそのコンテナ内では1000番uidに名前がついていないという事らしい
つまりコンテナ内では1000番にvagrntユーザーも含めユーザーは存在してないという事
でもuidは共通みたいだ

ならばコンテナ内から確認すればwww-dataがいるはず......

www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin

いた

結局どうすればいいのか

正解なのかは分からないけど思いついたのはコンテナ外で33番UIDでグループがvagrantのユーザーを作ってimagesフォルダの持ち主を33番に変える。
これが自分の中でしっくりきました。
というかこれ以外成功してない

$useradd -u 33 www-data -g vagrant
$chown www-data images

他にも
https://gtrt7.com/blog/nginx/docker_userid_share#3docker-composeyml
のようにしてvagrant側からではなくdocker側から合わせればいけるらしい。

自分はこんなエラー出てわからんくて投げてしまいましたが

web_1      | 2020/01/13 13:52:54 [emerg] 1#1: host not found in upstream "php-fpm" in /etc/nginx/conf.d/default.conf:20
web_1      | nginx: [emerg] host not found in upstream "php-fpm" in /etc/nginx/conf.d/default.conf:20

default.conf:20fastcgi_pass php-fpm:9000;

正直分からん

Dockerコンテナ内でファイル関係は完結させた方がいいんだろうなと感じてはいるもののいいやり方が浮かばず今回は断念。

ちょっと深堀するにはLinuxやnginx php-fpm等の知識が足りないので今はphpでファイルアップロードしたフォルダを作るさいは適宜権限変更するこのやり方のまま進んで、強くなったら戻ってこようと思いました。

いいやりかたあったら教えてほしい......

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

Ruby on Rails 「途中まで作ったアプリにDockerを導入したい」に挑戦してみる(MySQL / Sequel Pro)

はじめに

Dockerの公式Tutorialでは、DB:PostgreSQLで解説されている為、他のDB(例:MySQL)でDockerを導入してみたい初学者にとってはややハードルが高い状況です。

本記事では、途中まで作ったRailsアプリ(DB:MySQL)にDockerを導入する方法を解説します。

Dockerを途中から導入するアプリの例としては、scaffoldで作成した
簡単な蔵書管理アプリ(bookapp)を使用します。
(適宜、ご自身で開発中のアプリに置き換えて頂ければと思います)

DBはMySQL5.7で実装を進めます。

併せてDocker環境でもDB内容を視覚的に確認できるように
SequelProとの接続方法も解説します。

前提

目指すゴール

(1) 既存のRailsアプリにMySQLを使ったDocker環境を構築する

(2) Sequelproと接続し、Docker環境でもGUIでDB内容を確認できるようにする

実装の流れ

1 Dockerfile/docker-compose.ymlを作成

2 Dockerfileを編集

3 docker-compose.ymlを編集

4 database.yml編集

5 コンテナをbuildする docker-compose build

6 Dockerコンテナ上でDB作成&migrationを実行
 docker-compose run web bundle exec rake db:create / db:migrate

7 コンテナを起動する docker-compose up

8 DB接続設定を環境変数に置き換え - env_file

9 Docker環境のRailsアプリとSequelProを接続する

1 Dockerfile/docker-compose.ymlを作成

まずは既存アプリに必要なファイルを追加します。
具体的には(1)Dockerfile (2) docker-compose.ymlの2ファイルです。

アプリのルートディレクトリに以下のように2つのファイルを追加してください。
(ファイルの作成場所に注意してください)

bookapp ----|-- app
            |-- bin
            |-- config
            |-- db
            ・・・・・・
       ・・・・・・
            |-- Gemfile
            |-- Gemfile.lock
            |-- package.json
            |-- Rakefile
            |-- README.md
            |-- Dockerfile  #ここに追加
            |-- docker-compose.yml #ここに追加          

[解説]
Gemfile/Gemfile.lockについて
・アプリ開発当初からDockerを導入する記事では、ここでGemfile(railsのみ導入)/Gemfile.lock(空でOK)を作成するように記載されていますが、既に開発中のアプリで作成済かと思いますのでGemfileについては特に何もしなくてOKです。

2 Dockerfileを編集

次にDockerfileを以下のように編集します。

Dockerfile
FROM ruby:2.5

RUN apt-get update -qq && \
    apt-get install -y build-essential \ 
                       libpq-dev \        
                       nodejs      

RUN mkdir /app_name 

ENV APP_ROOT /app_name 
WORKDIR $APP_ROOT

ADD ./Gemfile $APP_ROOT/Gemfile
ADD ./Gemfile.lock $APP_ROOT/Gemfile.lock

RUN gem install bundler 
RUN bundle install
ADD . $APP_ROOT

[解説]
RUN gem install bundlerについて
Railsアプリに途中からDockerを導入する場合、Gemfileが既にある関係で上記のコマンド無しで進めようとするとエラーが出るかもしれません。(自分の環境ではBuildした時に以下のようなエラーが出ました)

ERROR: Service 'web' failed to build: The command '/bin/sh -c bundle install' returned a non-zero code: 20

対策として、DockerfileをBuildする際にbundlerを導入しておきます。

3 docker-compose.ymlを編集

続いて docker-compose.ymlを以下のように編集します。

docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: 'password'
    ports:
      - "4306:3306"

  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/app_name
    ports:
      - "3000:3000"
    depends_on:
      - db

[解説]
1 db:portsの設定について- "4306:3306"
DockerコンテナとSequelpro接続の為に必要な設定です。(詳細は後ほど)
とりあえず現段階では、上記の設定にしておいてください。

2 rm -f tmp/pids/server.pidについて
開発途中に何度もdockerコンテナの起動&停止を繰り返していると時々「A server is already running.」のエラーが出ます。(pidファイルが残っていることが原因)
上記コマンドでserver.pidを逐一削除することで未然にエラーを防ぎます。

関連記事
docker-compose upしたときに「A server is already running.」って言われないようにする

4 database.yml編集

次にconfig/database.ymlの設定をdocker-compose.ymlの内容に合わせます。
具体的には(1) password (2)host:dbの2つを追記してください。

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password
  socket: /tmp/mysql.sock
  host: db  

development:
  <<: *default
  database: bookapp_development

5 コンテナをbuildする docker-compose build

ここまで来たら、次はDocker環境のコンテナを作っていきます。
ターミナルで以下のコマンドを実行してください。
(初めて実行するときは少し時間がかかります)

[bookapp] $ docker-compose build 

6 Dockerコンテナ上でDB作成&migrationを実行

buildが無事完了したら、次はDB作成&migrationです。
ターミナルで以下のコマンドを実行してください。

[bookapp] $ docker-compose run web bundle exec rake db:create
Created database 'bookapp_development'
Created database 'bookapp_test'

[bookapp] $ docker-compose run web bundle exec rake db:migrate

== CreateBooks: migrating ======================================
-- create_table(:books)
   -> 0.0150s
== CreateBooks: migrated (0.0152s) =============================

[解説]
Dockerコンテナはlocalとは環境が異なります。
その為、再度DB作成&migrationの実行が必要になります

docker-compose run webコマンドについて
Dockerコンテナでのrailsコマンドは通常のコマンドの前に
「docker-compose run web」をつけて実行する必要があります。
通常のlocal環境のように「rake db:migrate」のみでは
変更が反映されないので注意してください。

7 コンテナを起動する docker-compose up

ここまで来たら、ブラウザでTopページ表示が可能になります。

まず以下のコマンドでDockerコンテナを立ち上げます。

[bookapp] $ docker-compose up 
Starting bookapp_db_1 ... done
Recreating bookapp_web_1 ... done

コンテナが立ち上がった後、ブラウザでTopページが表示されるか確認してみてください。
http://localhost:3000/

image.png

8 DB接続設定を環境変数に置き換え - env_file

ここまでの作業でDocker開発環境の構築は完了ですが、
DB接続関連の設定がファイルにベタ打ち(ハードコーディング)になっているのが
気になる方もいらっしゃるかもしれません。

Docker開発でも環境変数でDB接続関連の設定を読み込むことが可能です。

具体的には以下3つの作業を実施します。

(1) envファイル(db.env)を作成
(2) database.ymlを編集
(3) docker-compose.ymlを編集

1)envファイル(db.env)を作成

Dockerfileと同じルートディレクトリにenvファイルを作成します。
本記事ではファイル名を「db.env」とします。
以下ファイルを追加してください。

db.env
DB_USERNAME=root
DB_PASS=password
DB_HOST=db
MYSQL_ROOT_PASS=password

2)database.ymlを編集

上記で追加した環境変数を読み込む為、config/database.ymlを編集します。

database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV.fetch('DB_USERNAME', 'root') %>
  password: <%= ENV.fetch('DB_PASSWORD', 'pass') %>
  socket: /tmp/mysql.sock
  host: <%= ENV.fetch('DB_HOST', 'db') %>

development:
  <<: *default
  database: bookapp_development

[解説]
ENV.fetch(第一引数, 第二引数)について
第一引数に「環境変数」、第二引数に「デフォルト値」をセットします。
環境変数がnullの場合は第二引数のデフォルト値が読み込まれます。

3)docker-compose.ymlを編集

仕上げにdocker-compose.ymlの設定を変更します。
db、webの2箇所にdb.envを読み込む設定(env_file)を追加してください。

docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7
    ports:
      - "4306:3306"
    env_file: db.env

  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/app_name
    ports:
      - "3000:3000"
    depends_on:
      - db
    env_file: db.env

ここまで出来たら一度コンテナを停止&再起動し、
環境変数経由でもTopページが表示できることを確認します。

# docker-compose stop or ctrl+Cでコンテナの停止が可能です。
Stopping bookapp_web_1 ... done
Stopping bookapp_db_1  ... done

#コンテナを再起動
[bookapp] $ docker-compose up
Starting bookapp_db_1 ... done
Starting bookapp_web_1 ... done

http://localhost:3000/

9 Docker環境のRailsアプリとSequelProを接続する

それでは最後にDockerコンテナとSequelProを接続してみましょう。

以下のSequelPro接続設定のページを開いてください。

image.png

Dockerコンテナ用の接続設定を追加する

設定ページが開けたら以下のようにDockerコンテナ用の接続設定を追加してください。

image.png

「名前」・・・任意の名前でOKです。ここでは「docker-sql」としています。
「ホスト」・・・「127.0.0.1」
「ユーザー名/パスワード」・・・envファイルに合わせて記載してください。
「ポート」・・・ 「4306」で設定します。

[解説]
ポート:4306について
docker-compose.ymlのポート設定を思い出してください。

ports: "4306:3306"

これはホストが4306で接続した時にコンテナ上では3306に置き換えるという意味になります。 
localhostのMysqlのデフォルトのポート番号は3306です。
その為ポート番号:3306をそのまま使うと競合が発生して接続エラーが起こります。

エラー回避の為、上記のようにDocker環境ではポート:4306を使ってSeqelProに接続する設定にします。

上記設定が完了したら、早速確認してみましょう。
dockerコンテナが起動している状態でSequelProの「接続」をクリックします。

無事接続できていれば以下のようにDocker環境でもGUIでテーブル内容を確認できます。

image.png

参考記事

Docker 公式Tutorial
丁寧すぎるDocker-composeによるrails5 + MySQL on Dockerの環境構築(Docker for Mac)
既存のRailsアプリにDockerを導入する手順
Dockerで利用する環境変数をenv_fileを利用して一元管理する方法

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

Ruby on Rails 「途中まで作ったアプリにDockerを導入したい」に挑戦してみる(MySQL)

はじめに

Dockerの公式Tutorialでは、DB:PostgreSQLで解説されている為、他のDB(例:MySQL)でDockerを導入してみたい初学者にとってはややハードルが高い状況です。

本記事では、途中まで作ったRailsアプリ(DB:MySQL)にDockerを導入する方法を解説します。

Dockerを途中から導入するアプリの例としては、scaffoldで作成した
簡単な蔵書管理アプリ(bookapp)を使用します。
(適宜、ご自身で開発中のアプリに置き換えて頂ければと思います)

DBはMySQL5.7で実装を進めます。

併せてDocker環境でもDB内容を視覚的に確認できるように
SequelProとの接続方法も解説します。

前提

目指すゴール

(1) 既存のRailsアプリにMySQLを使ったDocker環境を構築する

(2) Sequelproと接続し、Docker環境でもGUIでDB内容を確認できるようにする

実装の流れ

1 Dockerfile/docker-compose.ymlを作成

2 Dockerfileを編集

3 docker-compose.ymlを編集

4 database.yml編集

5 コンテナをbuildする docker-compose build

6 Dockerコンテナ上でDB作成&migrationを実行
 docker-compose run web bundle exec rake db:create / db:migrate

7 コンテナを起動する docker-compose up

8 DB接続設定を環境変数に置き換え - env_file

9 Docker環境のRailsアプリとSequelProを接続する

1 Dockerfile/docker-compose.ymlを作成

まずは既存アプリに必要なファイルを追加します。
具体的には(1)Dockerfile (2) docker-compose.ymlの2ファイルです。

アプリのルートディレクトリに以下のように2つのファイルを追加してください。
(ファイルの作成場所に注意してください)

bookapp ----|-- app
            |-- bin
            |-- config
            |-- db
            ・・・・・・
       ・・・・・・
            |-- Gemfile
            |-- Gemfile.lock
            |-- package.json
            |-- Rakefile
            |-- README.md
            |-- Dockerfile  #ここに追加
            |-- docker-compose.yml #ここに追加          

[解説]
Gemfile/Gemfile.lockについて
・アプリ開発当初からDockerを導入する記事では、ここでGemfile(railsのみ導入)/Gemfile.lock(空でOK)を作成するように記載されていますが、既に開発中のアプリで作成済かと思いますのでGemfileについては特に何もしなくてOKです。

2 Dockerfileを編集

次にDockerfileを以下のように編集します。

Dockerfile
FROM ruby:2.5

RUN apt-get update -qq && \
    apt-get install -y build-essential \ 
                       libpq-dev \        
                       nodejs      

RUN mkdir /app_name 

ENV APP_ROOT /app_name 
WORKDIR $APP_ROOT

ADD ./Gemfile $APP_ROOT/Gemfile
ADD ./Gemfile.lock $APP_ROOT/Gemfile.lock

RUN gem install bundler 
RUN bundle install
ADD . $APP_ROOT

[解説]
RUN gem install bundlerについて
Railsアプリに途中からDockerを導入する場合、Gemfileが既にある関係で上記のコマンド無しで進めようとするとエラーが出るかもしれません。(自分の環境ではBuildした時に以下のようなエラーが出ました)

ERROR: Service 'web' failed to build: The command '/bin/sh -c bundle install' returned a non-zero code: 20

対策として、DockerfileをBuildする際にbundlerを導入しておきます。

3 docker-compose.ymlを編集

続いて docker-compose.ymlを以下のように編集します。

docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: 'password'
    ports:
      - "4306:3306"

  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/app_name
    ports:
      - "3000:3000"
    depends_on:
      - db

[解説]
1 db:portsの設定について- "4306:3306"
DockerコンテナとSequelpro接続の為に必要な設定です。(詳細は後ほど)
とりあえず現段階では、上記の設定にしておいてください。

2 rm -f tmp/pids/server.pidについて
開発途中に何度もdockerコンテナの起動&停止を繰り返していると時々「A server is already running.」のエラーが出ます。(pidファイルが残っていることが原因)
上記コマンドでserver.pidを逐一削除することで未然にエラーを防ぎます。

関連記事
docker-compose upしたときに「A server is already running.」って言われないようにする

4 database.yml編集

次にconfig/database.ymlの設定をdocker-compose.ymlの内容に合わせます。
具体的には(1) password (2)host:dbの2つを追記してください。

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password
  socket: /tmp/mysql.sock
  host: db  

development:
  <<: *default
  database: bookapp_development

5 コンテナをbuildする docker-compose build

ここまで来たら、次はDocker環境のコンテナを作っていきます。
ターミナルで以下のコマンドを実行してください。
(初めて実行するときは少し時間がかかります)

[bookapp] $ docker-compose build 

6 Dockerコンテナ上でDB作成&migrationを実行

buildが無事完了したら、次はDB作成&migrationです。
ターミナルで以下のコマンドを実行してください。

[bookapp] $ docker-compose run web bundle exec rake db:create
Created database 'bookapp_development'
Created database 'bookapp_test'

[bookapp] $ docker-compose run web bundle exec rake db:migrate

== CreateBooks: migrating ======================================
-- create_table(:books)
   -> 0.0150s
== CreateBooks: migrated (0.0152s) =============================

[解説]
Dockerコンテナはlocalとは環境が異なります。
その為、再度DB作成&migrationの実行が必要になります

docker-compose run webコマンドについて
Dockerコンテナでのrailsコマンドは通常のコマンドの前に
「docker-compose run web」をつけて実行する必要があります。
通常のlocal環境のように「rake db:migrate」のみでは
変更が反映されないので注意してください。

7 コンテナを起動する docker-compose up

ここまで来たら、ブラウザでTopページ表示が可能になります。

まず以下のコマンドでDockerコンテナを立ち上げます。

[bookapp] $ docker-compose up 
Starting bookapp_db_1 ... done
Recreating bookapp_web_1 ... done

コンテナが立ち上がった後、ブラウザでTopページが表示されるか確認してみてください。
http://localhost:3000/

image.png

8 DB接続設定を環境変数に置き換え - env_file

ここまでの作業でDocker開発環境の構築は完了ですが、
DB接続関連の設定がファイルにベタ打ち(ハードコーディング)になっているのが
気になる方もいらっしゃるかもしれません。

Docker開発でも環境変数でDB接続関連の設定を読み込むことが可能です。

具体的には以下3つの作業を実施します。

(1) envファイル(db.env)を作成
(2) database.ymlを編集
(3) docker-compose.ymlを編集

1)envファイル(db.env)を作成

Dockerfileと同じルートディレクトリにenvファイルを作成します。
本記事ではファイル名を「db.env」とします。
以下ファイルを追加してください。

db.env
DB_USERNAME=root
DB_PASS=password
DB_HOST=db
MYSQL_ROOT_PASS=password

2)database.ymlを編集

上記で追加した環境変数を読み込む為、config/database.ymlを編集します。

database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV.fetch('DB_USERNAME', 'root') %>
  password: <%= ENV.fetch('DB_PASSWORD', 'pass') %>
  socket: /tmp/mysql.sock
  host: <%= ENV.fetch('DB_HOST', 'db') %>

development:
  <<: *default
  database: bookapp_development

[解説]
ENV.fetch(第一引数, 第二引数)について
第一引数に「環境変数」、第二引数に「デフォルト値」をセットします。
環境変数がnullの場合は第二引数のデフォルト値が読み込まれます。

3)docker-compose.ymlを編集

仕上げにdocker-compose.ymlの設定を変更します。
db、webの2箇所にdb.envを読み込む設定(env_file)を追加してください。

docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7
    ports:
      - "4306:3306"
    env_file: db.env

  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/app_name
    ports:
      - "3000:3000"
    depends_on:
      - db
    env_file: db.env

ここまで出来たら一度コンテナを停止&再起動し、
環境変数経由でもTopページが表示できることを確認します。

# docker-compose stop or ctrl+Cでコンテナの停止が可能です。
Stopping bookapp_web_1 ... done
Stopping bookapp_db_1  ... done

#コンテナを再起動
[bookapp] $ docker-compose up
Starting bookapp_db_1 ... done
Starting bookapp_web_1 ... done

http://localhost:3000/

9 Docker環境のRailsアプリとSequelProを接続する

それでは最後にDockerコンテナとSequelProを接続してみましょう。

以下のSequelPro接続設定のページを開いてください。

image.png

Dockerコンテナ用の接続設定を追加する

設定ページが開けたら以下のようにDockerコンテナ用の接続設定を追加してください。

image.png

「名前」・・・任意の名前でOKです。ここでは「docker-sql」としています。
「ホスト」・・・「127.0.0.1」
「ユーザー名/パスワード」・・・envファイルに合わせて記載してください。
「ポート」・・・ 「4306」で設定します。

[解説]
ポート:4306について
docker-compose.ymlのポート設定を思い出してください。

ports: "4306:3306"

これはホストが4306で接続した時にコンテナ上では3306に置き換えるという意味になります。 
localhostのMysqlのデフォルトのポート番号は3306です。
その為ポート番号:3306をそのまま使うと競合が発生して接続エラーが起こります。

エラー回避の為、上記のようにDocker環境ではポート:4306を使ってSeqelProに接続する設定にします。

上記設定が完了したら、早速確認してみましょう。
dockerコンテナが起動している状態でSequelProの「接続」をクリックします。

無事接続できていれば以下のようにDocker環境でもGUIでテーブル内容を確認できます。

image.png

参考記事

Docker 公式Tutorial
丁寧すぎるDocker-composeによるrails5 + MySQL on Dockerの環境構築(Docker for Mac)
既存のRailsアプリにDockerを導入する手順
Dockerで利用する環境変数をenv_fileを利用して一元管理する方法

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

Docker Toolboxでvolumesでマウントができない場合の対策

Docker Toolbox とは

Windows環境でDockerを使用する場合、

  • Windows 10 Pro あるいは Enterprise -> Docker for Windows
  • Windows 10 Home(Docker for Windowsのシステム要件を満たしていないもの)-> Docker Toolbox

と使い分ける必要があります。
本記事は、後者のDocker Toolbox利用者向けの内容になります。

docker-compose.xmlのvolumes定義でマウント

Docker for WindowsやDocker for Macの場合は、docker-compose.xmlのvolumesに下記のように記述すると、
ホストのディレクトリをコンテナのディレクトリにマウントすることができます。(ホストのディレクトリ:コンテナのディレクトリ)。

docker-compose.xml
version: '3'
services:
  db:
   ~中略~
   environment:
   ~中略~
   volumes:
     - ./initdb:/docker-entrypoint-initdb.d
     - ./data:/var/lib/data

特に、コンテナ側に/docker-entrypoint-initdb.dを指定すると、
ホスト側に格納していおいた.sqlや.iniをコンテナの初回起動時に実行してくれるため、
DB作成直後のテーブル作成やレコード作成に活用することが多いですね。

Docker for WindowsやDocker for Macでは、この記述で期待通りマウントが行われますが、
Docker Toolboxでは、期待通りマウントが行われません。

例えば、/docker-entrypoint-initdb.d宛に初回起動時に実行されてほしいsqlのフォルダを指定しても、
コンテナの初回起動時のログに下記のように出力され、sql実行がignoringされてしまいます。
1.PNG
正しくマウントが行われていれば、コンテナの/docker-entrypoint-initdb.dディレクトリにsqlなどがコピー・実行されますが、
マウントに失敗するため、/docker-entrypoint-initdb.d配下にファイルが無く、何も実行されないのです。

マウントするための対策

原因は、Docker Toolboxでは、マウントできるホスト側のフォルダが限られているためです。
docker-compose.xmlでそのフォルダを指定する必要があります。

Docker Toolboxをインストールした際、VirtialBoxもインストールされたと思います。
VirtialBoxで、Default>右クリック>設定>共有フォルダーで、下記の設定が確認できます。
2.PNG
この「c/Users」が、マウントできるホストのフォルダです。

なので、docker-compose.xmlを下記のように書き換え、

docker-compose.xml
   volumes:
     - /c/Users/initdb:/docker-entrypoint-initdb.d

コンテナの再起動を行います。
(必要に応じて、コンテナの削除→起動を行ってください。)
すると、コンテナ起動時のログに、下記のように出力され、sqlが実行されたことが確認できます。
3.PNG
もちろん、コンテナに入ってもDBに入っても、期待通りマウントとsql実行が行われていることが確認できます。

最後に

解決のために、いろいろな記事を参考にさせて頂きました。ありがとうございました。

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

JubatusをDockerで動かす

Dockerを使うと良いこと

  • 公式イメージをpullするだけでJubatusを使うことができる。
  • 環境を汚さずバージョン違いのミドルウェアで悩まない。
  • 要らなくなった時に消すのが楽ちん。

Docker環境

・Docker for Mac

% docker version
Client: Docker Engine - Community
 Version:           19.03.4
 API version:       1.40
 Go version:        go1.12.10
 Git commit:        9013bf5
 Built:             Thu Oct 17 23:44:48 2019
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.4
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.10
  Git commit:       9013bf5
  Built:            Thu Oct 17 23:50:38 2019
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.2.10
  GitCommit:        b34a5c8af56e510852c35414db4c1f4fa6172339
 runc:
  Version:          1.0.0-rc8+dev
  GitCommit:        3e425f80a8c931f88e6d94a8c831b9d5aa481657
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

Jubatusサーバのインストール

Jubatusの公式や、Docker Hubを参考にする。

% docker pull jubatus/jubatus
Using default tag: latest
latest: Pulling from jubatus/jubatus
8284e13a281d: Pull complete 
26e1916a9297: Pull complete 
4102fc66d4ab: Pull complete 
1cf2b01777b2: Pull complete 
7f7a2d5e04ed: Pull complete 
4cb7073bed3b: Pull complete 
Digest: sha256:df700aa354604de1ae6b06d169e4203ac883385d803ec80a6e1fc5cf3fba118a
Status: Downloaded newer image for jubatus/jubatus:latest
docker.io/jubatus/jubatus:latest 

imageサイズを確認してみる

% docker images jubatus/jubatus
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
jubatus/jubatus     latest              349bb156c215        11 months ago       415MB

ちゃんとイメージが取れているっぽいので、Jubatusサーバをコンテナ内で起動してみる。今回は分類器 (Classifier) を同梱されている設定ファイルで起動している。

$ docker run --expose 9199 jubatus/jubatus jubaclassifier -f /opt/jubatus/share/jubatus/example/config/classifier/pa.json

起動したっぽいので、Ctrl+Cで一旦終了しておく。

2020-01-13 12:29:03,261 1 INFO  [server_util.cpp:429] starting jubaclassifier 1.1.1 RPC server at 172.17.0.2:9199
    pid                  : 1
    user                 : root
    mode                 : standalone mode
    timeout              : 10
    thread               : 2
    datadir              : /tmp
    logdir               : 
    log config           : 
    zookeeper            : 
    name                 : 
    interval sec         : 16
    interval count       : 512
    zookeeper timeout    : 10
    interconnect timeout : 10

2020-01-13 12:29:03,325 1 INFO  [server_util.cpp:165] load config from local file: /opt/jubatus/share/jubatus/example/config/classifier/pa.json
2020-01-13 12:29:03,325 1 INFO  [classifier_serv.cpp:116] config loaded: {
  "converter" : {
    "string_filter_types" : {},
    "string_filter_rules" : [],
    "num_filter_types" : {},
    "num_filter_rules" : [],
    "string_types" : {},
    "string_rules" : [
      { "key" : "*", "type" : "str", "sample_weight" : "bin", "global_weight" : "bin" }
    ],
    "num_types" : {},
    "num_rules" : [
      { "key" : "*", "type" : "num" }
    ]
  },
  "method" : "PA"
}

2020-01-13 12:29:03,326 1 INFO  [server_helper.hpp:226] start listening at port 9199
2020-01-13 12:29:03,326 1 INFO  [server_helper.hpp:233] jubaclassifier RPC server startup

Jubatusクライアントのインストール

Jubatusを使ったクライアントアプリケーションは C++, Python, Ruby または Java で記述することができる。今回はPythonで試してみる。

pip3 install jubatus

今度は外れ値検知を試してみる

Jubatusの公式を参考にして学習の設定ファイル(anomaly_config.json)とJubatusを使ったクライアントアプリケーション(anomaly.py)を作成する。それぞれのファイルを適当なディレクトリに置く。

設定ファイル(anomaly_config.json)は「/Users/katuemon/Documents/jubatus/conf/」に、クライアントアプリケーション(anomaly.py)は「/Users/katuemon/Documents/jubatus/」に置いた。

anomaly_config.json
{
 "method" : "lof",
 "parameter" : {
  "nearest_neighbor_num" : 10,
  "reverse_nearest_neighbor_num" : 30,
  "method" : "euclid_lsh",
  "parameter" : {
   "hash_num" : 8,
   "table_num" : 16,
   "probe_num" : 64,
   "bin_width" : 10,
   "seed" : 1234
  }
 },

 "converter" : {
  "string_filter_types": {},
  "string_filter_rules": [],
  "num_filter_types": {},
  "num_filter_rules": [],
  "string_types": {},
  "string_rules": [{"key":"*", "type":"str", "global_weight" : "bin", "sample_weight" : "bin"}],
  "num_types": {},
  "num_rules": [{"key" : "*", "type" : "num"}]
 }
}

anomaly.py(Jubatusを使ったクライアントアプリケーション)では、csvから読み込んだデータをJubatusにサーバ与え、外れ値を検出し結果を標準出力に出すようだ。

anomaly.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import signal
import sys, json
from jubatus.anomaly import client
from jubatus.common import Datum

NAME = "anom_kddcup";

# handle keyboard interruption"
def do_exit(sig, stack):
    print('You pressed Ctrl+C.')
    print('Stop running the job.')
    sys.exit(0)

if __name__ == '__main__':
    # 0. set KeyboardInterrupt handler
    signal.signal(signal.SIGINT, do_exit)

    # 1. set jubatus server
    anom = client.Anomaly("127.0.0.1", 9199, NAME)

    # 2. prepare training data
    with open('kddcup.data_10_percent.txt', mode='r') as file:
        for line in file:
            duration, protocol_type, service, flag, src_bytes, dst_bytes, land, wrong_fragment, urgent, hot, num_failed_logins, logged_in, num_compromised, root_shell, su_attempted, num_root, num_file_creations, num_shells, num_access_files, num_outbound_cmds, is_host_login, is_guest_login, count, srv_count, serror_rate, srv_serror_rate, rerror_rate, srv_rerror_rate, same_srv_rate, diff_srv_rate, srv_diff_host_rate, dst_host_count, dst_host_srv_count, dst_host_same_srv_rate, dst_host_diff_srv_rate, dst_host_same_src_port_rate, dst_host_srv_diff_host_rate, dst_host_serror_rate, dst_host_srv_serror_rate, dst_host_rerror_rate, dst_host_srv_rerror_rate, label = line[:-1].split(",")

            datum = Datum()
            for (k, v) in [
                    ["protocol_type", protocol_type],
                    ["service", service],
                    ["flag", flag],
                    ["land", land],
                    ["logged_in", logged_in],
                    ["is_host_login", is_host_login],
                    ["is_guest_login", is_guest_login],
                    ]:
                datum.add_string(k, v)

            for (k, v) in [
                    ["duration",float(duration)],
                    ["src_bytes", float(src_bytes)],
                    ["dst_bytes", float(dst_bytes)],
                    ["wrong_fragment", float(wrong_fragment)],
                    ["urgent", float(urgent)],
                    ["hot", float(hot)],
                    ["num_failed_logins", float(num_failed_logins)],
                    ["num_compromised", float(num_compromised)],
                    ["root_shell", float(root_shell)],
                    ["su_attempted", float(su_attempted)],
                    ["num_root", float(num_root)],
                    ["num_file_creations", float(num_file_creations)],
                    ["num_shells", float(num_shells)],
                    ["num_access_files", float(num_access_files)],
                    ["num_outbound_cmds",float(num_outbound_cmds)],
                    ["count", float(count)],
                    ["srv_count",float(srv_count)],
                    ["serror_rate", float(serror_rate)],
                    ["srv_serror_rate", float(srv_serror_rate)],
                    ["rerror_rate", float(rerror_rate)],
                    ["srv_rerror_rate",float( srv_rerror_rate)],
                    ["same_srv_rate", float(same_srv_rate)],
                    ["diff_srv_rate", float(diff_srv_rate)],
                    ["srv_diff_host_rate", float(srv_diff_host_rate)],
                    ["dst_host_count",float( dst_host_count)],
                    ["dst_host_srv_count", float(dst_host_srv_count)],
                    ["dst_host_same_srv_rate",float( dst_host_same_srv_rate)],
                    ["dst_host_same_src_port_rate",float( dst_host_same_src_port_rate)],
                    ["dst_host_diff_srv_rate", float(dst_host_diff_srv_rate)],
                    ["dst_host_srv_diff_host_rate",float(dst_host_srv_diff_host_rate)],
                    ["dst_host_serror_rate",float(dst_host_serror_rate)],
                    ["dst_host_srv_serror_rate",float(dst_host_srv_serror_rate)],
                    ["dst_host_rerror_rate",float(dst_host_rerror_rate)],
                    ["dst_host_srv_rerror_rate",float(dst_host_srv_rerror_rate)],
                    ]:
                datum.add_number(k, v)

            # 3. train data and update jubatus model
            ret = anom.add(datum)

            # 4. output results
            if (ret.score != float('Inf')) and (ret.score!= 1.0):
                print (ret, label)

サンプルプログラムの実行

データのダウンロード

% cd /Users/katuemon/Documents/jubatus/
% curl -OL http://kdd.ics.uci.edu/databases/kddcup99/kddcup.data_10_percent.gz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 2094k  100 2094k    0     0  1025k      0  0:00:02  0:00:02 --:--:-- 1025k
% gunzip kddcup.data_10_percent.gz 

# kddcup.data_10_percent.txtはanomaly.pyと同じディレクトリに置く。
% mv kddcup.data_10_percent kddcup.data_10_percent.txt

jubaanomalyを起動

% docker run -p 9199:9199 -v /Users/katuemon/Documents/jubatus/conf:/tmp/config jubatus/jubatus jubaanomaly -f /tmp/config/anomaly_config.json

起動したっぽい

2020-01-13 12:57:16,401 1 INFO  [server_util.cpp:429] starting jubaanomaly 1.1.1 RPC server at 172.17.0.2:9199
    pid                  : 1
    user                 : root
    mode                 : standalone mode
    timeout              : 10
    thread               : 2
    datadir              : /tmp
    logdir               : 
    log config           : 
    zookeeper            : 
    name                 : 
    interval sec         : 16
    interval count       : 512
    zookeeper timeout    : 10
    interconnect timeout : 10

2020-01-13 12:57:16,464 1 INFO  [server_util.cpp:165] load config from local file: /tmp/config/anomaly_config.json
2020-01-13 12:57:16,469 1 INFO  [anomaly_serv.cpp:140] config loaded: {
 "method" : "lof",
 "parameter" : {
  "nearest_neighbor_num" : 10,
  "reverse_nearest_neighbor_num" : 30,
  "method" : "euclid_lsh",
  "parameter" : {
   "hash_num" : 8,
   "table_num" : 16,
   "probe_num" : 64,
   "bin_width" : 10,
   "seed" : 1234
  }
 },

 "converter" : {
  "string_filter_types": {},
  "string_filter_rules": [],
  "num_filter_types": {},
  "num_filter_rules": [],
  "string_types": {},
  "string_rules": [{"key":"*", "type":"str", "global_weight" : "bin", "sample_weight" : "bin"}],
  "num_types": {},
  "num_rules": [{"key" : "*", "type" : "num"}]
 }
}

2020-01-13 12:57:16,470 1 INFO  [server_helper.hpp:226] start listening at port 9199
2020-01-13 12:57:16,470 1 INFO  [server_helper.hpp:233] jubaanomaly RPC server startup

Jubatus Clientを実行

% python3 anomaly.py

実行結果

ちゃんとスコアも表示されていて問題なさそうだ。

id_with_score{id: 190, score: 0.9999999999999419} normal.
id_with_score{id: 195, score: 1.0000313006855042} normal.
id_with_score{id: 308, score: 0.9999999999986386} normal.
id_with_score{id: 476, score: 0.9999999999999977} normal.
id_with_score{id: 485, score: 0.9999999999999997} normal.
id_with_score{id: 490, score: 0.9999999999999836} normal.
id_with_score{id: 495, score: 1.459560361462793} normal.
id_with_score{id: 498, score: 0.999999999999998} normal.
id_with_score{id: 642, score: 0.9999999999999577} normal.
id_with_score{id: 643, score: 0.999999999999923} normal.
id_with_score{id: 654, score: 0.9999999999999812} normal.
id_with_score{id: 657, score: 0.9999999999999506} normal.
id_with_score{id: 683, score: 0.9999999999999567} normal.
id_with_score{id: 696, score: 0.9999999999899615} normal.
id_with_score{id: 697, score: 0.999999999999993} normal.
id_with_score{id: 698, score: 0.9999999999996164} normal.
id_with_score{id: 704, score: 0.9999999999999855} normal.
id_with_score{id: 705, score: 0.9999999999999941} normal.
id_with_score{id: 711, score: 0.9999999999994069} normal.
id_with_score{id: 717, score: 0.9999999999999987} normal.
id_with_score{id: 835, score: 0.9999999999999999} normal.
id_with_score{id: 1123, score: 0.9999999999999933} normal.
id_with_score{id: 1128, score: 1.0641230203490077} normal.
id_with_score{id: 1129, score: 0.9999999999999983} normal.
id_with_score{id: 1149, score: 1.0401485737893268} normal.
id_with_score{id: 1150, score: 0.9999999999996244} normal.
id_with_score{id: 1166, score: 0.9999999999999964} normal.
id_with_score{id: 1173, score: 0.9999999999999908} normal.
id_with_score{id: 1175, score: 0.999999999999569} normal.
id_with_score{id: 1662, score: 0.9999999999999974} normal.
id_with_score{id: 1678, score: 0.9999999999999984} normal.
id_with_score{id: 1681, score: 0.9999999999999952} normal.
id_with_score{id: 1692, score: 0.9999999999992727} normal.
id_with_score{id: 1710, score: 1.2717678419359209} normal.
id_with_score{id: 1711, score: 0.9999999999999989} normal.
id_with_score{id: 1720, score: 0.9999999999999992} normal.
id_with_score{id: 1732, score: 0.9999999999999959} normal.
id_with_score{id: 1733, score: 0.9999999999999953} normal.
id_with_score{id: 1745, score: 0.9999999999999792} normal.
id_with_score{id: 1746, score: 0.9999999999999954} normal.
id_with_score{id: 1881, score: 0.9999999999999983} normal.
id_with_score{id: 2212, score: 0.9999999999999997} normal.
id_with_score{id: 2285, score: 0.9999999999999966} normal.
id_with_score{id: 2287, score: 0.9999999999999962} normal.
id_with_score{id: 2288, score: 0.9999999999999981} normal.
id_with_score{id: 2292, score: 1.388673102986125} normal.
id_with_score{id: 2337, score: 0.9999999999999991} normal.
id_with_score{id: 2347, score: 0.999999999999961} normal.
id_with_score{id: 2355, score: 0.9999999999999994} normal.
id_with_score{id: 2358, score: 1.0560593847508284} normal.
id_with_score{id: 2383, score: 0.9994229663750211} normal.
id_with_score{id: 2394, score: 0.9999999999999631} normal.
id_with_score{id: 2463, score: 0.9999999999999991} normal.
id_with_score{id: 2500, score: 0.7581637350497832} normal.
id_with_score{id: 2501, score: 0.0} normal.
id_with_score{id: 2509, score: 0.9999999999999997} normal.
id_with_score{id: 2529, score: 0.999999999999994} normal.
id_with_score{id: 2548, score: 0.9999999936138952} normal.

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

Dockerfileチートシートv19.03

概要

Dockerfile命令をまとめました。
※すべて網羅しているわけではありません。

  • Dockerバージョン:v19.03

早見表

命令 概要
FROM ベースイメージを指定
RUN 指定コマンドを実行
ENTRYPOINT コンテナ実行時のコマンドを指定
CMD コンテナ実行時のコマンドを指定(上書き可)
COPY ホストマシンからコンテナイメージへファイル/ディレクトリを単純コピー
ADD COPY+解凍 / URLからダウンロード(非推奨)
ENV 環境変数を追加
EXPOSE 指定ポートを開ける
WORKDIR カレントディレクトリを変更
MAINTAINER deprecated
現在はLABEL maintainer="maintainer@example.com"と指定すべき

FROM

ベースイメージを指定して取得する。1行目はこのコマンド。

Docker HubやプライベートDockerリポジトリからイメージを指定する。

  • タグ指定なし :latestでpull
Dockerfile
FROM centos:7.7.1908
ENTRYPOINT [ "/bin/bash" ]
$ docker image build --file Dockerfile -t testrepo:test .
$ docker image ls --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
REPOSITORY          TAG                 SIZE
testrepo            test                204MB # <- 今回作成されたイメージ
centos              7.7.1908            204MB # <- ベースイメージ
  • タグ指定あり :指定したタグでpull、基本的には指定すべき
Dockerfile
FROM centos
ENTRYPOINT ["/bin/bash"]
$ docker image build --file Dockerfile -t testrepo:test .
$ docker image ls --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
REPOSITORY          TAG                 SIZE
testrepo            test                220MB # <- 今回作成されたイメージ
centos              latest              220MB # <- ベースイメージ

RUN

指定したコマンドを実行する。
Tipsで後述していますが、できる限りまとめて記載すべきです。

Dockerfile
FROM centos:8
RUN yum install -y httpd iproute && \
    echo "Test" > /var/www/html/index.html
ENTRYPOINT ["/usr/sbin/httpd"]

ENTRYPOINT / CMD

コンテナを起動した際に実行されるコマンドを規定する。
Dockerfileの最後の方にそれぞれ1回ずつ記述されるイメージ。

Dockerfile
FROM centos:8

ENTRYPOINT ["df"]
CMD ["-H", "."]

指定するコマンドについては、半角スペースごとにコンマで区切って記述します。例えばgo run main.goを指定したい場合は以下のようになります。

Dockerfile
ENTRYPOINT ["go", "run", "main.go"]

ENTRYPOINTとCMDの違い

ENTRYPOINTは必ずそのまま実行される、
CMDはコンテナ起動時に引数を指定すると上書き可能です。
この特性を生かして、ENTRYPOINTにはコマンド部分、CMDにはコマンドのオプション部分を記載するという書き方ができます。

e.g.
上記のDockerfileでイメージをビルドしたとして、引数ありでコンテナ起動する場合と引数なしでコンテナ起動する場合の、具体的な実行結果の違いを以下に示しておきます。

  • 引数なし : df -H .が実行される。
Terminal
$ docker run override_test
Filesystem      Size  Used Avail Use% Mounted on
overlay          63G  2.1G   58G   4% /
  • 引数あり(--help) : df --helpが実行される。CMDの部分がコマンドラインに指定したオプションで上書きされていることがわかる。
Terminal
$ docker run docker_test --help
Usage: df [OPTION]... [FILE]...
Show information about the file system on which each FILE resides,
or all file systems by default.

Mandatory arguments to long options are mandatory for short options too.
  -a, --all             include pseudo, duplicate, inaccessible file systems
# 以下略

COPY

ホストマシンのファイルまたはディレクトリをコンテナ内にコピーする。

ホストマシンのディレクトリ構成
.
├── Dockerfile
├── copydir
│   ├── subfile.txt
│   └── subfile2.txt
└── copyfile.txt
Dockerfile
FROM centos:latest

RUN mkdir -p /app
# ファイル単位
COPY ./copyfile.txt /app
# ディレクトリ単位 ※コピー先のディレクトリ名称を指定する必要あり
COPY ./copydir /app/subdir
Terminal
$ docker image build --file Dockerfile -t centos:copytest .
$ docker container run centos:copytest ls -ltR /app
/app:
total 8
drwxr-xr-x 2 root root 4096 Jan 12 13:27 subdir
-rw-r--r-- 1 root root    6 Jan 12 13:18 copyfile.txt

/app/subdir:
total 4
-rw-r--r-- 1 root root 0 Jan 12 13:25 subfile2.txt
-rw-r--r-- 1 root root 8 Jan 12 13:19 subfile.txt

ADD

COPY同様、ホストマシンのファイルをコンテナ内へコピーします。(上記DockerfileのCOPYADDとしても同様の結果となります)
ただし、ADDを利用すると、コピーファイルが圧縮ファイルであった場合、自動的に解凍が行われます。

Dockerfile
FROM centos:8

RUN mkdir -p /app
# addtest.tarはcopydirを圧縮したファイル
ADD ./addtest.tar /app
Terminal
$ docker image build --file add/Dockerfile -t centos:addtest .
$ docker container run centos:addtest ls -lR /app
/app:
total 4
drwxr-xr-x 2 501 games 4096 Jan 12 13:25 copydir

/app/copydir:
total 4
-rw-r--r-- 1 501 games 8 Jan 12 13:19 subfile.txt
-rw-r--r-- 1 501 games 0 Jan 12 13:25 subfile2.txt
# tarファイルそのものはなく、解凍されたものが存在している

e.f. COPYの場合

Terminal
$ docker container run testrepo:copy ls -lR /app
/app:
total 4
-rw-r--r-- 1 root root 177 Jan 12 13:31 addtest.tar # 圧縮ファイルはそのまま

COPYとの棲み分け

  • 単純にコピーのみ行う場合 => COPY
  • コピーして解凍も行う場合 => ADD

ENV

実行コンテナに環境変数を追加する。

Dockerfile
FROM centos:8                                                                                                                                                              
ENV NEW_ENV='env-value'

ENTRYPOINT ["/bin/bash"]
Terminal
$ docker image build --file Dockerfile -t centos:envtest .
$ docker container run -it centos:envtest
[root@de6dc0f2577e /]# env  
LANG=en_US.UTF-8
HOSTNAME=de6dc0f2577e
NEW_ENV=env-value   # fileで追加した環境変数

EXPOSE

指定したポートをLISTEN状態とする。

Dockerfile
FROM python:3.8-slim-buster

COPY . /
RUN pip install -r /app/requirements.txt

EXPOSE 9876
ENTRYPOINT [ "gunicorn", "flask_app:app" ]
CMD [ "-c", "/app/config/gunicorn_settings.py" ]
Terminal
$ docker image build --file Dockerfile -t python:flask .
$ docker container run --name exposetest python:flask
$ docker container ps --format "table {{.Image}}\t{{.Ports}}"
IMAGE             PORTS               NAMES
flask             9876/tcp            exposetest
a2556b3a812                           worktest
# PORTSにLISTENEDのポート番号が表示される

WORKDIR

作業ディレクトリを変更する。
Dockerfileにおけるcd

Dockerfile
FROM centos:8
RUN mkdir -p /test
RUN echo "before workdir" > before.txt

WORKDIR /test
RUN echo "after workdir" > after.txt

ENTRYPOINT ["/bin/bash"]
Terminal
$ docker image build --file Dockerfile -t centos:workdir .
$ docker container run -it --name workdirtest centos:workdir 
[root@4cd5accab742 test]# pwd
/test   # 最後にWORKDIRしたパスがコンテナ起動時のベースパスとなる
[root@4cd5accab742 test]# ls .. | grep before
before.txt    # beforeはWORKDIR前に作成したのでルートディレクトリに存在
[root@4cd5accab742 test]# ls .
after.txt     # afterはWORKDIR後に作成したため/testディレクトリに存在

Tips

マルチステージビルドで軽量化

マルチステージビルドとは、最終完成イメージの軽量化をはかるためのテクニックのようなものです。
一例としてGoアプリケーションをマルチステージビルドを適用した場合とどうでない場合とのイメージサイズを比較してみます。

マルチステージビルドなし

Dockerfile
FROM golang:1.13-alpine
COPY ./main.go ./
RUN go build -o /app ./main.go
ENTRYPOINT ["/app"]

マルチステージビルドあり

Dockerfile
# 1段階め処理(ビルドを行う)
FROM golang:1.13-alpine as builder
COPY ./main.go ./
RUN go build -o /app ./main.go

# 2段階め(これが完成イメージ)
# ベースに最低限の要件を満たす軽量イメージを選択する
FROM alpine:3.11
# 1段階めのコンテナから必要物をコピー
COPY --from=builder /app .
ENTRYPOINT ["./app"]

サイズ確認

Terminal
REPOSITORY     TAG                 SIZE
golang         nonmultistage       361MB
golang         multistage          7.6MB # 圧倒的に軽い

命令はできるだけまとめる

Dockerfileの命令1つにつき、イメージキャッシュがそれぞれ構築されていきます。このイメージキャッシュレイヤー数を最小化することはベストプラクティスとされており、それにより軽量化やビルド高速化につながることがあります。具体的には以下の処置を行います。

  • RUN命令は「バックスラッシュ + &&」でつなげて記載
  • COPY/ADD命令もできるだけ少なくまとめる

特にRUNに関してはビルド時キャッシュの問題もあって、yum updateyum install&&でつなげて書かないとワナにはまる等といったことがあります。(キャッシュについては本記事では記載しません。)

ADD命令にはURLも指定可能でもそうは書かない

レイヤ数を少なくし、イメージ軽量化につなげるために、URLからリソースを取得する場合はcurlまたはwgetを利用します。

Example(Best practicesより抜粋)

Dockerfile
# Good
RUN mkdir -p /usr/src/things \
    && curl -SL http://example.com/big.tar.xz \
    | tar -xJC /usr/src/things \
    && make -C /usr/src/things all

# Bad
ADD http://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things
RUN make -C /usr/src/things all

Reference

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

【docker-compose】Nuxt.jsとGOでREST APIを構築した手順

現在のプロジェクトでは、Nuxt.jsとGOをGKEを使ってREST APIを構築しています。

Kubernetesを利用する場合、Dockerを必然的に利用することになる上に、ローカルの環境構築も色々面倒だったため、docker-composeを利用することにしました。

今回は、Nuxt.jsGOdocker-composeを利用して、REST APIを構築したため、その手順をまとめています。

docker-composeで構築したNuxt.jsとGOのREST APIの雑な全体像

表題の通り、めっちゃ雑なポンチ絵を作りました。

docker-composeのポンチ絵.001.jpeg

以上のようなデータの流れを再現するための手順をまとめます。

docker-composeでNuxt.jsとGOでREST APIを構築した手順の目次

ざっくりとした手順は以下です。

  • ディレクトリ構成※
  • Dockerfileを作成
  • my.cnfを作成
  • docker-compose.ymlを作成
  • docker-compose upを実行
  • アプリ側やコンテナ等の細かい設定変更

これから手順を詳細にまとめていきます。

※ディレクトリ構成は手順ではないですが目次には入れておきます。

ディレクトリ構成

Nuxt.jsGOでリポジトリが分かれている前提で手順を進めます。

Nuxt.js側のディレクトリ構成

ディレクトリ構成も雑に最低限共有します。

project_name
├──app/
├──nuxt.config.js
├──package.json
├──Dockerfile #本番用
├──Dockerfile.dev
└──docker-compose.yml

※app/は作業しているファイルが格納されているディレクトリ(ディレクトリ構成については以下にまとめました。)

参考:https://qiita.com/arthur_foreign/items/637f2976e9f5e7a89727

GO側のディレクトリ構成

project_name
├──app/
│  ├──main.go
│  └──database.go
├──config/ #tomlで見にいくDBを出し分け(現プロジェクトでは廃止予定ですが備忘録のため記載)
│  ├──config_dev.toml
│  ├──config_stg.toml
│  ├──config_prd.toml
│  └──config_docker.toml
├──db/
│  ├──migrations/ #マイグレーションファイル
│  ├──mysql/
│  │  └──my.cnf # MySQLの設定ファイル
│  └──seed/ #シードデータ
├──Dockerfile #本番用
├──Dockerfile.dev
└──docker-compose.yml

※appは作業ディレクトリ

Dockerfileを作成

まずは、Nuxt.jsGODockerfileを作成しました。

※本番環境とローカル環境で利用するDockerfileを分けるため、別名のDockerfileを用意しています。

※現プロジェクトのソースコードほぼコピペです。

Nuxt.js

Dockerfile.dev
FROM node:10.15.1-alpine as dev
WORKDIR /app
COPY . /app
RUN apk update && \
    apk add git
RUN yarn

CMD ["yarn", "dev"]

GO

Dockerfile.dev
FROM golang:1.12-alpine as builder
ADD . /go/src/github.com/project_name
WORKDIR /go/src/github.com/project_name

ENV GO111MODULE=on

RUN apk update && \
    apk add git && \
    go get github.com/jmoiron/sqlx && \
    GOOS=linux GOARCH=amd64 go build main.go

FROM alpine:3.9
WORKDIR /app
COPY --from=0 /go/src/github.com/project_name /app

CMD ["./main"]

multi-stage buildが出来るDockerfileの作成方法は、以下の記事に詳細をまとめましたので、参考にしていただけますと幸いです。

参考:https://qiita.com/arthur_foreign/items/fca369c1d9bde1701e38

my.cnfを作成

my.cnf
[mysqld]
character-set-server=utf8mb4
default_storage_engine=InnoDB
log-error=/var/log/mysql/mysqld.log
log_timestamps=SYSTEM
slow_query_log=ON
slow_query_log_file=/var/log/mysql/slow_query.log

[client]
default-character-set=utf8mb4

[mysql]
default-character-set=utf8mb4

[mysqldump]
default-character-set=utf8mb4

my.cnfについてはこの記事では触れません。

以下の記事を参考にしました。

参考1:https://qiita.com/at_1016/items/874788e375938837a027
参考2:https://blog.apar.jp/linux/6769/
参考3:https://qiita.com/YusukeHigaki/items/2cab311d2a559a543e3a
参考4:https://qiita.com/EigenPort_M/items/46bd1bf386eb56eba97e

docker-compose.ymlを作成

Nuxt.js

docker-compose.yml
version: '3'
services:
  front:
    build:
      context: .
      dockerfile: "Dockerfile.dev"
    volumes:
      - ./:/project_name/app
    ports:
      - "3000:3000"
    tty: true
    stdin_open: true
    networks:
      - project_name_default
networks:
  project_name_default:
    external: true

GO

docker-compose.yml
version: '3'
services:
  api:
    build:
      context: .
      dockerfile: "Dockerfile.dev"
    volumes:
      - ./:/go/src/github.com/project_name
    ports:
      - "8080:8080"
    environment:
      - APP_ENV=docker
    depends_on:
      - db
    tty: true
    stdin_open: true
    networks:
      - project_name_default
  db:
    image: mysql:5.7
    restart: always
    volumes:
      - ./db/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: project_name
      MYSQL_USER: root
    ports:
      - "3306:3306"
networks:
  project_name_default:
    external: true

networksには、docker network lsで出たネットワークを指定しました。(先にdocker-compose upしています。)

docker-compose.ymlについては、以下のドキュメントを参考にしました。

参考1:https://qiita.com/sanoyo/items/294e76cbcb80df35a32f
参考2:https://qiita.com/at-946/items/08de3c9d7611f62b1894

docker-compose upを実行

Nuxt.js側とGO側で以下のコマンドを実行しましょう。

$ docker-compose up

docker-compose up --build等のオプションは、以下のドキュメントが参考になりました。

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

うまくいかない時はアプリやコンテナの設定を変えたり、DBにデータをつっこんだりしましょう。

アプリ側やコンテナ等の細かい設定変更

以下は、プロジェクトで僕がやった手順をまとめただけです。

  • Nuxt.jsでリクエストするエンドポイントを指定
  • GO側で見にいくDBを指定(toml管理)
  • コンテナのlocale設定

※人によって手順が違うところもあると思うので、容赦無く読み飛ばしてもらえると嬉しいです。

Nuxt.jsでリクエストするエンドポイントを指定

エンドポイントの指定方法だけ書きます。

index.vue
$axios.$get('http://api:8080')

先ほど、docker-compose.ymlに同じnetworksNuxt.js側とGO側に設定しています。

docker-compose.ymlに書いたAPI側のサービス名「api」を、エンドポイントのホスト名に指定することで、API側にリクエストを届けることが出来ました。

また、エンドポイントのホスト名をlocalhostに指定すると、connect ECONNREFUSED 127.0.0.1:8080のエラーが発生しました。

GO側で見にいくDBを指定(toml管理)

docker-compose.ymlで設定した環境変数「APP_ENV=docker」によって、config_docker.tomlの設定を見にいくようにしています。

見にいくDBをtomlで管理しました。

config_docker.toml
[MySQL]
Host     = "db"
Username = "root"
Database = "project_name"
Password = "password"
database.go
import (
    "os"

    "github.com/BurntSushi/toml"
    _ "github.com/go-sql-driver/mysql"
    "github.com/jmoiron/sqlx"
)

var DB *sqlx.DB

type Config struct {
    MySQL MySQLConfig
}

type MySQLConfig struct {
    Username string
    Password string
    Host     string
    Database string
}

func init() {
    var config Config

    var configFilePath string
    basePath := os.Getenv("APP_PATH")

    env := os.Getenv("APP_ENV")

    switch env {
    case "production":
        configFilePath = basePath + "config/config_prd.toml"
    case "staging":
        configFilePath = basePath + "config/config_stg.toml"
    case "development":
        configFilePath = basePath + "config/config_dev.toml"
    case "test":
        configFilePath = basePath + "config/config_test.toml"
    case "docker":
        configFilePath = basePath + "config/config_docker.toml"
    default:
        configFilePath = basePath + "config/config_dev.toml"
    }

    _, err := toml.DecodeFile(configFilePath, &config)
    if err != nil {
        panic(err.Error())
    }

    dbConfig := config.MySQL.Username + ":" + config.MySQL.Password + "@tcp(" + config.MySQL.Host + ":3306)/" + config.MySQL.Database + "?parseTime=true"

    db, err := sqlx.Connect("mysql", dbConfig)
    if err != nil {
        panic(err.Error())
    }

    DB = db
}

コンテナのlocale設定

手順は以下の記事を参考にしました。

参考:https://qiita.com/maejima_f/items/4d5432aa471a9deaea7f

現プロジェクトでは中国語の繁体字を利用しているため、その設定をそのまま記事に書きます。

まず、MySQLのコンテナ名を出します。

$ docker ps
CONTAINER ID        IMAGE                     COMMAND                  CREATED             STATUS              PORTS                               NAMES
xxxxxxxxxxxx        mysql:5.7                 "docker-entrypoint.s…"   25 hours ago        Up 12 minutes       0.0.0.0:3306->3306/tcp, 33060/tcp   project_name
_db_1

次に、Dockerコンテナの中のシェルに入りましょう。

$ docker exec -it project_name_db_1 bash

中国語繁体字(zh_TW)のlocaleを追加します。

$ apt-get update
$ apt-get install locales locales-all
$ dpkg-reconfigure locales
$ export LANG=zh_TW.UTF-8

追加されたlocaleを確認してみましょう。

$ locale
LANG=zh_TW
LANGUAGE=
LC_CTYPE="zh_TW"
LC_NUMERIC="zh_TW"
LC_TIME="zh_TW"
LC_COLLATE="zh_TW"
LC_MONETARY="zh_TW"
LC_MESSAGES="zh_TW"
LC_PAPER="zh_TW"
LC_NAME="zh_TW"
LC_ADDRESS="zh_TW"
LC_TELEPHONE="zh_TW"
LC_MEASUREMENT="zh_TW"
LC_IDENTIFICATION="zh_TW"
LC_ALL=

INSERT文で適当なクエリを投げてちゃんと入力できていたら完了です。

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

【初学者向け】セキュリティ対策入門①〜XSS編〜

前提

確認環境

以下と同様です。
【初学者向け】セキュリティ対策入門⓪〜環境構築編〜

本シリーズの目的

以下と同様です。
【初学者向け】セキュリティ対策入門⓪〜環境構築編〜

本記事の目標

XSS(クロスサイトスクリプティング)の概要、原因、対策について理解することです。

本記事を読み進める上での必要事項

以下の内容を終えていることです。
【初学者向け】セキュリティ対策入門⓪〜環境構築編〜

概要

XSS(クロスサイトスクリプティング)とは

不正なスクリプトを混入・実行させることです。掲示板などエンドユーザからの入力値をWebページに表示させる機能を持ったWebアプリケーション全般で行われる可能性があります。

これだけだとわかりにくいと思いますので実際に見てみましょう。

実際に体験してみよう

本シリーズで使うコンテナを起動して
http://localhost:8080/xss/bad.php?name=J
にアクセスしてみましょう。

『こんにちは、Jさん』とブラウザに表示されていると思います。これはnameパラメータの内容をもとに挨拶文言を表示させているものです。ソースコードはtutorial-php-security/www/html/xss/bad.phpにあります。

tutorial-php-security/www/html/xss/bad.php
<?php
$name = $_GET['name'];
?>

<p>こんにちは、<?= $name; ?>さん</p>

$_GET['name']でクエリストリングスのnameパラメータを参照しています。これが上述するところの『エンドユーザからの入力値』の一例です。クエリストリングスだけではなく、POST送信で渡ってきたデータやDBに格納されたデータを出力する場合も、エンドユーザの入力によるものであれば同様です。

次に
http://localhost:8080/xss/bad.php?name=<script>alert(1)</script>
にアクセスしてみましょう。

以下2点が重要です。

  1. ダイアログが表示
  2. ダイアログで『OK』をクリックしたのち『こんにちは、さん』と表示

何が起きているかというとnameパラメータの<script>alert(1)</script>がスクリプトと見なされてJavaScriptのコードが実行されてしまっているのです。alert()はダイアログを表示させるJavaScriptの関数です。

今回は単にダイアログが表示されただけなので大したことがないと思うかもしれません。しかし、別のサーバにリダイレクトさせられたりCookie情報を盗まれてしまったり様々な被害が生じてしまいます。

原因

不正なスクリプトを表示させていることです。これを避けるにあたって様々な対策があります。その一部を以下で具体的に紹介していきます。

対策

エスケープする

以下にアクセスしてみましょう。

http://localhost:8080/xss/good.php?name=<script>alert(1)</script>

bad.phpではなくgood.phpにアクセスしていることに注意しましょう。実際にアクセスすると『ダイアログが表示されない』『「こんにちは、<script>alert(1)</script>さん」と表示されている』ことがわかると思います。

good.phpではある処理を追加しています。それを次に確認しましょう。

tutorial-php-security/www/html/xss/good.php
<?php
$name = $_GET['name'];
?>

<p>こんにちは、<?= htmlspecialchars($name, ENT_QUOTES | ENT_HTML5, 'UTF-8'); ?>さん</p>

ポイントはhtmlspecialchars()の部分です。これはエスケープするための関数です。エスケープとは<>などHTMLにおいて特殊な意味を持つ文字列をそれぞれ&lt;&gt;など安全な文字列に置き換えることです。詳細は『文字参照』などで検索してみてください。&lt;は画面上では<と表示されるため『こんにちは、<script>alert(1)</script>さん』と表示されているわけです。

そもそも出力しない

エスケープが機能しない場合もあります。たとえば

<script><?= $js_code; ?></script>
<a href=<?= $link_url; ?>>危ないリンク</a>

というケースを考えてみましょう。

前者ではalert(2)という文字列が$js_codeに格納されている場合、エスケープしたところで意味がありません。bad.phpと同様ダイアログが表示してしまいます。

後者ではJavaScript:alert(3)という文字列が$link_urlに格納されている場合、エスケープしたところで意味がありません。bad.phpと同様ダイアログが表示してしまいます。ちなみにJavaScript:XXXのような記述をJavaScript擬似プロトコルといいます。JavaScript擬似プロトコルで記述できる箇所ではエスケープが意味を成しません。

こういう場合では、ユーザが入力したデータをそのまま出力しない仕様にするのが得策です。

入力チェックを行う

ユーザの入力をDBに格納するときやユーザの入力データを出力する際に、入力チェックを行うことも有効です。極端な例ですが、nameパラメータに12という文字列以外入力を認めないようにバックエンドでチェックすればスクリプトは混入しなくなります。ちなみに、フロントエンドでの入力チェックやHTMLタグの属性などエンドユーザ側で書き換えできてしまうチェック方法は意味がないので気をつけてください。

ライブラリを使う

ブログアプリケーションを作成するときのように、ユーザにタグ入力を認める機能を実装する場合は、ユーザの入力データからスクリプトだけを上手く取り除くロジックが必要です。それを0から作るのはあまり現実的ではないためライブラリを導入ことが検討に値する解決策となります。

一例ですがHTML Purifierというものがあります。

参考文献

独習PHP 第3版

今回の内容は以上です。最後までご覧いただきありがとうございました。

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

Dockerで立ち上げたDjangoの内蔵サーバに同LAN上の別PCからアクセス

背景

LAN内PC上のDjangoサイト(Django内蔵サーバ)に同LAN内別PCからアクセスしたい。

環境

・Windows 10 Enterprise
・Docker for Windows 2.1.0.1
・Django 3.0(Dockerコンテナ)
・MySQL 5.7(Dockerコンテナ)
・プロキシあり

参考

こちらの記事の内容で行けました↓
Docker for Windowsのコンテナに同一ネットワークのスマホからアクセス

ファイアウォール設定が阻んでいた様です。

ポートフォワードはdocker-composeファイルで設定しているので無視。

上記設定後、サイトにアクセスするとエラー。
「Invalid HTTP_HOST header~」

これを調べると、setting.py中の「ALLOWED_HOSTS」が空白になっている事が原因だった。
「= ['*']」に変更する事でアクセス出来た。

参考:Django: 解決法 Invalid HTTP_HOST header

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

コンテナの起動とログインについて調べてみたメモ

はじめに

前回はdockerfileを作成してイメージ作成からapacheの起動までをやってみました。
その際いろいろ調べていて、起動やコンテナへのアクセスで不明点が結構あったので詳しく調べてみた内容をまとめたいと思います。

docker runしたのに起動しない!?

ubuntuのイメージを使ってみます。
イメージ名を指定してrunします。

$ docker run ubuntu
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

確認してみると、起動していません。
あれ?
-aオプションですべてのコンテナを確認してみます。
ありますね...

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
d1d595f53e6b        ubuntu              "/bin/bash"         14 seconds ago      Exited (0) 13 seconds ago                       funny_wozniak

STATUS欄がExted(0)...???
docker startしてみます。

$ docker start d1d595f53e6b
d1d595f53e6b
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

う・ご・か・な・い!!

動かない:pray_tone1:
なぜでしょうか。

docker run -it

いろいろ調べてみると、runするタイミングで-itオプションを付与していないのが原因らしい。
なにこれ。

$ docker run -d -it ubuntu
4af17d141f3e5f037539a49ee367785abda784d8cff501644820de7d1fdb9009

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
4af17d141f3e        ubuntu              "/bin/bash"         4 seconds ago       Up 4 seconds                            boring_hamilton

-i →標準入力を開いたままにする
-t →擬似ttyに接続。
-dはコンテナを作成後、コンテナプロセスの標準入出力から抜けておく感じのオプション。なのでいったん置いときます。

これをつけると問題なく起動して停止、起動も問題なくできました。

ログイン方法で挙動が違う。attachとexec

以下の記事を参考にさせていただきました。
@RyoMa_0923さん
Dockerコンテナ内で操作 attachとexecの違い

docker attach

コンテナ上で起動しているPID=1の標準入出力(STDIN/STDOUT)に接続するらしい。
attachはコンテナで立ち上がってるPID=1の標準入出力に接続する。
よってexitするとコンテナもろとも停止する模様。(恐ろしいですね)

docker exec

docker exec はコンテナ内で新たなプロセスを実行する。
よってexitを実行してもコンテナ自体は停止しないみたいです。

いつもの癖でexitで抜ける人はexecを使うほうがいいかもしれないなって思いました。

前回記事ではまったコンテナ起動してもapachのテストページが表示されな問題

dockerfileから作成したapacheのイメージを使用してコンテナを作成して起動します。

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
apache              latest              b5f41134a3a1        5 minutes ago       599MB
ubuntu              latest              549b9b86cb8d        3 weeks ago         64.2MB

$ docker run -d -p 80:80 apache /sbin/init
1fdab23f462e139188cf552d17ae88d8c4137f12e9ab9b4d50393264ec225ce8

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                NAMES
1fdab23f462e        apache              "/sbin/init"        9 seconds ago       Up 9 seconds        0.0.0.0:80->80/tcp   dazzling_pare

ちゃんと起動してますね。
実際にアクセスしてみますが、このサイトにアクセスできません。と例の悲しい表示がでます。

調べてでてきた謎のオプションを付与して再度コンテナを作成、起動します。

$ docker run --privileged -d -p 80:80 apache /sbin/init
8d83edca0037bf9a11c097084e2940a92dc3c8473d5c99c7a5e5faf1439bdbdb

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                NAMES
8d83edca0037        apache              "/sbin/init"        6 seconds ago       Up 6 seconds        0.0.0.0:80->80/tcp   admiring_varahamihira

起動までは問題なくできました。
ブラウザからアクセスしてみると、テストページが表示されました!!
変わったことといえば謎の「--privileged」これなに?
調べてみました。

docker run --privileged

dockerコンテナはデフォルトでは権限がないため、コンテナ中のデーモンを動かすことができないみたいです。
そこで使用するのが--privileged
これを使うとdockerはホスト上のすべてのデバイスに対して接続可能となるようです。

まとめ

  • docker run して起動せず、docker startもできない場合はオプション問題なのかも。
  • attachとexecは同じログインでも挙動が全く別物
  • コンテナはデフォルトではデーモンを動かすことができない。

以上です。調べながらもやや不明な点があったりもしたので間違ってるところなどあったらご指摘いただけると幸いです。

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

Dockerコンテナにパッケージ管理ツールpoetryを使って分析環境を構築する方法

昨年のとあるアドベントカレンダーでPythonのパッケージ管理ツールの一つpoetryが紹介されていました。
2020 年の Python パッケージ管理ベストプラクティス
普段業務で使っているのはpipenvなのですが、パッケージを作るのにsetup.pyとかその他諸々のファイルが必要で面倒だったり、pipfile.lockの作成に異常に時間がかかったりしていてストレスを感じていましたが、poetryだとそられがいい具合に解決されている?とのことだったので試してみました。
poetryを使ってDockerコンテナに分析環境を構築したので、その方法をメモします。
コードはこちらにあげています。

解説

ディレクトリ構造は以下の通りです。

tree
.
├── Dockerfile
├── README.md
├── docker-compose.yml
├── notebook # EDAで使用するnotebook
│   └── eda.ipynb
├── poetry.lock
├── pyproject.toml
├── src # パッケージ化したいモジュール
│   ├── __init__.py
│   └── helloworld.py
└── tests # テストコード
    ├── __init__.py
    └── test_helloworld.py

設定ファイル群Dockerfiledocker-compose.ymlpyproject.tomlは以下の通りです。

pyproject.toml

pandasnumpysklearnmatplotlibといったパッケージを入れています。
また、開発用にpytestも入れています。
※他パッケージの追加方法に関しては後述

pyproject.toml
[tool.poetry]
name = "poetry_docker"
version = "0.1.0"
description = ""
authors = ["yolo-kiyoshi"]

[tool.poetry.dependencies]
python = "^3.7"
sklearn = "^0.0"
jupyterlab = "*"
pandas = "*"
numpy = "*"
matplotlib = "*"
seaborn = "*"
japanize-matplotlib = "*"

[tool.poetry.dev-dependencies]
pytest = "^5.2"

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

Dockerfile

poetryはデフォルトで仮想環境venvを構築しますが、Dockerコンテナにわざわざvenvを構築する必要はないので、poetry config virtualenvs.create falseによってコンテナ上に直にパッケージをインストールするようにしてます。
その後、poetry installによってpoetry.lockの依存関係をみながらパッケージをインストールします。

Dockerfile
FROM python:3.7-slim

ENV PYTHONUNBUFFERED=1

WORKDIR /app

COPY poetry.lock pyproject.toml ./

RUN pip install poetry

RUN poetry config virtualenvs.create false \
  && poetry instal

docker-compose.yml

portsでローカルport8888とコンテナport8888を接続し、commandでjupyterを起動します。

docker-compose.yml
version: '3'
services:
  eda:
    build: ./
    user: root
    volumes:
      - ./:/app
    working_dir: /app
    ports:
      - "8888:8888"
    command: jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.token='' --port=8888

使い方

ソース一式をローカルにcloneします。

git clone https://github.com/yolo-kiyoshi/poetry_docker.git

ビルドとコンテナ起動

以下のコマンドによりDockerイメージのビルドとコンテナを起動できます。

docker-compose up -d

Dockerfileを変更しリビルドしたい場合、docker-compose up -d --buildを実行します

jupyterへのアクセス

コンテナ実行後、以下にアクセスすることでJupyter labにアクセスできます。

http://localhost:8888/lab

テスト実行

以下のコマンドでtests/配下のテストコードを実行できます。

pytest

pytestの詳しい使い方は以下がわかりやすいです。
 pytest ヘビー?ユーザーへの第一歩

パッケージの追加

以下のコマンドでPythonパッケージを追加できます。

poetry add <追加したいパッケージ>

感想

poetry 使いやすいと思います。
パッケージ作成が容易になったのもいいのですが、何よりpipenvよりも.lockファイルを作成にかかる時間が短いなと感じます。(定量的にはみていません、感覚的にです)
今年はバンバンpoetryを使って開発していこうと思いました。

参考

Integrating Python Poetry with Docker

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

Golang on DockerでEchoを動かす

TL;DR

Docker上でGolangのWebフレームワークであるEchoを動かしてみました。

DockerでGolang環境の構築

FROM golang:1.13.6-alpine

WORKDIR /go/src

COPY ./src /go/src

RUN apk update && apk add git
#RUN go get -u github.com/labstack/echo/...

dockerfile内のgo get -u github.com/labstack/echo/...が実行出来なかったのは何故だろう。。。

docker-compose.yml
version: '3'
services:
  app:
    build: .
    ports:
      - 8080:8080
    volumes:
      - ./src:/go/src
    tty: true
$ mkdir src
src/server.go
package main

import (
    "net/http"
    "github.com/labstack/echo"
)

func main() {
    e := echo.New()
    e.GET("/", func(c echo.Context) error {
        return c.String(http.StatusOK, "Hello, Echo World!!")
    })
    e.Logger.Fatal(e.Start(":8080"))
}
$ docker-compose build
$ docker-compose up -d

Echoのインストール

https://echo.labstack.com/
https://echo.labstack.com/guide

$ docker-compose exec app go get -u github.com/labstack/echo/...
$ docker-compose exec app go run server.go

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.1.13
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
⇨ http server started on [::]:8080

スクリーンショット 2020-01-13 15.07.08.png

出来ました:beer:
本当はdocker-compose up -dだけでEchoの実行までいきたいんですがね。。。

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

Apache Guacamoleをdocker-composeで構築する

感想

  • ブラウザだけあればリモートアクセスが出来そう(利用者がうれしい)
  • 操作ログや画面操作を動画で残すことが出来そう(管理者がうれしい)
  • docker版だとTomcatとか諸々のライブラリのインストールを考えなくて良くて構築が楽っぽい(自分がうれしい)

Apache Guacamoleとは

OSSのリモートアクセスゲートウェイソフトで、クライアントはHTML5対応のWEBブラウザだけで利用できる。使える接続プロトコルはVNC,RDP,SSH,TELNETがあり、Apache License、Version 2.0が適用されたOSS。またTOTPによるMFA(個人的に最近気に入っている)もできる。詳細はこちら公式にて。

構成イメージ

スクリーンショット 2020-01-13 10.02.48.png
※画像は公式より。

きっかけ

仕事で使っているリモートアクセス環境が、Windows Server 2008サポート終了のあおりを受けて更新しないといけなくなった。この際なのでセキュリティレベルを確保しつつ、コストを抑えた構成を考えようと思いついた。Windows OSからLinuxへ、有償シンクラソフトからOSSのApache Guacamoleへ。あわせてWEBブラウザだけでクライアント側に専用ソフトが不要なので、今まで温めていたアイデアが実現出来そうと言う淡い期待があった。今回はその際に実施した検証メモ。

前提

・環境
 検証用環境

・OS
 Ubuntu 18.04

・その他
 Docker and Docker-Compose はインストール済み。

適当にdocker-compose.ymlを作る

公式ドキュメントの手順を読むと、3種類のコンテナイメージを起動する必要がありそうなので、docker-composeで構築することにした。※以下のコンテナの役割は公式をGoogle翻訳。

guacamole/guacd

リリースされたguacamole-serverソースから構築されたguacdデーモンを提供し、VNC、RDP、SSH、およびtelnetをサポートします。

guacamole/guacamole

Tomcat 8内で実行されているGuacamole WebアプリケーションにWebSocketのサポートを提供します。guaker、MySQL、PostgreSQL、LDAPなどに接続するために必要な構成は、Dockerリンクまたは環境変数に基づいてイメージが開始されるときに自動的に生成されます。

mysql または postgresql

Guacamoleが接続構成データの認証と保存に使用するデータベースを提供します。

docker-compose.ymlの中身

本番環境はもうちょっと考える必要がある。
nginxはとりあえず的な感じで。

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

  postgres:
    image: postgres:latest
    restart: unless-stopped
    environment:
      PGDATA: /var/lib/postgresql/data/guacamole
      POSTGRES_DB: guacamole_db
      POSTGRES_PASSWORD: guacamole1234567890
      POSTGRES_USER: guacamole_user
    volumes:
      - ./pginit:/docker-entrypoint-initdb.d
      - ./pgdata:/var/lib/postgresql/data

  guacd:
    image: guacamole/guacd:latest
    restart: unless-stopped

  guacamole:
    image: guacamole/guacamole:latest
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      GUACD_HOSTNAME: guacd
      POSTGRES_DATABASE: guacamole_db
      POSTGRES_HOSTNAME: postgres
      POSTGRES_PASSWORD: guacamole1234567890
      POSTGRES_USER: guacamole_user
    depends_on:
      - postgres
      - guacd

  nginx:
    image: nginx:latest
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d

default.confの中身

ディレクトリを作って、そこに置く。

# mkdir -p ./nginx/conf.d
default.conf
server {
    listen       80;
    server_name  localhost;

    location /guacamole/ {
        proxy_pass http://guacamole:8080/guacamole/;
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

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

初期化用のSQL作成

PostgreSQLは公式Dockerイメージの初期化機能を使います。初期化スクリプト(.sql or .shとか?)が/docker-entrypoint-initdb.dディレクトリ内で見つかった場合に1度だけ実行されるらしいので、そこに作成しておく。

# mkdir ./pginit
# chmod -R +x ./pginit
# docker run --rm guacamole/guacamole /opt/guacamole/bin/initdb.sh --postgres > ./pginit/initdb.sql

起動

# docker-compose up -d
# docker-compose ps
        Name                      Command               State           Ports         
--------------------------------------------------------------------------------------
gucamole_guacamole_1   /opt/guacamole/bin/start.sh      Up      0.0.0.0:8080->8080/tcp
gucamole_guacd_1       /bin/sh -c /usr/local/guac ...   Up      4822/tcp              
gucamole_nginx_1       nginx -g daemon off;             Up      0.0.0.0:80->80/tcp    
gucamole_postgres_1    docker-entrypoint.sh postgres    Up      5432/tcp 

# docker-compose logs -f

動作確認

http://<IPアドレス or ホスト名>/guacamole/

ユーザー名:guacadmin
パスワード:guacadmin

でログイン。

スクリーンショット 2020-01-13 13.35.36.png

TOTPの設定

公式(ver1.0.0の場合)からTOTP用 jarファイルをダウンロードししてくる。その後、${GUACAMOLE_HOME}/extensions ディレクトリに置いて起動させる必要がある。なのでdocker-compose.ymlファイルをちょっと編集し3行追加。

docker-compose.yml(抜粋)
  guacamole:
    image: guacamole/guacamole:latest
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      GUACD_HOSTNAME: guacd
      POSTGRES_DATABASE: guacamole_db
      POSTGRES_HOSTNAME: postgres
      POSTGRES_PASSWORD: guacamole1234567890
      POSTGRES_USER: guacamole_user
      GUACAMOLE_HOME: /etc/guacamole  ⬅️①
    volumes:                          ⬅️②
      - ./guacamole:/etc/guacamole    ⬅️③
    depends_on:
      - postgres
      - guacd

ディレクトリを作る。

# mkdir -p ./gucamole/extensions

ダウンロード&解凍したTOTP用jarファイルをコピー。

# cp ~/Downloads/guacamole-auth-totp-1.0.0/guacamole-auth-totp-1.0.0.jar ./guacamole/extensions

再起動

docker-compose down
docker-compose up

初回 or 次回の認証後、登録画面が表示されます。あとは‎「Google Authenticator」や‎「IIJ SmartKey」で登録しておくと、次回以降は6桁のコード入力を求められます。

スクリーンショット 2020-01-13 14.11.43.png

ちょっとハマったとこ

ユーザにパスワード変更権限を付与しておかないとこのTOTPが有効にならないようです。

スクリーンショット 2020-01-13 13.55.07.png

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

Dockerの手ほどき

Dockerとは

簡単に軽量な仮想マシンを作ることができます。
コンテナ型のアプリケーション実行環境です。
※コンテナについてはこちらの記事がわかりやすいです!

そもそも仮想マシンとは?
仮想マシンとは1台のコンピュータで複数のコンピュータを動かす技術です。
手元にあるMacの中にWindows10やCentOSなど沢山のOS入るイメージです。

なぜ仮想マシンを使うの?
実際に動いているWebサービスは「本番環境」で稼働しています。
開発時は実際に動いているものを触る訳にはいかないので、開発用の環境を準備します。
※開発環境は本番環境と一緒の設定にする必要があります。

理由1: バージョンが違うとエラーが吐かれる等障害の元になる。
理由2:「本番環境しか再現しない」という現象が起き得る。

VirtualBox+Vagrantを使って仮想環境を作ることは可能ですが、

・メモリを沢山消費してしまう
・個別にソフトなどインストールをするなど手間がかかります。
(例:ソースコード・DBサーバーなど・・・)

→容易にしたものがDockerです。

Dockerの知識

Docker は Dockerfile から命令を読み込み、自動的にイメージを構築できます。

Dockerの基本用語 意味・役割
Image コンテナを構成するアプリケーション、設定に関する情報をまとめたもの。
コンテナ Imageをもとに具現化したアプリケーションの実行された環境。
DockerFile イメージの構成を定義したファイル。命令(インストラクション)を書いていきます。

Docker Fileの書き方

DockerFileはbuildでファイルに書かれた命令を順次実行してイメージを自動構築します。
あくまで「イメージを構築するための手順」です。

1コンテナ1DockerFileです。

FROM    : ベースとなるDockerImageを指定。(一番最初に書くこと!)
RUN     : Build時にコンテナ内で実行するコマンドを定義
WORKING : ディテクトの指定
COPY    : コンテナの中へファイルを送るコマンド。
ADD     : コンテナの中へファイルを送るコマンド。COPYにはないファイル展開機能がある。
ENV     : 作ったコンテナ内のみで使える環境変数の設定

※DockerBuildした時、まずFROMで指定したImageを Downloadします。

Dockerコンテナのライフサイクル

実行中
・ DockerFileをもとに定義されている命令を実行するとなる。
・ 基本的には異常終了するまで実行し続ける。

停止
・ユーザーが明示的にSTOPするとなる。
・実行されているアプリケーションが終了した時に自動停止する。

廃棄
・ 完全に不要になったら廃棄をしましょう(残ったままだとディスクを専有するため。)

複数のコンテナを立ち上げる

仕事で開発をする際、一般的には複数のコンテナを立上げる必要があります。
コンテナを立上らせる「docker build」を何度も実行するのは効率が良くありません。
そんな時はDocker composeという機能を利用します。

docker psでコンテナの情報を確認する。

項目名 コンテナのステータス内容
CONTAINER ID コンテナに付与される一意のID
IMAGE コンテナ作成に使われたDockerImage
COMMAND コンテナで実行されるアプリケーションのプロセス
CREATED コンテナが作成されてから経過した時間
STATUS コンテナの実行状況。up(実行中)、Exited(終了)
PORTS HostPortとコンテナPortの組みづけ
NAMES コンテナ付けられた名前

Docker composeとは

複数のコンテナをコードで管理します。
DB用、アプリケーション用など複数のコンテナからなるサービスを簡単に自動的に構築できます。

Docker composeを使う3-Step

  1. DockerFileを準備
  2. docker-compose.ymlを作り、コンテナの起動定義を書きます
  3. docker-compose up を実行(docker-compose.ymlに書かれたコンテナが起動します)

docker-compose.ymlの書き方

docker-compose.ymlを書く時、よく使うものなどをまとめてみました。

SERVICE : 
 hoge_name      : サービス名 
 image          : 使用するDockerImage
 links          : コンテナを他サービスへ繋げる
 container_name : コンテナ名を指定する(ない場合が自動的に命名される)
 enviroment     : 環境変数を追加
 volumes        : マウントするディレクトリ
 ports          : exportするディレクトリ(ホストポート=コンテナポート)

docker-compose コマンド

build    : docker-compose.ymlで書いたserviceで指定したビルドする
restart  : サービスの再起動をする
run      : サービスを起動
up       : サービスのimageをビルド・起動(build+run)
exec     : サービスに対してコマンド実行
logs     : サービスのログを出力

※コンテナIDを指定してdocker-conpose upすると、特定コンテナだけ起動することができます。

イメージ構築 コンテナ構築 コンテナ起動
build × ×
up
start × ×
run
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerのコマンドをVagrantのコマンドと並べて理解する

はじめに

今までVagrantを使っていてこれからDockerを学びたい人(自分を含む)向けにVagrantとDockerのコマンドを機能別に対応関係をまとめました。
厳密に言うと異なりますが、概要の理解に繋がれば幸いです。
まだDockerの勉強中の身なので、アドバイス等あればコメントでお待ちしています。

Vagrantとは

仮想環境を管理するためのソフトウェア

参考

Dockerとは

コンテナ型の仮想環境を管理するソフトウェア

参考

概念の対応関係

Vagrant Docker
ホストOS Linux,Windows,mac OS etc. Linux
扱う仮想化技術 ホストOS型 コンテナ型
仮想環境ベース パッケージ イメージ
仮想環境 Virtual Machine(VM) コンテナ
設定ファイル Vagrantfile dockerfile

コマンドの対応関係

ここではオプションは割愛します。
Docker v1.13以降でコマンド体系が変わったようなので可能な限り新旧どちらのコマンドも記載します。旧コマンドは「何を」操作しているか見えなかったのが、新コマンドは「何を」の部分が必要になったためVagrantの対応関係も比較的わかりやすいです。

仮想環境ベースの操作

操作 Vagrant Docker
追加 vagrant box add (新)docker image pull
(旧)docker pull
一覧 vagrant box list (新)docker image ls
(旧)docker images
作成 vagrant package (新)docker image build
(旧)docker build
削除 vagrant box remove (新)docker image rm
(旧)docker rmi

仮想環境の操作

操作 Vagrant Docker
初期化 vagrant init -
起動 vagrant up (新)docker container start
(旧)docker start
一覧 vagrant global-status (新)docker container ls
(旧)docker ps
終了 vagrant halt (新)docker container stop
(旧)docker stop
削除 vagrant destroy (新)docker container rm
(旧)docker rm
ssh接続 vagrant ssh (新)docker container exec -it {コンテナID} bash
(旧)docker exec -it {コンテナID} bash

参考

まとめ

  • Vagrantのpackage = Dockerのimage
  • VagrantのVM = Dockerのコンテナ
  • Dockerの新コマンドと一緒に覚えると分かりやすい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker を使う(docker swarm でアプリケーションをデプロイする)

前回の記事 の続きです。
ようやく完成しました・・・
今回は、コンテナオーケストレータの一つである Docker Swarm を使い、アプリケーションをデプロイしてみます。
アプリケーション開発からやっていてはいくら時間があっても足りないので、事前に用意しました。
GitHub にて公開していますので、 Clone もしくはダウンロードをお願いします。
https://github.com/landwarrior/flask-restful
こちらをベースに話を進めていきます。

アプリケーション仕様

今回用意したアプリケーションを箇条書きで説明します。

  • サーバーは 3 台必要
    • Docker 用が 2 台、 MySQL 用が 1 台
    • Vagrant を使用して VirtualBox 上に 3 台分を構築します
  • アプリケーションは Python で実装
    • Flask-RESTful を使用し、 REST API を構築
  • VM の構築に Chef Infra Client を使用
    • 誰でも同じように構築できるようにするため、 Chef を使用
  • VM の構築の簡略化のため、シェルスクリプトをそれぞれ用意
    • シェルスクリプトがほぼ全部やってくれます
    • 冪等性が保たれているかは微妙です
    • かなりやっつけなので書き方がおかしいところもあるかもしれないけど、気にしない

ということで、説明していきます。

VM の準備

GitHub からまるっと取得いただければ、ほぼ README.md に書いてありますが、ちゃんと説明します。

VM を構築するための前提条件は、以下の通りです。

  • Windows 10 Pro バージョン 1909
  • VirtualBox 6.0.14
  • Vagrant 2.2.6

VirtualBox の最新バージョンは 6.1 ですが、執筆時点で Vagrant の 2.2.6 は VirtualBox 6.1 に対応していないようなので、6.0 系を使う必要があります。
この時点で厄介です。。。?

フォルダの準備

VM は 3 台必要なので、フォルダを 3 つ用意しましょう。
自分の場合は、以下のようにしました。

  • D:\VirtualMachines\Vagrant\flask-restful-primary
  • D:\VirtualMachines\Vagrant\flask-restful_secondary
  • D:\VirtualMachines\Vagrant\mysql-server

フォルダ名は自身が分かればなんでもいいと思います。

Vagrantfile の準備

コマンドプロンプトを立ち上げ、それぞれのフォルダで以下のコマンドを実行します。

vagrant init bento/centos-7.7

それぞれのフォルダ内に作成された Vagrantfile を修正します。
アプリケーションの都合上、 IP アドレスは指定があります。
すでに使用済みの IP アドレスの場合、任意の IP アドレスを指定し、適宜読み替えてください。
アプリケーション上でも IP アドレスを記載している箇所があるので、その点の修正も忘れずに。

primary の場合

  • config.vm.network の設定
    • コメントアウトを外し、
      config.vm.network "private_network", ip: "192.168.33.10"
      とします
  • ホスト名を付ける(必須ではない)
    • config.vm.network の下に、
      config.vm.hostname = "flask-primary"
      を記載します
  • config.vm.synced_folder の設定
    • GitHub から取得いただいたソースを VM 上にマウントするため、
      config.vm.synced_folder "D:/program_src/flask-restful", "/vagrant_data"
      のように指定します(ソース配置場所は適宜読み替えてください)
    • バックスラッシュではなくスラッシュに変更する必要あり

secondary の場合

  • config.vm.network の設定
    • コメントアウトを外し、
      config.vm.network "private_network", ip: "192.168.33.11"
      とします
  • ホスト名を付ける(必須ではない)
    • config.vm.network の下に、
      config.vm.hostname = "flask-secondary"
      を記載します
  • config.vm.synced_folder の設定
    • GitHub から取得いただいたソースを VM 上にマウントするため、
      config.vm.synced_folder "D:/program_src/flask-restful", "/vagrant_data"
      のように指定します(ソース配置場所は適宜読み替えてください)
    • バックスラッシュではなくスラッシュに変更する必要あり

mysql-server の場合

  • config.vm.network の設定
    • コメントアウトを外し、
      config.vm.network "private_network", ip: "192.168.33.20"
      とします
  • ホスト名を付ける(必須ではない)
    • config.vm.network の下に、
      config.vm.hostname = "mysql-server"
      を記載します
  • config.vm.synced_folder の設定
    • GitHub から取得いただいたソースを VM 上にマウントするため、
      config.vm.synced_folder "D:/program_src/flask-restful", "/vagrant_data"
      のように指定します(ソース配置場所は適宜読み替えてください)
    • バックスラッシュではなくスラッシュに変更する必要あり

vagrant up していく

Vagrantfile の修正が終わったので、順次 vagrant up していきましょう。
コマンドプロンプトでそれぞれのフォルダに cd して vagrant up を実行するだけですね。

環境構築

ここを簡略化するために、シェルスクリプトを用意しました。
説明が大変なので・・・
それぞれの環境ごとに異なるシェルスクリプトを実行します。

primary の場合

VM にログインしたら、以下を実行しましょう。

sudo /vagrant_data/provisioning_primary.sh

諸々インストールし、 chef で docker をインストールし docker build までやってくれます。
最新バージョンが入るので、将来的に動かなくなることもありそう?

Docker Swarm の構築までやってくれちゃいますが、コマンドとしては以下を実行しているだけです。

docker swarm init --advertise-addr 192.168.33.10

これは、 Docker Swarm のリーダーとなるサーバーでのみ実行します。
オプションの意味は、よく分かっていません?
リーダーとなるサーバーの IP アドレスを指定すればちゃんと動くので、あまり気にしていませんね。。。

docker swarm init の後に、以下のコマンドも実行しています。

docker stack deploy --with-registry-auth -c /var/app/docker/docker-local.yml test

これは、 docker swarm 上でサービスを起動するときのコマンドです。
どのようにコンテナを起動するかを docker-local.yml で定義し、指定しています。
また、最後の test は、 docker swarm で立ち上げた際のサービス名です。
これがコンテナ名の先頭につきます。

だいぶ端折り気味ですが、セカンダリの説明に移ります。。。

secondary の場合

以下のコマンドを実行します。

sudo /vagrant_data/provisioning_secondary.sh

すでに docker swarm のリーダーはいるので、こちらは join する側になります。
join するためには、リーダーでトークンを発行する必要があります。
そのため、 primary の方で以下のコマンドを実行しましょう。

docker swarm join-token worker

そうすると、コマンドが表示されるかと思います。
そのコマンドをそのままコピペして実行しましょう。
実行すると、自動でコンテナが立ち上がります。
docker ps コマンドで確認できるかと思います。

ただし、 docker イメージはローカルにある必要があるため、シェルスクリプト内でこちらも docker build をしています。
Docker Hub のように外部にイメージがあり、そこから pull できる形であれば、ローカルで docker build しておく必要はないはず・・・?

docker swarm の状態を確認

primary の方で以下のコマンドを実行してみましょう。

docker node ls

すると、 secondary のサーバーが docker swarm の node として追加されているのが確認できます。
また、以下のコマンドでサービスの状態を確認可能です。

docker service ls

コンテナが片肺になっているかどうかなどを確認する時に使ったりします。

docker stack deploy したのにコンテナがうまく起動できないな、という時は、以下のコマンドで確認することができます。

docker stack ps test --no-trunc

これは、 test という名称で立ち上げた docker swarm のサービスのコンテナの状況を確認するコマンドです。
起動に失敗しているログが見えたりしますが、 docker swarm は起動できるまでコンテナの再起動を繰り返すので、多少のエラーは気にしなくても大丈夫です。

docker swarm で構築されたネットワークを確認するためには、まずネットワークの一覧を取得してみましょう。

docker network ls

デフォルトでは、 サービス名_default という名称でネットワークが作成されます。
今回の例では、 test_default となります。
では、このネットワークの詳細を確認しています。

docker network inspect test_default

ネットワークに所属するコンテナが見れます。
なお、 docker swarm で構築されたネットワークはデフォルトではアタッチ不可のため、 docker stack deploy した時に起動されたコンテナ以外はネットワークに入れません。
後でネットワークにコンテナを追加したい場合は、 docker-local.yml でしれっと指定しているのですが、 attachable の設定をする必要があります。
ただし、アタッチ可能にした場合、 docker run でネットワークを指定し、アタッチした際にネットワークの再構築が行われるらしく、瞬断が発生することがあるようです。
そのため、アタッチ可能にすることは非推奨かもしれません。

今度は、コンテナの詳細情報を見てみましょう。
以下のようなコマンドで確認可能です。

docker service inspect test_flask

簡略表示したい場合は、 --pretty オプションを付けます。

コンテナのログを見る場合は、以下のコマンドで確認します。

docker service logs -f test_flask

docker-local.yml で log-driver の指定をすれば、上記コマンドでなくてもログを指定した方法で転送可能ですが、ここでは取り扱いません。
そこまでの時間はありませんでした。。。
よく使われるのは、 fluentd とかですかね。

だいぶ書きなぐってしまいましたが、まだ MySQL の VM を構築してないので次に進みます。

mysql-server の場合

以下のコマンドを実行します。

sudo /vagrant_data/provisioning_db.sh

MySQL をインストールし、 mydb という DB を構築し、あとはソース上にある init.sql を実行するだけです。
ユーザー追加とテーブル追加をするだけですね。

これで構築完了です。

動作確認

REST API なので、動作確認をしてみましょう。
mysql-server で、 curl コマンドで確認可能です。
例えば、以下のコマンドをそれぞれ実行してみてください。

curl http://192.168.33.10
curl http://192.168.33.10/v1/groups
curl http://192.168.33.10/v1/users

JSON 形式でデータが返ってきたと思います。
チープな機能しか実装していないため、ユーザーとグループの CRUD しかできません。
さらに、ユーザーとグループは n:1 で紐づくようなテーブル構成になっていますが、 REST API の戻り値では特にそこを意識していません。
そこまでは手が回りませんでした・・・?

REST API は、以下のように操作します。

# 全グループを取得
curl http://192.168.33.10/v1/groups
# 指定したグループIDのデータを取得
curl http://192.168.33.10/v1/groups/1
# グループを追加
curl http://192.168.33.10/v1/groups -XPUT -H"content-type:application/json" -d'{"group_name": "group1"}'
# グループを更新
curl http://192.168.33.10/v1/groups/1 -XPOST -H"content-type:application/json" -d'{"group_name": "test_group1"}'
# グループを削除
curl http://192.168.33.10/v1/groups/1 -XDELETE

# 全ユーザーを取得
curl http://192.168.33.10/v1/users
# 指定したユーザーIDのデータを取得
curl http://192.168.33.10/v1/users/1
# ユーザーを追加
curl http://192.168.33.10/v1/users -XPUT -H"content-type:application/json" -d'{"user_name": "user1", "group_id": 1}'
# ユーザーを更新
curl http://192.168.33.10/v1/users/1 -XPOST -H"content-type:application/json" -d'{"user_name": "test_user1"}'
# ユーザーを削除
curl http://192.168.33.10/v1/users/1 -XDELETE

それぞれ動作確認してみてください。

終わりに

ちゃんと動いたら、めでたしめでたし。
ちなみに、 primary を vagrant halt で落としても、 192.168.33.11 の方で動作はできます。
primary を立ち上げなおすと、元通りになるみたいです。
なお、 secondary を worker ではなく manager として join すると、 primary を停止して立ち上げなおすとリーダーと manager が入れ替わりました。
この挙動の違いがどう影響するのかが分かっていません。。。

余談

拙くても、ようやく Docker Swarm まで記事にできました。
実際にこれを試してみようと思われた方、ありがとうございます。
記事にした甲斐がありました。

ただ、職場で試される方は要注意です。
プロキシ環境下で試される方は、 Docker のプロキシ設定が必要です。
以下のディレクトリを作成し、コンフィグファイルを配置する必要があります。

/etc/systemd/system/docker.service.d
proxy.conf
[Service]
Environment="HTTP_PROXY={proxy url}"

ファイル名は適当、 url は適宜設定してください。
また、 docker build 時にも proxy 設定が必要です。

docker build --build-arg HTTP_PROXY={proxy url} --build-arg HTTPS_PROXY={proxy url} -t hoge .

シェルスクリプトを書き換えて実行しましょう。

また、場合によっては SELinux も無効にしないといけないかも。
環境に合わせて構築しましょう。

ということで、ひとまず Docker の記事はこれにて終了です!
お疲れさまでした?

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

docker-composeでRundeckを起動してみる

豆知識Rundeckの404画面はスペース猫

image.png

docker-compose.yml
version: '3'

services:
    rundeck:
        image: rundeck/rundeck:SNAPSHOT
        links:
          - mysql
        environment:
            RUNDECK_DATABASE_DRIVER: com.mysql.jdbc.Driver
            RUNDECK_DATABASE_USERNAME: rundeck
            RUNDECK_DATABASE_PASSWORD: rundeck
            RUNDECK_DATABASE_URL: jdbc:mysql://mysql/rundeck?autoReconnect=true&useSSL=false
            RUNDECK_GRAILS_URL: http://example.com:4440
        volumes:
          - ${RUNDECK_LICENSE_FILE:-/dev/null}:/home/rundeck/etc/rundeckpro-license.key
        ports:
          - 4440:4440
    mysql:
        image: mysql:5.7
        expose:
          - 3306
        environment:
          - MYSQL_ROOT_PASSWORD=root
          - MYSQL_DATABASE=rundeck
          - MYSQL_USER=rundeck
          - MYSQL_PASSWORD=rundeck
        volumes:
          - ./dbdata:/var/lib/mysql
docker-compose up -d

ログイン画面では
Usernameはadmin、Passwordはadminでログインできる

rundeck/rundeck - Docker Hub
Running Docker Images | Rundeck Docs
docker-zoo/mysql at master · rundeck/docker-zoo
docker-zoo/docker-compose.yml at master · rundeck/docker-zoo

rundeckをdocker-composeで使う - Qiita
Rundeckによるジョブスケジューリング - Qiita
ジョブスケジューラ「Rundeck」を試してみる | Developers.IO
必殺ジョブスケジューラ!!Rundeck - Qiita

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

手っ取り早くGUI操作のできるコンテナをつくる

Apache Guacamoleというものを使うとGUI環境の無い踏み台からも楽にGUI操作を行えるようになって便利そうだったため、これを使ってなるべくコストをかけずに簡単にGUI操作を行えるようにする方法を考えてみました。


↑ブラウザの中でブラウザを開いている様子
(正確にはブラウザの中でXが動いてるLinuxのデスクトップを開いて、その中でブラウザを起動している)

TL;DR

  • クローズド環境で外部端末のブラウザから内部のネットワーク内のブラウザを開けるようにするよ
  • DockerでGuacamoleとSeleniumのデバッグ用のコンテナたてるよ
  • ブラウザからWindowsにRDPとかもできるよ

GitHub
https://github.com/sensq/bastion-guacamole

概要

一般的な環境だと需要はほとんど無いと思いますが、本番環境などのクローズドな環境では外部からは踏み台サーバを経由しての接続しか許されておらず、内部のWebアプリケーションやWindowsなどをGUI操作したいときにどうするかで困ったりします。
踏み台サーバにGUI環境が用意されていれば楽ですが、サーバをGUI化すると設計や構築や構成管理のコストが増えたり、いつの間にか便利ソフトで溢れて運用がブラックボックス化したりして嫌なので、ただ踏み台経由でhttpやRDPで接続したいためだけにGUI化をするのはなるべくやりたくないです。

image.png

別にシンプルにポートフォワードで繋ぐなどでも良いのですが、今のコンテナの時代、もっと楽で便利で汎用的なやり方があるのではないかと思い、上記のような環境で手っ取り早くGUI操作を行えるようにする方法を考えます。

Guacamoleの説明

Guacamoleとは、ネットワーク内のサーバへWeb上からRDP/Telnet/SSH/VNCで接続できるようにする正に踏み台のためにあるかのようなOSSのツール。「グアカモーレ」とか「ワカモレ」と読むらしい。
アーキテクチャなどの詳細はこちらのページがわかりやすい。
https://dev.classmethod.jp/cloud/aws/setup-apache-guacamole/

要するに踏み台サーバへGuacamoleを入れれば、外部のPCのブラウザから踏み台のGuacamoleにアクセスしてブラウザからWindowsに接続したり、ブラウザの中でクローズド環境内のブラウザを開いて外部からは見られないページを見たりすることができるようになる。

作るもの

Guacamole自体は結構古く、公式でイメージも提供されており、ググってみるとDockerが流行りだして間もない2016年2017年に既に同様のことをやっている記事が出てくる。
ただ、ブラウザで操作するUIがどんどん一般的になってきている昨今、踏み台からはVNCやRDPで接続するだけでなくブラウザを動かしてhttp(s)で接続したい需要も高まってきていると思われるため、Guacamoleと一緒にブラウザが動くだけのVNCで接続できるコンテナがあれば非常に便利そうである。

image.png

しかし残念ながらGoogleやMozillaはそんなコンテナイメージを提供しておらず、自分でDockerfileを用意してビルドするのは嫌なため、本来の用途とは異なるがSeleniumのデバッグ用のイメージを利用することにする。

https://hub.docker.com/r/selenium/standalone-chrome-debug
https://hub.docker.com/r/selenium/standalone-firefox-debug

このイメージはとても都合がいいことにコンテナを起動すると中でVNC Serverも起動するため、Guacamoleコンテナと一緒に起動させるだけで今回の目的を果たすことができる。
よって、必要なものは既に公式イメージで提供されているものだけで十分なため、作るものは本質的にはdocker-compose.yamlのみでよい。

また、当然だが別に1つの踏み台上で一緒に動かす必要はなく、同一ネットワーク内であればstandaloneのコンテナを別のサーバで動かすことも可能である。そのため、踏み台から直接アクセスできてしまうことを嫌がる場合や、踏み台がネストされているような場合にも対応することができる。

作ったもの

詳細はGitHubを参照
起動手順もこちらを参照
https://github.com/sensq/bastion-guacamole

上述した通り、公式のコンテナを組み合わせて起動するだけのため自分で作る必要のあるものは本質的にはdocker-compose.yamlのみ。ただし、手間を減らすために他にもいくつかファイルを置いている。
全体的になるべく作り込まないように作成しているため、必要があれば適宜カスタマイズしてください。

  • docker-compose.yaml
    • 最初DBはMySQLにしていたが、Postgres:alpineのイメージの方が300MBくらい軽かったのでPostgresを使うようにした
    • postgresコンテナにマウントしているinitdb.sqlinit.shを実行すると生成されるファイルのため、コンテナ起動前に実行しておくこと
      • ※ファイル存在しない状態でコンテナ起動してしまうとinitdb.sqlという名前のディレクトリが作成されてしまうので注意
  • user-mapping.xml
    • Guacamoleの接続設定を記載したデフォルトの設定ファイル
    • どの環境でも毎回同じ設定で作るものなため、特に変更する必要はない
    • これで作成した設定はSettingsのConnectionsに出てこない(DBに保存されていない)
    • ユーザ名とパスワードはデフォルトのguacadmin/guacadminにしているため、変えたかったら変えてもいい(書き換えてコンテナ起動すれば勝手にユーザが作られる)
  • .env
    • docker-compose downでコンテナ落とすときに数分かかるため、エラーにならないようにタイムアウトの値を増やしているだけ
  • init.sh
    • DB初期化用のSQLを生成するコマンドを実行するだけ
    • 普通にguacamoleの構築手順の中に書かれているコマンド
    • ※万が一initdb.sqlというディレクトリを作ってしまった場合はスクリプト実行前にディレクトリ削除すること
  • start_browser.sh
    • standaloneのコンテナ内でブラウザを起動するコマンドを実行するだけ
    • Chromeは普通に初めて起動したときのWelcomeページがやたらCPU使う重いページだったので適当なURLを指定して起動している。クローズドな環境だと404になりそうだが特に問題ないので気にしない。

なお、初回起動のみデータベースの初期化処理が行われ、完了するまではGuacamoleのURLにアクセスしてもログイン画面が出てきません。
数分で完了するはずです。

イメージのサイズ

参考までに2020/1/13時点での各イメージのサイズを載せておきます。合計で約2.5GBです。

           Repository                Tag      Size 
---------------------------------------------------
selenium/standalone-chrome-debug    latest   815 MB
selenium/standalone-firefox-debug   latest   751 MB
guacamole/guacamole                 latest   468 MB
guacamole/guacd                     latest   377 MB
postgres                            alpine   139 MB

MySQLはAlpine版が無く、latestは422MBでした。

使用感

普段遣い用として使うのはストレスたまりそうだが、踏み台の中でのみ使うものとすれば許容範囲な気はする。
まだ検証で試用してみただけなので使い続けていくと色々不満点や改善点が出てくるかもしれない。

参考

Guacamoleを動かすためのdocker-compose.yamlの書き方は以下2つの記事を参考にしました。
本記事よりも詳細にGuacamoleの設定方法などが書かれています。
https://kakakakakku.hatenablog.com/entry/2019/01/09/001605
https://blog.1q77.com/2019/01/guacamole-functions/

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

dockerfileについて調べてapache起動するまでのメモ

はじめに

前回はDockerのコマンド操作を学ぶために主な操作の記事を書かせて頂きました。
他にもいろいろ調べていたらdockerfileなるのもがありそれを使用してイメージを作成するとのことでした。
見た感じ難しそうで抵抗があったのですが、そういうものこそ試していかないといけないと思いやってみましたので備忘録件まとめ用として記事にしてみたいと思います。

dockerfielってなんぞ?

コンテナを作成する場合、docker hubから欲しいイメージを取得してコンテナを作成したり、最小限のイメージから自分で新しくミドルウェアを追加したものをイメージ化して展開したりするとたくさんの情報が出ていましたが、自分でイメージを一から作成する場合などに使用するファイルです。
dockerfileはベースとするイメージに対して行う操作を記載する。

dockerfileを使用する利点

  • OSや各種ミドルウェアなどの設定をコードとして管理できる
  • 上記により細かく設定した環境を漏れなく簡単に再作成したり同一環境を用意したい人に配布できる。
  • jenkinsなどのCIツールを使用することでイメージ作成やコンテナのデプロイを自動化することもできる(らしい)

書き方について

調べてみたのですが結構難しそうでした(笑)
まずは簡単にコードについて

  • FROM イメージ名 →ベースにするイメージを指定する
  • LABEL →管理用の情報などを記載する バージョン情報やなにを作るためのdockerfileですよ的な
  • RUN →コマンド実行を命令するリターンキーの役割(こいつがコマンドを実行していく感じ)
  • CMD →コンテナ起動時にファイルやコマンドを実行する命令を記載する
  • ENV →環境変数を設定する

おおまかにはこんな感じでした。ほかにもたくさんありましたが、いったんこれくらいにしておきました。(覚える自信ない)

実際に書いてみる

簡単にapache用のdockerfileを書いてみます。

dockerfile
FROM centos:centos7        #ベースになるイメージ指定
RUN yum -y update          #パッケージ更新
RUN yum -y upgrade         #更新、不要なもの削除(してくれるらしい)
RUN yum install -y httpd   #apacheインストール
RUN systemctl enable httpd #apache自動起動設定
EXPOSE 80                  #ポート開放

できたので上記ファイルを使用してイメージの作成をしてみます。

$ docker build -t apache .

ずーーーーと実行されて終わったみたいなのでイメージができてるか確認してみます。

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
apache              latest              827d4f21614f        15 seconds ago      599MB

ありました!apacheという名前で作成したのでできています。

イメージからコンテナ作成して起動

起動

$ docker run --privileged -d -p 80:80 apache /sbin/init
8a86bb505eca2b7143ac70a1033e34205ffbbccf48a3ca89e96031194b00ffd9

確認
しっかり起動しているのがわかりました!

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                NAMES
8a86bb505eca        apache              "/sbin/init"        9 seconds ago       Up 8 seconds        0.0.0.0:80->80/tcp   sweet_hodgkin

実際にアクセスして確認!!

20200113001.JPG

テストページが表示されたので問題なさそうです!!!

ちなみにcentos7の場合はコンテナの起動を上記のようにしないとうまくapacheがうまく起動しないようです。(めっちゃはまった)
ここら辺の起動の仕方とコンテナ内の動きについては調べなおして書かせていただければと思います。

まとめ

  • Dockerfileはベースのイメージに対して行う命令を記載するもの
  • 構築過程のコマンドや起動時の命令もコードとして管理できる
  • 実行するだけで何回でも同じ環境をミスなく作れる
  • まだまだ勉強しないとdockerを扱って環境構築は厳しそうw
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む