digdag タスクの docker 実行について

[ドキュメント](http://docs.digdag.io/architecture.html?highlight=docker#running-tasks-on-docker)に細かいことが書いてなかったのでメモしておく。

digdag のバージョンは [v0.9.31](https://github.com/treasure-data/digdag/blob/19dfc0c00e980a6d2c7dfb0f65cc66d62ac63268/digdag-standards/src/main/java/io/digdag/standards/command/DockerCommandExecutor.java)

## まえおき: digdag タスクの docker 実行

`_export` に docker オプションを指定すると各タスクが docker run で実行されるようになる。この機能についての解説。digdag 自体を Docker に入れ込むとかは別の話。

```yaml
_export:
  docker:
    image: ubuntu

+task1:
  py>: tasks.MyWorkflow.task1
```

## digdag docker オプションのオプション

必須パラメータである `image` に加えて以下の２つのオプションが使える

* build: String
* pull_always: Boolean

### build

build を指定すると docker build が走る

```yaml
_export:
  docker:
    image: ubuntu:latest
    build: |
      apt install -y git

+task1:
  py>: tasks.MyWorkflow.task1
```

### pull_always

`pull_always: true` とすると各タスクが実行される度に docker pull を試すようになる。
いまのところ、ワークフロー全体で最初に１度だけ docker pull するような機能はなさそうだ。


```yaml
_export:
  docker:
    image: ubuntu:latest
    pull_always: true

+task1:
  py>: tasks.MyWorkflow.task1
```

## コンテナ内で実行されるのは CommandExecutor なオペレータのみ

コマンドとして実行されるオペレータのみ docker run される。現状では以下の４つのみ

* py>:
* rb>:
* sh>:
* embulk>: (deprecated)

他のオペレータ (echo, s3_wait, gcs_wait など) は java ライブラリとして実装されていることもあり、docker コンテナ内ではなく、docker ホスト上で実行される。docker コンテナに GCP の鍵を登録してあっても、例えば gcs_wait はその鍵を使ってくれないので、この違いを認識しておく必要がある。


## コンテナ内のファイルシステムに書いたファイルは消える

タスクそれぞれが docker run されるので、コンテナ内のファイルシステムに書いたファイルは消える。

```yaml
_export:
  docker:
    image: ubuntu

+task1:
  sh>: touch /tmp/test

+task2:
  sh>: ls /tmp/test # <- Not found
```

そのため `&&` でつなげて実行するなどしないといけない。

```yaml
_export:
  docker:
    image: ubuntu

+task1:
  sh>: |
    touch /tmp/test &&
    ls /tmp/test
```

## docker run に渡されるオプション

現行の[v0.9.31](https://github.com/treasure-data/digdag/blob/19dfc0c00e980a6d2c7dfb0f65cc66d62ac63268/digdag-standards/src/main/java/io/digdag/standards/command/DockerCommandExecutor.java)では、タスクごとに以下のような docker run を実行しているようだ。
FYI: digdag 実行時に `-l debug` オプションをつけると確認できる。

```shell-session
$ docker run -i --rm -v プロジェクトパス:プロジェクトパス \
  -w プロジェクトパス -e 環境変数=値... イメージ名 コマンド
```

ポイントは以下のあたり

1. プロジェクトパスが docker コンテナにマウントされる
2. ワーキングディレクトリが Dockerfile の `WORKDIR` から digdag のプロジェクトパスに変更される
3. ホストの環境変数がコンテナに引き継がれる (`HOME` や `USER` も変更される)

プロジェクトパスは、digdag run の場合は実行しているプロジェクトのパスで、digdag server の場合は /tmp 下のてきとうな tmp ディレクトリにプロジェクトファイルが撒かれて使われる。その tmp ディレクトリはコマンド実行が終わったら消される。

そのため digdag run の時はカレントディレクトリ以下に書いたファイルは docker ホストに永続化できる。digdag server の時は結局消されるので永続化のつもりでは使えない。

また、digdag server が作る tmp ディレクトリを docker コンテナから読み書きできるようにしておく必要がある。docker コンテナ内を root (uid: 0) ユーザにしておくとか、digdag server を動かすユーザの uid と、Dockerfile の USER で指定するユーザの uid を合わせておく必要がある。

3によって、環境変数が上書きされるのは特に注意が必要。Docker 内で `HOME` 環境変数を使っているような場合(ほとんどそう)にハマるので、Tips としては以下のように `HOME` 環境変数を設定して元に戻してあげるのが良い。

```yaml
_export:
  HOME: もともとDockerfileで想定していたWORKDIR
  docker:
    ...
```

所感: これは ... 余計なお世話度が高いような...

## digdag secrets で登録した値を docker から参照したい

digdag secrets で渡した値を `_env` で環境変数として渡せばなんとかできる。

oO(`_env` はタスク毎に設定しないといけないのでかなり面倒

```yaml
+docker_run:
  _env:
     secret_gcp_credential: ${secret:gcp.credential}
     secret_aws_access_key_id: ${secret:aws.access_key_id}
     secret_aws_secret_access_key: ${secret:aws_secret_access_key}
  sh>: echo test
```

FYI: ちなみに `digdag secrets --set gcp.credentail=@gcpcredential.json` のようにファイルで渡したものは `${secret:gcp.crecential}` ではファイル内容が文字列として展開される。

----

Dockerfile では docker イメージに入れ込まないようにして、ENTRYPOINT で起動時に配置するようにすると digdag secrets から値を取れたことになる。

```dockerfile:Dockerfile
....

COPY docker-entrypoint.sh docker-entrypoint.sh
ENTRYPOINT ["./docker-entrypoint.sh"]
```

docker-entorypoint.sh

```bash
#!/bin/bash

# Add aws credential
mkdir -p .aws
cat > .aws/credentials <<EOF
[default]
aws_access_key_id = ${secret_aws_access_key_id}
aws_secret_access_key = ${secret_aws_secret_access_key}
EOF

# Add gcp credential and activate
mkdir -p .credential
echo "${secret_gcp_credential}" > .credential/gcpcredential.json
gcloud auth activate-service-account xxxxx --key-file .credential/gcpcredential.json --project xxxxx

exec "$@"
```

(AWSクレデンシャルに関しては、aws cli も aws sdk も環境変数 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY に対応してるのでそちらを使っても良さそう)

SEE ALSO: [digdag secrets を global な環境変数にマップする - Qiita](https://qiita.com/sonots/items/cb71834857235f965225)

## docker pull の前に $(aws ecr get-login) したい

[Amazon ECR レジストリ](https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/Registries.html) に docker イメージを push している場合、`$(aws ecr get-login)` のようにして docker login してからでないと docker pull できない。12時間でログイントークンが expire されるので厄介なやつ。

```yaml
+docker_login:
  sh>: eval $(aws ecr get-login --no-include-email)

+docker_run:
  _export:
    docker:
      image: ubuntu:latest
      pull_always: true

  +task1:
    py>: tasks.MyWorkflow.task1
```

このように書けば１台構成の場合は、docker login してから、以降のタスクを実行できるので、確実に docker pull できる。

しかし、複数台構成の場合、タスク毎にどのホストで実行されるかが不定なので、docker login していないホストで docker pull && docker run しようとして失敗する可能性がある。

色々調べた結論としては、https://github.com/awslabs/amazon-ecr-credential-helper を使うと、docker login を省いて docker pull できるようになり、digdag 側で頑張らなくても解決できるのでオススメする。

その際 `~/.docker/config.json` には

```json
{"credsStore": "ecr-login"}
```

よりも

```json
{
	"credHelpers": {
		"<aws_account_id>.dkr.ecr.<region>.amazonaws.com": "ecr-login"
	}
}
```

のように設定するのをオススメする。前者の場合、ECR 以外の例えば docker hub のイメージを pull する場合も ecr-login を試して ERROR メッセージが表示されてしまう(実際には docker pull は成功するが).

## docker run のオプションを設定したい

EDIT: 0.9.37 から docker run のオプションを指定できるようになりました。https://github.com/treasure-data/digdag/pull/1025

以下、古い文書

-----

いまのところ、docker run のオプションを自由に設定する `_export: docker:` のオプションはない。

発想の転換で、`_export: docker:` を使わず `sh>:` で docker run してしまえば自由度は得られる。好きなタイミングで docker pull も docker login もできる。

```yaml
_export:
  docker_image: ubuntu
  docker_run: docker run -i --rm -v /tmp:/tmp ${docker_image}

+docker_pull:
  sh>: docker pull ${docker_image}

+task1:
  sh>: ${docker_run} コマンド
```

## ポイントまとめ

* コンテナ内で実行されるのは CommandExecutor なオペレータ(py, rb, sh) のみ
* コンテナ内でローカルに書いたファイルは揮発する (--volume オプションの設定もできない)
* ホスト側の環境変数でコンテナ側の環境変数が上書きされる
* (今の所)docker のオプションを自由に設定することはできないので、どうしても必要なら `_export: docker:` をやめて `sh>: docker run` するしかない...

## 要望まとめ

* ホスト側の環境変数が全部引き継がれるのはちょっと余計なお世話度が高いような...
* docker run の `--volume` オプションとか色々オプション追加サポートして欲しい。とりあえず EFS マウントしたい。
* docker run の前に確実に docker login するようなオプションもあっても良いかもしれない (その時は `$(aws ecr get-login)` が実行できるようにシェル実行サポートして欲しい)