20190506のdockerに関する記事は12件です。

【Imagemagick対応】Dockerを利用して、RailsをHerokuに簡単デプロイ

はじめに

  • 初心者の方でも簡単に再現可能
  • Imagemagickが使用可能
  • この記事は、以下の記事を参考にして作成しています

DockerComposeでコンテナベースのRailsアプリを作成してそのままHerokuにデプロイする
https://qiita.com/akirakudo/items/16a01271b0a39316e439

  • 引用元記事での問題点

    • rmagickが使用できない
    • 引用元記事では、ベースイメージにalphineを採用
    • このベースイメージでは、Imagemagick 7 が使用されており、gem install rmagick でエラーが発生する
    • gem rmagick では、Imagemagick 6 までしか対応していないことが原因
  • 対策

    • ベースイメージを alphine から Debian へ変更

環境構築方法

  • コンテナの概要
    • RailsコンテナとPostgreSQLコンテナから構成
    • 両者は、local環境のカレントディレクトリにマウントされている
      (※カレントディレクトリ = Dockerfileが置いてあるディレクトリのこと)
    • Railsコンテナはポートフォワーディングにより、localhost:3000からアクセス可能
  • コンテナ構成.png

  • コンテナ構築方法

    • 以下記載の手順をそのまま実行してください

1. フォルダ、ファイルの準備

  • 好きな場所にフォルダを新規作成
  • フォルダ内に4つのファイルを作成(中身は空でOK)
  • 例)この記事では、フォルダ名をhoge_folderとしています

    • hoge_folder
      ├ Dockerfile
      ├ docker-compose.yml
      ├ Gemfile
      ├ Gemfile.lock

2. ファイルの編集

  • 1. で作成した4つのフォルダに以下の内容をコピペする

① Dockerfile

/hoge_folder/Dockerfile
FROM ruby:2.6

ENV RUNTIME_PACKAGES="linux-headers libxml2-dev libxslt-dev make gcc libc-dev nodejs tzdata postgresql-dev postgresql" \
    DEV_PACKAGES="build-base curl-dev" \
    HOME="/myapp"

WORKDIR $HOME

# Counter Measure to Error:"Autoprefixer doesn’t support Node v4.8.2. Update it"
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \
        && apt-get install -y nodejs

