プログラマのための IT 教科書

Linux サーバーの歩き方

この部の 4 / 12 章 ・ 全体で 25 / 76 章 ・ 読了目安 50 分

この章を読むとできるようになること
  • サーバーやコンテナに入って、設定とログの場所を自分で見つけられる
  • sudo を「とりあえず付ける」のではなく、原因を切り分けられる
  • ディスク不足とサービス起動失敗を自力で調べられる

手元は Mac、本番は Linux です。

普段は気にせず開発できます。問題が起きるまでは。

「デプロイしたのにサービスが起動しない」
「ログはどこにあるんですか」
「ディスクが 100% です、と監視から通知が来た」
「Permission denied と出る。sudo を付けたら動いたが、いいのか?」
「コンテナの中に日本語が化けて出る」

こうなった時、サーバーやコンテナに入って自分で調べられるかで、 解決までの時間が10倍変わります。

この章は、その最低限をまとめたものです。 サーバーを構築する話ではありません。入って、見て、切り分けるための知識です。

この章の位置づけ

ここで扱う内容は、Linux の入門資格である LPIC / LinuC の出題範囲とかなり重なります。 資格を取る必要はありませんが、あの範囲は「現場で実際に使うもの」の目録として優秀です。

もっと深く学びたくなったら、LPIC の出題範囲を眺めて、 知らない項目を潰していくのが効率的です。

どこに何があるか — FHS

Linux のディレクトリ構成には決まりがあります。FHS(Filesystem Hierarchy Standard)といいます。

これを知らないと、「設定ファイルを探す」だけで時間が溶けます。

場所何が入っているか新人が見る頻度
/etc設定ファイル。/etc/hosts /etc/nginx/ など高
/var/logログ。/var/log/syslog /var/log/nginx/高
/var/libサービスが持つデータ。/var/lib/postgresql/ など中
/tmp一時ファイル。残ることを期待してはいけない(いつ消えるかは設定次第)中
/home/<user>利用者のホーム。設定は ~/.ssh/ ~/.config/ へ高
/usr/bin /usr/local/binコマンドの実体。which が指す先中
/opt後から入れた大きなソフト一式低
/proc /sysファイルの形をした、カーネルの情報。実体はディスクに無い中
cat /proc/cpuinfo      # CPU の情報
cat /proc/meminfo      # メモリの情報
cat /etc/os-release    # このマシンは何の OS か
まず `/etc/os-release` を見る

入ったサーバーやコンテナが Debian 系なのか Red Hat 系なのか Alpine なのかで、 使えるパッケージ管理コマンドが変わります。

cat /etc/os-release
# NAME="Ubuntu"        → apt
# NAME="Alpine Linux"  → apk
# NAME="Amazon Linux"  → dnf / yum

「apt が無いと言われた」の原因はたいていこれです。

ユーザーとグループ

Linux はもともと1台を複数人で使う前提で作られています。 だから、すべてのファイルとプロセスに「誰のものか」が付いています。

whoami        # 今の自分
id            # 自分の UID / GID / 所属グループ
# uid=1000(intern) gid=1000(intern) groups=1000(intern),27(sudo),999(docker)

root(UID 0)は特別で、ファイルの所有者や権限による制限のほとんどを無視できます。 何でもできるということは、打ち間違いも何でも通るということです。

(厳密には root も万能ではありません。読み取り専用でマウントされた領域、 SELinux や AppArmor の制御、コンテナに与えられた権限の範囲には縛られます。)

所有者と権限を変える

コマンドラインで生きるで権限(755 など)を扱いました。もう2つあります。

chown app:app /var/www/html      # 所有者とグループを変える
chown -R app:app /var/www        # 配下すべて(-R は影響範囲に注意)
chmod g+w shared.txt             # グループに書き込みを足す(数値でなくてもよい)
`umask` — 新しく作ったファイルの権限が思ったのと違う

ファイルを作った時の初期権限は umask で決まります。

umask         # 0022 が一般的
# 644 のファイル、755 のディレクトリができる

「アプリが作ったファイルを別のプロセスが読めない」時は、 権限そのものではなく umask が原因のことがあります。

sudo — 管理者として実行する

なぜ root で作業しないのか

root でログインすれば、権限で困ることはありません。 それでも実務では常に一般ユーザーで作業し、必要な時だけ sudo を付けます。

理由は3つあります。

