20200519のdockerに関する記事は16件です。

alpineでC言語依存モジュールを pip install すると激重になる話

TL;DR

PyPiに上げられているC言語依存のpythonモジュールは、alpine標準のmuslには対応してないから毎回手元でコンパイルされるよ。
docker:alpineでどうしても使いたい場合は必要モジュールをビルドしたイメージを個別に用意しておくと良いよ。

alpineさんちのpip事情

pythonのモジュール管理行うpip。そのpipで採用されており快適にモジュールの導入を可能にしているwheel形式だが、alpineに対応した.whlがPyPi上に存在しないモジュールがある。

wheel

先に少しだけwheelの話。
wheelは元々Eggという形式の後継で作られたもので、Built Distributionと呼ばれるインストール形式であるそう。
Built Distributionここの用語集を見る限りでは以下の通り。

Distribution 形式のうち、中身のファイルとメタデータをターゲットシステムの正しい場所へ移動するだけでインストールができるもの。

Egg や Wheel の実体は zip/tar などの圧縮形式の拡張なので、pip install時はダウンロードして展開後にpipで管理している場所にファイルを移動しているだけとなる。なので速い。

コンパイル済み拡張モジュールを含んだパッケージ1も同様で取得・展開・移動するだけなので、これによりストレスフリーに使うことができている訳だ。
...ただしPyPi上に使用しているOS・アーキテクチャに対応したwheelファイルが存在する場合のみである。2

alineはmusl-libcで動く

alpineはmuslを採用している。
muslは軽量・高速・シンプルを目標に標準Cライブラリの実装を1から行っており、glibcなどの非標準な拡張ライブラリにも対応している。alpineにぴったりだ。
ただし、完全な互換はまだ実現できていない模様で稀に使いたい関数が存在しなかったりする。

で、今回のここで書く内容もalpineがmuslを使用している事が原因で掲題通りの事象が発生している。

ちなみに読み方はマッスルらしい。つよそう。

alpineはglibcを使っていない

今日、多くのLinuxディストリビューションはglibcを採用しているし、バイナリも共有ライブラリとして動的リンクさせてるのも少なくない。
numpyなどC言語を使用しているモジュールも例に漏れず3、PyPiにアップロードされているwheelファイルはglibc環境下で動くことを想定したビルドがなされている。つまり musl lib上で動く事を想定していない。4
そのためalpine上でpip install numpyをするとサポートされた.whlが存在しないため.zipやら.tar.gzが降ってくる。ビルド前のソースコードを丸々落としてきているのだ。

Collecting numpy
  Downloading numpy-1.18.3.zip (5.4 MB)
     |████████████████████████████████| 5.4 MB 1.3 MB/s 
  Installing build dependencies ... done

ダウンロード後はalpine用に一からコンパイルが実行されwheelファイルを作成する。そのあと出来上がったwheelファイルをpipは取り込み直すことでpython上でimportできるようにしている。
これがC言語依存モジュールをpip installすると時間がかかる原因である。

※ 他言語やOS・アーキテクチャに依存しないモジュールや、pureなpythonで書かれたモジュールだとalpineでもwheel形式で落ちてくるため時間はかからない。

浮かび上がる諸問題

C言語依存しているものでも数秒でコンパイルできるものもあるが、numpyを入れようとすると数分程度かかってしまうしnumpy依存のscipyなどを使おうとすると更に数十分コンパイルに時間がかかってしまうこともある。

一回限りのビルドで今後全ての開発環境を賄えるローカル環境であればそれでも良いかもしれないが、
製品やサービスをCI/CDを含めた環境構築することを考えていくとなると、膨大なコンパイル時間はとてつもないほど大きな障害となる。
git のブランチをフックにしてunitテストが走るCI環境、サーバーなどにサービスを安全にデリバリーするCD環境、etc、etc...
毎回毎回長いコンパイルが走ってしまうと細かい修正の確認ですら一時間単位で浪費してしまうことになるし、他の作業が滞ってしまう原因にもなる。
そしてなによりtwitterをする時間が減ってしまう。大問題だ。
実際に業務でpandasをalpineに突っ込んでしまった時は睡眠時間まで減ってしまったのだから、冗談抜きで死活問題である。

回避策

これを防ぐためには以下のものが考えられる。

  1. alpineを諦める
  2. コンパイル済みdocker imageで対処する
    1. ベースのディストリビューションとして使う
    2. multi stage ビルドを利用する

alpineを諦める

最も手間が少なく考える時間をかける必要がない有効な方法である。
ubuntuやcentosなどコンパイル済みで手段も確立しているディストリビューションに乗り換えてしまうのだ。
ただし、既に作り込んでしまっていて容易に乗り換えられないケースもあるのでは無かろうか?
業務で詰まった時は次の方法を取った。

コンパイル済みdocker imageで対処する

wheelのコンパイルが済んだ状態のdocker imageを自由に使える場所に置いてしまい、使いたい時にpullするというもの。

base-image
FROM python:3.8-alpine3.11
COPY require/requirements.txt /tmp
RUN apk update && \
  apk add --no-cache hoge-dev && \
  pip install --upgrade --no-cache-dir pip setuptools wheel && \
  pip install -r --no-cache-dir /tmp/requirements.txt

先ずはこんな感じでimageを作っておき、docker pushでdocker hubやawsのecrなどに保存しておく。

docker push hoge/huga:latest

2パターンあるがビルド完了しているものを使うという意味では同じ。

ベースのイメージとして使う

公式イメージなどと同じようにFROMを使ってベースイメージとして使う。
簡単だが、コンパイル時のみに必要なライブラリを消し忘れたりするとこちらのイメージサイズも大きくなってしまうなど、base-imagedockerfileへの依存度が高くなりメンテナンス性が下がってしまう。

builder-pattern
FROM hoge/huga:latest

multi stage ビルドとして使う

Docker17.05以上なら使える機能で、成果物だけイメージから取り出すことができる方法。
wheelコンパイルに関連するものはhoge/huga:latestに封じ込められるので後々気が楽になるから個人的におすすめ。

builder-pattern
FROM hoge/huga:latest as pip_build
FROM python:3.8-alpine3.11

COPY --from=pip_build /usr/local/lib/ /usr/local/lib/
COPY --from=pip_build /usr/local/bin/ /usr/local/bin/
COPY --from=pip_build /usr/local/include/ /usr/local/include/

上記だとディレクトリを直接張り付けてしまっているので、コンパイル済みの*.whlhoge/huga:latestから持ってきてpip installする方が安全かも。

欠点

解決策としなかったのは上記の方法だと、複数のdockerイメージのメンテナンスを避けられないためだ。
作成した実行環境よりもソースコードで使用するライブラリや書式の方が最新になってしまった場合が起こるたび、長大なコンパイルを実行しpushし直す必要が出てくる...
安眠できる日々は長くは続かなさそうだ。

参考文献

おまけ記事

alpineでC言語依存モジュールを pip install した時の時間を計測してみた


  1. 用語集曰くBinary Distributionと呼ぶらしく、拡張モジュールごと移動させ使用している。 

  2. PyPiのモジュールのページ左側[Navigation]->[Download files]から確認できる。 

  3. ダウンロードしたwheelをunzipで展開してやると*.soが見える。pipが正しく動いている以上、OS・アーキテクチャに最適化されたものが存在しているはずだ。 

  4. muslはアプリケーションを単一のポータブルなバイナリファイルとして配布できるように静的リンクに最適化している。(wikipediaから引用) ld-musl-x86_64.so.1しか存在しないのでld-linux-x86-64.so.2にダイナミックリンクしているものは軒並み落ちる。 

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

Dockerでの環境構築(Rails)超入門3 ~MySQLを立ち上げる~

これまで

Dockerでの環境構築(Rails)超入門1
Dockerでの環境構築(Rails)超入門2 ~Dockerfileの設定~

やること

前々回立ち上げたRailsは初期設定で「sqlite3」に接続している
今回はMySQLサーバーを立ち上げ、複数コンテナを動作させる

手順

  1. MySQLサーバーを立ち上げる
  2. Railsのプロジェクトを書き換える

実践

  1. MySQLサーバーを立ち上げる

1) docker-compose.yamlの編集

docker-compose.yaml
version: '3'
services:
  mysql:
    image: mysql:8.0.20
    command: --default-authentication-plugin=mysql_native_password
    volumes:
       - "./mysql-data:/var/lib/mysql"
    environment:
      MYSQL_ROOT_PASSWORD: root
  app:
    build: .
    volumes:
      - ".:/app"
    ports:
      - "3000:3000"
    tty: true
    depends_on:
      - mysql

2)「mysql-data」フォルダをappと同じ階層に作成

3) Dockerfileに以下を追加

Dockerfile
FROM ruby:2.6.6-stretch
RUN gem rails install
RUN apt-get update && \
    apt-get install -y node.js *mysql-client*

COPY Gemfile/Gemfile
COPY Gemfile.lock/Gemfile.lock

RUN bundle install

4) Mysqlに接続する

$ docker-compose up --build
$ docker exec -it practice_app_1 /bin/bash
/# mysql -u root -proot -h mysql
> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+

2 . Railsのプロジェクトを書き換える
・Gemfileを以下に編集

Gemfile
(省略)
gem 'sqlite3'→ gem *'mysql2'
(省略)

・config/database.yamlを編集

config/database.yaml
default: &default
  adapter: mysql2
  enconding: utf8
  username: root
  password: root
  host: mysql
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: practice_development

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: practice_test

production:
  <<: *default
  database: practice_production

・app/#に移動してrake db:createでデータベースを作成

