dockerのnamespaceについて色々考えて/調べて/まとめてみた

こんにちは。
株式会社クラスアクト インフラストラクチャ事業部の大塚です。
この記事ではdockerコンテナとその裏側で使用されているNetwork Namespaceについて、個人的に色々考えて/調べたので備忘録としてまとめておこうと思います。

# dockerコンテナから他のdockerコンテナのプロセスは見れないってはなし

dockerのコンテナは、docker環境上で複数立ち上げることができるのですが、前提としてコンテナのプロセスは他コンテナからは見れないという特性があります。
プロセスというと難しく聞こえるかもしれません。例えば、PCでいうとタスクマネージャを開くと色々出てくると思うのですが、それとイコールだと考えればすんなり入ってくると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/046df721-bcb9-e642-1d98-c115ec07ffeb.png)

実際に確認してみましょう。
docker環境で以下のように3つのコンテナを作成しています。
具体的にはnginx,apache,nextcloudコンテナをそれぞれ1つずつデプロイしています。
```
root@docker:~# docker ps
CONTAINER ID   IMAGE       COMMAND                  CREATED              STATUS              PORTS     NAMES
83f27d094780   nextcloud   "/entrypoint.sh apac…"   About a minute ago   Up About a minute   80/tcp    sad_tu
6c498e8d2477   httpd       "httpd-foreground"       6 minutes ago        Up 6 minutes        80/tcp    flamboyant_bhaskara
55ef2eae198a   nginx       "/docker-entrypoint.…"   7 minutes ago        Up 7 minutes        80/tcp    busy_poincare
```

この状態でnginxコンテナにアクセスし、psコマンドを叩いてみましょう。
コンテナにアクセスするためにはdocker execコマンドを使用します。
またコンテナにはデフォルトでpsコマンドが入っておりませんので、インストールして使用できるようにします。
```
root@docker:~# docker exec -it 55ef2eae198a /bin/bash
root@55ef2eae198a:/# apt update
root@55ef2eae198a:/# apt upgrade
root@55ef2eae198a:/# apt install procps
```

psコマンドを叩いた結果は以下となります。
nginxに関連するプロセスしかありません。
```
root@55ef2eae198a:/# ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 06:25 ?        00:00:00 nginx: master process nginx -g daemon off;
nginx         29       1  0 06:25 ?        00:00:00 nginx: worker process
nginx         30       1  0 06:25 ?        00:00:00 nginx: worker process
nginx         31       1  0 06:25 ?        00:00:00 nginx: worker process
nginx         32       1  0 06:25 ?        00:00:00 nginx: worker process
root          33       0  0 07:41 pts/0    00:00:00 /bin/bash
root         457      33  0 07:48 pts/0    00:00:00 ps -ef
```

同じことをapacheコンテナにもやってみます。
apache関連のプロセスは出力されますが、先ほどnginxコンテナ上で出ていたnginx関連のプロセスは出ていないことがわかると思います。
```
root@docker:~# docker exec -it 6c498e8d2477 /bin/bash
root@6c498e8d2477:/usr/local/apache2# apt update
root@6c498e8d2477:/usr/local/apache2# apt upgrade
root@6c498e8d2477:/usr/local/apache2# apt install procps

root@6c498e8d2477:/usr/local/apache2# ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 06:25 ?        00:00:01 httpd -DFOREGROUND
www-data       8       1  0 06:25 ?        00:00:00 httpd -DFOREGROUND
www-data       9       1  0 06:25 ?        00:00:00 httpd -DFOREGROUND
www-data      10       1  0 06:25 ?        00:00:00 httpd -DFOREGROUND
root          99       0  0 09:28 pts/0    00:00:00 /bin/bash
root         434      99  0 09:33 pts/0    00:00:00 ps -ef
```

