Ubuntu

Dockerで構築したシステムを他の環境に持っていく

Keycloakを使ってみて、是非これは本番環境に入れたいと思った。思ったついでに、作ったコンテナやイメージを自由にあっちこっち持って行けたらいいなと考えて、Dockerについて学習してみることにした。そもそも、コンテナとイメージの違いがよく分からない…



広告


結論はとても簡単。しかし、そこまでに費やした時間が凄かった。

 

移設の考え方を整理

イメージ・コンテナ・ボリューム

まずは、移設の対象となるモノを整理。

イメージはOSやアプリケーションが入ったROMみたいなもの。
状態(State)を持たず、変更することができない。
Dockerfileからビルドしたり、色々といじり倒したコンテナをコミットしたりして作られる。

コンテナはイメージを取り込み、実行用の様々な情報を加えて作られる。
状態(State)を持っていて、実行されたり停止されたりする。
コンテナに対する変更はコンテナ・レイヤーに保存される(そのため、コンテナを削除するとコンテナで行った変更は消える)。

ボリュームはデーターを永続化させる必要があれば(自分で)作成し、コンテナからマウントして利用する。
複数のコンテナで共有することができる。

色々と自分なりに調べた結果、それぞれの具体的な中身はこんな感じ。

        - Image -                 - Container -
┌───────────┐  ┌───────────┐
│ID:e28abce97d93~     │  │ID:2b3cd8ab8d94~     │
└───────────┘  └───────────┘
┌───────────┐  ┌───────────┐
│Config:               │==│Config:               │
│(環境変数群)          │  │(実行用環境変数群)    │
└───────────┘  └───────────┘
┌───────────┐  ┌───────────┐
│GraphDriver:          │  │GraphDriver:          │
│  LowerDir:~         │  │  LowerDir:~         │
│  MergedDir:~        │==│  MergedDir:~        │
│  UpperDir:~         │  │  UpperDir:~         │
│  WorkDir:~          │  │  WorkDir:~          │
│(OS/Application/Data) │  │(OS/Application/Data) │
└────┬┬─────┘  └────┬┬─────┘          - Volume -
┌────┴┴─────┐  ┌────┴┴─────┐  ┌───────────┐
│RootFS:               │  │コンテナ・レイヤー    │  │Mountpoint:           │
│  Layer:第1レイヤー   │  └───────────┘  │  ホストのディレクトリ│
│  Layer:第2レイヤー   │  ┌───────────┐  └───────────┘
│          …          │  │実行用の様々な情報    │
│  Layer:最終レイヤー  │  │  状態(State)         │
└───────────┘  │  hostname            │
┌───────────┐  │  hosts               │
│OS:Linux              │  │  resolv.conf         │
│Architecture:amd64    │  │  ~.log              │
│(特徴として持つ)      │  │  ネットワーク設定    │
└───────────┘  │  etc.                │
┌───────────┐  └───────────┘
│Parent:基にした       │  ┌───────────┐
│    イメージかコンテナ│  │Image:e28abce97d93~  │
└───────────┘  └───────────┘

※もちろん保持する情報はこれだけではないが、概要を理解し、大まかに差分を知る為に必要な項目をピックアップした。
※記号==で結んだところは多分Imageが基になって作られるんじゃないかと思った場所(必要な変更はなされるし、環境変数は足すことができる)。

inspectしてみるとコンテナには幾つもConfigっぽいのがあるとわかる。ここで書いたConfigは正しくないかもしれない。

それと、GraphDriverとRootFSの関係も理解がちょっと怪しいが…多分、同じものを表している。
GraphDriverにはImageやコンテナ・レイヤーが全て定義されているが、RootFSは何らかの基準(実際にデーターに変化があったこと?)で間引いたレイヤーを持つように見えた。

移設の対象

このように整理すると、今動いているシステムをそのまま移設するなら、以下を持って行く必要があると思った。

  • コンテナが取り込んでいるイメージ
  • コンテナの実行用環境変数+コンテナ・レイヤーに含まれる情報
  • ボリュームがあるならそれも

Docker+バックアップとかで探すと、saveとexportがヒットする。
日本語マニュアルで調べながら動作を確認してみることにした。

実際の移設

テストのために作った小さな環境を移設してみる。

docker save と load

saveはイメージをバックアップするので、コンテナ・レイヤーを保存しておく必要がある(docker commitする)。
イメージの段階ではボリュームをマウントできないので、ボリュームはsaveの対象外になる。

使い方: docker save [オプション] イメージ [イメージ…]Docs / save

イメージをtarにまとめる。

$ sudo docker save -o apache-test.tar 6b7f288de233

できあがったtarボールをまっさら環境に移してloadしてみる。

$ sudo docker load -i apache-test.tar
7ef368776582: Loading layer [==================================================>]  65.61MB/65.61MB
83f4287e1f04: Loading layer [==================================================>]  991.7kB/991.7kB
d3a6da143c91: Loading layer [==================================================>]  15.87kB/15.87kB
8682f9a74649: Loading layer [==================================================>]  3.072kB/3.072kB
12663bddb053: Loading layer [==================================================>]  128.1MB/128.1MB
22f494d3d2a8: Loading layer [==================================================>]  51.02MB/51.02MB
Loaded image ID: sha256:6b7f288de233269287e39bf9eace2bd83f887440b58f9a70f795a58331b54492

※下の方で作ったコンテナで、作った後ちょっといじってコミットし、6階層のレイヤーができあがっていたが、それらが全て取り込まれた。

loadしたイメージにリポジトリとタグがついていなかったので付けた。
その上で、移行元と同じパラメーターを付けて実行してみる。

$ sudo docker image tag 6b7f288de233 apache-test:latest
$ sudo docker run -p 9080:80 -d -h apache-host --name=apache-cont apache-test
28a4a571f6ca0c0fa562f16a11252c13b0387b5b4c95fe404f5c1e6071555af2

ちょっと改変を加えたApacheなんだけれども、ポート9080番でアクセスしてみたら改変状態が反映されており、上手く動作した。

docker export と import

コンテナにあるファイルをまるごとバックアップする。
レイヤーは意識せずに見えているファイルを全部持ってくるので、インポートするとレイヤーは1つになっている。
実行用の環境設定がらみの情報は取り出せないので、必要な設定は上からかぶせる。