RUN apt-get update && \
    apt-get install -y mysql-client \
    postgresql-client \
    sqlite3 \
    --no-install-recommends && \
    rm -rf /var/lib/apt/lists/*

ADD Gemfile      $HOME/Gemfile
ADD Gemfile.lock $HOME/Gemfile.lock

RUN bundle install

ADD ./ $HOME
COPY ./ $HOME

CMD ["rails", "server", "-b", "0.0.0.0"]

② docker-compose.yml

/hoge_folder/docker-compose.yml
version: '3'
services:
  db:
    container_name: postgress_db
    image: postgres:latest
  web:
    container_name: rails_app
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db

③ Gemfile

/hoge_folder/Gemfile
source 'https://rubygems.org'
gem 'rails', '5.2.2'

gem 'rmagick'

④ Gemfile.lock

/hoge_folder/Gemfile.lock

注)Gemfile.lock の中身は空です

  • これでコンテナを立ち上げる準備ができました。

3. コンテナの立ち上げ準備

  • CLIを起動する(bash,コマンドプロント等、ご自身の環境でお使いのものを起動してください。)
  • Dockerfileのあるディレクトリに移動する

    • この記事で言えば、 cd /home/...(以下略).../hoge_folder
  • 以下のコマンドを入力

コマンド
$ docker-compose run web rails new . --force --database=postgresql

※実際に入力する際は、$マークを消してください

  • コマンドを実行すると、作業フォルダ内(hoge_folder)に複数のフォルダが生成される

  • Databaseの設定を変更する

    • このままではpostgreSQLコンテナにアクセスできないため
    • config/database.ymlを開いて、下記内容を上書きする
/hoge_folder/config/database.yml
  default: &default
  adapter: postgresql
  encoding: unicode
  host: db           # 上書き
  username: postgres # 上書き
  password:          # 上書き
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  • docker-compose.ymlがあるフォルダ上でビルドする
コマンド
$ docker-compose build
  • データベースを作成し、マイグレーションする
コマンド
$ docker-compose run web rails db:create
$ docker-compose run web rails db:migrate
  • ここまでで、コンテナの準備は終了です。

4. railsの起動確認

  • docker-compose up でrailsは自動で立ち上がる
コマンド
$ docker-compose up
  • Webブラウザでhttp://localhost:3000にアクセスしてみる

    • Yey ! 君はrailsの上にいるze !
    • rails_top.png
  • railsの起動停止

コマンド
(再起動する場合)
$ docker-compose restart
(停止する場合)
$ docker-compose stop

作 戦 完 了 !

  • それでは、Rails on Dockerライフ をお楽しみください!

謝辞

  • @akirakudo さんの記事は、大変参考になりました

https://qiita.com/akirakudo/items/16a01271b0a39316e439

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

【Imagemagick対応】Dockerを利用して、Rails環境を作成

はじめに

  • 初心者の方でも簡単に再現可能
  • Imagemagickが使用可能
  • この記事は、以下の記事を参考にして作成しています
  • Heroku にもビルド可能なイメージです

    DockerComposeでコンテナベースのRailsアプリを作成してそのままHerokuにデプロイする
    https://qiita.com/akirakudo/items/16a01271b0a39316e439

  • 引用元記事での問題点

    • rmagickが使用できない
    • 引用元記事では、ベースイメージにalphineを採用
    • このベースイメージでは、Imagemagick 7 が使用されており、gem install rmagick でエラーが発生する
    • gem rmagick では、Imagemagick 6 までしか対応していないことが原因
  • 対策

    • ベースイメージを alphine から Debian へ変更

環境構築方法

  • コンテナの概要
    • RailsコンテナとPostgreSQLコンテナから構成
    • 両者は、local環境のカレントディレクトリにマウントされている
      (※カレントディレクトリ = Dockerfileが置いてあるディレクトリのこと)
    • Railsコンテナはポートフォワーディングにより、localhost:3000からアクセス可能
  • コンテナ構成.png

  • コンテナ構築方法

    • 以下記載の手順をそのまま実行してください

1. フォルダ、ファイルの準備

  • 好きな場所にフォルダを新規作成
  • フォルダ内に4つのファイルを作成(中身は空でOK)
  • 例)この記事では、フォルダ名をhoge_folderとしています

    • hoge_folder
      ├ Dockerfile
      ├ docker-compose.yml
      ├ Gemfile
      ├ Gemfile.lock

2. ファイルの編集

  • 1. で作成した4つのフォルダに以下の内容をコピペする

① Dockerfile

/hoge_folder/Dockerfile
FROM ruby:2.6

ENV RUNTIME_PACKAGES="linux-headers libxml2-dev libxslt-dev make gcc libc-dev nodejs tzdata postgresql-dev postgresql" \
    DEV_PACKAGES="build-base curl-dev" \
    HOME="/myapp"

WORKDIR $HOME

# Counter Measure to Error:"Autoprefixer doesn’t support Node v4.8.2. Update it"
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \
        && apt-get install -y nodejs

RUN apt-get update && \
    apt-get install -y mysql-client \
    postgresql-client \
    sqlite3 \
    --no-install-recommends && \
    rm -rf /var/lib/apt/lists/*

ADD Gemfile      $HOME/Gemfile
ADD Gemfile.lock $HOME/Gemfile.lock

RUN bundle install

ADD ./ $HOME
COPY ./ $HOME

CMD ["rails", "server", "-b", "0.0.0.0"]

② docker-compose.yml

/hoge_folder/docker-compose.yml
version: '3'
services:
  db:
    container_name: postgress_db
    image: postgres:latest
  web:
    container_name: rails_app
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db

③ Gemfile

/hoge_folder/Gemfile
source 'https://rubygems.org'
gem 'rails', '5.2.2'

gem 'rmagick'

④ Gemfile.lock

/hoge_folder/Gemfile.lock

注)Gemfile.lock の中身は空です

  • これでコンテナを立ち上げる準備ができました。

3. コンテナの立ち上げ準備

  • CLIを起動する(bash,コマンドプロント等、ご自身の環境でお使いのものを起動してください。)
  • Dockerfileのあるディレクトリに移動する

    • この記事で言えば、 cd /home/...(以下略).../hoge_folder
  • 以下のコマンドを入力

コマンド
$ docker-compose run web rails new . --force --database=postgresql

※実際に入力する際は、$マークを消してください

  • コマンドを実行すると、作業フォルダ内(hoge_folder)に複数のフォルダが生成される

  • Databaseの設定を変更する

    • このままではpostgreSQLコンテナにアクセスできないため
    • config/database.ymlを開いて、下記内容を上書きする
/hoge_folder/config/database.yml
  default: &default
  adapter: postgresql
  encoding: unicode
  host: db           # 上書き
  username: postgres # 上書き
  password:          # 上書き
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  • docker-compose.ymlがあるフォルダ上でビルドする
コマンド
$ docker-compose build
  • データベースを作成し、マイグレーションする
コマンド
$ docker-compose run web rails db:create
$ docker-compose run web rails db:migrate
  • ここまでで、コンテナの準備は終了です。

4. railsの起動確認

  • docker-compose up でrailsは自動で立ち上がる
コマンド
$ docker-compose up
  • Webブラウザでhttp://localhost:3000にアクセスしてみる

    • Yey ! 君はrailsの上にいるze !
    • rails_top.png
  • railsの起動停止

コマンド
(再起動する場合)
$ docker-compose restart
(停止する場合)
$ docker-compose stop

作 戦 完 了 !

  • それでは、Rails on Dockerライフ をお楽しみください!

謝辞

  • @akirakudo さんの記事は、大変参考になりました

https://qiita.com/akirakudo/items/16a01271b0a39316e439

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

KubernetesでWebアプリケーションを構築(サンプル編:SpringBoot)

前回は、Kubernetesの概要と背景にある考え方について、説明しました。

今回では、実際に簡単なWebアプリケーションをKubernetes上に構築するサンプルをご紹介します。

このサンプルアプリケーションは、SpringBootを使用したWebアプリケーションで、Kubernetesの構成を学ぶ上で、極力シンプルにしたものです。

アプリケーションの構成

前回の最後にアプリケーションの構成パターンとして、以下の3つを挙げましたが、いずれの場合も設定をどのように行うかは悩みどころです。

  1. ミドルウエア(+設定)のみ
  2. ミドルウエア(+設定)+データのみ
  3. ミドルウエア+アプリケーション+設定

基本的に、アプリケーションの設定の大部分はdockerイメージにまとめられています。しかし、すべての環境設定がDockerイメージに含まれている場合、特にテスト、ステージング、実稼働環境、または特定の目的のために異なる設定を行いたい場合は、それほど効率的ではありません。

その場合、イメージは共通またはデフォルト設定を持つ必要があり、特定の設定は環境変数またはKubernetesのConfigmapまたはSecretリソースを介して渡されるべきです。環境変数を使用する場合は、アプリケーション側が環境変数を読み取るような作りにする必要があります。configmapは単純な変数だけでなくnginx.confのような設定ファイルも扱うことができます。

データベースに関しては、データが更新されるため、コンテナ運用の原則である「Immutable」の対象にはなりませんし、またデータを永続的に保存する必要があることからPodで構築するのには向いていません。しかし、テスト環境であれば、KubernetesのPODでDBを構築するのもいいです。ここでは、サンプルのためPod上にDBを構築します。通常、DBにはテーブルや初期のマスタデータを登録する必要があります。ここでは、コンテナが作成されたときに初期化スクリプトを指定することによって実現しています。Dockerfileには通常使用されるENTRYPOINTとCMDがありますが、コンテナ起動時に多くのことをしたい場合は、イメージに依存するinitスクリプトを指定できます。このサンプルでは、​​postgresイメージを使い、シェルスクリプトを/docker-entrypoint-initdb.d/ディレクトリに置いてデータベースとテーブルを作成し、マスターデータを登録しています。

Kubernetesを使ったWebアプリケーションの例

それでは、Kubernetes上で動作するWebアプリケーションを作成します。

始める前に、Kubernetes環境を作成する必要があります。おすすめは以下のとおりです。

この記事はkubernetes自体をインストールする方法を説明しません。公式ページ(上記のリンク)で、十分に理解できると思います。
GKEは12ヶ月間300ドルの無料枠があり、これで勉強やテストに十分です。使用しない場合は、kubernetes環境をシャットダウンして削除するようにしてください。でないと、課金が続き、あっという間に無料枠がなくなります。

以下は、GKEの例で、gcloudコマンドをインストールし、GCPに接続できている状態のあとで、実行するもので、作成と削除は頻繁に使います(サンプルで課金が続かないようにするのと、Immutableアーキテクチャなので使い終わったら削除しましょう)。

gcloud init
gcloud container get-server-config --zone asia-northeast1-a

# 作成
gcloud container clusters create samplewebapp --cluster-version 1.12.7-gke.10 --zone asia-northeast1-a --num-nodes 3
gcloud container clusters get-credentials samplewebapp --zone asia-northeast1-a

#削除
gcloud container clusters delete samplewebapp --zone asia-northeast1-a

stern

ログを表示するのに便利なsternも必ずインストールしてください。

構造

サンプルアプリケーションは、Kubernetes環境で動作することを目的としたWebアプリケーションの基本設定をテストするためのもので、以下のような構造となっています。

K8s

アプリケーションは次のサーバで構成されています。

  • Webサーバ(nginxを使用)
  • アプリケーションサーバ(tomcatを使用[スプリングブートに埋め込まれています])
  • DBサーバ(postgresを使用)
  • セッションサーバ(アプリケーションサーバのセッションを維持するためにredisを使用)
  • NAS(PVCダイナミックプロビジョニングまたはnfsを使用)

Webサーバとアプリケーションサーバはステートレスなので、複数のPodに拡張できます。

DBとセッションサーバはステートフル(状態を保つ)です。特にDBサーバ上のデータは恒久的に保存する必要があります。セッションサーバは永続的である必要はありませんが、データが失われるとユーザーエクスペリエンスが悪くなり、ユーザーは再度ログインする必要があり、またユーザーが入力した情報が失われる可能性があります。しかしDBと比較すると、それほど重要ではありません。

セッション維持のために、前段のWebサーバにsticky sessionを設定して、セッションIDによって振り分け先のAPサーバを変える方法もありますが(この場合StatefulSetを使う)、Kubernetesでは、APサーバはいつ終了し再起動されるかわからないため、セッション情報はRedisなど外部のサーバに持たせるのがいいでしょう。Podは割当メモリを超過した場合、容易に削除・再作成されます。この点を考慮に入れておく必要があります。

このサンプルでは、DBサーバとセッションサーバの両方に単一のPodを使用します。実稼働環境で使用したい場合は、両方とも、少なくともDBサーバについてはPaaSを使用することを強くお勧めします。

アプリケーションサンプルはToDoを管理するためのもので、わずか3ページで構成されています。認証の機能とテーブルのCRUD機能を提供するだけです。

  • ログインページ(ID / PW = admin / 111111)
  • ToDoリストページ
  • Todo編集ページ

login
apppage

Todoは、ストレージにファイルを保存するテストの目的で、画像のアップロードを含みます。

ここでの目的はKubernetesの設定を知ることです、テストとして、最小限の機能をカバーするのにちょうど十分な、できるだけ簡潔なソースコードを作りました。これをプロダクトに適用する場合は、ヴァリデーション、セキュリティ、保守性、フレームワークの使用などを検討する必要があります。

サンプルコード全体と展開のマニフェストは、次のサイトにあります。ここでソースコードや設定と説明を入手できます。詳細な手順は、こちらのReadme.mdの方に書いています。

https://github.com/dayan888/springdemo_k8s

上記のソースコードは2つで構成されています。

  • アプリケーションのソースコード(Spring Boot)
  • デプロイ関連ファイル(Dockerfile、kubernetesマニフェスト、confファイルなど)

基本的には、標準のSpringBootのプロジェクト構造に、Dockerとkubernetesのマニフェストを追加したものです。

ローカルマシン上に開発環境構築

アプリケーションを改修するためには、開発用にローカルでJavaアプリケーション開発環境、docker image構築環境が必要になり、以下がインストールされている必要があります。

  • JDK 8以上
  • Docker
  • Postgres 9以上
  • Redis
  • IntelliJ(または他のIDE)
  • Docker Hub(アカウントを登録が必要)

dockerをインストールすれば、docker pullで、postgresとredisをローカル環境に簡単にインストールできます。

Kubernetes環境にデプロイする

早くセットアップしたい場合を考慮し、Kubernetesにマニフェストを適用するためのシェルスクリプトを用意しています。GKEを使用している場合は、deploy/sh/gke.shを実行してください。MinikubeまたはDocker Desktopを使用している場合は、deploy/sh/local.shを実行してください。

このシナリオでは、私個人のdocker hubがイメージリポジトリとして使われています。Dockerfileで画像を作成することから始めて全プロセスを試す場合は、docker hubのようなレジストリを各自作成し、それに応じてマニフェストを修正する必要があります。

マニフェストの説明

以下はシステムを構築するための正確な指示ではなく、要点の説明になります。詳細はReadme.mdを参照してください。
実行する際は、適用順序は重要です。従属変数(特にServiceの名前)が事前に定義されていない場合、Podの作成は失敗します。

デプロイは次の順序で行う必要があります。

  • Redis(アプリケーションサーバからアクセス)
  • DB(アプリケーションサーバからアクセス)
  • アプリケーションサーバ(Webサーバからアクセス)
  • Webサーバ(LB / Ingressからアクセス)
  • ロードバランサまたは入力

もしくは、配置を作成する前に、まずすべてのServiceを作成します。Serviceは、selectorとして指定した振り分け先のPodが存在しない場合でも作成可能です。
Redisはそのまま公式イメージで作成されますが、マニフェストを適用してDeploymentとServiceを作成する必要があります。

1.アプリケーションサーバ

Dockerfile

コンテナに何を入れるべきかを理解するためには、イメージを作成する方法を理解することが重要です。

FROM openjdk:8-jdk-alpine
ARG buildver=0.5.0
#RUN ./gradlew build -x test
COPY build/libs/springdemo-${buildver}.jar /app/springdemo.jar
ENTRYPOINT ["java", "-jar", "/app/springdemo.jar"]

まず適切なベースイメージを選択することが重要です。ゼロから作成することもできますが、DockerHubには既に作成済みのイメージがあります。イメージサイズを小さくしたい場合は、alpineとbusyboxが候補となります。しかし、これらは最小限のコマンドしか持っていないので、コンテナを調べてネットワークコマンドを実行したい場合は、適切ではありません。

Javaで実行されるアプリケーションサーバの場合、私はalpineベースのopenjdkイメージを選びました。次に、ビルドされたjarファイルをコピーして、コンテナの起動時に実行されるENTRYPOINTを設定します。このイメージ作成の際、gradleのビルドなどを一緒に行うことができます。このビルドおよびイメージ作成プロセスは、jenkinsなどのCI/CDサーバを使用した場合、若干異なります。

このサンプルでは、サーバ側のJavaのフレームワークとしてSpring Bootを使用しています。これはTomcatは埋め込まれているので、Tomcatをセットアップして設定する必要はなく、可搬性に優れているためコンテナと相性はいいです。ただしSpringのようなJavaのフレームワーク、DIコンテナは起動に時間がかかるため、その点コンテナ時代には向いていないと思います。最終的にネイティブにコンパイルするような仕組みが必要になってくるでしょう。

Spring Bootの設定に関しては、application.ymlを使用し、環境変数とデフォルト変数を以下のように設定します。

datasource:
 url: ${DB_URL:localhost}
 driverClassName: org.postgresql.Driver

環境変数は、kubernetesマニフェストまたはconfigmapによって設定されます。

Deploymentマニフェスト

apiVersion: apps/v1
kind: Deployment
metadata:
  name: 
    sbdemo-apserver
spec:
  replicas: 3
  selector:
    matchLabels:
      app: sbdemo-apserver
  template:
    metadata:
      labels:
        app: sbdemo-apserver
    spec:
      containers:
      - name: apserver
        image: dayan888/springdemo:apserver
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
        env:
          - name: SPRING_PROFILES_ACTIVE
            value: "prd"
          - name: DB_URL
            value: "jdbc:postgresql://sbdemo-postgres-service:5432/demodb?user=postgres&password=postgres"
          - name: PIC_DIR
            value: "/opt/picDir"
          - name: REDIS_HOST
            value: "sbdemo-redis-service"
          - name: REDIS_PORT
            value: "6379"     
        volumeMounts:
          - mountPath: "/opt/picDir"
            name: apserver-pvc
      volumes:
      - name: apserver-pvc
        persistentVolumeClaim:
          claimName: sbdemo-apserver-pvc

もしこれを初めて見ると、少し複雑に思えるでしょう。特にマニフェストの言葉や構造はわかりにくいです。しかし、実際には何も特別なことはありません。

このマニフェストの主なポイントは次のとおりです。

  • 前工程で作成したイメージを使用する
  • 冗長性のために3つのPodの複製を作成する
  • application.ymlによって参照される環境変数が設定

そして、3つの冗長化されたPodでアップロードされた画像を共有するためのPersistent Volumeをマウントします。画像アップロード機能を加えたのはこのPVを使うためです。
各アプリケーションサーバはアップロードされたイメージを読み書きする必要があるため、永続的なボリュームはReadWriteManyである必要があります。ローカルのkubernetesを使用している場合は、ボリュームにアクセスするノードが1つしかないため、大きな問題にはなりません。しかし、GCEディスクでReadWriteManyをサポートしていないためにGKEを実行する場合は、別の方法を検討する必要があります。

ここでは、k8s.gcr.io/volume-nfsイメージを使用してNFSサーバを作成します。NFSの内部では、Persistent Volume Claimが使用されます。nfsサーバ自体はアプリケーションサーバからマウントされるPersistent Volumeになります。ここは少しややこしいです。

ローカル用とGKE用に別のマニフェストを用意したので、サンプルコードをapplyするときは注意してください。

Serviceマニフェスト

これらのコンテナにアクセスするための「Service」を作成する必要があります。Kubernetesでは、サーバ(Pod)とそこへのアクセスを分けており、バッチ処理でない限り、通常セットで作成する必要があります。

apiVersion: v1
kind: Service
metadata:
  name: sbdemo-apserver-service
spec:
  type: ClusterIP
  ports:
   - name: "http-port"
     protocol: "TCP"
     port: 8080
     targetPort: 8080
  selector:
    app: sbdemo-apserver

主なポイントは以下のとおりです。

  • Serviceにアクセスするためのポートとアプリケーションサーバのポート(targetPort)を定義します。
  • どのPodへ転送されるべきかに使用されるSelectorを定義します
  • 他のPodからのアクセスに使用されるServiceの名前

その後、kubectlを実行してマニフェストを適用します。

kubectl apply -f deploy/app/pvc.yaml
kubectl apply -f deploy/app/deployment.yaml
kubectl apply -f deploy/app/service.yaml

2. Webサーバ

Dockerfile

FROM nginx:latest
COPY src/main/resources/static/css /usr/share/nginx/html/css
COPY src/main/resources/static/js /usr/share/nginx/html/js
COPY src/main/resources/static/img /usr/share/nginx/html/img

nginx画像を使用して、css、js、image、htmlファイルなどの静的コンテンツをコピーします。

構成マップ

nginx.confなどの設定ファイルはイメージに含めて、設定ファイルをソースコードで管理することができます。しかし、環境設定をソースコードから分離して再利用性を高めたい場合は、nginx.confのconfigmapを作成し、それをkubernetesマニフェストを介してコンテナに渡すようにします。

nginx.confでは、必ず出力ログを標準出力に設定してください。Kubernetesのプラクティスでは、ログは細分化せず、すべて標準出力に出し、分類はその後で行います。

access_log /dev/stdout;
error_log  /dev/stderr debug;

そしてserver.confを使用してバックエンドのアプリケーションサーバを次のようなService名で設定します。

location / {
  ...
  proxy_pass http://sbdemo-apserver-service:8080;
  proxy_cookie_path / /;
}

それから、kubectlを実行してconfigmapを作成します。

kubectl create configmap nginx-conf --from-file=deploy/web/nginx.conf
kubectl create configmap server-conf --from-file=deploy/web/server.conf

ここで、configmapのためにマニフェストを使うこともできます、しかし、その内容がファイルであるならば、コマンドだけを使う方がより簡単であると思います。

Deploymentマニフェスト

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sbdemo-nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: sbdemo-nginx
  template:
    metadata:
      labels:
        app: sbdemo-nginx
    spec:
      containers:
      - name: nginx
        image: dayan888/springdemo:nginx
        ports:
        - containerPort: 80
        readinessProbe:
          httpGet:
            path: /css/style.css
            port: 80
        volumeMounts:
        - name: nginx-conf
          mountPath: /etc/nginx/nginx.conf
          subPath: nginx.conf
        - name: server-conf
          mountPath: /etc/nginx/conf.d/server.conf
          subPath: server.conf
      volumes:
      - name: nginx-conf
        configMap: 
          name: nginx-conf
          items:
            - key: nginx.conf
              path: nginx.conf
      - name: server-conf
        configMap: 
          name: server-conf
          items:
            - key: server.conf
              path: server.conf

主な点は次のとおりです。

  • 前工程で作成したイメージを使用する
  • ConfigMapからconfigファイルを直接直接マウントする
  • 冗長性を定義する(3つのReplica)
  • Readiness Probeを定義します(GKEで実行すると、/を200に戻す必要があるため、この設定は重要です)。

次に、このDeployment用のServiceを作成します。

apiVersion: v1
kind: Service
metadata:
  name: sbdemo-nginx-service
spec:
  type: ClusterIP
  ports:
   - name: "http-port"
     protocol: "TCP"
     port: 80
     targetPort: 80
  selector:
    app: sbdemo-nginx

ここでは特別なことはありません。Service名は後でIngresのマニフェストで使用されます。

3.データベース

Dockerfile

FROM postgres:9.6
COPY deploy/db/init_ddl.sh /docker-entrypoint-initdb.d/
RUN chmod +x /docker-entrypoint-initdb.d/init_ddl.sh

データベースは既製のDockerイメージがあります。モジュールを構築したりコピーしたりする必要はありません。postgresql.confで定義されているデフォルトの振る舞いを変更したいかもしれません。その場合はconfigmapが便利です。

今回はデフォルト設定を使用しますが、データベース、テーブル、マスターデータなどを作成する必要があります。そのため、docker-entrypoint-initdb.dを使用して、シェルスクリプトをコピーします。

StatefulSetマニフェスト

今回は単一のdbサーバ構成のみが使用されますが、冗長性を設定する場合は、マスタとスレーブのdbを区別することが重要です。マスタの再起動時に、同じボリュームをマウントする必要があります。したがって、このサンプルではこれを確実にするためにStatefulSetを使用しています。StatefulSetはDeploymentとほぼ同じですが、違いは再起動時に同じPVC(永続ボリューム)がマウントされることです。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: 
    sbdemo-postgres-sfs
spec:
  serviceName: sbdemo-postgres-service
  replicas: 1
  selector:
    matchLabels:
      app: sbdemo-postgres-sfs
  template:
    metadata:
      labels:
        app: sbdemo-postgres-sfs
    spec:
      containers:
       - name: postgres
         image: dayan888/springdemo:postgres9.6
         ports:
          - containerPort: 5432
         volumeMounts:
         - name: pvc-db-volume
           mountPath: /var/lib/postgresql
  volumeClaimTemplates:
  - metadata:
      name: pvc-db-volume
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 1G

主なポイントは以下のとおりです。

  • StatefulSetを使用する
  • 前工程で作成したDockerイメージを使用する
  • このイメージのpostgresプロセスが参照する/var/lib/postgresqlにPVCがマウントされます。

Serviceマニフェスト

apiVersion: v1
kind: Service
metadata:
  name: sbdemo-postgres-service
spec:
  type: ClusterIP
  ports:
   - name: "db-port"
     protocol: "TCP"
     port: 5432
     targetPort: 5432
  selector:
    app: sbdemo-postgres-sfs

Service名はアプリケーションサーバのapplication.ymlに設定されています。このシナリオでは、Service名は事前に定義されている必要があります。何らかの命名規則が必要となるでしょう。

4.Ingres

IngresはHTTPプロトコルのようなL7で動作するロードバランサであり、ホスト名またはURLパスによってトラフィックを振り分けることができます。

ドメイン名ごとに(一意のグローバルIPアドレスを持つ)サービスを1つだけ運用する場合は、LoadBalancerだけで十分です(こちらはL4で動作するロードバランサ)。WebサーバのServiceのTypeを「LoadBalancer」に設定してから、グローバルIPアドレスをDNSサーバ上のドメインに設定します。

しかし、これは柔軟性と拡張性にかけるので、ingressを使用した方がいいでしょう。

まず、Ingressから転送される各WebサーバのServiceを作成する必要があります。

Ingresマニフェスト

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: sbdemo-ingress
spec:
  rules:
  - host: sbdemo.example.com
    http:
      paths:
      - path: /*
        backend:
          serviceName: sbdemo-nginx-np
          servicePort: 80
      - path: /v2/*
        backend:
          serviceName: web2
          servicePort: 8080
  backend:
    serviceName: sbdemo-nginx-np
    servicePort: 80
  tls:
  - hosts:
    - sbdemo.example.com
    secretName: tls-sbdemo

このサンプルは1つのWebシステムでしかないため、イングレスの機能をテストするために、簡単なWebサーバ("Hello、xxx"を返すだけ)を追加してみます。

主なポイントは以下のとおりです。

  • ホストとパスでルールを定義し、バックエンドのService名とポートにバインドします。
  • ホストのSSL証明書を定義します

httpsは現在「必須」であるため、IngresはSSL証明書を管理するのに非常に役立ちます。具体的な手順はサンプルサイトのREADME.mdに書かれています。サンプルでは自己署名証明書を使用しているため、実稼働環境に適用する場合は正しく署名する必要があります。

重要:このサンプルをローカルのkubernetes環境で実行すると、ロードバランサーのみが適用されます。Ingresを有効にするために、IngresをサポートしているKubernetes環境を選ぶか、もしくは多少複雑ですが自身でnginxで構成されたIngress Podを作成する必要があります。

結論

このサンプルアプリケーションを作成することで、私自身たくさんのことを学びました。特にPersistent Volumeに苦労しました。驚いたことに、多くのマネージドサービスはReadWriteManyモードを提供していません。そのため、レガシーなNFSサーバを作成しなければなりませんでした。ストレージはまだもっと考慮する余地があります。

Kubernetesマニフェストは扱いが簡単ですが、マニフェストの数が増えたときは、「helm」を適用することを検討する必要があります。私の仕事場ではAKSとhelmを使っていますが、今回は作る時間がないので、シェルスクリプトを作成しました。

これは、あくまでもサンプルなので多くのことを省いています。実稼働環境を適用するために考慮すべきことがまだたくさんあります。

何か質問・フィードバックがあると嬉しいです。

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

PDF.jsでネット上のPDFファイルを一瞬にして開くワンライナー

はじめに

Mouse DictionaryをPDFで使うために、PDF.jsを用いるという方法がある。(@kroton様の参考記事)

しかし、私のようなめんどくさがりは、これすらも面倒くさいので以下のようなツールを作った。

  • ワンラインでネット上のPDFをPDF.jsで開く
  • ローカルに落としたpdfをわざわざ削除しなくてもよい

完成物

https://github.com/monado3/pdfjs-broker

開発環境

  • Manjaro linux
  • Python 3.7.3
  • Docker 18.09.5-ce

実現方法

  1. Dockerコンテナ内にpdfを落とす。
  2. そのままDockerコンテナ内でPDF.jsが機能するPHPビルトインサーバを立てる。
  3. ローカルのブラウザからDockerコンテナ内のpdfを閲覧する。
  4. ダウンロードしたpdfはDockerコンテナが終了すれば(再起動時や明示的にコンテナを停止させた際)自動的に消える。

また、複数のpdfを同時閲覧することを前提とし、自動で動的にDockerコンテナとローカルのポートフォワーディングをするために、Pythonプログラムにコンテナ立ち上げを仲介させることにした。

課題点

DockerイメージのEntrypointとなっているシェルスクリプトが終了すると、例え-dオプションをつけてrunしてもコンテナはなぜか終了してしまう。
それにより、コンテナ内でPDFのサービングが開始したことをPython側にシンプルに伝える方法がなく、コンテナ内の必要処理が終了する前に、Pythonがブラウザを開いてしまう。

最後に

実装があまりスマートではないので、どしどしアドバイスやPR待ってます。

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

PDF.jsでネット上のPDFファイルを一瞬にして開くツール

はじめに

Mouse DictionaryをPDFで使うために、PDF.jsを用いるという方法がある。(@kroton様の参考記事)

しかし、私のようなめんどくさがりは、これすらも面倒くさいので以下のようなツールを作った。

  • ワンラインでネット上のPDFをPDF.jsで開く
  • ローカルに落としたpdfをわざわざ削除しなくてもよい

完成物

https://github.com/monado3/pdfjs-broker

開発環境

  • Manjaro linux
  • Python 3.7.3
  • Docker 18.09.5-ce

実現方法

  1. Dockerコンテナ内にpdfを落とす。
  2. そのままDockerコンテナ内でPDF.jsが機能するPHPビルトインサーバを立てる。
  3. ローカルのブラウザからDockerコンテナ内のpdfを閲覧する。
  4. ダウンロードしたpdfはDockerコンテナが終了すれば(再起動時や明示的にコンテナを停止させた際)自動的に消える。

また、複数のpdfを同時閲覧することを前提とし、自動で動的にDockerコンテナとローカルのポートフォワーディングをするために、Pythonプログラムにコンテナ立ち上げを仲介させることにした。

課題点

DockerイメージのEntrypointとなっているシェルスクリプトが終了すると、例え-dオプションをつけてrunしてもコンテナはなぜか終了してしまう。
それにより、コンテナ内でPDFのサービングが開始したことをPython側にシンプルに伝える方法がなく、コンテナ内の必要処理が終了する前に、Pythonがブラウザを開いてしまう。

最後に

実装があまりスマートではないので、どしどしアドバイスやPR待ってます。

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

Docker for mac のエラー「unauthorized: incorrect username or password.」対処法

Docker Desktop for mac をインストール後のPullで「unauthorized: incorrect username or password.」が発生。少しハマったので備忘として共有します。

Docker Hubより「pull」時にエラー発生

$docker run hello-world

Unable to find image 'hello-world:latest' locally
docker: Error response from daemon: Get https://registry-1.docker.io/v2/library/hello-world/manifests/latest: unauthorized: incorrect username or password.
See 'docker run --help'.

ID/PWDが異なるとの警告。原因はDockerHub登録時に利用したメールアドレスでログインしていたことでした。
メールアドレスで、DockerHubにログインする必要があります。

[対応]Docker HubにユーザIDでログインする。

image.png

メールアドレスではなくユーザIDでログインしてください!!

確認

DockerHubからPull出来ることを確認。

$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
1b930d010525: Pull complete 
Digest: sha256:92695bc579f31df7a63da6922075d0666e565ceccad16b59c3374d2cf4e8e50e
Status: Downloaded newer image for hello-world:latest

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

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:

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

一足遅れて Kubernetes を学び始める - 07. workloads その3 -

ストーリー

  1. 一足遅れて Kubernetes を学び始める - 01. 環境選択編 -
  2. 一足遅れて Kubernetes を学び始める - 02. Docker For Mac -
  3. 一足遅れて Kubernetes を学び始める - 03. Raspberry Pi -
  4. 一足遅れて Kubernetes を学び始める - 04. kubectl -
  5. 一足遅れて Kubernetes を学び始める - 05. workloads その1 -
  6. 一足遅れて Kubernetes を学び始める - 06. workloads その2 -
  7. 一足遅れて Kubernetes を学び始める - 07. workloads その3 -
  8. 一足遅れて Kubernetes を学び始める - 08. discovery&LB その1 -

前回

一足遅れて Kubernetes を学び始める - 06. workloads その2 -にて、DaemonSetとStatefulSet(一部)を学習しました。今回は、StatefulSetの続きとJob,CronJobを学習します。

StatefulSet

sample-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: sample-statefulset
spec:
  serviceName: sample-statefulset
  replicas: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: nginx-container
          image: nginx:1.12
          ports:
            - containerPort: 80
          volumeMounts:
          - name: www
            mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes:
      - ReadWriteMany
      storageClassName: managed-nfs-storage
      resources:
        requests:
          storage: 1Gi

永続的にデータが保存されるかどうか確認します。

pi@raspi001:~/tmp $ k apply -f sample-statefulset.yaml
pi@raspi001:~/tmp $ k exec -it sample-statefulset-0 -- df -h
Filesystem  Size  Used Avail Use% Mounted on
...
192.168.3.35:/home/data/default-www-sample-statefulset-0-pvc-*   15G  1.1G   13G   8% /usr/share/nginx/html
...
pi@raspi001:~/tmp $ k exec -it sample-statefulset-0 touch /usr/share/nginx/html/sample.html

sample.htmlというファイルを作りました。こちらが消えるかどうか確認します。

pi@raspi001:~/tmp $ k delete pod sample-statefulset-0
pi@raspi001:~/tmp $ k exec -it sample-statefulset-0 ls /usr/share/nginx/html/sample.html
/usr/share/nginx/html/sample.html

podを消してセルフヒーリングで復活した後、確認すると、sample.html残っています。

pi@raspi001:~/tmp $ k delete -f sample-statefulset.yaml
pi@raspi001:~/tmp $ k apply -f sample-statefulset.yaml
pi@raspi001:~/tmp $ k exec -it sample-statefulset-0 ls /usr/share/nginx/html/sample.html
/usr/share/nginx/html/sample.html

こちらも残っていますね。OKです。

スケーリング

StatefulSetでは、スケールアウトするときは、インデックスが小さいものから増えていきます。
逆にスケールインするときは、インデックスが大きいものから削除されていきます。
また、1つずつ増減します。そのため、一番始めに作られるPodは、一番最後に削除されることになります。
試してみます。

pi@raspi001:~ $ k get pod | grep sample-statefulset
sample-statefulset-0                      1/1     Running   1          10h
sample-statefulset-1                      1/1     Running   1          10h
sample-statefulset-2                      1/1     Running   1          10h
pi@raspi001:~/tmp $ vim sample-statefulset.yaml # replica:3→4
pi@raspi001:~/tmp $ k apply -f sample-statefulset.yaml
pi@raspi001:~/tmp $ k get pod | grep sample-statefulset
sample-statefulset-0                      1/1     Running             1          10h
sample-statefulset-1                      1/1     Running             1          10h
sample-statefulset-2                      1/1     Running             1          10h
sample-statefulset-3                      0/1     ContainerCreating   0          6s
pi@raspi001:~/tmp $ vim sample-statefulset.yaml # replica:4→2
pi@raspi001:~/tmp $ k apply -f sample-statefulset.yaml
pi@raspi001:~/tmp $ k get pod | grep sample-statefulset
sample-statefulset-0                      1/1     Running       1          10h
sample-statefulset-1                      1/1     Running       1          10h
sample-statefulset-2                      1/1     Running       1          10h
sample-statefulset-3                      0/1     Terminating   0          2m4s
pi@raspi001:~/tmp $ k get pod | grep sample-statefulset
sample-statefulset-0                      1/1     Running       1          10h
sample-statefulset-1                      1/1     Running       1          10h
sample-statefulset-2                      0/1     Terminating   0          10h

期待通りですね。1つずつではなく、並列して作成したい場合は、spec.podManagementPolicyをparallelにすれば実現できます。

アップデート戦略

戦略は2通りあり、OnDeleteとRollingUpdateがあります。前者は、削除された(マニュフェスト更新ではなく、delete)タイミングに更新され、後者は、即時更新します。StatefulSetの更新では、アップデート中の過不足分の調整(maxUnavailable, maxSurge)は一切できません。また、partitionというフィールドのによって、どのインデックス以降を更新するかを調整することもできます。これは、ステートフルならではの機能です。
Deploymentでは試してませんでしたが、こちらで試してみようと思います。

デフォルトの戦略はRollingUpdateです。これは何度も動作して確認できているので、OnDeleteを試そうと思います。(partitionは置いとく)

sample-statefulset.yaml
...
spec:
  updateStrategy:
   type: OnDelete
...
  template:
    spec:
      containers:
        - name: nginx-container
          image: nginx:1.13
...

アップデート戦略をOnDeleteにし、nginxイメージを1.12から1.13に更新しました。

pi@raspi001:~/tmp $ k delete -f sample-statefulset.yaml
pi@raspi001:~/tmp $ k apply -f sample-statefulset.yaml
pi@raspi001:~/tmp $ k describe pod sample-statefulset-0 | grep "Image:"
    Image:          nginx:1.12
pi@raspi001:~/tmp $ k delete pod sample-statefulset-0
pi@raspi001:~/tmp $ k get pod | grep sample-statefulset
sample-statefulset-0                      0/1     ContainerCreating   0          5s
sample-statefulset-1                      1/1     Running             0          2m59s
pi@raspi001:~/tmp $ k describe pod sample-statefulset-0 | grep "Image:"
    Image:          nginx:1.13

期待通りですね。明示的にpodを削除すればnginxが更新されました。

Job

一度限りの処理を実行させるリソース。
replicaSetのように複製ができる。
バッチ処理に向いている。

10秒sleepするだけのjobを実行してみます。

sample-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: sample-job
spec:
  completions: 1
  parallelism: 1
  backoffLimit: 10
  template:
    spec:
      containers:
      - name: sleep-container
        image: nginx:1.12
        command: ["sleep"]
        args: ["10"]
      restartPolicy: Never
pi@raspi001:~/tmp $ k apply -f sample-job.yaml
pi@raspi001:~/tmp $ k get pod
NAME                                      READY   STATUS      RESTARTS   AGE
sample-job-d7465                          0/1     Completed   0          3m17s
pi@raspi001:~/tmp $ k get job
NAME         COMPLETIONS   DURATION   AGE
sample-job   1/1           27s        4m8s

jobの実行が終わると、podが消えていますね。そして、jobのCOMPLETIONSが1/1になっているので正常終了したみたいです。逆に正常終了しなかった場合、restartPolicyに沿って再実行することになります。種類としてNeverとOnFailureがあります。Neverは、新規にPodを作って再実行、OnFailureは、既存Podを使って再実行するそうです。ただし、データ自体は消失することになるので、ご注意下さい。

completionsは目標成功数で、parallelismは並列数、backoffLimitは失敗許容値です。
目的に合う設定にすれば良いですね。
また、completionsを未指定にするとjobを止めるまでずっと動き続けます。backoffLimitを未指定にすると6回までとなります。

んー、特に興味が惹かれることもなく、終わります。笑

CronJob

Jobをスケジュールされた時間で実行するリソース。
DeploymentとReplicaSetの関係と似ていて、Cronjobがjobを管理する。

1分毎に50%の確率で成功するjobを用意して、試してみます。

sample-cronjob.yaml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: sample-cronjob
spec:
  schedule: "*/1 * * * *"
  concurrencyPolicy: Allow
  startingDeadlineSeconds: 30
  successfulJobsHistoryLimit: 5
  failedJobsHistoryLimit: 3
  suspend: false
  jobTemplate:
    spec:
      completions: 1
      parallelism: 1
      backoffLimit: 1
      template:
        spec:
          containers:
          - name: sleep-container
            image: nginx:1.12
            command:
            - "sh"
            - "-c"
            args:
            # 約50%の確率で成功するコマンド
            - "sleep 40; date +'%N' | cut -c 9 | egrep '[1|3|5|7|9]'"
          restartPolicy: Never