一方でこれらのプロセスはホスト側（dockerが稼働しているサーバ。私の場合ubuntu22.04）では確認することができます。
ホスト側で同じコマンドを実行してみます。
```
root@docker:~# ps -ef

中略
root       26271   26249  0 15:25 ?        00:00:00 nginx: master process nginx -g daemon off;
systemd+   26321   26271  0 15:25 ?        00:00:00 nginx: worker process
systemd+   26322   26271  0 15:25 ?        00:00:00 nginx: worker process
systemd+   26323   26271  0 15:25 ?        00:00:00 nginx: worker process
systemd+   26324   26271  0 15:25 ?        00:00:00 nginx: worker process

中略
root       26491   26469  0 15:25 ?        00:00:01 httpd -DFOREGROUND
www-data   26523   26491  0 15:25 ?        00:00:00 httpd -DFOREGROUND
www-data   26524   26491  0 15:25 ?        00:00:00 httpd -DFOREGROUND
www-data   26525   26491  0 15:25 ?        00:00:00 httpd -DFOREGROUND
```

この状態をイメージ図に起こしてみると以下のようになるかと思います。
コンテナどうしてプロセスを見れない一方で、ホスト側からは各コンテナのプロセスを確認することができます。

![docker-ページ2.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/64821011-001f-3085-0c47-985544ebd0dd.png)

# Namespaceについて

Network Namespace（以降"ns"と省略）はこのdockerコンテナを稼働させるために使用している技術であります。
なので、本当であれば各ns間のプロセスは他のnsからは見えないはずだと想定することができます。
なぜならばdockerコンテナ≒nsと考えることができるからです。

しかし、以前nsを触っているときにプロセスについて確認したところ、その想定はあっさり裏切られました。私が以前作成した記事の【未解決】部分についてがそれに該当します。
この後に本docker環境のnsについても同様に確認してみます。（が準備に少し手間がかかります。）

https://qiita.com/ohtsuka-shota/items/3d81e2b46461bb67f152

# dockerで作成したnsがデフォルトで見れない件

dockerkコンテナをデプロイする際、nsを使用していることは前述のとおりです。nsの存在を確認するときは基本的に"ip netns list"コマンドを叩くかと思います。
そこでdocker環境下でも叩いてみます。想定であれば本環境の場合3つ（dockerコンテナが3つデプロイされているので）nsが出力されるはずです。
しかしここでも期待を裏切られます。結果は1つとして出力されないのです。

```
root@docker:~# ip netns list
root@docker:~#
``` 

これについて調べてみると、どうも上記"ip netns list"コマンドはサーバ内のnsを直接認識している？というよりは/var/run/netns/配下にnsに関連するものがあるかどうかで判断しているみたいです。
以下のQiita記事に次のように書かれています。

>実はdockerが作るnamespaceは ip netnsコマンドで操作できないのです。。。
ip netnsで既存のnamespaceリストを表示できますが、ここでの情報は厳密には、namespaceではありません。/var/run/netns/ 配下のファイル一覧を単純に表示しているだけです。

https://qiita.com/ukinau/items/92740bd0bafc1e7427c5

実際に私のdocker環境において/var/run/netns/配下には何もありませんでした。ここに何もないから、ip netns listコマンドは何も吐かないということになります。
```
root@docker:~# ls -ltr /var/run/netns/
total 0
```

では、dockerコンテナ（≒ns）の情報はどこにあるのか？ということですが、それは以下の記事に書かれておりました。
どうも/proc/配下にあるようです。

https://blog-smatsuzaki.hatenablog.com/entry/2020/02/16/000517

