CircleCIでCloud SQLに接続した時の問題と解決策

この記事は ウェブクルー Advent Calendar 2022 25日目の記事です。
昨日は @hiro19840624 さんの「[初めてロゴコンペに応募したときの話](https://qiita.com/hiro19840624/items/4ae588e054d32b9ef354)」でした。

## はじめに
JenkinsからCircleCIに移行を進める中で（記事投稿時点で絶賛作業中）発生した問題と、それを解決するために使ったツールを紹介します。

## 発生した問題
CircleCIでDBのマイグレーションを実行するため、`gcr.io/cloudsql-docker/gce-proxy`のDocker イメージを使って接続するジョブを書いたところ、２つ問題が発生しました。

### gce-proxyを起動すると後続のステップが実行されない
ジョブに`gcr.io/cloudsql-docker/gce-proxy`を起動するステップとDBを操作するステップを書いて実行したところ、gce-proxyのコンテナを起動するステップがフォアグラウンドで起動するため、後続のステップが実行されませんでした。

```yml
# このステップがフォアグラウンドで起動して「Run dbwork」のステップが実行されない
- run:
  name: Cloud SQL Auth Proxy setup & run
  command: |
    echo <<parameters.gcloud-service-key>> > google_service_account_key_file.json
    docker run \
      -v `pwd`/google_service_account_key_file.json:/config \
      -p 127.0.0.1:1433:1433 \
      gcr.io/cloudsql-docker/gce-proxy:1.31.2 /cloud_sql_proxy \
      -instances={Cloud SQLインスタンスの接続文字列}=tcp:0.0.0.0:1433 -credential_file=/config
- run:
  name: Run dbwork
  command: 
    docker run --rm --name migrate-db --network host asia.gcr.io/${<<parameters.google-project-id>>}/migrate-example-db:latest \
    {DBを操作するコマンド}
```

### バックグラウンドでgce-proxyを起動すると、Cloud SQLに接続できない
１つ目の問題の対策として、gce-proxyのコンテナを起動するステップをバックグラウンドで起動するように変更したところ、今度はコンテナが起動する前にDBを操作するステップが実行され、Cloud SQLへの接続を確立できずジョブが失敗しました。

```yml
- run:
  name: Cloud SQL Auth Proxy setup & run
  command: |
    echo <<parameters.gcloud-service-key>> > google_service_account_key_file.json
    docker run \
      -v `pwd`/google_service_account_key_file.json:/config \
      -p 127.0.0.1:1433:1433 \
      gcr.io/cloudsql-docker/gce-proxy:1.31.2 /cloud_sql_proxy \
      -instances={Cloud SQLインスタンスの接続文字列}=tcp:0.0.0.0:1433 -credential_file=/config
  # バックグラウンド実行のオプション
  background: true
# 「Cloud SQL Auth Proxy setup & run」をバックグラウンド実行すると、
# コンテナが起動する前にこのステップが実行されてしまう
- run:
  name: Run dbwork
  command: 
    docker run --rm --name migrate-db --network host asia.gcr.io/${<<parameters.google-project-id>>}/migrate-example-db:latest \
    {DBを操作するコマンド}
```

上記の問題が発生したため、『コンテナの起動待ちをする良い方法』を調べて、dockerizeを見つけました。

## dockerizeとは
Utility to simplify running applications in docker containers.
Dockerコンテナを使ったアプリケーション実行の手間を簡素にするユーティリティです。

https://github.com/jwilder/dockerize

## dockerizeの使い方
ジョブのステップでdockerizeをインストールして、gce-proxyのコンテナを起動待ちするだけです。

```yml
# dockerizeをインストール
- run:
    name: Install dockerize
    command: |
      wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
      sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
      rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
    environment: 
      DOCKERIZE_VERSION: v0.6.1
# バックグラウンドでgce-proxyを起動
- run:
  name: Cloud SQL Auth Proxy setup & run
  command: |
    echo <<parameters.gcloud-service-key>> > google_service_account_key_file.json
    docker run \
      -v `pwd`/google_service_account_key_file.json:/config \
      -p 127.0.0.1:1433:1433 \
      gcr.io/cloudsql-docker/gce-proxy:1.31.2 /cloud_sql_proxy \
      -instances={Cloud SQLインスタンスの接続文字列}=tcp:0.0.0.0:1433 -credential_file=/config
  background: true
# 起動したgce-proxyの起動を待つ
- run:
  name: Wait for Cloud SQL Auth Proxy
  command: dockerize -wait tcp://127.0.0.1:1433 -timeout 1m
- run:
  name: Run dbwork
  command: 
    docker run --rm --name migrate-db --network host asia.gcr.io/${<<parameters.google-project-id>>}/migrate-example-db:latest \
    {DBを操作するコマンド}
```

CircleCI上にはこのような表示になります。
![スクリーンショット 2022-12-23 23.40.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71234/3521e424-3ac3-55a4-be42-f662e432350a.png)

## dockerize -wait tcp://127.0.0.1:1433 -timeout 1m は何をしているか
dockerizeはgoで実装されていて、コードを読み解くは難しくありません。
https://github.com/jwilder/dockerize/blob/master/main.go

`waitForDependencies`の実装を読むと、与えられたurlからプロトコルを判定し、指定された時間分スリープしながら無限ループを回しているだけです。
私の場合はtcpでパラメータを渡しているため、`net.DialTimeout`でコネクションを確率できるかを判断していました。

```go
func waitForSocket(scheme, addr string, timeout time.Duration) {
	wg.Add(1)
	go func() {
		defer wg.Done()
		for {
			conn, err := net.DialTimeout(scheme, addr, waitTimeoutFlag)
			if err != nil {
				log.Printf("Problem with dial: %v. Sleeping %s\n", err.Error(), waitRetryInterval)
				time.Sleep(waitRetryInterval)
			}
			if conn != nil {
				log.Printf("Connected to %s://%s\n", scheme, addr)
				return
			}
		}
	}()
}
```

## 最後に
ここで最後にdockerizeの気になる点ですが、最終のリリースが2018年です。
しかし、[CircleCIのドキュメント](https://circleci.com/docs/ja/databases/#using-dockerize-to-wait-for-dependencies)にもdockerizeについて記載されているため、しばらくはdockerizeを使えそうだなと思います。

これで今年のアドベントカレンダーは終わりです。

皆さん良いお年を！