pi@raspi001:~/tmp $ k apply -f sample-cronjob.yaml
pi@raspi001:~/tmp $ k get all
NAME                           SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
cronjob.batch/sample-cronjob   */1 * * * *   False     0        <none>          9s

時間がくるまで、job,podは作成されないようです。
数分待ってみました。

pi@raspi001:~/tmp $ k get all
NAME                                          READY   STATUS      RESTARTS   AGE
pod/sample-cronjob-1557115320-dsdvg           0/1     Error       0          2m18s
pod/sample-cronjob-1557115320-qkgtp           0/1     Completed   0          87s
pod/sample-cronjob-1557115380-r57sw           0/1     Completed   0          78s
pod/sample-cronjob-1557115440-2phzb           1/1     Running     0          17s

NAME                                  COMPLETIONS   DURATION   AGE
job.batch/sample-cronjob-1557115320   1/1           105s       2m18s
job.batch/sample-cronjob-1557115380   1/1           52s        78s
job.batch/sample-cronjob-1557115440   0/1           17s        17s

NAME                           SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
cronjob.batch/sample-cronjob   */1 * * * *   False     1        20s             3m12s

名前の命名ルールがあるので、どう関連しているのか一目瞭然ですね。
Podが残っているのは、failedJobsHistoryLimitとsuccessfulJobsHistoryLimitの値の影響ですね。
logで確認できるように残しておくそうですが、ログ収集基盤に集約した方が良いとも言われています。