上記記事を参考に私の環境でも確認してみます。
まずdockerコンテナのプロセスを確認してみます。
nginx,apache,nextcloudそれぞれPIDが26271,26491,26936であることがわかりました。
```
root@docker:~# docker ps 
CONTAINER ID   IMAGE       COMMAND                  CREATED          STATUS          PORTS     NAMES
83f27d094780   nextcloud   "/entrypoint.sh apac…"   15 minutes ago   Up 15 minutes   80/tcp    sad_tu
6c498e8d2477   httpd       "httpd-foreground"       20 minutes ago   Up 20 minutes   80/tcp    flamboyant_bhaskara
55ef2eae198a   nginx       "/docker-entrypoint.…"   21 minutes ago   Up 21 minutes   80/tcp    busy_poincare
root@docker:~# docker inspect 83f27d094780 --format '{{.State.Pid}}'
26936
root@docker:~# docker inspect 6c498e8d2477 --format '{{.State.Pid}}'
26491
root@docker:~# docker inspect 55ef2eae198a --format '{{.State.Pid}}'
26271
```

このプロセス番号が/proc/配下にディレクトリとしてあるようです。
確認してみると確かにあるっぽいです。これがdockerコンテナ専用のnsなんですかね？
```
root@docker:~# ls -l /proc/26936/ns/
total 0
lrwxrwxrwx 1 root root 0 Apr 15 15:41 cgroup -> 'cgroup:[4026532458]'
lrwxrwxrwx 1 root root 0 Apr 15 15:41 ipc -> 'ipc:[4026532400]'
lrwxrwxrwx 1 root root 0 Apr 15 15:41 mnt -> 'mnt:[4026532398]'
lrwxrwxrwx 1 root root 0 Apr 15 15:31 net -> 'net:[4026532402]'
lrwxrwxrwx 1 root root 0 Apr 15 15:41 pid -> 'pid:[4026532401]'
lrwxrwxrwx 1 root root 0 Apr 15 15:41 pid_for_children -> 'pid:[4026532401]'
lrwxrwxrwx 1 root root 0 Apr 15 15:41 time -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Apr 15 15:41 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Apr 15 15:41 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Apr 15 15:41 uts -> 'uts:[4026532399]'
root@docker:~# ls -l /proc/26491/ns/
total 0
lrwxrwxrwx 1 root root 0 Apr 15 15:54 cgroup -> 'cgroup:[4026532395]'
lrwxrwxrwx 1 root root 0 Apr 15 15:54 ipc -> 'ipc:[4026532337]'
lrwxrwxrwx 1 root root 0 Apr 15 15:54 mnt -> 'mnt:[4026532335]'
lrwxrwxrwx 1 root root 0 Apr 15 15:25 net -> 'net:[4026532339]'
lrwxrwxrwx 1 root root 0 Apr 15 15:54 pid -> 'pid:[4026532338]'
lrwxrwxrwx 1 root root 0 Apr 15 15:54 pid_for_children -> 'pid:[4026532338]'
lrwxrwxrwx 1 root root 0 Apr 15 15:54 time -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Apr 15 15:54 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Apr 15 15:54 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Apr 15 15:54 uts -> 'uts:[4026532336]'
root@docker:~# ls -l /proc/26271/ns/
total 0
lrwxrwxrwx 1 root root 0 Apr 15 15:55 cgroup -> 'cgroup:[4026532332]'
lrwxrwxrwx 1 root root 0 Apr 15 15:55 ipc -> 'ipc:[4026532272]'
lrwxrwxrwx 1 root root 0 Apr 15 15:55 mnt -> 'mnt:[4026532270]'
lrwxrwxrwx 1 root root 0 Apr 15 15:25 net -> 'net:[4026532274]'
lrwxrwxrwx 1 root root 0 Apr 15 15:55 pid -> 'pid:[4026532273]'
lrwxrwxrwx 1 root root 0 Apr 15 15:55 pid_for_children -> 'pid:[4026532273]'
lrwxrwxrwx 1 root root 0 Apr 15 15:55 time -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Apr 15 15:55 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Apr 15 15:55 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Apr 15 15:55 uts -> 'uts:[4026532271]'
```