理由中身
事故が致命傷になるrm -rf /var/log を root で打つと本当に消える。一般ユーザーなら「権限がありません」で止まる
誰がやったか分からなくなる全員が root で入ると、ログはすべて root の操作になる。sudo は誰がどのコマンドを実行したかを記録する(ただし sudo -i で root シェルに入った後の操作は個別に残らない)
乗っ取られた時の被害範囲一般ユーザーの権限で侵入されるのと、root で侵入されるのとでは、被害がまるで違う

3番目は最小権限——「必要な権限を、必要な期間だけ与える」という原則です。 侵入された時の被害範囲がそのまま変わります。 詳しくはセキュリティを設計に組み込むで扱います。

使い方

sudo systemctl restart nginx    # このコマンドだけ管理者として実行する
sudo -u postgres psql           # 別のユーザーとして実行する
sudo -i                         # root のシェルに入る(最後の手段)
sudo -l                         # 自分に何が許可されているか確認する

誰が sudo を使えるかは /etc/sudoers で決まります(編集は必ず visudo で行う。 構文を壊すと誰も sudo できなくなり、復旧が非常に面倒です)。

`sudo` を付けたら動いた、で終わらせない

権限エラーが出た時、sudo を付けると多くの場合は通ります。 しかしそれが正しいとは限りません。

□ そのファイルは本当に管理者しか触れないべきものか?
□ アプリの実行ユーザーが間違っていないか?
□ 誰かが root で作ったファイルが残っているだけではないか?

よくあるのが最後です。一度 root で動かしたせいで root 所有のファイルができ、 以後アプリが書き込めなくなる——これを sudo で押し切ると、原因が固定化します。

ls -l で所有者を見て、chown で直すのが本来の対処です。

`sudo` を付ける前に、コマンドを読む

ネットで見つけたコマンドを sudo 付きでそのまま貼るのは、 知らない人に管理者パスワードを渡すのと同じです。

curl https://example.com/install.sh | sudo bash   # 何が起きるか誰も知らない

最低限、curl で落として中身を読んでから実行してください。 業務のサーバーでは、そもそもチームの許可を取ってからです。

Docker と sudo

docker グループに入っているユーザーは、実質 root と同じことができます。 コンテナでホストのディレクトリをマウントできるからです。

docker run -v /:/host -it alpine sh   # ホストのファイルが全部見える

「docker グループに入れる」は「root を渡す」に近い、と理解してください。

パッケージ管理

Dockerfile を書けば必ず出てきます。

系統代表更新インストール
Debian / Ubuntuaptapt-get updateapt-get install -y curl
Red Hat / Amazon Linuxdnf(旧 yum)不要dnf install -y curl
Alpineapkapk updateapk add --no-cache curl
Dockerfile ではこう書く
# 悪い例: update と install が別レイヤーになり、古いキャッシュを掴む
RUN apt-get update
RUN apt-get install -y curl
 
