PostgreSQL10 から PostgreSQL12 へのアップグレード手順(pg_upgrade)

古いPostgreSQLのバージョンがインストールされている開発マシンがあったので、pg_upgradeの公式ドキュメントを参考に、PostgreSQL10.15 から PostgreSQL12.5 に移行してみました。

ドキュメントに よると

pg_upgradeは8.4.X以降から現時点のPostgreSQLのメジャーリリース(スナップショット版やβリリースを含む)へのアップグレードをサポートします。

ということで、古めのバージョンであってもpg_upgradeで一気にバージョンを上げられます。

前提

  • 環境: Ubuntu 18.04 LTS デスクトップ
  • 移行前バージョン: PostgreSQL10.15
  • 移行後バージョン PostgreSQL12.5

バックアップを取得

念の為全DatabaseのDumpバックアップを取っておきます。

$ pg_dumpall > backup.sql

postgresql12をインストール

UbuntuへのPostgresqlのインストールは、公式の手順に従いました。

GPG keyを追加して、aptのsource.listにリポジトリを追加します。

$ wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
$ echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" |sudo tee  /etc/apt/sources.list.d/pgdg.list

このリポジトリには

といった一般的なパッケージやサードパーティアドオンのような様々なパッケージが含まれているようです。

apt updateして、postgresql12をインストールします。

$ sudo apt update
$ sudo apt -y install postgresql-12 postgresql-client-12

インストールが終わったら、念の為がpostgresql12のクラスタサービスが起動しているか確認します。