途中で止めたいときは、spec.suspendをtrueにすることで実現可能になります。
同時実行する制限として、concurrencyPolicyがあり、Allow,Forbid,Replaceがあります。
Allowは、特に制限しない。
Forbidは、前のjobが終わらない限り実行しない。
Replaceは、前のjobを削除し、jobを実行する。

遅延がどのぐらい許容できるかは、startingDeadlineSecondsで指定します。

こちらも、特に何事もなく終わりました。笑

お片付け

pi@raspi001:~/tmp $ k delete -f sample-statefulset.yaml -f sample-job.yaml -f sample-cronjob.yaml
pi@raspi001:~/tmp $ k delete pvc www-sample-statefulset-{0,1,2,3}

終わりに

ようやく、workloadsが終わりました。最後はざっくり進めてしまった感がありました。
次回はこちらです。

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

Dockerではじめる『Everyday Rails - RSpecによるRailsテスト入門』

概要

Railsチュートリアルも完了し、さて次は何をしようかと思案していたところ、
『Everyday Rails - RSpecによるRailsテスト入門』がゴールデンウィークセール中でしたので、これはと思い購入。
Everyday Rails… Aaron Sumner 著 et al. [Leanpub PDF/iPad/Kindle]
Railsチュートリアルに引きづつき、Dockerで環境を構築。

