🐳

DockerCompose を用いて起動順序を制御する

2022/03/08に公開

初めに

Docker を用いた開発環境が主流になってきているが、起動順序を意識されていないことが多くまれに起動に失敗する。起動順序の制御方法を Docker Compose の公式ドキュメントと Compose Specification と実装を参照し、起動順序による起動の失敗が起きない環境の設定方法をまとめる。

公式ドキュメントを読む

まずは公式ドキュメントの(Control startup and shutdown order in Compose)を参照する。

https://docs.docker.com/compose/startup-order/

ドキュメントには下記のように記載があり depends_on オプションのことが記載されている。

You can control the order of service startup and shutdown with the depends_on option. Compose always starts and stops containers in dependency order, where dependencies are determined by depends_on, links, volumes_from, and network_mode: "service:...".

depends_on を使えば制御できるらしくドキュメントページ(Compose file version 3 reference)へのリンクが付いているので詳細を確認してみる。

https://docs.docker.com/compose/compose-file/compose-file-v3/#depends_on

Express dependency between services. Service dependencies cause the following behaviors

サービス間の依存関係を制御できるのでこのオプションをうまく利用すると良いことがわかる。
例が含まれているので例を参考に見ていく。

docker-compose.yml
version: "3.9"
services:
  web:
    build: .
    depends_on:
      - db
      - redis
  redis:
    image: redis
  db:
    image: postgres

この場合 web は redis と db サービス の起動に依存していて起動済みであれば web を起動するようになる。しかしドキュメントでも2つの問題点が記載されています。

depends_on does not wait for db and redis to be “ready” before starting web - only until they have been started. If you need to wait for a service to be ready, see Controlling startup order for more on this problem and strategies for solving it.
The depends_on option is ignored when deploying a stack in swarm mode with a version 3 Compose file.

2つ目の問題は swarm mode のデプロイでのみ起きる問題で swarm mode で運用したことがないことと、開発環境の起動順序の話なので今回は割愛する。

1つ目の問題の解決方法は Control startup and shutdown order in Compose を参照するように記載されている。相互にリンクされておりここで通常は depends_on では解決不可能となり、 wait-for-it, dockerize, sh-compatible wait-for, or RelayAndContainers など small wrapper scripts を利用することが推奨されているように見える。
wait-for-it.sh を利用した例は下記。

docker-compose.yml
version: "2"
services:
  web:
    build: .
    ports:
      - "80:8000"
    depends_on:
      - "db"
    command: ["./wait-for-it.sh", "db:5432", "--", "python", "app.py"]
  db:
    image: postgres

これを見ていて気づいた点が version: "2" なのでおそらく長いことメンテされていない雰囲気を受ける。

ここで Compose file reference を見てみる。

https://docs.docker.com/compose/compose-file/

These topics describe the Docker Compose implementation of the Compose format. Docker Compose 1.27.0+ implements the format defined by the Compose Specification. Previous Docker Compose versions have support for several Compose file formats – 2, 2.x, and 3.x. The Compose specification is a unified 2.x and 3.x file format, aggregating properties across these formats.

Docker Compose 1.27.0+ 以降は Compose Specification で定義された形式を利用していることがわかる。
※ Compose Specification は、 2.x および 3.x で定義された形式を統合したもの

互換性のマトリクス表は下記のようになっており、 Docker Engine 19.03.0 は 2019/07/22 にリリースされているため 現在では Compose Specification で定義された形式以外が利用されているケースは稀であると考えれる。

Compose file format Docker Engine release
Compose Specification 19.03.0+
3.8 19.03.0+
3.7 18.06.0+
3.6 18.02.0+
3.5 17.12.0+

https://docs.docker.com/engine/release-notes/19.03/#19030

Compose Specification を読む

つぎに、 Compose Specification を参照する。

https://github.com/compose-spec/compose-spec/blob/master/spec.md

Requirements and optional attributes を読むと下記のように記載されている。

We acknowledge that no Compose implementation is expected to support all attributes, and that support for some properties is Platform dependent and can only be confirmed at runtime. The definition of a versioned schema to control the supported properties in a Compose file, established by the docker-compose tool where the Compose file format was designed, doesn't offer any guarantee to the end-user attributes will be actually implemented.

すべての attributes をサポートする Compose の実装は期待されておらず、実装されることを保証するものではないためCompose の実装を確認する必要がある。

実装を読む前に Compose Specification では depends_on はどのように定義されているか確認しておく。

https://github.com/compose-spec/compose-spec/blob/master/spec.md#depends_on

version 3 のドキュメントには存在しなかった Long syntax の項目が存在することが確認できる。

Long syntax を利用すると、 short syntax では表現できない依存関係の定義ができる。その中でも service_healthy はヘルスチェックが正常な場合に起動することが可能になる。
ここでついに、 Control startup and shutdown order in Compose では不可能とされていた問題を解決することが可能になった。しかし先程みたように実装されているかは不明なため Docker Compose の実装を読んでいく。

Docker Compose の実装を読む

読んでいく前に Docker Compose には v1 と v2 が存在する。

https://github.com/docker/compose#about-update-and-backward-compatibility