# 良い例: 1つの RUN にまとめ、キャッシュも消す
RUN apt-get update \
 && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*
  • && でつなぐ — 別レイヤーだと update の結果が古いまま固定される
  • --no-install-recommends — 使わない依存が大量に入るのを防ぐ
  • rm -rf /var/lib/apt/lists/* — 消さないとイメージに数十 MB のゴミが残る

この3点はDocker を使いこなすのレイヤーの話と直結しています。

ディスクが足りない

実務で最も多いサーバー障害の1つです。しかも、たいてい深夜に起きます。

df -h                    # ディスク全体の使用率
du -sh /var/log/*        # どのディレクトリが太っているか
du -sh * | sort -h       # 今いる場所で大きい順に

df で全体を見て、du で犯人を探す、という順番です。

消したのに空きが増えない

2つの原因があります。

1. プロセスがまだファイルを掴んでいる

lsof | grep deleted     # 削除済みだが開かれたままのファイル

rm してもプロセスが開いている間は解放されません。 そのプロセスを再起動するまで空きは戻りません。ログファイルでよく起きます。

2. inode が枯渇している

df -i                   # inode の使用率

容量は空いているのに「No space left on device」と言われる時はこれです。 小さいファイルを大量に作った(セッションファイル、キャッシュ)のが原因です。

ログは放っておくと必ず溢れる

だから logrotate があります。設定は /etc/logrotate.d/ です。

コンテナの場合はログをファイルに書かず標準出力に出すのが作法で、 回収と保存はプラットフォーム側の仕事になります (監視とオンコール)。

サービスとログ — systemd

Ubuntu・Debian・RHEL 系のサーバーでは、常駐プログラムはほぼ systemd が管理しています。

ただしどこでも systemd とは限りません。Alpine は OpenRC を使い、 コンテナの中には systemd がありません(アプリのプロセスが直接 PID 1 になります)。 systemctl が無いと言われたら、まず /etc/os-release と ps 1 を見てください。

systemctl status nginx      # 動いているか、直近のログは何か  ← まずこれ
systemctl start nginx
systemctl stop nginx
systemctl restart nginx
systemctl enable nginx      # 次回の起動時から自動で立ち上げる
systemctl list-units --failed   # 落ちているものを一覧する

enable と start は別物です。start は今動かすだけなので、 再起動すると消えます。「サーバーを再起動したら上がってこなかった」の原因の定番です。

ログを読む

journalctl -u nginx           # そのサービスのログ
journalctl -u nginx -f        # 追い続ける(tail -f と同じ感覚)
journalctl -u nginx --since '10 min ago'
journalctl -p err -b          # 今回の起動以降のエラーだけ

/var/log にファイルとして出ているものは、tail -f と grep で読みます。

tail -f /var/log/nginx/error.log
grep -i 'out of memory' /var/log/syslog
起動しない時に見る順番
1. systemctl status <service>     何と言って落ちたか
2. journalctl -u <service> -n 50  直前50行
3. 設定ファイルの構文チェック      nginx -t など、たいてい専用コマンドがある
4. 権限とパス                     実行ユーザーがそのファイルを読めるか

1と2を見ずに設定をいじり始めるのが、最も遠回りです。

定期実行 — cron

決まった時刻に動かす仕組みです。

crontab -l          # 自分の設定を見る
crontab -e          # 編集する
# 分 時 日 月 曜日  コマンド
0 3 * * *      /usr/local/bin/backup.sh        # 毎日 3:00
*/5 * * * *    /usr/local/bin/health-check.sh  # 5分おき
0 0 1 * *      /usr/local/bin/monthly.sh       # 毎月1日
cron は「手元では動くのに」の宝庫

cron から実行されるコマンドは、あなたのシェルとは別の環境で動きます。

□ PATH が最小限(/usr/bin:/bin だけのことが多い)→ コマンドが見つからない
□ .zshrc は読まれない → 環境変数が無い
□ カレントディレクトリが違う → 相対パスが壊れる
□ 標準出力は所有者へメールされる実装が多いが、メールの設定が無ければ誰も読めない

対策は、絶対パスで書く・必要な環境変数はスクリプト内で設定する・ ログにリダイレクトするの3点です。

0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

なお、アプリケーションとしての定期実行は バッチとジョブ、Kubernetes なら CronJob を使います。

時刻とロケール

時刻がずれると何が起きるか

□ ログの時刻が食い違い、障害の順序が分からなくなる
□ TLS 証明書が「まだ有効になっていない」と判定される
□ トークンの有効期限の判定がおかしくなる

だからサーバーの時刻は NTP で自動的に合わせます。

timedatectl               # 現在時刻・タイムゾーン・同期状態
date -u                   # UTC で表示する

サーバーのタイムゾーンは UTC が基本です。 表示するときだけ日本時間に直します(見つけにくいバグで扱った通り)。

文字化けの正体

locale                    # 今の設定
# LANG=C.UTF-8 なら概ね安全

コンテナの中は LANG が設定されておらず、C(= ASCII 前提) になっていることがあります。 その状態で日本語を出力すると化けたり、Python が UnicodeEncodeError を投げたりします。

ENV LANG=C.UTF-8

詳しくは文字コードと改行コードを参照してください。

アーカイブと圧縮

ログを持ち帰る、バックアップを取る、といった場面で使います。

tar czf logs.tar.gz /var/log/myapp/    # c=作る z=gzip f=ファイル名
tar xzf logs.tar.gz                    # x=展開
tar tzf logs.tar.gz                    # t=中身を見るだけ  ← 展開前に必ず
展開する前に中身を見る

tar xzf は、アーカイブに書かれたパスに沿って、今いる場所の下に展開します (先頭の / は通常取り除かれます)。 問題は、同名のファイルを確認なしに上書きすることです。

tar tzf logs.tar.gz          # まず一覧を見る
mkdir tmp && cd tmp          # 出所の怪しいものは空の場所で開く