・再度Mysqlに接続してデータベースが作成されていることを確認

+----------------------+
| Database             |
+----------------------+
| information_schema   |
| mysql                |
| performance_schema   |
| practice_development |
| practice_test        |
| sys                  |
+----------------------+

最後にrails s -b 0.0.0.0でローカルに接続すれば開発環境の構築はOK!

補足

  1. 別のコマンドでコンテナに入る

今まで$ docker exec -it pracitce /bin/bashでコンテナに入っていたが、「docker-compose.yaml」が入っているファイルがあれば、

$ docker-compose exec コンテナ名(app)/bin/bash

2 . コンテナに入ったあと、自動的にappに移動したい
1) docker-compose.yamlの編集

docker-compose.yaml
version: '3'
services:
  mysql:
    image: mysql:8.0.20
    command: --default-authentication-plugin=mysql_native_password
    volumes:
       - "./mysql-data:/var/lib/mysql"
    environment:
      MYSQL_ROOT_PASSWORD: root
  app:
    build: .
    volumes:
      - ".:/app"
    ports:
      - "3000:3000"
    tty: true
    depends_on:
      - mysql
#以下を追加
    *working_dir: "/app"*

これでdocker exec compose app /bin/bashの後、自動的にapp/#に移動している

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

Docker / ECR / ECS コンテナ入門まとめ②

前後編

参考文献

1. Dockerfile

◆ mysql-client問題

  • Debian10 "buster"(Ubuntuの母らしい)では、"mysql-client"は存在しない
  • "default-mysql-client"パッケージを使用する必要がある