環境

PC:MacBookPro
docker for mac

環境構築

RSpec用のコンテナ準備

まずは、ターミナルで適当なフォルダを作成して、カレントディレクトリに移動。

mkdir myRspec && cd $_

続いてDockerコンテナを起動。

$ docker container run -it --name myRspec -p 3000:3000 -v `pwd`:/myapp  ruby:2.4.6 /bin/bash

内容について軽く補足
* -it  : 標準入出力を繋げる。要するにターミナルで起動したコンテナを操作できるようにする。
* --name  : 名前付け。あるとわかりやすい。
* -p 3000:3000 : ホストとなるOSとDockerイメージのポートを繋げる。
* -v `pwd`:/myapp : ホストのカレントディレクトリと、Dockerのmyappディレクトリを同期させる。
  (同時にコンテナ内のルート直下にmyappディレクトリも作成される)
* ruby:2.4.6 : 引っ張ってくるDocker Image。サンプルアプリはRuby2.4なので今回はタグで2.4.6を指定。
* /bin/bash :実行するコマンド。

ターミナルの最初の部分がUSERnoMacBook-Pro:~からroot@:(12文字の英数字)に変わっていたら成功。
そのまま、次のコマンドを実行。

$ apt update
$ apt install -y nodejs

Githubからサンプルアプリケーションをクローン