dockerのnsをip netns listで出力させるためにはこの/proc/"コンテナのPID"/ns/配下にあるnetを/var/run/netns/配下にシンボリックさせればいいようです。
以下のように実行してみます。
```
root@docker:~# ln -s /proc/26936/ns/net /var/run/netns/docker-ns-26936
root@docker:~# ln -s /proc/26491/ns/net /var/run/netns/docker-ns-26491
root@docker:~# ln -s /proc/26271/ns/net /var/run/netns/docker-ns-26271

root@docker:~# ls -ltr /var/run/netns/
total 0
lrwxrwxrwx 1 root root 18 Apr 15 16:26 docker-ns-26936 -> /proc/26936/ns/net
lrwxrwxrwx 1 root root 18 Apr 15 16:26 docker-ns-26491 -> /proc/26491/ns/net
lrwxrwxrwx 1 root root 18 Apr 15 16:27 docker-ns-26271 -> /proc/26271/ns/net
```

この状態でip netns listコマンドを実行してみると、先ほどはdockerに関するnsは出てきませんでしたが、今度は出てきました。

```
root@docker:~# ip netns list
docker-ns-26271 (id: 0)
docker-ns-26491 (id: 1)
docker-ns-26936 (id: 2)
```

これでip netnsコマンドでnsに入る方法とdocker execコマンドでdockerコンテナ（＝ns）に入る方法が確立したことになります。

イメージで表すと以下のようになるでしょうか？
dockerコンテナに入り操作する方法として以下の2つの方法ができたということです。
**①docker exec コマンドを使う**
**②ip netns exec コマンドを使う**
※図は必ずも正しいとは思っておりません。docker execコマンドを叩いたときにdocker0のBridgeを通るとも思っておりませんし、ip netns execコマンドを叩いたときにNetwork Namespaceなんて未知の何かを通るとも思っていません。あくまでグラフィカルに表示させるために描いただけです。

![docker-ページ3.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/6d7ff79b-830f-6497-9ae0-d505ef989f14.png)

先ほどシンボリックリンクの設定をしたのは、ディスクでいうマウントをしたようなイメージを持てると少しイメージしやすくなると思います。
ディスクもマウントしないと中身見れないと思います。シンボリックリンクを設定したことによってNetwork Namespace側にもマウントされて中身が見れるようになったということなのかなと。。。

# ip netns execでns（コンテナ）に接続し、プロセスを確認してみる

では、dockerコンテナに対してip netns execコマンドでアクセスし、psコマンドを実行しプロセスを見てみます。
docker execで入りpsコマンドを叩いたところ、自分以外のコンテナのプロセスは見えておりませんでしたね。
入っている場所は同じになるので先ほどと同じ結果になるはずだと推測することができます。

実際にアクセスしてみます。下記アクセスしたnsはnginxコンテナになります。
```
root@docker:~# ip netns exec docker-ns-26271 bash
root@docker:~# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
4: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
```

nginxコンテナであることの証拠も併せて提示します。
IPアドレスが同じですね。
```
root@docker:~# docker ps
CONTAINER ID   IMAGE       COMMAND                  CREATED             STATUS             PORTS     NAMES
83f27d094780   nextcloud   "/entrypoint.sh apac…"   About an hour ago   Up About an hour   80/tcp    sad_tu
6c498e8d2477   httpd       "httpd-foreground"       About an hour ago   Up About an hour   80/tcp    flamboyant_bhaskara
55ef2eae198a   nginx       "/docker-entrypoint.…"   About an hour ago   Up About an hour   80/tcp    busy_poincare
root@docker:~# docker inspect 55ef2eae198a | grep IPAddress
            "SecondaryIPAddresses": null,
            "IPAddress": "172.17.0.2",
                    "IPAddress": "172.17.0.2",
```

nginxコンテナであることを確認したため、psコマンドを実際に叩きプロセスを確認します。
以下に結果を示しますが、nginxからapacheのプロセスが見えているので、docker execで入った時と出力が違うように見えますね。。。