使い方: docker export [オプション] コンテナ

Docs / export

ボリュームはマウントしている部分だけを出力する、とされている。
ボリュームを使うように構成した場合についても、どんな風に移設するのか理解しなきゃならなそうだが、そうした問題に直面するまでは後回しにしよう。

コンテナをtarにまとめる。

$ sudo docker export -o apache-test.tar sad_dhawan

できあがったtarボールをまっさら環境に移してimportしてみる。
importはコンテナではなく、イメージになるので注意。

$ sudo docker import apache-test.tar apache-test
sha256:98cbad280c0085956c7081ad6d3e934c811b28f92c80414d964797148113f064

移行元と同じパラメーターを付けて実行してみる。

$ sudo docker run -p 9080:80 -d -h apache-host --name=apache-cont apache-test
docker: Error response from daemon: No command specified.

エラーが起きて調べてみたら…あぁ、実行のために必要な環境変数とかCMDが全部なくなってる。

$ sudo docker inspect apache-test
…
        "Config": {
…
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "APACHE_RUN_USER=www-data",
                "APACHE_RUN_GROUP=www-data",
                "APACHE_PID_FILE=/var/run/apache2.pid",
                "APACHE_RUN_DIR=/var/run/apache2",
                "APACHE_LOG_DIR=/var/log/apache2",
                "APACHE_LOCK_DIR=/var/lock/apache2",
                "DEBCONF_NOWARNINGS=yes"
            ],
            "Cmd": [
                "apachectl",
                "-D",
                "FOREGROUND"
            ],

※元環境にはあるEnvとかCmdがごっそり抜けていた。

早速Dockerfileを準備して…

Dockerfile

FROM apache-test:latest
MAINTAINER rohhie

ENV APACHE_RUN_USER www-data
ENV APACHE_RUN_GROUP www-data
ENV APACHE_PID_FILE /var/run/apache2.pid
ENV APACHE_RUN_DIR /var/run/apache2
ENV APACHE_LOG_DIR /var/log/apache2
ENV APACHE_LOCK_DIR /var/lock/apache2

ENV DEBCONF_NOWARNINGS yes

EXPOSE 80

CMD ["apachectl", "-D", "FOREGROUND"]

Qiita / Dockerでapache2起動より抜粋+いくつかの変更。
※インポートしたイメージを親イメージとし、Apacheインストールを削除している。

buildする。

$ sudo docker build -t apache-test .
$ sudo docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
apache-test         latest              276bc92398b8        About a minute ago   236MB
$ sudo docker image history apache-test
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
276bc92398b8        2 minutes ago       /bin/sh -c #(nop)  CMD ["apachectl" "-D" "FO…   0B
dd8ab46c78be        2 minutes ago       /bin/sh -c #(nop)  EXPOSE 80                    0B
d63de5c127c6        2 minutes ago       /bin/sh -c #(nop)  ENV DEBCONF_NOWARNINGS=yes   0B
f13ecacea79a        2 minutes ago       /bin/sh -c #(nop)  ENV APACHE_LOCK_DIR=/var/…   0B
1947eafe9b92        2 minutes ago       /bin/sh -c #(nop)  ENV APACHE_LOG_DIR=/var/l…   0B
1983ee0e863e        2 minutes ago       /bin/sh -c #(nop)  ENV APACHE_RUN_DIR=/var/r…   0B
c061f62fe100        2 minutes ago       /bin/sh -c #(nop)  ENV APACHE_PID_FILE=/var/…   0B
cfe2f67e8568        2 minutes ago       /bin/sh -c #(nop)  ENV APACHE_RUN_GROUP=www-…   0B
ce02e891d09d        2 minutes ago       /bin/sh -c #(nop)  ENV APACHE_RUN_USER=www-d…   0B
af9539ff2783        2 minutes ago       /bin/sh -c #(nop)  MAINTAINER rohhie            0B
98cbad280c00        30 minutes ago                                                      236MB               Imported from -

※想像していたのと違う結果になった。別のイメージができるのではなく、同じイメージの履歴となった。

よし、環境設定ができた。改めて起動。

$ sudo docker run -p 9080:80 -d -h apache-host --name=apache-cont apache-test
ca25ac7bb5c835e8abb51284c18e27fee1cf1f9c7eeb217758a8fae1b912c8aa

ポート9080番にアクセスしてページが表示されることを確認した。

ここでは、Dockerfileを使って環境変数を設定しているが、後日、docker-composeを利用したコンテナの起動も試している。※2021/05/29追記

save/loadとexport/importのどちらを使うか

最後の状態が復元できれば良い、ということなら、どちらを使っても良さそう。

save/loadをする場合はイメージを移設する形になるから、事前にコミットする必要があってレイヤーが1層増えるけど、お手軽。
レイヤーの数はパフォーマンスにあまり影響しないのかな。

export/importの場合は、環境変数とか実行コマンドを復元する必要があるので、少し手間が掛かるかもしれない。
とはいえ、inspectすれば設定値は見つかるから、どうにでもなるような気もする。
レイヤーが1層にまとまるので何らかの制限に引っかかった場合には回避策になるかもしれない。

最新バージョンのDockerを導入する

少なくとも、移行先のDockerは最新バージョンにしていくのが良いと思う。
移行元についても最新バージョンの方が良いだろう。バックアップを取っておけたらなお安心。

手順はほぼ公式に従う。
docker docks / Install Docker Engine on Ubuntu

事前に古いバージョンを削除しておく

導入済みのパッケージを調べてみる。