$  systemctl status postgresql@12-main.service 
● postgresql@12-main.service - PostgreSQL Cluster 12-main
   Loaded: loaded (/lib/systemd/system/postgresql@.service; indirect; vendor pre
   Active: active (running) since Sat 2021-01-02 12:13:16 JST; 14min ago
  Process: 19223 ExecStop=/usr/bin/pg_ctlcluster --skip-systemctl-redirect -m fa
  Process: 22868 ExecStart=/usr/bin/pg_ctlcluster --skip-systemctl-redirect 12-m
 Main PID: 22877 (postgres)
    Tasks: 7 (limit: 4915)
   CGroup: /system.slice/system-postgresql.slice/postgresql@12-main.service
           ├─22877 /usr/lib/postgresql/12/bin/postgres -D /var/lib/postgresql/12
           ├─22882 postgres: 12/main: checkpointer   
           ├─22883 postgres: 12/main: background writer   
           ├─22884 postgres: 12/main: walwriter   
           ├─22885 postgres: 12/main: autovacuum launcher   
           ├─22886 postgres: 12/main: stats collector   
           └─22887 postgres: 12/main: logical replication launcher

バージョンを戻すことを考慮に入れて一応postgresqlサービスの状態を確認したうえで、停止しておきます。

$ systemctl is-enabled postgresql
enabled

$ sudo systemctl stop postgresql.service

アップグレード

postgresユーザでpg_upgradeを実行します。

$ sudo su postgres

 /usr/lib/postgresql/12/bin/pg_upgrade \
     --old-datadir=/var/lib/postgresql/10/main \
     --new-datadir=/var/lib/postgresql/12/main \
     --old-bindir=/usr/lib/postgresql/10/bin \
     --new-bindir=/usr/lib/postgresql/12/bin \
     --old-options '-c config_file=/etc/postgresql/10/main/postgresql.conf' \
     --new-options '-c config_file=/etc/postgresql/12/main/postgresql.conf'

整合性チェックを実行しています。
-----------------------------
Checking cluster versions                                   ok
Checking database user is the install user                  ok
Checking database connection settings                       ok
Checking for prepared transactions                          ok
Checking for reg* data types in user tables                 ok
Checking for contrib/isn with bigint-passing mismatch       ok
Checking for tables WITH OIDS                               ok
Checking for invalid "sql_identifier" user columns          ok
Creating dump of global objects                             ok
Creating dump of database schemas
                                                            ok
Checking for presence of required libraries                 ok
Checking database user is the install user                  ok
Checking for prepared transactions                          ok
Checking for new cluster tablespace directories             ok

この後pg_upgradeが失敗した場合は、続ける前に新しいクラスタを
initdbで再作成する必要があります。

アップグレードを実行しています。
------------------
Analyzing all rows in the new cluster                       ok
Freezing all rows in the new cluster                        ok
Deleting files from new pg_xact                             ok
Copying old pg_xact to new server                           ok
Setting next transaction ID and epoch for new cluster       ok
Deleting files from new pg_multixact/offsets                ok
Copying old pg_multixact/offsets to new server              ok
Deleting files from new pg_multixact/members                ok
Copying old pg_multixact/members to new server              ok
Setting next multixact ID and offset for new cluster        ok
Resetting WAL archives                                      ok
Setting frozenxid and minmxid counters in new cluster       ok
Restoring global objects in the new cluster                 ok
Restoring database schemas in the new cluster
                                                            ok
ユーザリレーションのファイルをコピーしています
                                                            ok
Setting next OID for new cluster                            ok
Sync data directory to disk                                 ok
Creating script to analyze new cluster                      ok
Creating script to delete old cluster                       ok

アップグレードが完了しました
----------------
オプティマイザーの統計は、pg_upgrade では転送されません。そのため
新サーバーを起動した後、./analyze_new_cluster.sh を動かすことを検討してください。


このスクリプトを実行すると、旧クラスタのデータファイル ./delete_old_cluster.shが削除されます:

アップグレードが終わったので、postgresユーザから一般ユーザに戻します。

  exit

新しくPostgreSQLバージョンのクラスタのポートがデフォルトポートではないため、postgresql.confのport番号を変更します。

$ sudo vim /etc/postgresql/12/main/postgresql.conf
port = 5433                             # (change requires restart)
=> port = 5432                             # (change requires restart)

同様にpostgresql10のポートも変更します。

$ sudo vim /etc/postgresql/10/main/postgresql.conf
port = 5432                             # (change requires restart)
=> port = 5433                             # (change requires restart)

もし、他にもpostgresql.confを変更している箇所があったり、rootユーザパスワードの認証なしでログインできる等の設定をpg_hba.confで行っている場合は、PostgreSQL12の方でも手動変更しておきます。

再度postgresサービスを起動し、接続を確認してみます。

sudo systemctl start postgresql.service

再度、postgresユーザに戻りpsqlコマンドがpostgresql12の方に接続しに行っているか確認します。

$ sudo su postgres
$  psql -c "SELECT version();"
PostgreSQL 12.5 (Ubuntu 12.5-1.pgdg18.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0, 64-bit

続いて、pg_upgrade実行時に生成されたanalyze_new_cluster.shを実行し、オプティマイザの統計情報を収集しておきます。

$ ./analyze_new_cluster.sh
vacuumdb: データベース"calendar_development"の処理中です: 最適化のための情報を最小限生成します(1対象)
vacuumdb: データベース"calendar_development"の処理中です: 最適化のための情報を最小限生成します(1対象)
〜〜〜
vacuumdb: データベース"world_myroom_development"の処理中です: 最適化のための情報をデフォルト数(全て)生成します

Done

postgresql10の削除

接続が確認できたらpostgresql10は不要なので削除します。

$ sudo apt-get remove postgresql-10 postgresql-server-dev-10
$ sudo rm -rf /etc/postgresql/10/

再度、postgresユーザに戻って、pg_upgrade時に生成されたdelete_old_cluster.shを実行しておきます。

$ sudo su postgres
./delete_old_cluster.sh

参考資料

【React hooks】"Warning: Can't perform a React state update on an unmounted component."の止め方

React hooksで実装したSPAアプリをChromeで動かした際に、以下のWarningが表示されていたので対処してみました。

Warning: Can't perform a React state update on an unmounted 
component. This is a no-op, but it indicates a memory leak in 
your application. To fix, cancel all subscriptions and asynchronous 
tasks in %s.%s","a useEffect cleanup function","\n in UserPage 

例えば、以下のようなコードで useEffect の中でAPIにアクセスして、返ってきたら state にセットするようなアプリケーションでよく発生するWarningかと思います。

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [user, setUser] = 
    useState({ 
      id: 1, 
      first_name: "安倍", 
      last_name: "晋三"
    });

  useEffect(() => {
    axios.get('https://reqres.in/api/users/2')
      .then((response) => {
        if (!!response.data) {
          setUser(response.data.data)
        }
      })
  }, []);

  return (
    <div className="App">
      <header className="App-body">
        ID:{user.id}、名前:{user.first_name} {user.last_name}
      </header>
    </div>
  );
}

export default App;

前提

  • React: 17.0.1
  • Axios: 0.21.1

Warningの発生原因

Warningを出しているReactのコードを確認したところ以下のコメントが書いてありました。

Updating state from within an effect cleanup function is sometimes a necessary pattern, e.g.: 1. Updating an ancestor that a component had registered itself with on mount. 2. Resetting state when a component is hidden after going offscreen.

例えば、APIからレスポンスが返ってくるまでに時間がかかっていて、レスポンスを受け取る前に別ページに遷移した等、コンポーネントがアンマウントされている場合に、stateを保存する(上記の例だとsetUser)タイミングで保存できずにWarningが表示されるようです。

Warningの表示を管理している変数のコメントを確認したところ、開発環境でしか表示されないWarningのようなので、本番環境におけるメモリリークを回避するためにも開発時に見かけたタイミングで対処しておきたいWarningです。

対応パターン①:アンマウント状態を変数で管理する

以下のようにアンマウント時にisMounted変数を更新するクリーンナップ関数を追加したうえで、既にアンマウントされている状態の場合に保存しないようにしたらWarningがなくなりました。

useEffect の戻り値に関数が指定されている場合、それはクリーンナップ関数になり、コンポーネントのアンマウント時に1度だけ実行されます。

useEffect(() => {
  let isMounted = true
  axios.get('https://reqres.in/api/users/2')
    .then((response) => {
      if (!!response.data) {
        if (isMounted) {
          setUser(response.data.data)
        }
      }
    })

  return () => { isMounted = false };
}, []);

クリーンナップ関数の詳細は、公式ドキュメントに記載されています。 reactjs.org

メリット

この対応のメリットとしては

  • 様々なケースで対処できる
    • ケース①:APIからレスポンスが遅い場合
    • ケース②:APIからレスポンスを取得した後の処理が長い場合

デメリット

この対応のデメリットとしては

  • 管理する変数が1つ増える

ため汎用的ではありますが、コードが煩雑になり、保守しづらくなることかと思います。

対応パターン②Ajaxをキャンセルする

APIからレスポンスが返ってくるのが遅いことが主な原因であれば、以下のように axios のキャンセルトークン等を利用してAPIリクエストをキャンセルすると良さそうです。

useEffect(() => {
  const source = axios.CancelToken.source()
  axios.get('https://reqres.in/api/users/2')
    .then((response) => {
      if (!!response.data) {
        setUser(response.data.data)
      }
    })
  return () => { 
    source.cancel("APIはキャンセルされました");
  };
}, []);

メリット・デメリット

この対応は、変数( source )が増えてはいますが、成功時の処理の中(thenの中)では使われないので、対応パターン①とは反対に保守がしやすいというメリットはあるものの、汎用的ではないためAjaxをキャンセルしてWarningがなくなるケース以外ではWarningが残るというデメリットがありそうです。

参考資料

DockerのRootless modeで、rootユーザ以外でデーモンを実行してみた

Docker 19.03(2019年7月リリース)で入った機能の1つであるRootless modeを触ってみました。

Rootless モード(Rootless mode)は Docker デーモンとコンテナを root 以外のユーザが実行できるようにするもので、デーモンやコンテナ・ランタイムにおける潜在的脆弱性を回避してくれるものです。

CVE-2019–5736のようなコンテナの中からホスト上の権限を奪取することが出来る脆弱性が見つかっているので、特に実運用環境ではRootless modeで実行されるようにした方が良さそうです。

NTT須田さんの記事に詳しく書いてあるのでリンクを貼っておきます。 medium.com

Rootlessモードの場合とそうでない場合で、Dockerdのプロセスとコンテナ内で動くプロセスのユーザがどうなっているか確認してみました。

Rootlessモードではない場合

Dockerをインストール済のホストマシンで、起動中のDockerデーモンの実行ユーザを確認してみました。

psをコマンドでdockerdの実行ユーザを確認すると

f:id:moritamorie:20201222015545p:plain

たしかにrootで実行されていそうです。

次にUSERを指定しないでプロセスを起動しているコンテナに入って実行中のプロセス情報を確認してみます。

f:id:moritamorie:20201222024151p:plain

コンテナ内のプロセスの実行ユーザもrootになっていました。

Rootlessモードの場合

手元のマシンでは、既にrootlessモードではないdockerdが起動しているので、Amazon EC2(ubuntu 20.04 LTS, t2.nano )環境にDockerをインストールしrootlessモードで起動してみます。

前提条件は環境によって異なります。Ubuntuでは事前準備は不要ですが、他の環境で試される方はドキュメントを参考にセットアップしてみてください。

$ curl -fsSL https://get.docker.com/rootless | sh
# Installing stable version 20.10.1
# Missing system requirements. Please run following commands to
# install the requirements and run this installer again.
# Alternatively iptables checks can be disabled with SKIP_IPTABLES=1

cat <<EOF | sudo sh -x
apt-get install -y uidmap
EOF

uidmapのインストールが必要のようなので、インストールしてみました。

$ sudo apt update
$ cat <<EOF | sudo sh -x
apt-get install -y uidmap
EOF 

再度、dockerをインストールしてみます。

$ curl -fsSL https://get.docker.com/rootless | sh
# Installing stable version 20.10.1

〜〜〜

[INFO] Creating /home/ubuntu/.config/systemd/user/docker.service
[INFO] starting systemd service docker.service
+ systemctl --user start docker.service
+ sleep 3
+ systemctl --user --no-pager --full status docker.service
● docker.service - Docker Application Container Engine (Rootless)
     Loaded: loaded (/home/ubuntu/.config/systemd/user/docker.service; disabled; vendor preset: enabled)

〜〜〜

[INFO] Installed docker.service successfully.
[INFO] To control docker.service, run: `systemctl --user (start|stop|restart) docker.service`
[INFO] To run docker.service on system startup, run: `sudo loginctl enable-linger ubuntu`

[INFO] Make sure the following environment variables are set (or add them to ~/.bashrc):

export PATH=/home/ubuntu/bin:$PATH
export DOCKER_HOST=unix:///run/user/1000/docker.sock

無事にdockerのインストールができました。インストールプロセスの中でsystemctlコマンドを使ってdocker.serviceを起動しているので、既にDockerデーモンは起動している状態です。

dockerコマンドにパスを通すために~/.bashrc環境変数を追加して、sourceコマンドで反映してみます。

export PATH=/home/ubuntu/bin:$PATH
export DOCKER_HOST=unix:///run/user/1000/docker.sock
$ source ~/.bashrc

Dockerデーモンの実行ユーザを確認してみました。psをコマンドでdockerdの実行ユーザを確認すると f:id:moritamorie:20201223000227p:plain

ubuntuユーザで実行されていて、rootではなくなっていることが確認できました。

次にUSERを指定しないでプロセスを起動しているコンテナに入って実行中のプロセス情報を確認してみます。

f:id:moritamorie:20201223003935p:plain

おや。rootユーザで実行されていて、Rootless modeではない場合とユーザと同じになっていそうです。

以下の Docker公式のブログ記事を読んでみると

ルートレスモードは、最初にユーザ名前空間を作成し、リマップされた名前空間で既にデーモンを起動することを除いて、同様の方法で動作します。デーモンとコンテナは、ホストとは異なる同じユーザー名空間を使用します。

https://i2.wp.com/cdn-images-1.medium.com/max/2000/1*SfAokC2YQ-f04Wc2WhSRCw.png?ssl=1

という内容が記載されているので、コンテナ内ではrootユーザではあるものの、ホストマシンのubuntuユーザにマッピングされて実行されているようなので、万が一コンテナが乗っ取られてもホスト上の権限を奪取できない状態になっていそうです。

参考記事

Github ActionsでGolangプロジェクトをHerokuに自動デプロイしてみた

前々回、前回の記事でHerokuでGolangプロジェクトを動かすのと、Heroku Container RegistoryにDockerイメージをPushし、web dynoにリリースするというのをやりました。

今回は、Github Actionsを使ってHerokuに自動デプロイしてみます。

simple-minds-think-alike.hatenablog.com

simple-minds-think-alike.hatenablog.com

.githubディレクトリを追加

自動デプロイというと大げさに聞こえますが、前回まででHerokuのgitリポジトリにpushすれば自動的にweb dynoにデプロイするようになっているので、PRがmasterブランチにマージされた際に自動的にgit pushするようにするだけです。

プロジェクトにファイル.github/workflows/heroku-push.ymlを追加し、 akhileshns/heroku-deploy を使ってHerokuのgitリポジトリへの自動pushが実行されるようにします。

name: Heroku push
on:
  push:
    branches:
      - master
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: akhileshns/heroku-deploy@v3.7.8
        with:
          heroku_api_key: ${{secrets.HEROKU_API_KEY}}
          heroku_app_name: "golang-sample-202005"
          heroku_email: ${{secrets.HEROKU_EMAIL}}

Secretsの設定

GithubのSetting/Secretsに、環境変数 HEROKU_API_KEY, HEROKU_EMAIL を登録しておきます。

f:id:moritamorie:20201219173529p:plain

masterにPRをマージしてみる

PRをマージすると、Actionsでworkflowが動き始め、以下のようなログが表示されます。

f:id:moritamorie:20201219175415p:plain

Heroku上で

  • Dockerfileのビルド
  • Heroku Container Registoryへの登録
  • web dynoへの反映

が自動的に行われます。

参考情報

Docker イメージのレイヤーの概念とファイルの整理

Docker イメージのレイヤーの理解を深めるために、Dockerホストのコンソール上で牛にMooと言わせるだけのコンテナを動かし、そのコンテナを例にとってDockerイメージのレイヤーの状態を整理してみました。

いまいちDockerイメージのレイヤーの概念がよく分からないという方に参考になれば幸いです。 (※各ファイルのディレクトリの構成は、現在ストレージドライバの標準になっているoverlay2を使った場合の例です。)

サンプルのDockerコンテナで実現すること

牛に"Moo"と言わせるだけです。

f:id:moritamorie:20201218031718p:plain

Dockerfileを作る

Dockerホストの任意のディレクトリ(ここで は cowsay としました)に、以下のようなDockerfileを作ってみます。

FROM debian:buster

RUN apt-get update && apt-get install -y cowsay
  • debian:busterのイメージをDocker hubから取得
  • 牛を表示するアプリケーションcowsay をインストール

というシンプルなDockerfileです。

Dockerfileをビルドし、イメージを作る

Dockerfileがあるディレクトリ内に移動し、作ったDockerfileをビルドし、イメージを作ります。

$ cd cowsay
$ docker build -t test/cowsay .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM debian:buster
 ---> 6d6b00c22231
Step 2/2 : RUN apt-get update && apt-get install -y cowsay
 ---> Using cache
 ---> 5b2abe87f6d2
Successfully built 5b2abe87f6d2
Successfully tagged test/cowsay:latest

まずは docker images を実行して、test/cowsayのDockerイメージが出来ていることを確認します。

$ docker images test/cowsay:latest

REPOSITORY    TAG       IMAGE ID       CREATED          SIZE
test/cowsay   latest    20961704943e   16 seconds ago   179MB

次に docker historyを実行してイメージの状態を確認してみます。2つのイメージレイヤーが出来ていることを確認できました。

$docker history test/cowsay:latest

IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
5b2abe87f6d2   27 seconds ago   /bin/sh -c apt-get update && apt-get install…   64.5MB    
6d6b00c22231   6 days ago       /bin/sh -c #(nop)  CMD ["bash"]                 0B        
<missing>      6 days ago       /bin/sh -c #(nop) ADD file:6014cd9d7466825f8…   114MB  

この2つのイメージレイヤーはそれぞれDockerホストの異なるディレクトリにファイルの差分が保存されます。

  • 上の方のイメージレイヤー(IMAGE: 5b2abe87f6d2)は、牛が表示されるアプリケーションcowsayをインストールした後のレイヤー
  • 下の方のイメージレイヤ(IMAGE: 6d6b00c22231)は、ベースイメージdebian:busterをPULLした後のレイヤー

です。

f:id:moritamorie:20201218105911p:plain

それぞれのレイヤーの差分がDockerホスト上のどのディレクトリは保存されるかは、コンテナが出来た後docker container inspect というコマンド( 後述)を実行して確認できます。

Dockerホスト上のディレクトリを参照すると、各レイヤーはそれぞれ差分の情報しかもっておらず

  • 上の方のイメージレイヤー(IMAGE: 5b2abe87f6d2)には、アプリケーションcowsayがあるが、PULLしたベースイメージ debian:busterのファイルはない
  • 下の方のイメージレイヤ(IMAGE: 6d6b00c22231)は、ベースイメージdebian:busterのファイルはあるがcowsay はない

ということがわかります。

この例はシンプルなDockerfileなのでレイヤーが少ないですが、Dockerfileが大きくなるほど、レイヤーは多くなり、複数のレイヤーの差分を重ねることで1つのDockerイメージができます。

コンテナを触ってみる

bashで入る① (cowsayがあるイメージからコンテナを作る)

イメージからコンテナを作り、 bash で入ってみます。この際、docker historyの上の方のイメージ(5b2abe87f6d2: cowsayインストール後にできたイメージ)を指定してみます。

$ docker run -it 5b2abe87f6d2 /bin/bash

root@b2b28d1d223f:/# ls /usr/games/cowsay
/usr/games/cowsay

docker historyの上の方のイメージ(5b2abe87f6d2)を指定すると、下のイメージの差分も入っていることを確認できます。( bash コマンドは、ベースイメージdebian:busterの中にあります。)

コンテナに入る際、以下のように名前でイメージを指定しても結果は同じです。

$ docker run -it test/cowsay /bin/bash

bashで入る② (cowsayがないイメージからコンテナを作る)

次に、docker historyの下の方のイメージ(6d6b00c22231: cowsayインストール前のイメージ)を指定してみます。

$ docker run -it 6d6b00c22231 /bin/bash

root@4f9452a93ebd:/# ls /usr/games/cowsay
ls: cannot access '/usr/games/cowsay': No such file or directory

これは cowsay インストール前なので、 cowsay の実行ファイルがないことが確認できます

Dockerホストから cowsay を実行してみます

$ docker run 6d6b00c22231 /usr/games/cowsay "Moo"
 _____
< Moo >
 -----
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

cowsay インストール前のイメージを指定するとエラーが返ります。

$ docker run 6d6b00c22231 /usr/games/cowsay "Moo"
docker: Error response from daemon: OCI runtime create failed: container_linux.go:370: starting container process caused: exec: "/usr/games/cowsay": stat /usr/games/cowsay: no such file or directory: unknown.
ERRO[0000] error waiting for container: context canceled

Dockerホスト上のディレクトリの場所の確認方法

docker container inspect にコンテナ名を指定すると関連するファイルの場所を確認できます。

$ docker container ls -l
CONTAINER ID   IMAGE         COMMAND                  CREATED       STATUS                   PORTS     NAMES
c23fee232a48   test/cowsay   "/usr/games/cowsay M…"   8 hours ago   Exited (0) 8 hours ago             awesome_banach

ドキュメントを参照すると、イメージレイヤのファイルはLowerDirに保存されるようです。

$ $ docker container inspect awesome_banach -f "{{json .GraphDriver.Data}}" | jq .

{
  "LowerDir": "/var/lib/docker/overlay2/4e1a74ac2ac471b3484c196250a7e1be8f36eb8dc64ade4fcc4bdec1e4a95010-init/diff:/var/lib/docker/overlay2/d867fc751ea99450c01330ebdac487c1f12db47f35eeab6c628683a3f762310c/diff:/var/lib/docker/overlay2/6431aa52a32ab412806f379a4830b6cc059c3fb5f9380c8d5b8df298ae005e5b/diff",
  "MergedDir": "/var/lib/docker/overlay2/4e1a74ac2ac471b3484c196250a7e1be8f36eb8dc64ade4fcc4bdec1e4a95010/merged",
  "UpperDir": "/var/lib/docker/overlay2/4e1a74ac2ac471b3484c196250a7e1be8f36eb8dc64ade4fcc4bdec1e4a95010/diff",
  "WorkDir": "/var/lib/docker/overlay2/4e1a74ac2ac471b3484c196250a7e1be8f36eb8dc64ade4fcc4bdec1e4a95010/work"
}

参考資料

【トラブルシューティング】Docker (daemon) の手動起動、デバッグ・ログ出力に関して調べてみた

Dockerのトラブルシューティングがうまくできるようになりたいと思い

に関して調べてみました。

今回使用したdockerのバージョンは20.10.0です。2020/12/15時点での情報のため、dockerのコマンドや設定ファイルのパス等、今後変更になる可能性があります。

Dockerエンジンの基本的な構成

まず、Dockerエンジンは、以下の図のように主に3つのコンポーネントから構成されています。

  • Docker CLI (dockerコマンド): コンテナやイメージ、ネットワーク等の操作をHTTP経由のAPIを介して、Docker daemonと通信するクライアント。
  • Dockerエンジン API: CLIやプログラムからのHTTP経由で要求を受け付ける。
  • Docker daemon: コンテナの生成、実行、モニタリング等を行い、結果を返す。

f:id:moritamorie:20201216002943p:plain

今回調べたトラブルシューティング時に追えるようになる情報は、Docker daemonの部分です。

デーモンのログ出力を詳細を見れることでデーモンが起動していない場合でも発生したエラーの内容を見れるので、特に普段dockerコマンド(Docker CLI)しか使わないという場合、トラブル対応時に役に立つかと思います。

なお、Docker CLIdocker logs はコンテナのログを出力するものなので、デーモンのログとは別のものです。docker logs にはコンテナ作成時のエラー等は出力されません。

Docker daemonの起動(自動起動)

Dockerをインストールするとシステムユーティリティー(多くのLinuxのディストリの場合Systemd)がOS起動時に自動的にデーモンを起動しています。

試しにdocker.serviceのステータスを確認すると

$ sudo systemctl status docker

● docker.service - Docker Application Container Engine
   Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
   Active: active (running) since Tue 2020-12-15 16:29:17 JST; 6h ago
     Docs: https://docs.docker.com
 Main PID: 1740 (dockerd)
    Tasks: 80
   CGroup: /system.slice/docker.service
           ├─1740 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
           ├─2987 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 15432 -container-ip 172.22.0.3 -container-port 5432
           └─3059 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 1080 -container-ip 172.22.0.4 -container-port 80

1215 16:29:12 takashi dockerd[1740]: time="2020-12-15T16:29:12.851824229+09:00" level=warning msg="Your kernel does not support swap memory limit"

Main PID: 1740 (dockerd) という出力があり、デーモンが動いているのが確認できるかと思います。

Docker daemonの手動起動

もし、systemd自動起動 or systemd start docker で デーモンが起動できない場合などに、手動でdockerd コマンドを実行して起動することもできます。

基本的には1つのdockerホスト内で複数のdocker daemonを起動できないため、systemdから起動されたデーモンが起動中の場合、新しいデーモンの起動には失敗します。

$ sudo dockerd
INFO[2020-12-15T23:03:29.443621640+09:00] Starting up                                  
failed to start daemon: pid file found, ensure docker is not running or delete /var/run/docker.pid

一旦 systemddocker.service を停止すると起動でき、起動時のログがコンソールに出力されます。

sudo systemctl stop docker
takashi@tamo:/var/lib/docker$ sudo dockerd
INFO[2020-12-15T23:05:18.685050913+09:00] Starting up
INFO[2020-12-15T23:05:18.685967243+09:00] detected 127.0.0.53 nameserver, assuming systemd-resolved, so using resolv.conf: /run/systemd/resolve/resolv.conf 
︙

Docker daemonの設定

自動起動の場合

Linuxの場合 /etc/docker/daemon.json をデーモンの設定ファイルとして自動的に読み込みます。(Mac, Windowの場合はGUI上のPreferences / Daemon / Advancedから設定)

設定できるオプションは様々ありますが、トラブルシューティング時に

  • デバッグモードを有効にし、デーモンからより詳細な出力を得られるようにする(デフォルトは無効)
  • ログレベルをdebugにする(デフォルトはinfo)

という場合は、以下のように記載した設定ファイルを配置します。

{
  "debug": true,
  "log-level": "debug"
}

※設定ファイルの他のオプションに関しては、こちらに記載されています。

Linuxの場合、以下のコマンドでデーモンに対して HUP シグナルを送信し、設定を再読み込みさせることができます。

$ sudo kill -SIGHUP $(pidof dockerd)

手動起動の場合

docker daemon は、設定ファイルを自動的に読み込むようになっていますが、同時に手動起動の場合はオプションも併せて指定することができます。

$ dockerd --debug --log-level debug

設定でオプションを変更したらデーモンが起動しなくなった場合などは、手動起動でどのオプションを変更したら動かなくなるかを確認すると良さそうです。

自動的に読み込まれる設定ファイルとオプションが重複する場合は、以下のようなエラーが発生し、デーモン起動は失敗します。

unable to configure the Docker daemon with file /etc/docker/daemon.json:
the following directives are specified both as a flag and in the configuration
file: hosts: (from flag: [unix:///var/run/docker.sock], from file: [tcp://127.0.0.1:2376])

デーモンのログのパス/確認方法

ログファイルのパス/確認方法はOS毎に異なります。

他のOSのログファイルに関しては、詳細はドキュメント を参照してください。

スタックトレースの確認

デーモンの反応がない場合は、Linuxだと以下のコマンドでスタックトレースを強制的に出力できます。スタックトレースがログに出力されますが、デーモンは停止しません。

$ sudo kill -SIGUSR1 $(pidof dockerd)

Docker deamonの起動確認

いくつかの方法で起動を確認できます。Docker CLIのコマンドを実行して、反応がなかったり、エラーが発生した場合に起動しているか状態を確認すると良さそうです。

  • OSに依らない確認方法
    • Docker CLIを使う
      • docker info
  • OSに依る確認方法
    • Systemdを使う
      • sudo systemctl is-active docker
      • sudo systemctl status docker
      • sudo service docker status
    • プロセスの状態を確認する
      • ps
      • top

参考資料

【node-sass】 nodeバージョンを上げた時に発生したError: Can't find Python executable "python", you can set the PYTHON env variable.の直し方

最近触っていなかったReactアプリを久しぶりにheroku (Container Registry) 環境にデプロイしようとしたら、Dockerfileのreact-scripts buildを実行している箇所で、以下のエラーが発生するようになっていた。

remote: npm ERR! gyp verb check python checking for Python executable "python" in the PATH
remote: npm ERR! gyp verb `which` failed Error: not found: python
remote: npm ERR! gyp verb `which` failed     at getNotFoundError (/node_modules/which/which.js:13:12)
remote: npm ERR! gyp verb `which` failed     at F (/node_modules/which/which.js:68:19)
remote: npm ERR! gyp verb `which` failed     at E (/node_modules/which/which.js:80:29)
remote: npm ERR! gyp verb `which` failed     at /node_modules/which/which.js:89:16
remote: npm ERR! gyp verb `which` failed     at /node_modules/isexe/index.js:42:5
remote: npm ERR! gyp verb `which` failed     at /node_modules/isexe/mode.js:8:5
remote: npm ERR! gyp verb `which` failed     at FSReqCallback.oncomplete (node:fs:192:21)
remote: npm ERR! gyp verb `which` failed  python Error: not found: python
remote: npm ERR! gyp verb `which` failed     at getNotFoundError (/node_modules/which/which.js:13:12)
remote: npm ERR! gyp verb `which` failed     at F (/node_modules/which/which.js:68:19)
remote: npm ERR! gyp verb `which` failed     at E (/node_modules/which/which.js:80:29)
remote: npm ERR! gyp verb `which` failed     at /node_modules/which/which.js:89:16
remote: npm ERR! gyp verb `which` failed     at /node_modules/isexe/index.js:42:5
remote: npm ERR! gyp verb `which` failed     at /node_modules/isexe/mode.js:8:5
remote: npm ERR! gyp verb `which` failed     at FSReqCallback.oncomplete (node:fs:192:21) {
remote: npm ERR! gyp verb `which` failed   code: 'ENOENT'
remote: npm ERR! gyp verb `which` failed }
remote: npm ERR! gyp ERR! configure error 
remote: npm ERR! gyp ERR! stack Error: Can't find Python executable "python", you can set the PYTHON env variable.
remote: npm ERR! gyp ERR! stack     at PythonFinder.failNoPython (/node_modules/node-gyp/lib/configure.js:484:19)
remote: npm ERR! gyp ERR! stack     at PythonFinder.<anonymous> (/node_modules/node-gyp/lib/configure.js:406:16)
remote: npm ERR! gyp ERR! stack     at F (/node_modules/which/which.js:68:16)
remote: npm ERR! gyp ERR! stack     at E (/node_modules/which/which.js:80:29)
remote: npm ERR! gyp ERR! stack     at /node_modules/which/which.js:89:16
remote: npm ERR! gyp ERR! stack     at /node_modules/isexe/index.js:42:5
remote: npm ERR! gyp ERR! stack     at /node_modules/isexe/mode.js:8:5
remote: npm ERR! gyp ERR! stack     at FSReqCallback.oncomplete (node:fs:192:21)
remote: npm ERR! gyp ERR! System Linux 4.14.177-139.254.amzn2.x86_64
remote: npm ERR! gyp ERR! command "/usr/local/bin/node" "/node_modules/node-gyp/bin/node-gyp.js" "rebuild" "--verbose" "--libsass_ext=" "--libsass_cflags=" "--libsass_ldflags=" "--libsass_library="
remote: npm ERR! gyp ERR! cwd /node_modules/node-sass
remote: npm ERR! gyp ERR! node -v v15.3.0
remote: npm ERR! gyp ERR! node-gyp -v v3.8.0
remote: npm ERR! gyp ERR! not ok 
remote: npm ERR! Build failed with error code: 1
remote: 
remote: npm ERR! A complete log of this run can be found in:
remote: npm ERR!     /root/.npm/_logs/2020-12-03T17_30_50_380Z-debug.log

原因を色々調べてみるとどうやら Dockerfile に指定していた node:alpine イメージのnodeバージョン v15.3.0 に変わったことで、node-sass のバージョンとの不整合が起こるようになったことが原因だった。

FROM node:alpine AS node_builder

node-sassのgithubリポジトリを見るとたしかに node-sass 4.14.1は、nodeバージョン v14までしか対応していないと書いてある。

f:id:moritamorie:20201204030204p:plain

node-sass 5.0.0 が出たばかりということもあって、 node-sass の方のバージョンを上げるのも怖いので、node のバージョンを14まで下げた。

FROM node:14.15.1-alpine AS node_builder

このようにDockerfileを変えたら、heroku環境デプロイ時の react-scripts build 実行時にエラーが発生せず、デプロイできるようになった。

参考