サンプルアプリケーションはGitHubにあります。
https://github.com/everydayrails/everydayrails-rspec-2017
クローンの手順はやったことがなかったので、ググりながら進めます。

$ git clone https://github.com/everydayrails/everydayrails-rspec-2017.git

everydayrails-rspec-2017というディレクトが作成され、コードが取得できました。
まずはcd everydayrails-rspec-2017/でディレクトリに移動。
この状態では、Masterブランチのみ取得している状態です。
ここから2章開始の状態にするために、本文に従ってブランチを切り替えます。

$ git checkout -b my-02-setup origin/01-untested

これで、ソースコードの準備が整いました。先述の通り、ホストの myRspecディレクトリはmyappディレクトリと同期しています。
ホストPCのmyRspec下のeverydayrails-rspec-2017をVSCodeでひらけば、そのまま開発環境として利用できます。
ただし、Railsコマンドの実行などは、Dockerコンテナを動かしているターミナルからでないと使えません。

仕上げ

ターミナルで最後の仕上げです。

$ bundle install
$ bin/rails db:create:all
$ bin/rails db:migrate RAILS_ENV=development

これで準備完了のはずです。ターミナルにrails sと入力し、ブラウザからhttp://localhost:3000/に接続してみます。

スクリーンショット 2019-05-06 12.49.52.png

問題なく動いているようですね。それでは勉強開始!

参考

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

「docker-compose up」をしようとしたところ「OCI runtime create failed」

DockerでPHPの開発環境を構築していたところエラーが発生

環境

  • macOS mojava(バージョン10.14.4)
  • Docker version 18.09.2

エラーの内容

docker-compose up -d

でコンテナを一斉に立ち上げた際に以下のエラーが発生

Creating php_mysql_nginx_db_1 ... error

ERROR: for php_mysql_nginx_db_1  Cannot start service db: OCI runtime create failed: container_linux.go:344: starting container process caused "process_linux.go:424: container init caused \"rootfs_linux.go:58: mounting \\\"/Users/UserName/WorkSpace/php_mysql_nginx/mysql/data\\\" to rootfs \\\"/var/lib/docker/overlay2/51d8c0970ffca60b031d04ce28ec8224c6d81ec33635095cc3e2bb6b78cc1a53/merged\\\" at \\\"/var/lib/docker/overlay2/51d8c0970ffca60b031d04ce28ec8224c6d81ec33635095cc3e2bb6b78cc1a53/merged/var/lib/mysql\\\" caused \\\"not a directory\\\"\"": unknown: Are you trying to mount a directory onto a file (or vice-versa)? Check if the specified host path exists and is the expected type

ERROR: for db  Cannot start service db: OCI runtime create failed: container_linux.go:344: starting container process caused "process_linux.go:424: container init caused \"rootfs_linux.go:58: mounting \\\"/Users/UserUserName/WorkSpace/php_mysql_nginx/mysql/data\\\" to rootfs \\\"/var/lib/docker/overlay2/51d8c0970ffca60b031d04ce28ec8224c6d81ec33635095cc3e2bb6b78cc1a53/merged\\\" at \\\"/var/lib/docker/overlay2/51d8c0970ffca60b031d04ce28ec8224c6d81ec33635095cc3e2bb6b78cc1a53/merged/var/lib/mysql\\\" caused \\\"not a directory\\\"\"": unknown: Are you trying to mount a directory onto a file (or vice-versa)? Check if the specified host path exists and is the expected type
ERROR: Encountered errors while bringing up the project.


試したこと

1. docker volumeの削除

未使用のvolumeを一括削除

docker volume rm $(docker volume ls -qf dangling=true)

それでもって再度docker-compose up -dを試みるも同じエラー


2. エラーが起きている箇所docker-compose.ymlファイルのdb箇所のvolume部分だからその部分のパスを変える

db:
    image: mysql:5.7
    ports:
      - 13306:3306
    volumes:
      - ./mysql/data:/var/lib/mysql

こうなっていたのを以下のようにしたところ上手くいった!

db:
    image: mysql:5.7
    ports:
      - 13306:3306
    volumes:
      - ./mysql:/var/lib/mysql
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

メモ PHP docker xdebug

何がしたい

dockerコンテナとしてnginxとphp-fpm動作させて、PhpStormを使ったxdebugによるリモートデバッグできるまでをやる

Overview

  • mac
  • dockerはdocker-for-mac
  • dockerコンテナ上でnginxが動作
  • dockerコンテナ上でphp-fpmが動作
    • with Xdebug
  • PhpStormでXdebugリモートデバッグをする

Source

https://github.com/tttreal/local-php-debug