```
root@docker:~# ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
省略

root       26271   26249  0 15:25 ?        00:00:00 nginx: master process nginx -g daemon off;
systemd+   26321   26271  0 15:25 ?        00:00:00 nginx: worker process
systemd+   26322   26271  0 15:25 ?        00:00:00 nginx: worker process
systemd+   26323   26271  0 15:25 ?        00:00:00 nginx: worker process
systemd+   26324   26271  0 15:25 ?        00:00:00 nginx: worker process
root       26469       1  0 15:25 ?        00:00:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 6c498e8d24774cf7250df99fd945344a3e68c7200400ade3a756dd7bb6af2f93 -address /run/containerd/co
root       26491   26469  0 15:25 ?        00:00:00 httpd -DFOREGROUND
www-data   26523   26491  0 15:25 ?        00:00:00 httpd -DFOREGROUND
www-data   26524   26491  0 15:25 ?        00:00:00 httpd -DFOREGROUND
www-data   26525   26491  0 15:25 ?        00:00:00 httpd -DFOREGROUND
root       26915       1  0 15:31 ?        00:00:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 83f27d094780b7dbab4b0b36f4c44afbfc8f4c82ddd01a3da0ba929fe74f34e1 -address /run/containerd/co
root       26936   26915  0 15:31 ?        00:00:00 apache2 -DFOREGROUND
www-data   27026   26936  0 15:31 ?        00:00:00 apache2 -DFOREGROUND
www-data   27027   26936  0 15:31 ?        00:00:00 apache2 -DFOREGROUND
www-data   27028   26936  0 15:31 ?        00:00:00 apache2 -DFOREGROUND
www-data   27029   26936  0 15:31 ?        00:00:00 apache2 -DFOREGROUND
www-data   27030   26936  0 15:31 ?        00:00:00 apache2 -DFOREGROUND
root       27559       2  0 16:05 ?        00:00:00 [kworker/2:1-events]
root       27573       2  0 16:05 ?        00:00:00 [kworker/u8:3-events_unbound]
root       27750       2  0 16:05 ?        00:00:00 [kworker/1:2-events]
root       27769       2  0 16:05 ?        00:00:00 [kworker/0:0-events]
root       28077       2  0 16:09 ?        00:00:00 [kworker/3:2-events]
root       28419       2  0 16:22 ?        00:00:00 [kworker/3:1-events]
root       28426       2  0 16:22 ?        00:00:00 [kworker/2:2]
root       28439       2  0 16:23 ?        00:00:00 [kworker/0:1-events]
root       28449       2  0 16:23 ?        00:00:00 [kworker/1:0-mm_percpu_wq]
root       28555   23578  0 16:25 ?        00:00:00 sshd: test [priv]
test       28655   28555  0 16:25 ?        00:00:00 sshd: test@pts/2
test       28657   28655  0 16:25 pts/2    00:00:00 -bash
root       28672   28657  0 16:25 pts/2    00:00:00 sudo su -
root       28674   28672  0 16:25 pts/3    00:00:00 sudo su -
root       28675   28674  0 16:25 pts/3    00:00:00 su -
root       28676   28675  0 16:25 pts/3    00:00:00 -bash
root       28710   23578  0 16:32 ?        00:00:00 sshd: test [priv]
test       28768   28710  0 16:32 ?        00:00:00 sshd: test@pts/4
test       28769   28768  0 16:32 pts/4    00:00:00 -bash
root       28778   28769  0 16:32 pts/4    00:00:00 sudo su -
root       28779       2  0 16:32 ?        00:00:00 [kworker/u8:0-events_unbound]
root       28780   28778  0 16:32 pts/5    00:00:00 sudo su -
root       28781   28780  0 16:32 pts/5    00:00:00 su -
root       28782   28781  0 16:32 pts/5    00:00:00 -bash
root       29046       2  0 16:38 ?        00:00:00 [kworker/u8:1-events_unbound]
root       29070   10277  0 16:38 pts/1    00:00:00 bash
root       29078   29070  0 16:38 pts/1    00:00:00 ps -ef
```