この2つを習慣にしてください。

SSH — サーバーに入る

ssh intern@203.0.113.10
ssh -i ~/.ssh/id_ed25519 intern@example.com

鍵を作る

ssh-keygen -t ed25519 -C "you@example.com"
# ~/.ssh/id_ed25519      秘密鍵  ← 絶対に他人に渡さない
# ~/.ssh/id_ed25519.pub  公開鍵  ← これをサーバーに登録する

パスワードではなく鍵で入るのが基本です。理由は暗号と鍵の扱いにあります。

秘密鍵は「自分以外が読めない」権限にする
chmod 600 ~/.ssh/id_ed25519    # 400 でもよい。要は他人に読ませないこと

Permissions 0644 for 'id_ed25519' are too open と出たらこれです。 他人に読める場所に秘密鍵を置くなという、SSH 側の親切な拒否です。

~/.ssh/config を書く

毎回長いコマンドを打たないために、接続情報をファイルに書けます。

Host staging
  HostName 203.0.113.10
  User intern
  IdentityFile ~/.ssh/id_ed25519
  Port 22

Host db-via-bastion
  HostName 10.0.1.20
  User intern
  ProxyJump staging          # 踏み台を経由する

これで ssh staging だけで繋がります。 踏み台(bastion)経由でしか入れない構成は実務で一般的なので、ProxyJump は覚えておくと効きます。

共有ライブラリ

「バイナリはあるのに動かない」時の話です。

ldd /usr/local/bin/myapp    # このプログラムが必要としているライブラリ
# libssl.so.3 => not found   ← これが原因
Alpine でビルドしたバイナリが動かない

Alpine は musl libc、Debian や Ubuntu は glibc を使っています。 別物なので、片方でビルドしたバイナリがもう片方で動かないことがあります。

「ローカル(Debian ベース)では動くのに、Alpine のイメージだと
 no such file or directory と言われる」

ファイルは存在しています。足りないのはライブラリのほうです。 Go なら静的リンク(CGO_ENABLED=0)にするのが定番の回避策です。

実務の落とし穴まとめ

症状疑うところ
apt が無いAlpine(apk)か Red Hat 系(dnf)。まず /etc/os-release
再起動したらサービスが上がらないsystemctl enable を忘れている
消したのにディスクが空かないプロセスが掴んでいる(lsof の deleted)か inode 枯渇(df -i)
cron だけ失敗するPATH と環境変数。ログにリダイレクトしていない
日本語が化けるLANG が C のまま
SSH の鍵が弾かれる秘密鍵が 600 になっていない
not found なのにファイルはある共有ライブラリ(ldd)。musl と glibc の違い
sudo を付けたら動いた所有者が root になっている。chown で直すのが本筋

まとめ

  • FHS を知っていれば、設定(/etc)とログ(/var/log)はすぐ見つかる
  • root で常用しない。 sudo は事故を減らし、誰が何をしたかを記録する
  • sudo で押し切らない。 所有者が間違っているだけのことが多い
  • サービスは systemctl status → journalctl -u の順で見る。設定をいじるのはその後
  • ディスクは df で全体、du で犯人。消したのに空かないなら lsof と df -i
  • cron はあなたのシェルとは別の環境で動く。絶対パスとログ出力
  • サーバーの時刻は UTC、文字コードは UTF-8 を明示する
  • 秘密鍵は 600。~/.ssh/config を書くと世界が変わる

公式ドキュメント

迷ったら一次情報に戻ってください。

対象リンク
Filesystem Hierarchy Standardhttps://refspecs.linuxfoundation.org/fhs.shtml
systemd マニュアルhttps://www.freedesktop.org/software/systemd/man/
Ubuntu Server ドキュメントhttps://documentation.ubuntu.com/server/
OpenSSH マニュアルhttps://www.openssh.com/manual.html
Linux Professional Institute(出題範囲)https://lpi.or.jp/

章末問題

デプロイ後、アプリが書き込みエラーで起動しません。sudo を付けて実行すると起動します。どうするのが適切でしょうか?

サーバーの監視から「ディスク使用率 100%」の通知が来ました。大きなログファイルを rm したのに、df -h の数値が変わりません。次に確認すべきことは何でしょうか?

次は、同じ「動かない・遅い」でも、 書いたコードそのものが遅い場合の話です。

読み終わったら記録しておくと、目次で進み具合が分かります。