HTTPリクエストの処理概要

  • nginxにport:80でリクエストが飛ぶ
  • nginxからphp-fpmにport:9000でリクエストが飛ぶ(9000はphp-fpmのデフォルト設定)
  • php-fpmはphpを動作させてレスポンスする
    • php上でxdebugが動作する (xdebug.remote_autostart=1 なので、勝手に動作)
    • php上のxdebugが、phpの処理中に、xdebug.remote_hostに対してDBGp接続(これなに?)を試みる
    • 公式の説明gif: https://xdebug.org/images/docs/dbgp-setup.gif
    • PhpStormがDBGpをうまくやっている・・・んだと思う(曖昧・・・?)
  • nginxがphp-fpmのレスポンスをクライアントに返す

(URLに?XDEBUG_SESSION_START=xxxとかつける必要はないよ)

docker

  • nginx用のコンテナ、php-fpm用のコンテナ を作成する
  • docker-for-macだと、"dockerのホストマシン"というのはmacではなく、mac上で動いている仮想環境
  • コンテナから見たmacは、docker.for.mac.host.internal
    • (ちなみにホストマシンはhost.docker.internal)
  • せっかくなので、docker-composeを使う

Xdebug

  • なんでかXdebugがデフォルトで使うport:9000と、php-fpmがデフォルトで使うport:9000とが、被っているので、どちらかを変える必要がある
    • なんでやねん
  • 設定例
xdebug.idekey="PHPSTORM"
xdebug.remote_enable=1
xdebug.remote_host=docker.for.mac.host.internal
xdebug.remote_port=9001
xdebug.remote_autostart=1
; xdebug.remote_connect_back=1

PhpStorm

よくわかんなかったんで適当にやった・・・

image.png

image.png

image.png

image.png

image.png

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

一足遅れて Kubernetes を学び始める - 06. workloads その2 -

ストーリー

  1. 一足遅れて Kubernetes を学び始める - 01. 環境選択編 -
  2. 一足遅れて Kubernetes を学び始める - 02. Docker For Mac -
  3. 一足遅れて Kubernetes を学び始める - 03. Raspberry Pi -
  4. 一足遅れて Kubernetes を学び始める - 04. kubectl -
  5. 一足遅れて Kubernetes を学び始める - 05. workloads その1 -
  6. 一足遅れて Kubernetes を学び始める - 06. workloads その2 -
  7. 一足遅れて Kubernetes を学び始める - 07. workloads その3 -
  8. 一足遅れて Kubernetes を学び始める - 08. discovery&LB その1 -

前回

一足遅れて Kubernetes を学び始める - 05. workloads その1 -では、Pod,ReplicaSet,Deploymentの3つを学習しました。今回はDaemonSet,StatefulSet(一部)を学びます。

DaemonSet

ReplicaSetとほぼ同じ機能のリソース。
ReplicaSetとの違いは、各ノードに1つずつ配置するのがDaemonSet,バラバラなのがReplicaSet。
用途として、モニタリングツールやログ収集のPodに使う。

さっそく、試してみます。

sample-ds.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: sample-ds
spec:
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: nginx-container
          image: nginx:1.12
          ports:
            - containerPort: 80
pi@raspi001:~/tmp $ k apply -f . --all --prune
daemonset.apps/sample-ds created
pi@raspi001:~/tmp $ k get all -o=wide
NAME                  READY   STATUS    RESTARTS   AGE   IP            NODE       NOMINATED NODE   READINESS GATES
pod/sample-ds-wxzbw   1/1     Running   0          60s   10.244.2.24   raspi003   <none>           <none>
pod/sample-ds-xjjtp   1/1     Running   0          60s   10.244.1.37   raspi002   <none>           <none>

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE    SELECTOR
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   6d1h   <none>

NAME                       DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE   CONTAINERS        IMAGES       SELECTOR
daemonset.apps/sample-ds   2         2         2       2            2           <none>          60s   nginx-container   nginx:1.12   app=sample-app

ReplicaSetと大きく違いはありません。
また、各ノードに対してpodが作られていることがわかります。

Deploymentと似ているアップデート戦略があり、OnDeleteとRollingUpdate(デフォルト)があります。前者は、podを明示的に削除した(k delete)際に更新する戦略です。DaemonSetは、死活監視やログ収集に使うので、手動でのタイミングが効くOnDeleteが好まれます。後者は、Deploymentと同じ動きで、即時更新していく戦略です。

ReplicaSetと似ているようで、機能的にはDeploymentに近い感じですね。ReplicaSetはpodが削除されたら複製されますけど、アップデートされません。DaemonSetはpodが削除されたら複製するし、アップデートもされます。試してみます。

sample-ds.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: sample-ds
spec:
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: nginx-container
          image: nginx:1.13
          ports:
            - containerPort: 80

nginxのバージョンを1.12から1.13に変更しました。

pi@raspi001:~/tmp $ k apply -f . --all --prune
daemonset.apps/sample-ds configured
pi@raspi001:~/tmp $ k get all -o=wide
NAME                  READY   STATUS              RESTARTS   AGE   IP            NODE       NOMINATED NODE   READINESS GATES
pod/sample-ds-sx4mv   0/1     ContainerCreating   0          5s    <none>        raspi003   <none>           <none>
pod/sample-ds-xjjtp   1/1     Running             0          12m   10.244.1.37   raspi002   <none>           <none>

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE    SELECTOR
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   6d2h   <none>

NAME                       DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE   CONTAINERS        IMAGES       SELECTOR
daemonset.apps/sample-ds   2         2         1       1            1           <none>          12m   nginx-container   nginx:1.13   app=sample-app

applyしてみると、一台ずつupdateされています(containerCreating)。Deploymentと違うのは、最大pod数が1のために、一時的にpodが機能しなくなるタイミングが生まれます(超過分の設定不可)。

pi@raspi001:~/tmp $ k delete pod sample-ds-sx4mv
pod "sample-ds-sx4mv" deleted
pi@raspi001:~/tmp $ k get all -o=wide
NAME                  READY   STATUS              RESTARTS   AGE     IP            NODE       NOMINATED NODE   READINESS GATES
pod/sample-ds-hgvtv   0/1     ContainerCreating   0          6s      <none>        raspi003   <none>           <none>
pod/sample-ds-k8cfx   1/1     Running             0          4m38s   10.244.1.38   raspi002   <none>           <none>

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE    SELECTOR
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   6d2h   <none>

NAME                       DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE   CONTAINERS        IMAGES       SELECTOR
daemonset.apps/sample-ds   2         2         1       2            1           <none>          17m   nginx-container   nginx:1.13   app=sample-app

podを削除しても、セルフヒーリングで復活します。

StatefulSet

ステートレスなpodではなく、DBのようなステートフルなpod向けのリソース。
podを削除しても、データを永続的に保存する仕組みが存在。
動作自体は、replicaSetと似ている。

さっそく、試してみます。

sample-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: sample-statefulset
spec:
  serviceName: sample-statefulset
  replicas: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: nginx-container
          image: nginx:1.12
          ports:
            - containerPort: 80
          volumeMounts:
          - name: www
            mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 1G

mountPathで指定したマウントしたいパスを、volumeClaimTemplatesでマウントしてくれます。 どこに? :thinking:
Storageに関しては別で学習することにします。
ひとまず、applyします。

pi@raspi001:~/tmp $ k apply -f . --all --prune
daemonset.apps/sample-ds unchanged
statefulset.apps/sample-statefulset created
pi@raspi001:~/tmp $ k get pod
NAME                   READY   STATUS    RESTARTS   AGE
sample-ds-hgvtv        1/1     Running   0          54m
sample-ds-k8cfx        1/1     Running   0          58m
sample-statefulset-0   0/1     Pending   0          5m19s
pi@raspi001:~/tmp $ k describe pod sample-statefulset-0
...
Events:
  Type     Reason            Age                  From               Message
  ----     ------            ----                 ----               -------
  Warning  FailedScheduling  70s (x3 over 2m28s)  default-scheduler  pod has unbound immediate PersistentVolumeClaims (repeated 2 times)

おや、Pendingになってしまいました。 pod has unbound immediate PersistentVolumeClaims :thinking:

PersistentVolumeとPersistentVolumeClaims

PersistentVolume(永続的ボリューム)は、名前の通りで、データを永続的に保存しておく場所のリソースです。
マネージドサービスを利用すると、デフォルトでPresistentVolumeが用意されているそうです。
私の環境は、マネージドサービスではなく、自作環境であるので、PresistentVolumeを用意する必要があります。

PersistentVolumeClaims(永続的ボリューム要求)は、これも名前の通りで、「PresistentVolumeを使わせて」というリソースです。
このリソースで、PresistentVolumeのnameを指定し、applyすることで、初めてマウントができます。
例えば、PodからPersistentVolumeClaimsの名前を指定してあげると、そのPodはClaimしたPersistentVolumeをマウントすることができます。
volumeClaimTemplatesというのは、「わざわざPersistentVolumeClaimsを定義しなくてもテンプレートに沿って書けばClaimsできるよ」というものです。