# docker execでコンテナに入るときとip netns execでnsに入るときの違い

同じdockerコンテナ（Network Namespaceから見たときはns）に入ったはずなのにpsコマンドの出力の違いが出てしまうのはなぜなのでしょうか？

この疑問について以下の記事を読むと精度は高くありませんが、イメージをつけることができました。

>Dockerのネットワーク周りのトラシューをする時、
docker execでコンテナ内に入ってしまうとデバッグ用のコマンドが使えないことがあって不便なものです。
そんな時はNetwork Namespaceを切り替えることで、ホスト側のコマンド全て使える状態で対象のDockerコンテナのネットワーク環境で作業できて便利です。

https://qiita.com/ishii1648/items/ec6e127eaeee6a0b7987

つまり、docker側でコマンドを叩いているのか、ホスト側でコマンドを叩いているのかが、そもそも違っていたということなのかと思います。

簡潔に書くと以下の違いがあるということになるかと思います。
- **docker execでコンテナに入る場合、コンテナ側でコマンドが実行。**
- **ip netns execでns（コンテナと同義）に入ると、ホスト側でコマンドが実行。**

図に起こしてみます。
再三になりますが、dockerコンテナにdocker execでアクセスしていると、コンテナ上でコマンドを実行していることになるようす。
![docker-ページ4.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/17b33b79-696b-8fb7-71ce-487604fbd90a.png)

これはコンテナを使わずゲストOSを使うパターンのものとほぼ同じといえるでしょう。
例えばUbuntuにKVMをインストールして仮想化基盤を構築するパターン。
![docker-ページ5.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/4325221e-78d3-0c07-dc58-009c20b68b18.png)

あるいはベアメタルサーバに直接インストールして使用するような仮想化基盤なども同様です。
![docker-ページ6.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/e7495a81-8fa8-4685-766e-284c2373f1a3.png)


一方でNamespaceは、「1つのOS空間をいくつかに分離する"だけ"」であるので、イメージとしては以下のようになると思います。
あくまでも空間を切っているだけなので、どのnsでpsコマンドを叩いても出力結果は同じだし、一部のnsに追加アプリをインストールしようとすると、それはホストでインストールをするのと同じであるということなのかと。。。
![docker-ページ7.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/51999a55-7ed1-8b22-f907-4b0e743a3790.png)

# まとめ

docker及びNetwork Namespaceについて調査の現状をまとめておきます。

### ① docker runでも ip netns addでもコンテナ（≒ns）を作成できるが、nsが作成される場所が違う

docker run：/proc/配下に作成される
ip netns add：/var/run/netns/配下に作成される

### ② dockerコンテナをnsとして操作するためには/var/run/netns/に対してシンボリックリンクを設定する必要がある

これを実行しておくと、ホスト側のコマンドをコンテナ内で使用することが出来るので、デバックが便利（らしい？）

### ③ docker execでコンテナに入っているとコンテナ側でコマンドが実行され、ip netns execでns（≒コンテナ）に入っていると、ホスト側でコマンドが実行される

ホスト環境を汚したくないなら素直にdocker execで入るほうが吉？


# 今後調べたいこと

Network Namespaceごとにプロセスが分離していると、確認する方法。
dockerコンテナでプロセスが分離出来ているのに、そのおおもとの技術となるNetwork Namespaceでそれが実現できないのはおかしい気がしています。
おそらく、Network Namespaceだけでは名前空間を完全には分離できず、unshareコマンドなどを使用することで
出来るようになるのかな？と思っています。
今後要検証ですね。。。汗

以下を参考にしていきたい・・・！

https://www.mtioutput.com/entry/docker-namespace-tutorial