Dockerfile
FROM ruby:2.6.5    ※GemfileのRubyバージョン要確認
RUN apt-get update && apt-get install -y nodejs --no-install-recommends && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y default-mysql-client --no-install-recommends && rm -rf /var/lib/apt/lists/*
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
RUN mkdir /app   ※アプリケーション名
WORKDIR /app
ADD Gemfile /app/Gemfile
ADD Gemfile.lock /app/Gemfile.lock
RUN bundle install
ADD . /app

2. docker-compose.yml

◆ ビルドコンテキストについて

  • docker build実行時の"カレントディレクトリ"を指定する
  • デフォルトでは、Dockerfileが"カレントディレクトリ"と認識される
docker-compose.yml
version: '2'
services:
  db:
    image: mysql:latest
    environment:
      MYSQL_DATABASE: データベース名
      MYSQL_ROOT_PASSWORD: XXXXXXX
      MYSQL_USER: ユーザ名
      MYSQL_PASSWORD: XXXXXXX
    ports:
      - "3306:3306" 
  web:
    build:
      context: .
      dockerfile: Dockerfile
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    tty: true
    stdin_open: true
    depends_on:
      - db 
    ports:
      - "3000:3000" 
    volumes:  
      - .:/app   ※アプリケーション名

3. database.yml

◆ 環境変数について

 password: <%= ENV['DOCKER_DATABASE_PASSWORD'] %>
$ export DOCKER_DATABASE_PASSWORD=password  ※パスワード

database.yml

database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: 5
  host: db

development:
  <<: *default
  username: ユーザ名
  password: XXXXXXX
  database: データベース名

production:
  <<: *default
  database: データベース名
  username: ユーザ名
  password: <%= ENV['DOCKER_DATABASE_PASSWORD'] %>

4. ECRプッシュ作業

AWS CLI設定 ※AWSマネジメントコンソール"マイセキュリティ資格情報"参照

$ aws configure --profile ecr
AWS Access Key ID [None]: ***********************
AWS Secret Access Key [None]: *************************
Default region name [None]: ap-northeast-1
Default output format [None]: json

ECRログインコマンド

$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ***.ecr.ap-northeast-1.amazonaws.com

イメージ作成

$ docker build -t 【イメージ名】 .

タグ付け

$ docker tag イメージ名:タグ名 ***.dkr.ecr.ap-northeast-1.amazonaws.com/イメージ名:タグ名

プッシュコマンド

$ docker push ***.dkr.ecr.ap-northeast-1.amazonaws.com/イメージ名:タグ名
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ubuntu 20.04 LTS 日本語Remixへの Docker と Docker-compose インストール

ubuntu 20.04 LTS 日本語Remix に DockerとDocker-compose をインストールしてみます。

導入の仕方を決める。

インストールされるパッケージを比較したら、aptでさくっと入れられるバージョンと
Docker公式から入手できる物に大きな差はなさそう。
と言う事で、簡単な標準リポジトリからインストールしちゃいます。

2020年5月19日 の比較

パッケージ 標準リポジトリ Docker公式サイト
docker.io 19.03.8-0 19.03.8
docker-compose 1.25.0-1 1.25.5

Docker/Docker-composeのインストール

依存関係の確認

Docker-composeをインストールすればDockerもインストールされるみたい。

user01@ubuntu20:~$ apt depends docker-compose
docker-compose
  依存: python3-cached-property (>= 1.2.0)
  依存: python3-docker (>= 4.0.0)
  依存: python3-dockerpty (>= 0.4.1)
  依存: python3-docopt (>= 0.6.1)
  依存: python3-jsonschema
  依存: python3-requests (>= 2.20.0)
  依存: python3-six (<< 2)
  依存: python3-six (>= 1.3.0)
  依存: python3-texttable (>= 0.9.0)
  依存: python3-websocket (>= 0.32.0)
  依存: python3-yaml (>= 3.10)
  依存: <python3:any> (>= 3.6~)
    python3:i386
    python3
  依存: python3-distutils
  推奨: docker.io (>= 1.9.0)
user01@ubuntu20:~$ 

Docker-composeのインストール

Docker-composeを指定してインストールをすれば、docker含め一式インストールできるから
インストールコマンドがとてもスッキリしてます。

user01@ubuntu20:~$ sudo apt -y install docker-compose
[sudo] user01 のパスワード: 
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています                
状態情報を読み取っています... 完了
以下の追加パッケージがインストールされます:
  bridge-utils cgroupfs-mount containerd docker.io git git-man liberror-perl pigz python3-attr python3-cached-property
  python3-distutils python3-docker python3-dockerpty python3-docopt python3-importlib-metadata python3-jsonschema
  python3-more-itertools python3-pyrsistent python3-setuptools python3-texttable python3-websocket python3-zipp runc
  ubuntu-fan
提案パッケージ:
  ifupdown aufs-tools btrfs-progs debootstrap docker-doc rinse zfs-fuse | zfsutils git-daemon-run | git-daemon-sysvinit
  git-doc git-el git-email git-gui gitk gitweb git-cvs git-mediawiki git-svn python-attr-doc python-jsonschema-doc
  python-setuptools-doc
以下のパッケージが新たにインストールされます:
  bridge-utils cgroupfs-mount containerd docker-compose docker.io git git-man liberror-perl pigz python3-attr
  python3-cached-property python3-distutils python3-docker python3-dockerpty python3-docopt python3-importlib-metadata
  python3-jsonschema python3-more-itertools python3-pyrsistent python3-setuptools python3-texttable python3-websocket
  python3-zipp runc ubuntu-fan
アップグレード: 0 個、新規インストール: 25 個、削除: 0 個、保留: 0 個。

〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

user01@ubuntu20:~$

Dockerサービスの有効化

インストール後、docker.serviceを有効化しましょう。

user01@ubuntu20:~$ systemctl list-unit-files |grep docker
docker.service                             disabled        enabled      
docker.socket                              enabled         enabled      
user01@ubuntu20:~$ sudo systemctl start docker
[sudo] user01 のパスワード: 
user01@ubuntu20:~$ sudo systemctl enable docker
Created symlink /etc/systemd/system/multi-user.target.wants/docker.service → /lib/systemd/system/docker.service.
user01@ubuntu20:~$ systemctl list-unit-files |grep docker
docker.service                             enabled         enabled      
docker.socket                              enabled         enabled      
user01@ubuntu20:~$ 

一般ユーザ(Docker管理者)をグループ docker に追加

一般ユーザ(Docker管理者)が sudo なしで docker を操作出来るように、グループdockerに追加します。

グループ docker に参加させることで sudo せずに dockerコマンドを実行できるようになります。
この設定をしなくてもsudoすれば良いだけなので必要に応じて任意で実行してください。
以下の設定例では $USER (ログイン中のユーザ) をDocker管理者に追加しています。
※ログイン中のユーザにはgroupの変更は反映されません。docker の操作前に再ログインしてください。

user01@ubuntu20:~$ sudo usermod -aG docker $USER
[sudo] user01 のパスワード: 
user01@ubuntu20:~$ id $USER
uid=1000(user01) gid=1000(user01) groups=1000(user01),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),120(lpadmin),131(lxd),132(sambashare),133(docker)
user01@ubuntu20:~$ 

動作確認

hello-world を 実行

こんなメッセージが表示されれば、ちゃんと動いてると思います。

user01@ubuntu20:~$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
0e03bdcc26d7: Pull complete 
Digest: sha256:6a65f928fb91fcfbc963f7aa6d57c8eeb426ad9a20c7ee045538ef34847f44f1
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:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

user01@ubuntu20:~$ 

参考

Ubuntuを入手:Ubuntu Japanese Team
Dockerインストール手順(テスト手順を参照):Install Docker Engine on Ubuntu

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

Dockerの開発環境で通常の開発言語のコマンドで実行する方法

Dockerの開発環境ではDockerの開発環境特有のコマンドでの操作が必要になりますが、スクリプト言語のコマンドをローカル環境で実行するときと同じコマンドで開発ができる方法を紹介します。

環境

Docker 19.03.8
Ruby 2.5
Rails 5.2

Dockerとローカルの開発環境で実行するときの違い

例えば以下のRuby on Railsの開発で用いられるコマンド

$ rails db:migrate

このコマンドをDockerで実行する場合

$ docker-compose run web rails db:migrate

このようなコマンドになります。
それ以外にも操作方法がありますが、Ruby on Railsで開発するならRubyやRailsの通常のコマンドの方が楽ですよね。その場合は以下の手順でコマンドを実行します。

Rubyの通常のコマンドで開発を行うための準備

Dockerを立ち上げます。

$ docker-compose start

コンテナ名を調べます。

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
a9b19e00552e        recipegram_web      "entrypoint.sh bash …"   44 hours ago        Up 11 seconds       0.0.0.0:3000->3000/tcp   recipegram_web_1
975875c12c76        postgres            "docker-entrypoint.s…"   46 hours ago        Up 12 seconds       5432/tcp                 recipegram_db_1

コンテナ名を入れて下記のコマンドを実行します。

$ docker exec -it a9b19e00552e bash

そうするとこのように切り替わるのでこの状態で今回であればRubyやRailsの実行コマンドで開発が行えます^_^

root@a9b19e00552e:/myapp#
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerでJupyter、JupyterLabを使うまで

DockerでJupyter、JupyterLabを使うまで

実行環境

  • Windows10 (64bit)

原理的にはこの方法で行えばMacでもできると思います。

設定手段

docker-anacondaを参考にdocker pullを行う。

docker pull jupyter/datascience-notebook

その後書かれた通りに以下を実行すると、ハマる。

docker run -i -t -p 8888:8888 continuumio/anaconda3 /bin/bash -c "/opt/conda/bin/conda install jupyter -y --quiet && mkdir /opt/notebooks && /opt/conda/bin/jupyter notebook --notebook-dir=/opt/notebooks --ip='*' --port=8888 --no-browser"

Running as root is not recommended. Use --allow-root to bypass.

そこで、--allow-rootを加えろとの言われたので加える。

docker run -i -t -p 8888:8888 continuumio/anaconda3 /bin/bash -c "/opt/conda/bin/conda install jupyter -y --quiet && mkdir /opt/notebooks && /opt/conda/bin/jupyter notebook --notebook-dir=/opt/notebooks --ip='*' --port=8888 --no-browser --allow-root"

上記のように実行するとlocalhostのurlが出てくるので実行するとJupyter notebookが使える。

docker run -i -t -p 8888:8888 continuumio/anaconda3 /bin/bash -c "/opt/conda/bin/conda install jupyter -y --quiet && mkdir /opt/notebooks && /opt/conda/bin/jupyter-lab --notebook-dir=/opt/notebooks --ip='*' --port=8888 --no-browser --allow-root"

上記のように実行するとJupyterLabを使うことができる。

参考文献

https://hub.docker.com/r/continuumio/anaconda3/
https://github.com/ContinuumIO/docker-images/issues/94

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

今更だけどDockerについて学んでみた

Dockerとは?

Dockerとは、従来のホスト型(VMware等)やハイパーバイザー型(ESXi等)の仮想マシン技術とは異なり、ホストOS上に用意した仮想空間”コンテナ”内で、OSやアプリケーションを実行できる仮想コンテナ技術を提供するOSSソフトウェアのこと。

2013年にdotCloud社(現 Docker Inc.)からリリースされた。現在は「DevOps環境の構築」や「ソフトウェア開発環境の構築」等の様々な分野で注目されている。

無償版と有償版

現在、Dockerエンジンには大きくわけて2種類存在する。1つは、無償版のDockerエンジンであり、もう1つは有償版のDockerエンジンである。無償版のDockerエンジンは、Docker Community Edition(DockerCE)と呼ばれる。一方有償版のDockerエンジンは、Docker Enterprise Edition(DockerEE)と呼ばれる。

活用事例

メリット/デメリット

メリット

  • 「アプリケーションの開発環境と実行環境をパッケージ化」し、迅速な配備や破棄が可能である。
  • 仮想化技術の一種であり、1台の物理サーバ上に複数のコンテナを集約できるため、管理のコストや不可が削減できる
  • 従来の仮想マシン(VMware等)と比較して、仮想OSを複数立ち上げる必要がなく、リソースの消費を抑えれるため、非常に軽量である
  • イミューダブル・インフラストラクチャ(一旦、サーバを構築すると、ソフトウェアのバージョンアップやパッチ適用等の煩雑な管理を一切放棄するシステム)が可能である
  • 『Docker Hub』と呼ばれるコミュニティサービスが活発であるため、既に多くのアプリケーションイメージが提供されている

デメリット

  • 大量のDockerコンテナの管理が大変である。そこで、コンテナ化したアプリケーションのデプロイ、スケーリング、及び管理を行うためにコンテナオーケストレーション(コンテナを効率良く運用・開発するための技術)システムであるKubernetesが利用されるケースが増加している。
  • キャパシティプランニング(計画・開発中あるいは稼働中のITシステムに求められるサービス需要/サービスレベルからシステムリソースの処理能力や数量などを見積もり、最適なシステム構成を計画すること)のベストプラクティスの事例が少ないため、明確に定義することが難しい。
  • ライブマイグレーション(動作中の仮想マシンを停止させることなく、別のサーバに移動して処理を継続させる機能)をサポートしていない。
  • ホストOS上のコンテナで稼働させるOSに制約がある。例えば、ホストOSがLinuxの場合には、その上で稼働するコンテナもLinuxに限定されてしまう。

概要と概要図

Docker概要図.png

  • Dokcerエンジン: アプリケーションのパッケージ化やコンテナの実行を担う
  • Dockerイメージ: OSやアプリケーションを含んだテンプレートのベースイメージ
  • Docker Hub レジストリ :公開されているDockerイメージをSaaS経由で提供する
  • Dokcer プライベートレジストリ :ローカルで作成・保管したイメージの保管庫
  • Docker コンテナ:分離された名前空間とアプリケーションの実行環境
  • Docker クライアント :ユーザがコマンドを発行し、Dockerデーモンと通信を行う

Dockerでは、様々なOSとアプリケーションをパッケージ化された環境が、Docker Hubと呼ばれるレジストリサービスで用意されている。Docker Hubは「Docker Hub レジストリ」とも呼ばれ、インターネットを経由して、OS環境とアプリケーションを含んだイメージを入手することができる。

このDocker Hubから入手したイメージは「Dockerイメージ」と呼ばれる。Dockerイメージは、ベースイメージとも呼ばれており、Linux OSとアプリケーションを含んだ一種のテンプレートである。

Dockerのレジストリには、世界中のユーザや開発者が作成したDockerイメージが大量に保管されている。イメージを一から作成することも可能であるが、既にDocker Hubに用意されているイメージをそのまま利用できるため、OSやアプリケーションのインストールや初期設定等にかかる煩雑な作業工程を大幅に削減できるメリットがある。

一方で、インターネットを使わずに、ローカルのシステム用に配信するイメージの保管庫は「Docker プライベートレジストリ」と呼ばれる。ローカルのシステムにおいて、OSとアプリケーションのパッケージ化や、イメージを使ったコンテナの実行は、Docker本体が担う。このDockerの本体は、「Dockerエンジン」と呼ばれる。Dockerエンジンが実行するコンテナは、「Dockerコンテナ」と呼ばれる。

Dockerコンテナは、ホストOS上で複数同時に起動させることができ、ホストOSから見ると分離された名前空間として見える。通常ユーザは、Dockerデーモンが稼働するホストOS上でコマンドラインから操作を行うが、このDockerデーモンの操作を担う各種コマンドなどのインターフェースは「Dockerクライアント」と呼ばれる。Dockerクライアントは、Dockerデーモンが稼働するホストOS上でコマンドラインで操作できるが、遠隔地にあるDockerデーモンと通信を行うことも可能である。

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

今更だけどDockerについて学んでみよう

Dockerとは?

Dockerとは、従来のホスト型(VMware等)やハイパーバイザー型(ESXi等)の仮想マシン技術とは異なり、ホストOS上に用意した仮想空間”コンテナ”内で、OSやアプリケーションを実行できる仮想コンテナ技術を提供するOSSソフトウェアのこと。

2013年にdotCloud社(現 Docker Inc.)からリリースされた。現在は「DevOps環境の構築」や「ソフトウェア開発環境の構築」等の様々な分野で注目されている。

無償版と有償版

現在、Dockerエンジンには大きくわけて2種類存在する。1つは、無償版のDockerエンジンであり、もう1つは有償版のDockerエンジンである。無償版のDockerエンジンは、Docker Community Edition(DockerCE)と呼ばれる。一方有償版のDockerエンジンは、Docker Enterprise Edition(DockerEE)と呼ばれる。

活用事例

メリット/デメリット

メリット

  • 「アプリケーションの開発環境と実行環境をパッケージ化」し、迅速な配備や破棄が可能である。
  • 仮想化技術の一種であり、1台の物理サーバ上に複数のコンテナを集約できるため、管理のコストや不可が削減できる
  • 従来の仮想マシン(VMware等)と比較して、仮想OSを複数立ち上げる必要がなく、リソースの消費を抑えれるため、非常に軽量である
  • イミューダブル・インフラストラクチャ(一旦、サーバを構築すると、ソフトウェアのバージョンアップやパッチ適用等の煩雑な管理を一切放棄するシステム)が可能である
  • 『Docker Hub』と呼ばれるコミュニティサービスが活発であるため、既に多くのアプリケーションイメージが提供されている

デメリット

  • 大量のDockerコンテナの管理が大変である。そこで、コンテナ化したアプリケーションのデプロイ、スケーリング、及び管理を行うためにコンテナオーケストレーション(コンテナを効率良く運用・開発するための技術)システムであるKubernetesが利用されるケースが増加している。
  • キャパシティプランニング(計画・開発中あるいは稼働中のITシステムに求められるサービス需要/サービスレベルからシステムリソースの処理能力や数量などを見積もり、最適なシステム構成を計画すること)のベストプラクティスの事例が少ないため、明確に定義することが難しい。
  • ライブマイグレーション(動作中の仮想マシンを停止させることなく、別のサーバに移動して処理を継続させる機能)をサポートしていない。
  • ホストOS上のコンテナで稼働させるOSに制約がある。例えば、ホストOSがLinuxの場合には、その上で稼働するコンテナもLinuxに限定されてしまう。

概要と概要図

Docker概要図.png

  • Dokcerエンジン: アプリケーションのパッケージ化やコンテナの実行を担う
  • Dockerイメージ: OSやアプリケーションを含んだテンプレートのベースイメージ
  • Docker Hub レジストリ :公開されているDockerイメージをSaaS経由で提供する
  • Dokcer プライベートレジストリ :ローカルで作成・保管したイメージの保管庫
  • Docker コンテナ:分離された名前空間とアプリケーションの実行環境
  • Docker クライアント :ユーザがコマンドを発行し、Dockerデーモンと通信を行う

Dockerでは、様々なOSとアプリケーションをパッケージ化された環境が、Docker Hubと呼ばれるレジストリサービスで用意されている。Docker Hubは「Docker Hub レジストリ」とも呼ばれ、インターネットを経由して、OS環境とアプリケーションを含んだイメージを入手することができる。

このDocker Hubから入手したイメージは「Dockerイメージ」と呼ばれる。Dockerイメージは、ベースイメージとも呼ばれており、Linux OSとアプリケーションを含んだ一種のテンプレートである。

Dockerのレジストリには、世界中のユーザや開発者が作成したDockerイメージが大量に保管されている。イメージを一から作成することも可能であるが、既にDocker Hubに用意されているイメージをそのまま利用できるため、OSやアプリケーションのインストールや初期設定等にかかる煩雑な作業工程を大幅に削減できるメリットがある。

一方で、インターネットを使わずに、ローカルのシステム用に配信するイメージの保管庫は「Docker プライベートレジストリ」と呼ばれる。ローカルのシステムにおいて、OSとアプリケーションのパッケージ化や、イメージを使ったコンテナの実行は、Docker本体が担う。このDockerの本体は、「Dockerエンジン」と呼ばれる。Dockerエンジンが実行するコンテナは、「Dockerコンテナ」と呼ばれる。

Dockerコンテナは、ホストOS上で複数同時に起動させることができ、ホストOSから見ると分離された名前空間として見える。通常ユーザは、Dockerデーモンが稼働するホストOS上でコマンドラインから操作を行うが、このDockerデーモンの操作を担う各種コマンドなどのインターフェースは「Dockerクライアント」と呼ばれる。Dockerクライアントは、Dockerデーモンが稼働するホストOS上でコマンドラインで操作できるが、遠隔地にあるDockerデーモンと通信を行うことも可能である。

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

Docker, Docker compose コマンドメモ

  • よく使う&忘れがちなdocker のコマンドを中心にメモ.

存在しているコンテナのうち,停止しているコンテナを起動させる

docker start (comtainer id)
ex) docker stop f4a7cfeb3445

起動しているコンテナを停止させる

docker stop (comtainer id)
ex) docker stop f4a7cfeb3445

今起動中のコンテナの一覧を表示

docker ps 

今存在している,起動中or停止中のコンテナの一覧を表示

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

Docker コマンド

よく使うDockerコマンド集

実行中のコンテナ表示

$ docker ps

存在するコンテナ表示

$ docker ps -a

イメージの構築

$ docker-compose build

キャッシュがあるとき、イメージの構築、コンテナの構築・起動を一括でやってくれる

$ docker-compose up

-d オプションを付けてバックグランドで実行

$ docker-compose up -d

-build キャッシュ無いときはbuildして、イメージを構築する

$ docker-compose up --build -d

docker-compose.yml書いたら、これ実行している。

image.png

同じコンテナ名があるのでコンテナ作成できませんでしたという警告がでるときがある

$ docker stop python3

$ docker rm python3

一旦動作しているコンテナを停止し、コンテナを削除する。

$ docker-compose exec python3 bash

$ docker-compose exec db bash

$ docker exec -it db bash

Docker API
https://docs.docker.com/compose/reference/exec/

Docker内の実行しているコンテナでbashを開始する

参考

docker-composeのupとbuildの違いについて
https://qiita.com/tegnike/items/bcdcee0320e11a928d46

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

Docker コマンド 作業メモ

よく使うDockerコマンド集

実行中のコンテナ表示

$ docker ps

存在するコンテナ表示

$ docker ps -a

イメージの構築

$ docker-compose build

キャッシュがあるとき、イメージの構築、コンテナの構築・起動を一括でやってくれる

$ docker-compose up

-d オプションを付けてバックグランドで実行

$ docker-compose up -d

-build キャッシュ無いときはbuildして、イメージを構築する

$ docker-compose up --build -d

docker-compose.yml書いたら、これ実行している。

image.png

同じコンテナ名があるのでコンテナ作成できませんでしたという警告がでるときがある

$ docker stop python3

$ docker rm python3

一旦動作しているコンテナを停止し、コンテナを削除する。

$ docker-compose exec python3 bash

$ docker-compose exec db bash

$ docker exec -it db bash

Docker API
https://docs.docker.com/compose/reference/exec/

Docker内の実行しているコンテナでbashを開始する

参考

docker-composeのupとbuildの違いについて
https://qiita.com/tegnike/items/bcdcee0320e11a928d46

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

Dockerコンテナ実行時に、You must use Bundler 2 or greater with this lockfile.

dockerコンテナ上でdocker-compose exec app rails consoleコマンドでrailsのコンソールを立ち上げようとしたところ、You must use Bundler 2 or greater with this lockfile.と出てしまったときの対処法。

[メモ]

docker-compose exec app gem install bundler
docker-compose exec app bundle install

以上。

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

Docker ミニ用語集 手順書フォルダ

Docker: Dockerはコンテナと呼ばれる仮想化のための技術。
Dockerfile: インフラの構成をコードに落とし込み宣言的に環境を構築する手法としての機能
Image: 特定の時点のスナップショットとしての機能を提供するもの

学んだサイト/動画
https://qiita.com/phorizon20/items/57277fab1fd7aa994502
https://qiita.com/y-do/items/e127211b32296d65803a
https://www.youtube.com/watch?v=DS5HBTMG1RI&list=PLtpYHR4V8Mg-jbuk4yoXhXwJtreodnvzg

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

【備忘録】Rails3ルーティング確認

はじめに

今回はタイトルの通りにRailsにおけるルーティングの確認方法を残します。
触れているシステムのバージョンが古いため、Webブラウザでの確認が出来ずに詰まりました・・・。

環境

Rails 3.0.19
docker-compose version 2

Dockerコンテナからコマンドで確認する

1. サービス用コンテナを起動する

docker-compose up -d

-d:デタッチド・モード: バックグラウンドでコンテナを実行し、新しいコンテナ名を表示

2. railsがインストールされているコンテナに入る(今回はappコンテナ)

docker-compose run app /bin/bash

dockerやRailsにバージョンによって、コマンドが変わります。
直近で多く見られるコマンドは以下の通り。

docker-compose exec app bash

3. ルーティング確認コマンドを実行する

[root@[コンテナID] trunk]# bundle exec rake routes

上記コマンドですと全件出力されてしまい見づらいため、grepコマンドを利用すると見やすいです。

[root@[コンテナID] trunk]# bundle exec rake routes | grep [絞り込みたい文字列]

参考

Docker ドキュメント日本語化プロジェクト
docker-composeコマンド

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

macOS上でSQL Serverを使用してC#アプリを作成する

はじめに

この記事では、Microsoft 社が公開している Build an app using SQL Server の内容に従い、SQL Server を使用した C# アプリを作成します。

環境

  • OS: macOS Catalina バージョン 10.15.4
  • SQL Server: SQL Server 2019
  • .NET Core: 3.1 LTS

環境のセットアップ

ここでは、SQL Server を Docker 上で取得します。その後、SQL Server で .NET Core アプリを作成するために必要な依存関係をインストールします。

SQL Server のインストール

  1. macOS で SQL Server を実行するには、SQL Server on Linux の Docker イメージ を使用します。そのためには、Docker for Mac をインストールする必要があります。
  2. Docker 環境に最低 4GB のメモリを設定し、パフォーマンスを評価したい場合は複数のコアを追加することも検討します。これは、メニューバーの [環境設定] -> [詳細設定] オプションで行うことができます。
  3. 新しいターミナルプロンプトを起動し、以下のコマンドを使用して SQL Server on Linux Docker イメージをダウンロードして起動します。SA_PASSWORD の部分は特殊文字を使用した強力なパスワードを使用するように書き換えてください。
sudo docker pull microsoft/mssql-server-linux:2017-latest
docker run -e 'HOMEBREW_NO_ENV_FILTERING=1' -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=yourStrong(!)Password' -p 1433:1433 -d microsoft/mssql-server-linux

筆者は、Docker Desktop をインストールすると一緒に利用可能になる、Docker Compose を利用して、SQL Server 2019 on Linux をインストールしています。docker-compose.yaml については、以下の GitHub リポジトリを参照してください。

なお、ウェブ上で公開されている Docker イメージ の URL は、Ubuntu ベースの SQL Server 2017 on Linux の Docker イメージ です。SQL Server 2019では、従来の Ubuntu ベースに加え、RHEL ベース の SQL Server on Linux の Docker イメージ もサポートされるようになりました。
Ubuntu ベースを利用するか、RHEL ベースを利用するかは、好きな方を選択してください。(SQL Server における機能に差はありません)

Homebrew と .NET Core のインストール

すでに .NET Core 3.1 LTS がインストールされている場合は、このステップをスキップしてください。

公式インストーラー をダウンロードして、.NET Coreをインストールします。インストーラーは、Build apps - SDK のものを選択してください。.NET Core 3.1.x SDK および、.NET Core ランタイムを一緒にインストールできます。

  • ASP.NET Core Runtime
  • Desktop Runtime
  • .NET Core Runtime (上の2つを一緒にしたもの)

.NET Core では、作成したアプリを実行するためのランタイムが、種類によって分けられています。
ダウンロードサイト上では分かれて表示されているため、悩んでしまう可能性がありますが、ここでは気にしないでください。

なお、本家のサイトにあるリンクは .NET Core 2.0 のダウンロードリンクになっています。これは既にサポート切れ、かつ LTS ではないため、最新の .NET Core 3.1.x をダウンロードし、インストールするようにしてください。.NET Core 3.1 は LTS バージョンになります。

なお、筆者は、SQL Server 2019 と同様、Docker Compose を使用して、.NET Core 3.1 のコンテナーを作成し、開発を進めています。docker-compose.yaml については、以下の GitHub リポジトリを参照してください。

SQL Server および .NET Core 3.1 を Docker 上で利用する場合は、同じ docker-compose.yaml ファイル内に記述します。container_name を記述することで、コンテナ名を使ってコンテナ同士の相互通信が可能になります。

docker-compose.yaml(例)
version: '3'

services:
  app:
    image: mcr.microsoft.com/dotnet/core/sdk:latest
    container_name: dotnetcoreapp
    tty: true
    ports:
      - 10080:80
    volumes:
      - ./src:/src
    working_dir: "/src"

  mssql:
    image: mcr.microsoft.com/mssql/rhel/server:2019-latest
    container_name: 'mssql2019'
    environment:
      - MSSQL_SA_PASSWORD=databaseadmin@1
      - ACCEPT_EULA=Y
    ports:
      - 1433:1433
    # volumes: # Mounting a volume does not work on Docker for Mac
    #   - ./mssql/log:/var/opt/mssql/log
    #   - ./mssql/data:/var/opt/mssql/data

SQL Server を使った C# アプリケーションを作成

ここでは、以下、2 つのシンプルな C# アプリを作成します。

  • 基本的な Insert、Update、Delete、Select を実行するアプリ
  • .NET Core の ORM フレームワークの中でも特に人気のある Entity Framework Core を利用してInsert、Update、Delete、Select を実行するアプリ

SQL Server に接続してクエリを実行する C# アプリを作成

開発を行うワークディレクトリに移動し、新しい .NET Core プロジェクトを作成します。
基本的な .NET Core の Program.cs と csproj ファイルを含むプロジェクトディレクトリが作成されます。

cd ~/
dotnet new console -o SqlServerSample

Docker Compose で行う場合は、以下の通りです。

docker-compose run --rm app dotnet new console -o SqlServerSample

SqlServerSample.csproj というファイルが SqlServerSample ディレクトリ以下に作成されます。
任意のテキストエディタで SqlServerSample.csproj ファイルを開き、コードを以下の通りに書き換え、System.Data.SqlClient をプロジェクトに追加します。保存してファイルを閉じます。

SqlServerSample.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Data.SqlClient" Version="4.8.1" />
  </ItemGroup>

</Project>

SqlServerSample ディレクトリ以下にある Program.cs ファイルを開き、コードを以下の通りに書き換え、保存してファイルを閉じます。
ユーザー名とパスワードを自分のものに置き換えることを忘れないでください。

Program.cs
using System;
using System.Data.SqlClient;

namespace SqlServerSample
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                // 接続文字列の構築
                SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
                builder.DataSource = "localhost";   // 接続先の SQL Server インスタンス
                builder.UserID = "sa";              // 接続ユーザー名
                builder.Password = "your_password"; // 接続パスワード
                builder.InitialCatalog = "master";  // 接続するデータベース(ここは変えないでください)
                // builder.ConnectTimeout = 60000;  // 接続タイムアウトの秒数(ms) デフォルトは 15 秒

                // SQL Server に接続
                Console.Write("SQL Server に接続しています... ");
                using (SqlConnection connection = new SqlConnection(builder.ConnectionString))
                {
                    connection.Open();
                    Console.WriteLine("接続成功。");
                }
            }
            catch (SqlException e)
            {
                Console.WriteLine(e.ToString());
            }

            Console.WriteLine("すべてが完了しました。任意のキーを押してアプリを終了します...");
            Console.ReadKey(true);
        }
    }
}

SqlServerSample ディレクトリに戻り、以下のコマンドを実行して csproj 内の依存関係を復元します。

cd ~/SqlServerSample
dotnet restore

完了したら、ビルド実行を行います。

dotnet run

なお、Docker Compose で実行している場合は、以下のようなコマンドを実行することで上記を実現できます。

docker-compose run -w /src/SqlServerSample --rm app dotnet restore
docker-compose run -w /src/SqlServerSample --rm app dotnet run

SqlServerSample1.gif

これで、SQL Server に接続を行うコンソールアプリができました。ただし、このアプリでは単にデータベースへの接続だけを行っているだけで、クエリは実行していません。
次に、Program.cs 内にコードを追加して、データベースやテーブルの作成、INSERT/UPDATE/DELETE/SELECT などのクエリを実行するように変更します。
ユーザー名とパスワードは自分のものに置き換えることを忘れないでください。
書き換えた後、ファイルを保存し、プロジェクトをビルドして実行します。

Program.cs
using System;
using System.Text;
using System.Data.SqlClient;

namespace SqlServerSample
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Console.WriteLine("SQL Server に接続し、Create、Read、Update、Delete 操作のデモを行います。");

                // 接続文字列の構築
                SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
                builder.DataSource = "localhost";   // 接続先の SQL Server インスタンス
                builder.UserID = "sa";              // 接続ユーザー名
                builder.Password = "your_password"; // 接続パスワード
                builder.InitialCatalog = "master";  // 接続するデータベース(ここは変えないでください)
                // builder.ConnectTimeout = 60000;  // 接続タイムアウトの秒数(ms) デフォルトは 15 秒

                // SQL Server に接続
                Console.Write("SQL Server に接続しています... ");
                using (SqlConnection connection = new SqlConnection(builder.ConnectionString))
                {
                    connection.Open();
                    Console.WriteLine("接続成功。");

                    // サンプルデータベースの作成
                    Console.Write("既に作成されている SampleDB データベースを削除し、再作成します... ");
                    String sql = "DROP DATABASE IF EXISTS [SampleDB]; CREATE DATABASE [SampleDB]";
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.ExecuteNonQuery();
                        Console.WriteLine("SampleDB データベースを作成しました。");
                    }

                    // テーブルを作成しサンプルデータを登録
                    Console.Write("サンプルテーブルを作成しデータを登録します。任意のキーを押して続行します...");
                    Console.ReadKey(true);
                    StringBuilder sb = new StringBuilder();
                    sb.Append("USE SampleDB; ");
                    sb.Append("CREATE TABLE Employees ( ");
                    sb.Append(" Id INT IDENTITY(1,1) NOT NULL PRIMARY KEY, ");
                    sb.Append(" Name NVARCHAR(50), ");
                    sb.Append(" Location NVARCHAR(50) ");
                    sb.Append("); ");
                    sb.Append("INSERT INTO Employees (Name, Location) VALUES ");
                    sb.Append("(N'Jared', N'Australia'), ");
                    sb.Append("(N'Nikita', N'India'), ");
                    sb.Append("(N'Tom', N'Germany'); ");
                    sql = sb.ToString();
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.ExecuteNonQuery();
                        Console.WriteLine("作成完了");
                    }

                    // INSERT デモ
                    Console.Write("テーブルに新しい行を挿入するには、任意のキーを押して続行します...");
                    Console.ReadKey(true);
                    sb.Clear();
                    sb.Append("INSERT Employees (Name, Location) ");
                    sb.Append("VALUES (@name, @location);");
                    sql = sb.ToString();
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.Parameters.AddWithValue("@name", "Jake");
                        command.Parameters.AddWithValue("@location", "United States");
                        int rowsAffected = command.ExecuteNonQuery();
                        Console.WriteLine(rowsAffected + " 行 挿入されました");
                    }

                    // UPDATE デモ
                    String userToUpdate = "Nikita";
                    Console.Write("ユーザー '" + userToUpdate + "' の 'Location' を更新するには、任意のキーを押して続行します...");
                    Console.ReadKey(true);
                    sb.Clear();
                    sb.Append("UPDATE Employees SET Location = N'United States' WHERE Name = @name");
                    sql = sb.ToString();
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.Parameters.AddWithValue("@name", userToUpdate);
                        int rowsAffected = command.ExecuteNonQuery();
                        Console.WriteLine(rowsAffected + " 行 更新されました");
                    }

                    // DELETE デモ
                    String userToDelete = "Jared";
                    Console.Write("ユーザー '" + userToDelete + "' を削除するには、任意のキーを押して続行します...");
                    Console.ReadKey(true);
                    sb.Clear();
                    sb.Append("DELETE FROM Employees WHERE Name = @name;");
                    sql = sb.ToString();
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.Parameters.AddWithValue("@name", userToDelete);
                        int rowsAffected = command.ExecuteNonQuery();
                        Console.WriteLine(rowsAffected + " 行 削除されました");
                    }

                    // READ デモ
                    Console.WriteLine("テーブルからデータを読み取るには、任意のキーを押して続行します...");
                    Console.ReadKey(true);
                    sql = "SELECT Id, Name, Location FROM Employees;";
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        using (SqlDataReader reader = command.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                Console.WriteLine("{0} {1} {2}", reader.GetInt32(0), reader.GetString(1), reader.GetString(2));
                            }
                        }
                    }
                }
            }
            catch (SqlException e)
            {
                Console.WriteLine(e.ToString());
            }

            Console.WriteLine("すべて完了しました。任意のキーを押して終了します...");
            Console.ReadKey(true);
        }
    }
}

SqlServerSample2.gif

これで、macOS 上の .NET Core を使って、初めて C# + SQL Server アプリを作成できました。次は、ORM を使って C# アプリを作成します。

.NET Core で Entity Framework Core ORM を使用して SQL Server に接続する C# アプリを作成

ワークディレクトリに戻り、新しい.NET Coreプロジェクトを作成します。

cd ~/
dotnet new console -o SqlServerEFSample

Docker Compose で行う場合は、以下の通りです。

docker-compose run --rm app dotnet new console -o SqlServerSampleEF

SqlServerEFSample.csproj というファイルが SqlServerEFSample ディレクトリ以下に作成されます。
任意のテキストエディタで SqlServerEFSample.csproj ファイルを開き、コードを以下の通りに書き換え、Entity Framework Core をプロジェクトに追加します。保存してファイルを閉じます。

SqlServerEFSample.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Data.SqlClient" Version="4.8.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.4" />
  </ItemGroup>

</Project>

このサンプルでは、2つのテーブルを作成します。1つ目は「ユーザー」に関するデータを保持し、もう1つは「タスク」に関するデータを保持するものです。

User.cs を作成します。

User クラスを定義します。SqlServerEFSample ディレクトリ以下に User.cs ファイルを作成します。
このクラスは、User テーブルに紐づくモデルのクラスです。書き換えた後、ファイルを保存して閉じます。
この時点では、Task クラスがないためコンパイルエラーとなりますが、問題ありません。

User.cs
using System;
using System.Collections.Generic;

namespace SqlServerEFSample
{
    public class User
    {
        public int UserId { get; set; }
        public String FirstName { get; set; }
        public String LastName { get; set; }
        public virtual IList<Task> Tasks { get; set; }

        public String GetFullName()
        {
            return this.FirstName + " " + this.LastName;
        }
        public override string ToString()
        {
            return "User [id=" + this.UserId + ", name=" + this.GetFullName() + "]";
        }
    }
}

Task.cs を作成します。

Task クラスを定義します。SqlServerEFSample ディレクトリ以下に Task.cs ファイルを作成します。
このクラスは、Task テーブルに紐づくモデルのクラスです。書き換えた後、ファイルを保存して閉じます。

Task.cs
using System;

namespace SqlServerEFSample
{
    public class Task
    {
        public int TaskId { get; set; }
        public string Title { get; set; }
        public DateTime DueDate { get; set; }
        public bool IsComplete { get; set; }
        public virtual User AssignedTo { get; set; }

        public override string ToString()
        {
            return "Task [id=" + this.TaskId + ", title=" + this.Title + ", dueDate=" + this.DueDate.ToString() + ", IsComplete=" + this.IsComplete + "]";
        }
    }
}

EFSampleContext.cs を作成します。

EFSampleContext クラスを定義します。SqlServerEFSample ディレクトリ以下に EFSampleContext.cs ファイルを作成します。
このクラスは、Entity Framework Core を使用し、.NET オブジェクトを利用してデータのクエリ、挿入、更新、および削除を行うためのクラスです。User クラスと Task クラスを使用しています。
書き換えた後、ファイルを保存して閉じます。

EFSampleContext.cs
using Microsoft.EntityFrameworkCore;

namespace SqlServerEFSample
{
    public class EFSampleContext : DbContext
    {
        string _connectionString;
        public EFSampleContext(string connectionString)
        {
            this._connectionString = connectionString;
        }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(this._connectionString);
        }

        public DbSet<User> Users { get; set; }
        public DbSet<Task> Tasks { get; set; }
    }
}

Entity Framework (.NET Framework) と違う点としては、OnConfiguring メソッドが新たにオーバーライドされ、逆に Database.SetInitializer(IDatabaseInitializer) を EFSampleContext のコンストラクタ内で指定しなくなっている点です。

最後に Program.cs を更新します。これまで作成したクラスを使用するための設定を行います。
ユーザー名とパスワードを自分のものに更新することを忘れないでください。
保存してファイルを閉じます。

Program.cs
using System;
using System.Linq;
using System.Data.SqlClient;
using System.Collections.Generic;

namespace SqlServerEFSample
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("** Entity Framework Core と SQL Server を使用した C# CRUD のサンプル **\n");
            try
            {
                // 接続文字列を構築
                SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
                builder.DataSource = "localhost";     // 接続先の SQL Server インスタンス
                builder.UserID = "sa";                // 接続ユーザー名
                builder.Password = "your_password";   // 接続パスワード
                builder.InitialCatalog = "EFSampleDB";// 接続するデータベース(ここは変えないでください)
                // builder.ConnectTimeout = 60000;    // 接続タイムアウトの秒数(ms) デフォルトは 15 秒

                using (EFSampleContext context = new EFSampleContext(builder.ConnectionString))
                {
                    context.Database.EnsureDeleted();
                    context.Database.EnsureCreated();
                    Console.WriteLine("C#のクラスからデータベーススキーマを作成しました。");

                    // Create デモ: ユーザーインスタンスを作成し、データベースに保存
                    User newUser = new User { FirstName = "Anna", LastName = "Shrestinian" };
                    context.Users.Add(newUser);
                    context.SaveChanges();
                    Console.WriteLine("\n作成されたユーザー: " + newUser.ToString());

                    // Create デモ: タスクインスタンスを作成し、データベースに保存
                    Task newTask = new Task() { Title = "Ship Helsinki", IsComplete = false, DueDate = DateTime.Parse("04-01-2017") };
                    context.Tasks.Add(newTask);
                    context.SaveChanges();
                    Console.WriteLine("\nCreated Task: " + newTask.ToString());

                    // Association demo: Assign task to user
                    newTask.AssignedTo = newUser;
                    context.SaveChanges();
                    Console.WriteLine("\n作成されたタスク: '" + newTask.Title + "' 割り当てられたユーザー: '" + newUser.GetFullName() + "'");

                    // Read デモ: ユーザー 'Anna' に割り当てられた未完了のタスクを見つける
                    Console.WriteLine("\n'Anna' に割り当てられた未完了のタスク:");
                    var query = from t in context.Tasks
                                where t.IsComplete == false &&
                                t.AssignedTo.FirstName.Equals("Anna")
                                select t;
                    foreach(var t in query)
                    {
                        Console.WriteLine(t.ToString());
                    }

                    // Update デモ: タスクの '期限' を変更
                    Task taskToUpdate = context.Tasks.First(); // 最初のタスクを取得
                    Console.WriteLine("\nタスクをアップデート中: " + taskToUpdate.ToString());
                    taskToUpdate.DueDate = DateTime.Parse("06-30-2016");
                    context.SaveChanges();
                    Console.WriteLine("変更された期限: : " + taskToUpdate.ToString());

                    // Delete デモ: 2016年が期限になっているすべてのタスクを削除
                    Console.WriteLine("\n期限が2016年になっているすべてのタスクを削除します。");
                    DateTime dueDate2016 = DateTime.Parse("12-31-2016");
                    query = from t in context.Tasks
                            where t.DueDate < dueDate2016
                            select t;
                    foreach(Task t in query)
                    {
                        Console.WriteLine("Deleting task: " + t.ToString());
                        context.Tasks.Remove(t);
                    }
                    context.SaveChanges();

                    // 'Delete' 操作の後にタスクを表示 - 0個のタスクがあるはず
                    Console.WriteLine("\n削除後のタスク:");
                    List<Task> tasksAfterDelete = (from t in context.Tasks select t).ToList<Task>();
                    if (tasksAfterDelete.Count == 0)
                    {
                        Console.WriteLine("[なし]");
                    }
                    else
                    {
                        foreach (Task t in query)
                        {
                            Console.WriteLine(t.ToString());
                        }
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }

            Console.WriteLine("すべて完了しました。任意のキーを押して終了します...");
            Console.ReadKey(true);
        }
    }
}

EFSampleContext クラスのコンストラクタで Database.SetInitializer(IDatabaseInitializer) を行っていないため、Program.cs 内で context.Database.EnsureDeleted() と context.Database.EnsureCreated() が行われていますね。

SqlServerSampleEF ディレクトリに戻り、以下のコマンドを実行して csproj 内の依存関係を復元します。

cd ~/SqlServerEFSample
dotnet restore

完了したら、ビルド実行を行います。

dotnet run

Docker Compose で実行する場合は、以下のコマンドを実行してください。

docker-compose run -w /src/SqlServerEFSample --rm app dotnet restore
docker-compose run -w /src/SqlServerEFSample --rm app dotnet run

SqlServerEFSample.gif

これで、2つ目の C# アプリの作成が終わりました。最後に、SQL Server の カラムストア機能を使って C# アプリを高速化する方法について学びます。

C# アプリを 100 倍速にする

これまでで基本的なことは理解できたと思います。最後は、SQL Server を使用してアプリをより良くする方法を見てみます。このモジュールでは、カラムストアインデックスの簡単な例と、カラムストアインデックスがどのようにデータ処理速度を向上させるかを確認します。カラムストアインデックスは、従来の列ストアインデックスに比べて、分析ワークロードでは最大 100 倍のパフォーマンス向上、データ圧縮では最大 10 倍のパフォーマンス向上を実現できます。

カラムストアインデックスの機能を確認するために、500 万行のサンプルデータベースとサンプルテーブルを作成し、カラムストアインデックスを追加する前と後の簡単なクエリを実行する C# アプリケーションを作成します。

ワークディレクトリに戻り、新しい.NET Coreプロジェクトを作成します。

cd ~/
dotnet new console -o SqlServerColumnstoreSample

Docker Compose で行う場合は、以下の通りです。

docker-compose run --rm app dotnet new console -o SqlServerColumnstoreSample

SqlServerColumnstoreSample.csproj というファイルが SqlServerColumnstoreSample ディレクトリ以下に作成されます。
任意のテキストエディタで SqlServerColumnstoreSample.csproj ファイルを開き、コードを以下の通りに書き換え、System.Data.SqlClient をプロジェクトに追加します。保存してファイルを閉じます。

SqlServerColumnstoreSample.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Data.SqlClient" Version="4.8.1" />
  </ItemGroup>

</Project>

Program.cs の内容を書き換えます。
ユーザー名とパスワードは自分のものに置き換えることを忘れないでください。
保存してファイルを閉じます。

Program.cs
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SqlServerColumnstoreSample
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Console.WriteLine("*** SQL Server カラムストアのデモ ***");

                // 接続文字列の構築
                SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
                builder.DataSource = "localhost";   // 接続先の SQL Server インスタンス
                builder.UserID = "sa";              // 接続ユーザー名
                builder.Password = "your_password"; // 接続パスワード
                builder.InitialCatalog = "master";  // 接続するデータベース(ここは変えないでください)
                // builder.ConnectTimeout = 60000;  // 接続タイムアウトの秒数(ms) デフォルトは 15 秒

                // SQL Server に接続
                Console.Write("SQL Serverへ接続しています... ");
                using (SqlConnection connection = new SqlConnection(builder.ConnectionString))
                {
                    connection.Open();
                    Console.WriteLine("接続完了。");

                    // サンプルデータベースの作成
                    Console.Write("'SampleDB' を再作成しています... ");
                    String sql = "DROP DATABASE IF EXISTS [SampleDB]; CREATE DATABASE [SampleDB]";
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.ExecuteNonQuery();
                        Console.WriteLine("完了。");
                    }

                    // 'Table_with_5M_rows' テーブルに500万行を挿入
                    Console.Write("テーブル 'Table_with_5M_rows' に500万行を挿入します。1分ほどかかりますが、お待ちください... ");
                    StringBuilder sb = new StringBuilder();
                    sb.Append("USE SampleDB; ");
                    sb.Append("WITH a AS (SELECT * FROM (VALUES(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) AS a(a))");
                    sb.Append("SELECT TOP(5000000)");
                    sb.Append("ROW_NUMBER() OVER (ORDER BY a.a) AS OrderItemId ");
                    sb.Append(",a.a + b.a + c.a + d.a + e.a + f.a + g.a + h.a AS OrderId ");
                    sb.Append(",a.a * 10 AS Price ");
                    sb.Append(",CONCAT(a.a, N' ', b.a, N' ', c.a, N' ', d.a, N' ', e.a, N' ', f.a, N' ', g.a, N' ', h.a) AS ProductName ");
                    sb.Append("INTO Table_with_5M_rows ");
                    sb.Append("FROM a, a AS b, a AS c, a AS d, a AS e, a AS f, a AS g, a AS h;");
                    sql = sb.ToString();
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.ExecuteNonQuery();
                        Console.WriteLine("完了。");
                    }

                    // カラムストアインデックスなしで SQL クエリを実行
                    double elapsedTimeWithoutIndex = SumPrice(connection);
                    Console.WriteLine("カラムストアインデックスなしのクエリ時間: " + elapsedTimeWithoutIndex + "ms");

                    // カラムストアインデックスを追加
                    Console.Write("'Table_with_5M_rows' テーブルにカラムストアインデックスを追加中... ");
                    sql = "CREATE CLUSTERED COLUMNSTORE INDEX columnstoreindex ON Table_with_5M_rows;";
                    using (SqlCommand command = new SqlCommand(sql, connection))
                    {
                        command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                        command.ExecuteNonQuery();
                        Console.WriteLine("完了。");
                    }

                    // カラムストアインデックスが追加された後、再度同じ SQL クエリを実行
                    double elapsedTimeWithIndex = SumPrice(connection);
                    Console.WriteLine("カラムストアありのクエリ時間: " + elapsedTimeWithIndex + "ms");

                    // カラムストアインデックスの追加によるパフォーマンス向上を計算
                    Console.WriteLine("カラムストアインデックスによる性能向上: "
                        + Math.Round(elapsedTimeWithoutIndex / elapsedTimeWithIndex) + "x!");
                }
                Console.WriteLine("すべて完了しました。任意のキーを押して終了します...");
                Console.ReadKey(true);
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }

        public static double SumPrice(SqlConnection connection)
        {
            String sql = "SELECT SUM(Price) FROM Table_with_5M_rows";
            long startTicks = DateTime.Now.Ticks;
            using (SqlCommand command = new SqlCommand(sql, connection))
            {
                try
                {
                    command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒
                    var sum = command.ExecuteScalar();
                    TimeSpan elapsed = TimeSpan.FromTicks(DateTime.Now.Ticks) - TimeSpan.FromTicks(startTicks);
                    return elapsed.TotalMilliseconds;
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.ToString());
                }
            }
            return 0;
        }
    }
}

SqlServerColumnstoreSample ディレクトリに戻り、以下のコマンドを実行して csproj 内の依存関係を復元します。

cd ~/SqlServerColumnstoreSample
dotnet restore

完了したら、ビルド実行を行います。

dotnet run

Docker Compose で実行する場合は、以下のコマンドを実行してください。

docker-compose run -w /src/SqlServerColumnstoreSample --rm app dotnet restore
docker-compose run -w /src/SqlServerColumnstoreSample --rm app dotnet run

SqlServerColumnStoreSample.gif

おめでとうございます。カラムストアインデックスを使って C# アプリを高速化しました!

おわりに

以上で、「macOS上でSQL Serverを使用してC#アプリを作成する」は終了です。Build an app using SQL Server には、他言語での SQL Server アプリを作成するチュートリアルがあります。ぜひ、他の言語でも試してみてください。


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

DockerのUID/GIDあれこれ: Linux版通常モード & Docker Desktop for Mac編

1. はじめに

 Dockerコンテナからボリュームマウントを通じてファイルを書き出す場合、ユーザID/グループID(以下、UID/GID)を指定しないとファイルのオーナがroot(UID0)になってしまい、困ることがあります。
docker container runコマンドの--userオプションでUID/GIDを指定することができますが、Linux版DockerとDocker Desktop for Macでは挙動に差があります。
これらの挙動について自分なりに整理するために、いろいろ実験しました。
なお、DockerfileUSER命令を使用してUID/GIDを指定する方法もありますが、今回はそちらについては触れていませんのでご了承ください。

 余裕があれば、Rootlessモード編、userns-remapモード編も書きたいと思います。というか、そちらが本命なのですが。

2. Linux版 通常モード

 まずは、Linux版Dockerの通常モード(Rootlessモードでも、userns-remapモードでもない、一般的なモード)での挙動を調査しました。

2.1. 環境

 調査した環境は以下の通りです。

  • ハードウェア: Raspberry Pi 4 2GB版
  • OS: Ubuntu Server 20.04 LTS ARM64(64ビット)版
  • Docker Engine: 19.03.8
ubuntu$ cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04 LTS"
VERSION_ID="20.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=focal
UBUNTU_CODENAME=focal

ubuntu$ uname -a
Linux ubuntu 5.4.0-1008-raspi #8-Ubuntu SMP Wed Apr 8 11:13:06 UTC 2020 aarch64 aarch64 aarch64 GNU/Linux

ubuntu$ sudo docker version
Client:
 Version:           19.03.8
 API version:       1.40
 Go version:        go1.13.8
 Git commit:        afacb8b7f0
 Built:             Wed Mar 11 23:43:15 2020
 OS/Arch:           linux/arm64
 Experimental:      false

Server:
 Engine:
  Version:          19.03.8
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.13.8
  Git commit:       afacb8b7f0
  Built:            Wed Mar 11 22:48:33 2020
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          1.3.3-0ubuntu2
  GitCommit:
 runc:
  Version:          spec: 1.0.1-dev
  GitCommit:
 docker-init:
  Version:          0.18.0
  GitCommit:

ubuntu$ sudo docker info
...
 Security Options:
  apparmor
  seccomp
   Profile: default
...

 なお、以下のコマンドは標準のubuntuユーザ(UID1000、GID1000)で実行しています。
ubuntuユーザはdockerグループには意図的に入れていないため、dockerコマンドの実行にはsudoを利用しています。
Rootlessモード、userns-remapモードは使用していませんが、念のため/etc/subuid/etc/subgidの内容も示します。

ubuntu$ id
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),115(netdev),118(lxd)

ubuntu$ cat /etc/subuid
ubuntu:100000:65536

ubuntu$ cat /etc/subgid
ubuntu:100000:65536

2.2. 実験

 実験は以下の通りです。インラインで説明を記載しています。
なお、ファイルの読み込み権限を分かりやすくするためにumaskでパーミッションのマスクを設定しています。
また、今回の実験ではUIDとGIDに同一の値を設定していますが、もちろん別々の値でも構いません。

# 実験用のディレクトリを作成します。(他ユーザでも書き込める必要があります)
ubuntu$ mkdir -p -m 777 /tmp/docker/normal

# UID/GIDを指定せず、idコマンドを実行してUID/GIDを確認します。
# →UID/GIDは0:0です。
ubuntu$ sudo docker container run --tty --rm ubuntu:20.04 id
uid=0(root) gid=0(root) groups=0(root)

# UID/GIDを指定せず、touchコマンドで空ファイルを生成して、ファイルのUID/GIDを確認します。
# →ファイルのUID/GIDは0:0です。
ubuntu$ sudo docker container run --tty --rm --volume /tmp/docker/normal:/out ubuntu:20.04 /bin/bash -c "umask 0077 && touch /out/without_user && ls -ln /out/without_user"
-rw------- 1 0 0 0 May 18 15:53 /out/without_user

# UID/GIDとして1000:1000を指定し、idコマンドを実行してUID/GIDを確認します。
# →UID/GIDは1000:1000です。
ubuntu$ sudo docker container run --tty --user 1000:1000 ubuntu:20.04 id
uid=1000 gid=1000 groups=1000

# UID/GIDとして1000:1000を指定し、touchコマンドで空ファイルを生成して、ファイルのUID/GIDを確認します。
# →ファイルのUID/GIDは1000:1000です。
ubuntu$ sudo docker container run --tty --user 1000:1000 --volume /tmp/docker/normal:/out ubuntu:20.04 /bin/bash -c "umask 0077 && touch /out/with_user_1000 && ls -ln /out/with_user_1000"
-rw------- 1 1000 1000 0 May 18 15:54 /out/with_user_1000

# UID/GIDとして2000:2000を指定し、idコマンドを実行してUID/GIDを確認します。
# →UIG/GIDは2000:2000です。
ubuntu$ sudo docker container run --tty --user 2000:2000 ubuntu:20.04 id
uid=2000 gid=2000 groups=2000

# UID/GIDとして2000:2000を指定し、touchコマンドで空ファイルを生成して、ファイルのUID/GIDを確認します。
# →ファイルのUID/GIDは2000:2000です。
ubuntu$ sudo docker container run --tty --user 2000:2000 --volume /tmp/docker/normal:/out ubuntu:20.04 /bin/bash -c "umask 0077 && touch /out/with_user_2000 && ls -ln /out/with_user_2000"
-rw------- 1 2000 2000 0 May 18 15:54 /out/with_user_2000

# Dockerホスト側のファイルのUID/GIDを確認します。
# →UID/GIDはDockerコンテナ内と同一です。
ubuntu$ ls -ln /tmp/docker/normal/
total 0
-rw------- 1    0    0 0 May 18 15:53 without_user
-rw------- 1 1000 1000 0 May 18 15:54 with_user_1000
-rw------- 1 2000 2000 0 May 18 15:54 with_user_2000

# Dockerホスト側でファイルを読めるかどうか確認します。
# →ubuntuユーザでは0:0のファイルを読むことができません。
ubuntu$ cat /tmp/docker/normal/without_user
cat: /tmp/docker/normal/without_user: Permission denied

# →ubuntuユーザでは1000:1000のファイルを読むことができます。
ubuntu$ cat /tmp/docker/normal/with_user_1000

# →ubuntuユーザでは2000:2000のファイルを読むことができません。
ubuntu$ cat /tmp/docker/normal/with_user_2000
cat: /tmp/docker/normal/with_user_2000: Permission denied

 Linux版Dockerの通常モードでは、割と素直な結果になりました。整理すると以下の通りです。

  • --userオプションを指定しない場合:
    • Dockerコンテナ内での実行UID/GIDは0:0となり、ファイルのUID/GIDも同一となる。
    • また、Dockerホスト側から見てもファイルのUID/GIDはDockerコンテナ内と同一となる。
  • --userオプションを指定した場合:
    • Dockerコンテナ内での実行UID/GIDは--userオプションで指定した値となり、ファイルのUID/GIDも同一となる。
    • また、Dockerホスト側から見てもファイルのUID/GIDはDockerコンテナ内と同一となる。

3. Docker Desktop for Mac

 続いてmacOS上で動作する「Docker Desktop for Mac」での挙動を調査しました。

3.1. 環境

 調査した環境は以下の通りです。

  • ハードウェア: MacBook Pro 2018
  • OS: macOS 10.14.6(Mojave)
  • Docker Desktop for Mac: 2.3.0.2
  • Docker Engine: 19.03.8
mac$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G103

mac$ docker version
Client: Docker Engine - Community
 Version:           19.03.8
 API version:       1.40
 Go version:        go1.12.17
 Git commit:        afacb8b
 Built:             Wed Mar 11 01:21:11 2020
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.8
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.17
  Git commit:       afacb8b
  Built:            Wed Mar 11 01:29:16 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.2.13
  GitCommit:        7ad184331fa3e55e52b890ea95e65ba581ae3429
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

mac$ docker info
...
 Security Options:
  seccomp
   Profile: default
...

 なお、以下のコマンドは作業用のユーザ(UID501、GID20)で実行しています。

mac$ id -u
501

mac$ id -g
20

3.2. 実験

 実験は以下の通りです。インラインで説明を記載しています。

# 実験用のディレクトリを作成します。(他ユーザでも書き込める必要があります)
mac$ mkdir -p -m 777 /tmp/docker/mac

# UID/GIDを指定せず、idコマンドを実行してUID/GIDを確認します。
# →UID/GIDは0:0です。
mac$ docker container run --tty --rm ubuntu:20.04 id
uid=0(root) gid=0(root) groups=0(root)

# UID/GIDを指定せず、touchコマンドで空ファイルを生成して、ファイルのUID/GIDを確認します。
# →ファイルのUID/GIDは0:0です。
mac$ docker container run --tty --rm --volume /tmp/docker/mac:/out ubuntu:20.04 /bin/bash -c "umask 0077 && touch /out/without_user && ls -ln /out/without_user"
-rw------- 1 0 0 0 May 18 15:23 /out/without_user

# UID/GIDとして1000:1000を指定し、idコマンドを実行してUID/GIDを確認します。
# →UID/GIDは1000:1000です。
mac$ docker container run --tty --user 1000:1000 ubuntu:20.04 id
uid=1000 gid=1000 groups=1000

# UID/GIDとして1000:1000を指定し、touchコマンドで空ファイルを生成して、ファイルのUID/GIDを確認します。
# →ファイルのUID/GIDは1000:1000です。
mac$ docker container run --tty --user 1000:1000 --volume /tmp/docker/mac:/out ubuntu:20.04 /bin/bash -c "umask 0077 && touch /out/with_user_1000 && ls -ln /out/with_user_1000"
-rw------- 1 1000 1000 0 May 18 15:24 /out/with_user_1000

# UID/GIDとして2000:2000を指定し、idコマンドを実行してUID/GIDを確認します。
# →UIG/GIDは2000:2000です。
mac$ docker container run --tty --user 2000:2000 ubuntu:20.04 id
uid=2000 gid=2000 groups=2000

# UID/GIDとして2000:2000を指定し、touchコマンドで空ファイルを生成して、ファイルのUID/GIDを確認します。
# →ファイルのUID/GIDは2000:2000です。
mac$ docker container run --tty --user 2000:2000 --volume /tmp/docker/mac:/out ubuntu:20.04 /bin/bash -c "umask 0077 && touch /out/with_user_2000 && ls -ln /out/with_user_2000"
-rw------- 1 2000 2000 0 May 18 15:24 /out/with_user_2000

# Dockerホスト側のファイルのUID/GIDを確認します。
# →UID/GIDはDockerコンテナ内と異なり、UIDはすべてmacOS側の実行ユーザ、GIDはすべて0です。
mac$ ls -ln /tmp/docker/mac/
total 0
-rw-------  1 501  0  0 May 19 00:24 with_user_1000
-rw-------  1 501  0  0 May 19 00:24 with_user_2000
-rw-------  1 501  0  0 May 19 00:23 without_user

# Dockerホスト側でファイルを読めるかどうか確認します。
# →UIDが同一のため、すべてのファイルはエラーなく読むことができます。
mac$ cat /tmp/docker/mac/without_user
mac$ cat /tmp/docker/mac/with_user_1000
mac$ cat /tmp/docker/mac/with_user_2000

 Docker Desktop for Macでは、Linux版Dockerの通常モードとは異なる結果になりました。整理すると以下の通りです。

  • --userオプションを指定しない場合:
    • Dockerコンテナ内での実行UID/GIDは0:0となり、ファイルのUID/GIDも同一となる。(Linux版通常モードと同一の挙動)
    • ただし、Dockerホスト側から見たファイルのUID/GIDはmacOS側のUID:0となる。(Linux版通常モードと異なる挙動)
  • --userオプションを指定した場合:
    • Dockerコンテナ内での実行UID/GIDは指定した値となり、ファイルのUID/GIDも同一となる。(Linux版通常モードと同一の挙動)
    • ただし、Dockerホスト側から見たファイルのUID/GIDはmacOS側のUID:0となる。(Linux版通常モードと異なる挙動)

4. 最後に

 本命はRootlessモード、userns-remapモードの挙動の調査なので、頑張って続きを書きたいと思います。

参考

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