V1 は Python で記載されていたが V2 は Golang で 0 から書き直されています。
下記の公式ドキュメントとロードマップを見る限りでは V1 はまだまだサポートをやめる予定はないようです。

https://docs.docker.com/compose/cli-command/#transitioning-to-ga-for-compose-v2
https://github.com/docker/roadmap/issues/257

Mac と Windows では Docker Desktop を利用していることが多いと思うので V2 の方の実装を見ていくことにします。
また、docker-compose.yml の parser を確認しオプションが存在すれば実装されているはずなのでそこを見ていく。(Golang は詳しくないので勘違いしていることがあれば教えてほしい)

schema に service_healthy が存在することを確認。

https://github.com/compose-spec/compose-go/blob/98c14dd30a7919fa909b1a3006c8d3ba81f08fc9/schema/compose-spec.json#L184-L205

        "depends_on": {
          "oneOf": [
            {"$ref": "#/definitions/list_of_strings"},
            {
              "type": "object",
              "additionalProperties": false,
              "patternProperties": {
                "^[a-zA-Z0-9._-]+$": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "condition": {
                      "type": "string",
                      "enum": ["service_started", "service_healthy", "service_completed_successfully"]
                    }
                  },
                  "required": ["condition"]
                }
              }
            }
          ]
        },

types に service_healthy が存在することを確認。

https://github.com/compose-spec/compose-go/blob/98c14dd30a7919fa909b1a3006c8d3ba81f08fc9/types/types.go#L836-L845

const (
	// ServiceConditionCompletedSuccessfully is the type for waiting until a service has completed successfully (exit code 0).
	ServiceConditionCompletedSuccessfully = "service_completed_successfully"

	// ServiceConditionHealthy is the type for waiting until a service is healthy.
	ServiceConditionHealthy = "service_healthy"

	// ServiceConditionStarted is the type for waiting until a service has started (default).
	ServiceConditionStarted = "service_started"
)

loader のところで読み込んでいるのが確認できる。

https://github.com/compose-spec/compose-go/blob/master/loader/loader.go#L924-L941

var transformDependsOnConfig TransformerFunc = func(data interface{}) (interface{}, error) {
	switch value := data.(type) {
	case []interface{}:
		transformed := map[string]interface{}{}
		for _, serviceIntf := range value {
			service, ok := serviceIntf.(string)
			if !ok {
				return data, errors.Errorf("invalid type %T for service depends_on elementn, expected string", value)
			}
			transformed[service] = map[string]interface{}{"condition": types.ServiceConditionStarted}
		}
		return transformed, nil
	case map[string]interface{}:
		return groupXFieldsIntoExtensions(data.(map[string]interface{})), nil
	default:
		return data, errors.Errorf("invalid type %T for service depends_on", value)
	}
}

というわけで現在の実装では service_healthy を利用しても問題ないことがわかった。

ついでに Docker Compose 側のテストで service_healthy が利用されているものもあった。

https://github.com/docker/compose/pull/9092

service_healthy を利用した docker-compose.yml の例

Control startup and shutdown order in Compose に記載のあった例を service_healthy を利用して small wrapper scripts を利用しない docker-compose.yml は下記になる。

docker-compose.yml
services:
  web:
    build: .
    ports:
      - "80:8000"
    depends_on:
      db:
        condition: service_healthy
    command: ["python", "app.py"]
  db:
    image: postgres
    healthcheck:
      test: ["CMD-SHELL", "pg_isready"]
      interval: 1s
      retries: 3

※ postgres の場合は pg_isready を利用する。
https://www.postgresql.org/docs/current/app-pg-isready.html

※ mysql の場合は mysqladmin の ping を利用する。
https://dev.mysql.com/doc/refman/8.0/en/mysqladmin.html

  db:
    image: mysql
    healthcheck:
      test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
      interval: 1s
      retries: 3

最後に

起動順序の制御方法を Docker Compose の公式ドキュメントを参照しただけだと small wrapper scripts を利用して制御できることがわかる。しかし Compose Specification と 実装を参照していくうちに depends_on の service_healthy と healthcheck を組み合わせることで small wrapper scripts を利用せずに制御することができることがわかる。

どちらの方法を選択するかは自由だが、small wrapper scripts を image をビルドする際に埋め込まなければならず好みではないので、 depends_on の service_healthy と healthcheck を利用していく。

ちなみに

https://github.com/compose-spec/compose-spec/blob/master/spec.md#compose-file

The Compose file is a YAML file defining version (DEPRECATED), services (REQUIRED), networks, volumes, configs and secrets. The default path for a Compose file is compose.yaml (preferred) or compose.yml in working directory. Compose implementations SHOULD also support docker-compose.yaml and docker-compose.yml for backward compatibility. If both files exist, Compose implementations MUST prefer canonical compose.yaml one.

https://github.com/compose-spec/compose-spec/blob/master/spec.md#version-top-level-element

Top-level version property is defined by the specification for backward compatibility but is only informative.

version に関する内容を見ると DEPRECATED になっていて、参考情報程度となっているので書く必要がなくなっている。

Discussion