$ dpkg -l *docker* containerd runc
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name                     Version           Architecture      Description
+++-========================-=================-=================-=====================================================
ii  containerd               1.3.3-0ubuntu1~18 amd64             daemon to control runC
ii  docker                   1.5-1build1       amd64             System tray for KDE3/GNOME2 docklet applications
ii  docker-compose           1.17.1-2          all               Punctual, lightweight development environments using
un  docker-doc               <none>            <none>            (no description available)
ii  docker.io                19.03.6-0ubuntu1~ amd64             Linux container runtime
ii  golang-docker-credential 0.5.0-2           amd64             Use native stores to safeguard Docker credentials
ii  python-docker            2.5.1-1           all               Python wrapper to access docker.io's control socket
ii  python-dockerpty         0.4.1-1           all               Pseudo-tty handler for docker Python client (Python 2
ii  python-dockerpycreds     0.2.1-1           all               Python bindings for the docker credentials store API
ii  runc                     1.0.0~rc10-0ubunt amd64             Open Container Project - runtime

パッケージが入っているので削除する。

$ sudo apt remove docker docker-engine docker.io containerd runc
$ dpkg -l *docker* containerd runc
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name                     Version           Architecture      Description
+++-========================-=================-=================-=====================================================
rc  containerd               1.3.3-0ubuntu1~18 amd64             daemon to control runC
rc  docker                   1.5-1build1       amd64             System tray for KDE3/GNOME2 docklet applications
ii  docker-compose           1.17.1-2          all               Punctual, lightweight development environments using
un  docker-doc               >none<            >none<            (no description available)
rc  docker.io                19.03.6-0ubuntu1~ amd64             Linux container runtime
ii  golang-docker-credential 0.5.0-2           amd64             Use native stores to safeguard Docker credentials
ii  python-docker            2.5.1-1           all               Python wrapper to access docker.io's control socket
ii  python-dockerpty         0.4.1-1           all               Pseudo-tty handler for docker Python client (Python 2
ii  python-dockerpycreds     0.2.1-1           all               Python bindings for the docker credentials store API
un  runc                     >none<            >none<            (no description available)

※いくつか残っているけれども、これはこれで良いのかもしれない。

何にもインストールされていないまっさら環境だとこんな感じ。

$ dpkg -l *docker* containerd runc
dpkg-query: no packages found matching *docker*
dpkg-query: no packages found matching containerd
dpkg-query: no packages found matching runc

 

リポジトリの追加

httpsのリポジトリからインストールができるように必要なパッケージをインストールする、とある。

公式のGPGキーを追加し、確認する。

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
OK
$ sudo apt-key fingerprint 0EBFCD88
pub   rsa4096 2017-02-22 [SCEA]
      9DC8 5822 9FC7 DD38 854A  E2D8 8D81 803C 0EBF CD88
uid           [ unknown] Docker Release (CE deb) <docker@docker.com>
sub   rsa4096 2017-02-22 [S]

※curlの部分だけ実行してみたところ、PGP公開鍵が表示された。GPG(GnuPG)はPGPと互換性があるらしい。

リポジトリを追加する。

$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
Hit:1 http://jp.archive.ubuntu.com/ubuntu bionic InRelease
Hit:2 http://jp.archive.ubuntu.com/ubuntu bionic-updates InRelease
Hit:3 http://jp.archive.ubuntu.com/ubuntu bionic-backports InRelease
Hit:4 http://jp.archive.ubuntu.com/ubuntu bionic-security InRelease
Get:5 https://download.docker.com/linux/ubuntu bionic InRelease [64.4 kB]
Get:6 https://download.docker.com/linux/ubuntu bionic/stable amd64 Packages [12.5 kB]
Fetched 76.9 kB in 1s (90.7 kB/s)
Reading package lists... Done

※apt update相当は行われているように見える。

ソースに以下の行が追加されていた。

/etc/apt/sources.list

deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable
# deb-src [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable

※add-apt-repositoryで$(lsb_release -cs)とした部分は、bionicに変わっていた。

ここで一度全てを更新して再起動しておくと良いかもしれない。

$ sudo apt update; sudo apt -y dist-upgrade; sudo apt -y autoremove
$ sudo reboot

 

Dockerのインストール

公式によると、docker-ce docker-ce-cli containerd.ioをインストールすることになっている。

$ sudo apt install docker-ce docker-ce-cli containerd.io
$ docker -v
Docker version 19.03.12, build 48a66213fe

※元々インストールしてあったパッケージdockerも依然としてインストールは可能。だが、これらをインストールして使っていくのだろう。

念のため再起動して動作確認。
移行元の古いバージョンで動かしていたコンテナ達も無事に起動した。

追加ファイルのインストール(docker buildでエラーが出る場合のみ)

docker build したときに発生する free(): invalid pointer エラーを回避する。
まっさら環境にインストールした場合には発生しない問題、アンインストールの仕方が上手くないのかもしれないが、パッケージの関係上仕方がないと割り切り。
Github / docker / for-linux / Docker build says free(): invalid pointer with valid Dockerfile #563

$ wget https://github.com/docker/docker-credential-helpers/releases/download/v0.6.3/docker-credential-secretservice-v0.6.3-amd64.tar.gz
$ tar -xf docker-credential-secretservice-v0.6.3-amd64.tar.gz
$ sudo cp --preserve=mode,ownership --attributes-only /usr/bin/docker-credential-secretservice
./docker-credential-secretservice
$ sudo mv /usr/bin/docker-credential-secretservice /usr/bin/docker-credential-secretservice.org
$ sudo mv ./docker-credential-secretservice /usr/bin/

他にもあるかもしれないが、ひとまず気付いたところ。

移設に必要な用語の理解

色々と調べてみたけど言葉の意味が分からない、どうにも腑に落ちない…となって、片っ端から引っ張ってきてメモ。
docker docs / Glossary
docker-docs-ja / 用語集

イメージ

Docker images are the basis of containers. An Image is an ordered collection of root filesystem changes and the corresponding execution parameters for use within a container runtime. An image typically contains a union of layered filesystems stacked on top of each other. An image does not have state and it never changes.

Dockerイメージはコンテナーの基礎です。イメージは、ルートファイルシステムの変更と、コンテナランタイム内で使用するための対応する実行パラメーターの順序付けられたコレクションです。通常、イメージには、互いに積み重ねられた階層化ファイルシステムの結合が含まれています。イメージには状態がなく、変更されることはありません。

docker docs / Glossary

イメージは、コンテナイメージやDockerイメージと表現されることがあるみたい。

Container images become containers at runtime and in the case of Docker containers - images become containers when they run on Docker Engine.

コンテナイメージは実行時にコンテナになります。Dockerコンテナの場合は、Dockerエンジンで実行されたイメージがコンテナになります。

Docker - What is a Container?

Dockerイメージはファイルシステムと実行時設定の集合である。アプリケーションおよび実行に必要なソフトウェアさらに実行時設定が含まれているDockerイメージを基にコンテナを生成すれば、コンテナは起動と共にアプリケーションとして機能する。

ウィキペディア - Docker

おかげで、イメージとコンテナの区別がつかない。

ベースイメージ

A base image has no parent image specified in its Dockerfile. It is created using a Dockerfile with the FROM scratch directive.

ベースイメージには、Dockerfileで指定された親イメージがありません。 FROMスクラッチディレクティブを含むDockerfileを使用して作成されます。

docker docs / Glossary

Dockerfileで最初に FROM scratchを書く、ないしはFROMを省略すると、それはベースイメージになる模様。

ペアレントイメージ

An image’s parent image is the image designated in the FROM directive in the image’s Dockerfile. All subsequent commands are based on this parent image. A Dockerfile with the FROM scratch directive uses no parent image, and creates a base image.

イメージの親イメージは、イメージのDockerfileのFROMディレクティブで指定されたイメージです。後続のすべてのコマンドは、この親イメージに基づいています。 FROMスクラッチディレクティブを含むDockerfileは、親イメージを使用せず、ベースイメージを作成します。

docker docks / Glossary

使いたいアプリを親として、必要なところだけをカスタマイズするのが楽チンだろうと思った。

コンテナ

A container is a runtime instance of a docker image.
A Docker container consists of
A Docker image
An execution environment
A standard set of instructions
The concept is borrowed from Shipping Containers, which define a standard to ship goods globally. Docker defines a standard to ship software.

コンテナはDockerイメージの実行時インスタンスです。
Dockerコンテナは、Dockerイメージ、実行環境、一般的な指示一式からなります。
このコンセプトは、運送用コンテナがグローバルに商品を発送する際の一般的な荷札※から取り入れました。Dockerのソフトウェア発送の一般的な荷札です。

※define a standardってのが上手く訳せない。荷主、中身、宛先とかがびっしり書かれた紙を想像した。

docker docs / Glossary

イメージのところでもコンテナは実行されたものと書かれている。停止したコンテナはイメージなの?というと、イメージは状態を持たないようなので一致しない。

コンテナはLinuxの通常のプロセスとほぼ同じものだが、利用できる名前空間やリソースが他のプロセスやコンテナからは隔離され、それぞれ固有の設定を持てるようになっている。そのためコンテナ内のアプリケーションから見ると、独立したコンピュータ上で動作しているように振る舞う。コンテナを管理するコストはプロセスを管理するコストとほとんど変わらず、仮想マシンを管理するコストと比較すると非常に軽い。

@IT - 第1回 Dockerとは

結局、具体的な持ち物(データー)が分からないからなのか、イメージとコンテナの違いがどうにもすっきりとはまらない。
違いはこの後で調べている

ボリューム

A volume is a specially-designated directory within one or more containers that bypasses the Union File System. Volumes are designed to persist data, independent of the container’s life cycle. Docker therefore never automatically deletes volumes when you remove a container, nor will it “garbage collect” volumes that are no longer referenced by a container. Also known as: data volume

There are three types of volumes: host, anonymous, and named:
A host volume lives on the Docker host’s filesystem and can be accessed from within the container.
A named volume is a volume which Docker manages where on disk the volume is created, but it is given a name.
An anonymous volume is similar to a named volume, however, it can be difficult, to refer to the same volume over time when it is an anonymous volumes. Docker handle where the files are stored.

ボリュームは1つ以上のコンテナの範囲内で特別に指定されたディレクトリで、ユニオンファイルシステムを迂回します。ボリュームは、コンテナのライフサイクルとは関係なく、データを永続化するように設計されています。したがって、Dockerはコンテナーを削除するときにボリュームを自動的に削除することも、コンテナーによって参照されなくなったボリュームを「ガベージコレクション」することもありません。データーボリュームとしても知られています。

ボリュームには、ホスト、匿名、名前付きの3つのタイプがあります。
ホストボリュームはDockerホストのファイルシステム上に存在し、コンテナ内からアクセスできます。
名前付きボリュームは、Dockerが管理するボリュームであり、ディスク上でボリュームが作成されますが、名前が付けられています。
匿名ボリュームは名前付きボリュームに似ていますが、匿名ボリュームの場合、時間の経過とともに同じボリュームを参照することが難しくなることがあります。Dockerはファイルが保存される場所を制御します。

docker docs / Glossary

コンテナが削除されてもデーターが消えないようにしたいときには、ボリュームを作って、そこにデーターを入れておけば良いらしい。

ボリュームは自動的に作られるものだとばかり思っていたが、そうじゃなかった。
DiscourseやKeycloadを運用しているシステムでdocker volume ls しても何も表示されない。

Discourseは launcher rebuild してもデーターが消えないのだが、それは何故なのかと思ってlauncherを見てみた。
はっきりとは分からなかったが、必要なときにはvolumeを作ってそこにバックアップを持たせているのではないかと思われた。

ユニオンファイルシステム

ボリュームはデーターを永続化させるために使われる。となると、迂回されるユニオンファイルシステムがなんなのかを知らないと…これは永続化されないのだろうから。

Union file systems implement a union mount and operate by creating layers. Docker uses union file systems in conjunction with copy-on-write techniques to provide the building blocks for containers, making them very lightweight and fast.
For more on Docker and union file systems, see Docker and AUFS in practice, Docker and Btrfs in practice, and Docker and OverlayFS in practice.
Example implementations of union file systems are UnionFS, AUFS, and Btrfs.

ユニオンファイルシステムは、ユニオンマウントを実装し、レイヤーを作成することによって動作します。 Dockerは、ユニオンファイルシステムをコピーオンライト技術と組み合わせて使用​​して、コンテナーにビルディングブロックを提供し、非常に軽量で高速にします。
Dockerとユニオンファイルシステムの詳細については、実際のDockerとAUFS、実際のDockerとBtrfs、実際のDockerとOverlayFSをご覧ください。
ユニオンファイルシステムの実装例は、UnionFS、AUFS、およびBtrfsです。

docker docs / Glossary

ユニオンマウント

複数のディレクトリを組み合わせて、組み合わせた全てのディレクトリの中身が1つのディレクトリの中で見えるような仕掛け。
例えば、CD-ROMに書き込みはできないが、書き込み可能なディレクトリと透過的に重ね合わせることでCD-ROMにデーターが書き込まれたように見える。
Wikipedia / Union mount

コピーオンライト

Docker uses a copy-on-write technique and a union file system for both images and containers to optimize resources and speed performance. Multiple copies of an entity share the same instance and each one makes only specific changes to its unique layer.
Multiple containers can share access to the same image, and make container-specific changes on a writable layer which is deleted when the container is removed. This speeds up container start times and performance.
Images are essentially layers of filesystems typically predicated on a base image under a writable layer, and built up with layers of differences from the base image. This minimizes the footprint of the image and enables shared development.
For more about copy-on-write in the context of Docker, see Understand images, containers, and storage drivers.

Dockerは、イメージとコンテナの両方にコピーオンライト技術とユニオンファイルシステムを使用して、リソースを最適化し、パフォーマンスを高速化します。本体の複数のコピーが同じインスタンスを共有し、それぞれがその固有のレイヤーに特定の変更のみを加えます。
複数のコンテナが同じイメージへのアクセスを共有し、書き込み可能なレイヤーにコンテナ固有の変更を加えることができます。書き込み可能なレイヤーは、コンテナが削除されると削除されます。これにより、コンテナの起動時間とパフォーマンスが向上します。
イメージは、基本的には書き込み可能なレイヤーの下のベース画像に基づいたファイルシステムのレイヤーであり、基礎となるイメージとは異なるレイヤーで構築されています。これにより、イメージの足跡が最小化され、共有開発が可能になります。
Dockerのコンテキストでのコピーオンライトの詳細については、Understand images, containers, and storage driversを参照してください。

docker docs / Glossary

複数のコンテナが同じイメージへのアクセスを共有!?と思ったけれども…

Ubuntuのイメージがあったとして、これをベースにコンテナ1(Apache)とコンテナ2(DHCPサーバー)を作ったとして、

  • ベースとなるUbuntuは1つですよ。
  • コンテナ1はUbuntuのイメージを読み取り専用で取り込み、コンテナ・レイヤーにApacheをインストールできるよ。
    コンテナの中から見ていると、読み取り専用であることは分からなくて、必要なら書き込みができるように見えるよ。
  • コンテナ2はコンテナ1と同じUbuntuを読み取り専用で読み込み、コンテナ・レイヤーにDHCPサーバーをインストールできるよ。

ということを指しているものと思われた。

Dockerfile

A Dockerfile is a text document that contains all the commands you would normally execute manually in order to build a Docker image. Docker can build images automatically by reading the instructions from a Dockerfile.

Dockerfileは、Dockerイメージを構築するために通常手動で実行するすべてのコマンドを含むテキストドキュメントです。 Dockerは、Dockerfileから指示を読み取ることにより、イメージを自動的に構築できます。

docker docs / Glossary

docker build のmanpageを見ると、イメージを作る、とされている。でも、実際にはコンテナも作られるケースがあった。

やったこと

公式サイトやDockerについて取り扱うサイトを調べてみたが、読んでも理解ができなかった。
分かるまでやってみるしかなかった。

学習のための最低限のDockerfile

Dockerfileの作成

Ubuntuをベースとした起動しっぱなしになるイメージを作りたい。
とりあえず、Apacheを動かしておいてログインできるようなヤツ…と思って探してみたら、こちらに。
Qiita / Dockerでapache2起動

探していたのはこれです。ありがとうございます。学習のために転記させていただきます。

Dockerfile

FROM ubuntu:18.04
MAINTAINER rohhie

ENV APACHE_RUN_USER www-data
ENV APACHE_RUN_GROUP www-data
ENV APACHE_PID_FILE /var/run/apache2.pid
ENV APACHE_RUN_DIR /var/run/apache2
ENV APACHE_LOG_DIR /var/log/apache2
ENV APACHE_LOCK_DIR /var/lock/apache2

ENV DEBCONF_NOWARNINGS yes

RUN apt-get update; apt-get install -y apache2

EXPOSE 80

CMD ["apachectl", "-D", "FOREGROUND"]

Qiita / Dockerでapache2起動より抜粋+いくつかの変更

FROMでベースイメージを指定。恐らく、Docker公式のリポジトリから持ってくるんだろうと思われる。
MAINTAINERは生成する作者の名前とのことなので、rohhieとしてみた。
ENVは環境変数名と値のセットなんだけれど…きっとこれが自動でセットされず苦労して編みだしたものと思われる。※
RUNでapache2をインストール。
EXPOSEはコンテナ実行時にListenするポートを伝える。実際にアクセスするためには、コンテナ実行時にマップされるポートを-pパラメータで伝える必要がある。
CMDはapachectlで、これはapache2を起動する。-DでFOREGROUNDを指定することで、終わらないコンテナにしていると思われる。

※ENVの設定値について調べてみたら、apache2.confにその記載があると教えてくれた。
Otapps / Ubuntuでapt-getしたApacheの実行ユーザの変更方法。

なお、apt-getをaptにすると、以下の警告が出る。標準出力や戻り値に期待していないので無視しても良い。

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

また、apt-getコマンドを使うと以下の警告が出るが、apt-utilsをインストールする際にも出るみたいなので、警告を出ないように設定を加えた。
Qiita / "debconf: delaying package configuration, since apt-utils is not installed"を表示しないようにする

debconf: delaying package configuration, since apt-utils is not installed

 

イメージの構築

早速、イメージを構築してみる。

$ sudo docker build -t apache-test .
…
Successfully built e28abce97d93
Successfully tagged apache-test:latest

さっきまで docker build でコンテナまでできていたのだが、このDockerfileではコンテナができなかった。
どうしてなのか原因を探ろうと思ったけれども、さらっとは見つからない感じ。

イメージ構築の過程で、イメージがどんどん積み上げられていることが分かる。

$ sudo docker image history apache-test
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
e28abce97d93        12 hours ago        /bin/sh -c #(nop)  CMD ["apachectl" "-D" "FO…   0B
39227f98777a        12 hours ago        /bin/sh -c #(nop)  EXPOSE 80                    0B
28ff7663f8c4        12 hours ago        /bin/sh -c apt-get update; apt-get install -…   126MB ← ここでレイヤーが5層になった
52ff4315d5fb        12 hours ago        /bin/sh -c #(nop)  ENV DEBCONF_NOWARNINGS=yes   0B     ← ここまでレイヤーは4層
ffb952972ed3        12 hours ago        /bin/sh -c #(nop)  ENV APACHE_LOCK_DIR=/var/…   0B
d00ee41cee9f        12 hours ago        /bin/sh -c #(nop)  ENV APACHE_LOG_DIR=/var/l…   0B
36e573a5786f        12 hours ago        /bin/sh -c #(nop)  ENV APACHE_RUN_DIR=/var/r…   0B
bf506c9c26c8        12 hours ago        /bin/sh -c #(nop)  ENV APACHE_PID_FILE=/var/…   0B
eb8ee33510c5        12 hours ago        /bin/sh -c #(nop)  ENV APACHE_RUN_GROUP=www-…   0B
4f7c1e3d97a2        12 hours ago        /bin/sh -c #(nop)  ENV APACHE_RUN_USER=www-d…   0B
3a481532297f        12 hours ago        /bin/sh -c #(nop)  MAINTAINER rohhie            0B
2eb2d388e1a2        2 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>           2 weeks ago         /bin/sh -c mkdir -p /run/systemd && echo 'do…   7B
<missing>           2 weeks ago         /bin/sh -c set -xe   && echo '#!/bin/sh' > /…   745B
<missing>           2 weeks ago         /bin/sh -c [ -z "$(apt-get indextargets)" ]     987kB
<missing>           2 weeks ago         /bin/sh -c #(nop) ADD file:7d9bbf45a5b2510d4…   63.2MB

※<missing>と表示されている行は、他のシステムで構築されていることを表す。このサーバーでは扱えない。

普段古いイメージを見ることはないかもしれないが、ID指定でinspectできたりもする。

これらのイメージは、そのままレイヤーになるわけではないようだ。
なんらかのロジック(ディスクに何らかの変更が掛かったとき?)でレイヤーとして整理されてイメージの中でRootFSとして保持される模様。

コンテナの実行

利用中のポートを避けて、ポート80番で公開されているサービスにホストからアクセスできるようにする。
利用中のポートはこれで調べる

$ sudo ss -pnA tcp,udp,raw state listening state unconnected

今回は、ポート9080番でアクセスすることにした。

$ sudo docker run -p 9080:80 -d -h apache-cont apache-test

※-hでコンテナの名前を指定したつもりだったけれど、実際にはホスト名が設定されただけで、コンテナはsad_dhawanになっていた。この場合は--nameがよかったのかも。

ブラウザで9080ポートにアクセスすると、Apacheのデフォルトページが表示された。

コンテナの中を少しいじってイメージ化してみる

都合でサーバーを再起動した関係で、コンテナが起動していなかった。
起動してログインし、中を少しいじってみる。目的は、移設先で「確かに変更が反映されている」を確認したいから。

$ sudo docker start sad_dhawan
$ sudo docker exec -u 0 -it sad_dhawan /bin/bash --login
root@apache-cont:/# apt install vim
root@apache-cont:~# vi /var/www/html/index.html
編集後
root@apache-cont:~# logout

vimをインストールして、Apacheの「Ubuntu Default Page」のところを「Docker Custom Page」に書き換えただけ。
でも、違いははっきり出るかなと。

早速コミットしてみる。

$ sudo docker commit -a="Rohhie.Net" -m="install vim and change index.html" sad_dhawan
sha256:6b7f288de233269287e39bf9eace2bd83f887440b58f9a70f795a58331b54492
$ sudo docker image ls
REPOSITORY                  TAG                 IMAGE ID            CREATED              SIZE
<none>                      <none>              6b7f288de233        About a minute ago   239MB
apache-test                 latest              e28abce97d93        16 hours ago         190MB
…

※リポジトリとタグを付けても良かったのかもしれない。公式にゴミを作ることを懸念したが、多分、そのためにはアカウントが必要なんだろう。

イメージとコンテナの比較

イメージとコンテナをinspectしたものを比較して、違いを認識してみる。

イメージにあるRootFSがコンテナにはない。しかし、コンテナにはRootFSで示されたレイヤーの上にレイヤーがあって、コンテナに加えた変更はこのレイヤーに書き込まれるとのこと
RootFSで示されたデーターを見に行ってみたが、GraphDriverで示されているIDとつながっているようで、これらが一体となってユニオンファイルシステムを形成していると想定される。

項目Image(Dockerfileから構築) →→ Container →→ Image(ContainerをCommit)備考
Idsha256:e28abce97d93~※12b3cd8ab8d94~sha256:6b7f288de233~
Created2020-08-12T10:06:382020-08-12T10:51:51UTCっぽい。
ConfigHostname: 無指定
環境変数ENV 等々
Image: sha256:39227f98777a~
CMD指定がDockerfileそのまま
Hostname: apache-cont
環境変数ENV 等々
Image: apache-test
CMD指定がDockerfileそのまま
Hostname: apache-cont
環境変数ENV 等々
Image: apache-test
CMD指定がDockerfileそのまま
Imageが親を指している。
GraphDriverLowerDir: /var/lib/docker/overlay2/8137c789cf7f~
MergedDir,UpperDir,WorkDir: /var/lib/docker/overlay2/9c4c7234df8b~
→LowerDirは複数指定されている。
Name: overlay2
LowerDir: /var/lib/docker/overlay2/a2a2d041f354~
MergedDir,UpperDir,WorkDir: /var/lib/docker/overlay2/a2a2d041f354~等
→LowerDirはImageのLowerDirを含んでいた。
Name: overlay2
LowerDir: /var/lib/docker/overlay2/9c4c7234df8b~
MergedDir,UpperDir,WorkDir: /var/lib/docker/overlay2/7e77931ed7b5~
→LowerDirはImageのLowerDirを含んでいた。
Name: overlay2
GraphDriverはスナップショットの前身。
スナップショットをオーバーレイしている。
RepoTagsapache-test:latestなし[]
RepoDigests[]なし[]
Parentsha256:39227f98777a~なしsha256:e28abce97d93~※2docker image history e28abce97d93
で見ると親が分かる。
Comment無指定なしinstall vim and change index.htmlコミットしたときに指定したメッセージ。
Container1e7f4cf112d5~なし2b3cd8ab8d94~1e7f4cf112d5~が見つからない。
2b3cd8ab8d94~コミットしたコンテナ
ContainerConfigHostname: 1e7f4cf112d5
環境変数ENV 等々
Image: sha256:39227f98777a~
CMD指定が変形している
なしHostname: apache-cont
環境変数ENV 等々
Image: apache-test
CMD指定がDockerfileそのまま
DockerVersion19.03.12なし19.03.12
AuthorrohhieなしRohhie.Netコミットしたときに指定した作者。
Architectureamd64なしamd64
OSlinuxなしlinux
Size189913406なし239015238
VirtualSize189913406なし239015238
RootFSType: layers + レイヤー5階層なしType: layers + レイヤー6階層5階層までは一致。
MetadataLastTagTime: 2020-08-12T19:09:37なしLastTagTime: 0001-01-01T00:00:00ZJSTっぽい。
Pathなしapachectlなし
Argsなし-D FOREGROUNDなし
StateなしStatus: running 等々なし
Imageなしsha256:e28abce97d93~※1※2なし※1 親イメージを指している。
ResolvConfPathなし/var/lib/docker/containers/2b3cd8ab8d94~/resolv.confなし
HostnamePathなし/var/lib/docker/containers/2b3cd8ab8d94~/hostnameなし
HostsPathなし/var/lib/docker/containers/2b3cd8ab8d94~/hostsなし
LogPathなし/var/lib/docker/containers/2b3cd8ab8d94~/~.logなし
Nameなし/sad_dhawanなし
RestartCountなし0なし
Driverなしoverlay2なし
Platformなしlinuxなし
MountLabelなし無指定なし
ProcessLabelなし無指定なし
AppArmorProfileなしdocker-defaultなし
ExecIDsなしnullなし
HostConfigなしたくさんの設定なし
Mountsなし[]なし
NetworkSettingsなしポートの割り当てやIPアドレスなどなし

Dockerfileを理解するために(失敗)

ある程度理解が進んだ今となっては突っ込みどころ満載。とはいえ、リンク集的な意味もあるので、セクションを残して青文字で突っ込む。

コピーオンライトを学習していたら、複数のコンテナが同じイメージへのアクセスを共有、といわれた。
イメージへのアクセス?となって理解ページに飛んだら、いきなりDockerfileを検討してください、ときた。

うちでも2つのコンテナを動かしているのだから、どっかにDockerfileってのがあるんじゃないかと思って探したけれど見つからない。
docker hubでイメージを見ると、Dockerfileへのリンクがある。例えばここ

分からないんだからしょうがない、学習。
docker docs / Dockerfile reference

$ docker build .
unable to prepare context: unable to evaluate symlinks in Dockerfile path: lstat /home/rohhie/work/docker/Dockerfile: no such file or directory

Dockerfileがないよ、と。…最初に書かれたコマンドが動かない。もう、挫折しそう…
ここにある説明をコピってファイルを作ってみる。

/home/rohhie/work/docker/Dockerfile

FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py

やってみる。

$ docker build .
…
どば~っとなにかが表示される。
…
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post http://%2Fvar%2Frun%2Fdocker.sock/v1.40/build?buildargs=%7B%7D&cachefrom=%5B%5D&cgroupparent=&cpuperiod=0&cpuquota=0&cpusetcpus=&cpusetmems=&cpushares=0&dockerfile=Dockerfile&labels=%7B%7D&memory=0&memswap=0&networkmode=default&rm=1&session=kininkoxgbsiv5b6vq56tlc5d&shmsize=0&target=&ulimits=null&version=1: dial unix /var/run/docker.sock: connect: permission denied

うちで作った環境では、Dockerはroot権限で実行しないと上手く動かない。
→色々と深く刺さっている仕組みなので当たり前か。安全設定はRootlessモードというみたい。

$ sudo docker build .
free(): invalid pointer … ①
SIGABRT: abort
PC=0x7f3680fe4f47 m=0 sigcode=18446744073709551610
signal arrived during cgo execution

goroutine 1 [syscall, locked to thread]:
runtime.cgocall(0x4afd50, 0xc420047cc0, 0xc420047ce8)
        /usr/lib/go-1.8/src/runtime/cgocall.go:131 +0xe2 fp=0xc420047c90 sp=0xc420047c50
github.com/docker/docker-credential-helpers/secretservice._Cfunc_free(0x274cda0)
        github.com/docker/docker-credential-helpers/secretservice/_obj/_cgo_gotypes.go:111 +0x41 fp=0xc420047cc0 sp=0xc420047c90
github.com/docker/docker-credential-helpers/secretservice.Secretservice.List.func5(0x274cda0)
        /build/golang-github-docker-docker-credential-helpers-cMhSy1/golang-github-docker-docker-credential-helpers-0.5.0/obj-x86_64-linux-gnu/src/github.com/docker/docker-credential-helpers/secretservice/secretservice_linux.go:96 +0x60 fp=0xc420047cf8 sp=0xc420047cc0
github.com/docker/docker-credential-helpers/secretservice.Secretservice.List(0x0, 0x756060, 0xc4200642c0)
        /build/golang-github-docker-docker-credential-helpers-cMhSy1/golang-github-docker-docker-credential-helpers-0.5.0/obj-x86_64-linux-gnu/src/github.com/docker/docker-credential-helpers/secretservice/secretservice_linux.go:97 +0x217 fp=0xc420047da0 sp=0xc420047cf8
github.com/docker/docker-credential-helpers/secretservice.(*Secretservice).List(0x77e548, 0xc420047e88, 0x410022, 0xc420064220)
        <autogenerated>:4 +0x46 fp=0xc420047de0 sp=0xc420047da0
github.com/docker/docker-credential-helpers/credentials.List(0x756ba0, 0x77e548, 0x7560e0, 0xc420082008, 0x0, 0x10)
        /build/golang-github-docker-docker-credential-helpers-cMhSy1/golang-github-docker-docker-credential-helpers-0.5.0/obj-x86_64-linux-gnu/src/github.com/docker/docker-credential-helpers/credentials/credentials.go:145 +0x3e fp=0xc420047e68 sp=0xc420047de0
github.com/docker/docker-credential-helpers/credentials.HandleCommand(0x756ba0, 0x77e548, 0x7ffe4bdf18c9, 0x4, 0x7560a0, 0xc420082000, 0x7560e0, 0xc420082008, 0x40e398, 0x4d35c0)
        /build/golang-github-docker-docker-credential-helpers-cMhSy1/golang-github-docker-docker-credential-helpers-0.5.0/obj-x86_64-linux-gnu/src/github.com/docker/docker-credential-helpers/credentials/credentials.go:60 +0x16d fp=0xc420047ed8 sp=0xc420047e68
github.com/docker/docker-credential-helpers/credentials.Serve(0x756ba0, 0x77e548)
        /build/golang-github-docker-docker-credential-helpers-cMhSy1/golang-github-docker-docker-credential-helpers-0.5.0/obj-x86_64-linux-gnu/src/github.com/docker/docker-credential-helpers/credentials/credentials.go:41 +0x1cb fp=0xc420047f58 sp=0xc420047ed8
main.main()
        /build/golang-github-docker-docker-credential-helpers-cMhSy1/golang-github-docker-docker-credential-helpers-0.5.0/secretservice/cmd/main_linux.go:9 +0x4f fp=0xc420047f88 sp=0xc420047f58
runtime.main()
        /usr/lib/go-1.8/src/runtime/proc.go:185 +0x20a fp=0xc420047fe0 sp=0xc420047f88
runtime.goexit()
        /usr/lib/go-1.8/src/runtime/asm_amd64.s:2197 +0x1 fp=0xc420047fe8 sp=0xc420047fe0

goroutine 17 [syscall, locked to thread]:
runtime.goexit()
        /usr/lib/go-1.8/src/runtime/asm_amd64.s:2197 +0x1

rax    0x0
rbx    0x7ffe4bdf0a90
rcx    0x7f3680fe4f47
rdx    0x0
rdi    0x2
rsi    0x7ffe4bdf0820
rbp    0x7ffe4bdf0b90
rsp    0x7ffe4bdf0820
r8     0x0
r9     0x7ffe4bdf0820
r10    0x8
r11    0x246
r12    0x7ffe4bdf0a90
r13    0x1000
r14    0x0
r15    0x30
rip    0x7f3680fe4f47
rflags 0x246
cs     0x33
fs     0x0
gs     0x0 … ①' 恐らくここまでエラー表示
Sending build context to Docker daemon  2.048kB … ②
Step 1/4 : FROM ubuntu:18.04
18.04: Pulling from library/ubuntu
7595c8c21622: Pull complete
d13af8ca898f: Pull complete
70799171ddba: Pull complete
b6c12202c5ef: Pull complete
Digest: sha256:a61728f6128fb4a7a20efaa7597607ed6e69973ee9b9123e3b4fd28b7bba100b
Status: Downloaded newer image for ubuntu:18.04 … ③
 ---> 2eb2d388e1a2
Step 2/4 : COPY . /app
 ---> 0f5e643d00de
Step 3/4 : RUN make /app
 ---> Running in c7fec60d3045
/bin/sh: 1: make: not found
The command '/bin/sh -c make /app' returned a non-zero code: 127

①どうやら、Ubuntuのサポートするパッケージが古いために発生している模様。
ビルドはこのコマンドではなくDockerデーモンで実行されると書かれていた。確かにそうなった。
③Dockerfileの1行目でUbuntu:18.04と指定している。

まず、大量に出ているエラーをどうにかしたいと思い、dockerを最新版にした

コンテナを削除して作り直してみる。

$ sudo docker ps -a
CONTAINER ID        IMAGE                              COMMAND                  CREATED             STATUS                     PORTS                              NAMES
c7fec60d3045        0f5e643d00de                       "/bin/sh -c 'make /a…"   3 hours ago         Exited (127) 3 hours ago                                      great_curran

$ sudo docker rm great_curran
great_curran

$ sudo docker build .
Sending build context to Docker daemon  1.275MB
Step 1/4 : FROM ubuntu:18.04
 ---> 2eb2d388e1a2
Step 2/4 : COPY . /app
 ---> 71b5cec9734f
Step 3/4 : RUN make /app
 ---> Running in 4c9e44126995
/bin/sh: 1: make: not found
The command '/bin/sh -c make /app' returned a non-zero code: 127

※とりあえずは大量に出ていたエラーが消えて、通常の動作になったようだ。

結果を見ると、COPY . /app は現在のディレクトリをそのまま/appにコピーすることを表しており、Dockerfileとかがコピーされたとみられる。
RUN make /app でmakeが見つからないか、makeはあるけれどもMakefileが見つからなくて異常終了している。

中に入ってみる。

$ sudo docker exec -u 0 -it pedantic_shtern /bin/bash --login
Error response from daemon: Container 4c9e44126995d11dff8f4c04192f933e3d7c59b0f95068ea23cc430b415d7b88 is not running

怒られた。確かに、このコンテナは動作していない。
無駄なコンテナが作られるけど、runしてみるか…。

$ sudo docker container rm pedantic_shtern
pedantic_shtern
$ sudo docker run -it 71b5cec9734f bash ← --rm パラメーターを付けると終了後にコンテナは削除される
root@993c63ff9418:/# make
bash: make: command not found

※何度もdocker buildしていて、このタイミングではイメージが 71b5cec9734f だった。

せめてmakeコマンドくらいあるんだと思ったけど、ないのか…。
これを学習のネタにするのは無理だな…
→チュートリアルを探せオレ…

さいごに

正直なところ、ネットを探しても混乱するだけだった。

原因はイメージとコンテナの定義が分かりづらいこと。
ぱっと見では真逆の表現がなされていたり、考えているものと言葉が一致しなかったりして、何をすると安全にシステムを移設できるのか分からない。

恐らくはこの記事も似たようなものだろうが、自分用メモとして後でぼんやりしたときに見返して思い出せればいい、と割り切ってリリースする。

広告

コメントはこちらから お気軽にどうぞ ~ 投稿に関するご意見・感想・他