で、何が問題だったの?

pod has unbound immediate PersistentVolumeClaimsのとおりで、「PersistentVolumeの要求をしたけど、Volume割当できなかったよ」とのことです。

PersistentVolume(pv)があるのか確認してみます。

pi@raspi001:~/tmp $ k get pv
No resources found.

たしかにないです。PersistentVolumeを用意しないといけないのですが、どうしましょう。
解決手段として考えたのは3点です。

  1. GCPやAWS,Azureのサービスを使う
  2. LocalVolumeを使う
  3. NFSを使う

types-of-persistent-volumes

1は、書いておいてなんですが、却下です。理由は、せっかくraspberryPiで構築したのでクラウドサービスを利用したくないからです。

2は、Kubernetes: Local Volumeの検証の参考にして試したのですが、 記事にも書いてあるとおり「Local Volumeは他のPodから共有で利用することができない」ため、statefulsetがreplica:1でなければ動きません。それはそれで動くので学習になり良いのですが、せっかくならreplicaの制限なしにしたいです(ReadWriteManyにしたい)。

3は、もう一台raspberryPiを用意して、それをNFSと見立ててPersistentVolumeにしてみる方法です。

3を進めようと思います。

NFS導入

サーバ設定

NFS用の新たなraspberryPiを用意します。設定手順はこちらを参考にしました。
その後の続きは下記です。

NFSのホスト名はnfspiとします。

~ $ slogin pi@nfspi.local
pi@nfspi:~ $ sudo apt-get install nfs-kernel-server
pi@nfspi:~ $ sudo vim /etc/exports
/etc/exports
/home/data/ 192.168.3.0/255.255.255.0(rw,sync,no_subtree_check,fsid=0)

意味としては、「指定範囲のIPアドレスからのマウントを許可する」。オプションは、こちらを参照。

host ip
iMac 192.168.3.3
raspi001(master) 192.168.3.32
raspi002(worker) 192.168.3.33
raspi003(worker) 192.168.3.34
nfspi(NFS) 192.168.3.35
pi@nfspi:~ $ sudo mkdir -p /home/data
pi@nfspi:~ $ sudo chmod 755 /home/data
pi@nfspi:~ $ sudo chown pi:pi /home/data
pi@nfspi:~ $ sudo /etc/init.d/nfs-kernel-server restart
pi@nfspi:~ $ service rpcbind status
pi@nfspi:~ $ systemctl status nfs-server.service

正しく設定されたか、iMacから確認してみます。

~ $ mkdir share
~ $ sudo mount_nfs -P nfspi.local:/home/data ./share/
~ $ sudo umount share

OK

クライアント設定

各ノードに対して下記を実行します。

pi@raspi001:~ $ sudo apt-get install nfs-common

nfs-client導入

raspberryPi環境では、真っ白な状態なので、一からPersistentVolumeを用意する必要があります。それにはVolumeとなるStorageの型を用意する必要もあるのですが、Storage Classesを見る限り、NFS用の型は標準で存在しません。そこで、nfs-clientを使ってNFS用のStorageClassを作成します。

pi@raspi001:~ $ git clone https://github.com/kubernetes-incubator/external-storage.git && cd cd external-storage/nfs-client/
pi@raspi001:~/external-storage/nfs-client $ NS=$(kubectl config get-contexts|grep -e "^\*" |awk '{print $5}')
pi@raspi001:~/external-storage/nfs-client $ NAMESPACE=${NS:-default}
pi@raspi001:~/external-storage/nfs-client $ sed -i'' "s/namespace:.*/namespace: $NAMESPACE/g" ./deploy/rbac.yaml
pi@raspi001:~/external-storage/nfs-client $ k apply -f deploy/rbac.yaml

rbac.yamlにあるnamespaceを現在動かしている環境のnamespaceに置換して、applyしています。

pi@raspi001:~/external-storage/nfs-client $ k apply -f deploy/deployment-arm.yaml
pi@raspi001:~/external-storage/nfs-client $ k apply -f deploy/class.yaml

deployment-arm.yamlでは、NFSサーバのIPアドレス(192.168.3.35)とマウントパス(/home/data)を設定しました。
class.yamlが、今回欲していたNFSのstorageClass(managed-nfs-storage)になります。

※ raspberryPiのイメージはRaspbianを使っているので、arm用のdeployment-arm.yamlを使います。Wiki
これに随分とハマってしまいました... :cry:

pi@raspi001:~/external-storage/nfs-client $ k apply -f deploy/test-claim.yaml -f deploy/test-pod.yaml

試しにマウント先にファイルが作成できているのかテストしています。確認します。

nfspiに移動

pi@nfspi:~ $ ls /home/data

あれば成功です。あれば、下記で片付けます。

pi@raspi001:~/external-storage/nfs-client $ k delete -f deploy/test-pod.yaml -f deploy/test-claim.yaml

statefulsetをリトライ

以上で、StorageClassを用意できました。よって後は、PersistentVolume作って、PersistentVolumeClaim作って...となる予定でした。
しかし、nfs-clientには、dynamic provisioningという機能が備わっており、PersistentVolumeを作らなくても、PersistentVolumeClaimするだけで良くなります。この件については、storageを学習する際に書きます。

raspi001に移動して、sample-statefulset.yamlをもう一度applyします。
(storageClassName: managed-nfs-storageを追加, ReadWriteOnce→ReadWriteManyに変更)

sample-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: sample-statefulset
spec:
  serviceName: sample-statefulset
  replicas: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: nginx-container
          image: nginx:1.12
          ports:
            - containerPort: 80
          volumeMounts:
          - name: www
            mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes:
      - ReadWriteMany
      storageClassName: managed-nfs-storage
      resources:
        requests:
          storage: 1Gi
pi@raspi001:~/tmp $ k apply -f sample-statefulset.yaml

nfapiに移動して、あるか確認。

pi@nfspi:~ $ ls -la /home/data
total 20
drwxrwxrwx 5 pi     pi      4096 May  5 17:18 .
drwxr-xr-x 4 root   root    4096 May  4 15:50 ..
drwxrwxrwx 2 nobody nogroup 4096 May  5 17:17 default-www-sample-statefulset-0-pvc-5911505b-6f51-11e9-bb47-b827eb8ccd80
drwxrwxrwx 2 nobody nogroup 4096 May  5 17:18 default-www-sample-statefulset-1-pvc-5f2fd68e-6f51-11e9-bb47-b827eb8ccd80
drwxrwxrwx 2 nobody nogroup 4096 May  5 17:18 default-www-sample-statefulset-2-pvc-69bee568-6f51-11e9-bb47-b827eb8ccd80

ありました! マウントできています!

お片付け

--pruneでも良いのですが、下記のほうが使いやすかったです。

pi@raspi001:~/tmp $ k delete -f sample-ds.yaml -f sample-statefulset.yaml
pi@raspi001:~/tmp $ k delete pvc www-sample-statefulset-{0,1,2}

k get pvk get pvcを試して頂き、今回作ったリソースがありましたら削除お願いします。?

おわりに

StatefulSetを使える状態にするまでに記事が大きくなってしまいました。次回に詳しく学んでいこうと思います。笑
あと、nfs-clientを見て思ったのが、kubernetesのパッケージマネージャであるhelmを導入した方が、遥かに便利だと思いつつ、手動設定しました。。。
次回は、こちらです。

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

Dockerとは?Dockerと仮想マシンの違い

今回はDockerとは何か、Dockerと仮想マシンの違いについてを簡単に紹介します。
プログラミング初心者であるため、内容に誤りがあるかもしれません。
もし、誤りがあれば修正するのでどんどん指摘してください。

Docerって何?

開発者やシステム管理者 がコンテナを使用してアプリケーションを開発、デプロイ(いつでも使える状態にする)、および実行するためのプラットフォームのことです。
ここで、コンテナとは、Dockerイメージ(コンテナを作成するためのファイルシステムや設定をまとめたもの)を起動し、その内部でアプリケーションが稼動している状態のことです。
Dockerの特徴は、アプリケーションとその実行環境、展開/操作方法(スクリプト)などをまとめて1つのパッケージにし、保存/配布しているDockerイメージのダウンロードや起動が非常に軽量なことです。

Dockerと仮想マシンの違い

仮想マシンは、ホストOS上でハイパーバイザと呼ばれるソフトウェアを利用し仮想マシンを作成後、ゲストOSを動かし、その上でミドルウェアなどを動かします。
一方、Dockerでは、各コンテナはホストOSのカーネルは共有しつつも、各コンテナ内のアプリケーションは分離された場所で実行されます。
このように、プロセスやユーザなどを隔離することで、あたかも別のマシンが動いているかのように動かすことができ、軽量で高速に起動、停止などが可能です。

docker_vs_hv.png

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