🍎

AWS EC2でカスタムAMIから作成したインスタンスにSSHでログインできないことがたまにあって調べた

2022/01/20に公開

経緯

今年3月に本番環境として稼働開始するサーバーをTerraformでAWS上構築した。今回はコンテナは使わずサーバーはEC2、OSは Ubuntu 20.04 LTS を使用。

ほぼ問題なく終わったのだけど、なぜかたまにSSHでログインできないホストがある。「たまに」と書いたのは、必ずそうなるわけではなく、たまにSSHログインできなくなるのだ。一度そうなると最後、そのインスタンスはsshでログインできない。でもインスタンスを終了して同じAMIから作り直すとログインできたりする。

そしてsshログインできない場合も、インスタンス自体は起動しているらしく、EC2のコンソールから EC2シリアルコンソール を使って接続、rootでログインすることはできた。OSは起動しているがsshdが起動できていないようだ。

応急処置

なぜsshdが起動していないのか調査するため、問題のインスタンス上で # /usr/sbin/sshd -t を実行すると、SSHのキーがないと言っている。

とりあえず応急処置のために

# ssh-keygen -A

してキーを生成したのちに $ sudo systemctl start ssh したら無事sshdが起動できた。

原因調査

一旦シリアルコンソールを閉じて ssh でログイン。
そもそも何故、キーが生成されなかったのかと思い、 $ sudo less /valog/syslog したら、以下のようなエラーが出た。sudoが正常に動作しないようだ。

>>> /etc/sudoers.d/README: syntax error near line 1 <<<
parse error in /etc/sudoers.d/README near line 1
no valid sudoers sources found, quitting
unable to initialize policy plugin

どうも sudoresファイルの一つが構文が不正だよって言ってる。でもそんなところ編集してないんだけどなと思い、 /etc/sudoers.d/90-cloud-init-users を開いてみると。

# Created by cloud-init v. 20.2-45-g5f7825e2-0ubuntu1~18.04.1 on Mon, 24 Aug 2020 23:53:25 +0000

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

# User rules for ubuntu
ubuntu ALL=(ALL) NOPASSWD:ALL

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

なんだか同じ設定が繰り返し追加されている。しかも、ファイルの末尾にバイナリと思われる、おかしな書き込みがされているようだ。
これで構文エラーとなりsudoが使えず、SSHキーを生成できなかったらしい。

とりあえず、よくわからん文字化けしてる行を削除し、重複している設定も削除したら正常に sudo できるようになった。

原因

上記の設定追加は、新たにAMIを作って、そのAMIからインスタンスを作るたびに一つづつ増えていっていたようだ。こんな感じ

  1. 開発環境のホストからカスタムAMIを作る。
  2. そのAMIをもとに、開発環境自体にデプロイする(ステージングやプロダクションにもデプロイする)
  3. この時、 /etc/sudoers.d/90-cloud-init-usersubuntu ALL=(ALL) NOPASSWD:ALL が追記される。
  4. 以下、1〜3を繰り返すごとに3の設定が追記されていく。
  5. そしてある時、なにかの上限を叩くのか、おかしなバイナリっぽい文字列がファイルに書き出されてsudoが実行できなくなる。

開発環境から生成したAMIを元に開発環境自体にインスタンスを作るのは意味ないけど、terraformでリソース管理しており、構成変更を行うときはtfファイルを編集してデプロイしていた。
この時、AMIが更新されていると、これを元にインスタンスが新たに生成されており、この時に設定が追記されていたようだ。

対策

上記の /etc/sudoers.d/90-cloud-init-userscloud-init によって設定が追記されているらしい。
色々調べて、その設定は /etc/cloud/cloud.cfg にあることがわかった。以下のようになってる(抜粋)。

(....)
# System and/or distro specific settings
# (not accessible to handlers/transforms)
system_info:
   # This will affect which distro class gets used
   distro: ubuntu
   # Default user name + that default users groups (if added/used)
   default_user:
     name: ubuntu
     lock_passwd: True
     gecos: Ubuntu
     groups: [adm, audio, cdrom, dialout, dip, floppy, lxd, netdev, plugdev, sudo, video]
     sudo: ["ALL=(ALL) NOPASSWD:ALL"]
     shell: /bin/bash

上記のうち

     sudo: ["ALL=(ALL) NOPASSWD:ALL"]

の行が該当の処理を行なっているので

     #sudo: ["ALL=(ALL) NOPASSWD:ALL"]

のようにコメントアウトする。

一応これで、 ホストAからカスタムAMIを作成 → 作成したAMIをもとにホストAのインスタンスを更新
としても、設定が追記されていくことはなくなった。

ただ、そもそも自分自身から作ったAMIで自分自身を更新していたことが真の原因かもしれない。
TerraformでEC2以外のリソースとEC2を分けて実行できるようにしたほうが良さそう。

謎は全て解けた

だが、そもそも何故 /etc/sudores.d/90-cloud-init-users におかしな文字化けしたデータが書き込まれたりしたのだろう。

そう思って terraform のコードを調べてると、EC2インスタンスに異常が発生した場合の自動リカバリーのために、こんな設定を書いてた。

resource "aws_cloudwatch_metric_alarm" "autorecovery_host_1" {
  dimensions                = { InstanceId = aws_instance.host_1.id }
  depends_on                = [aws_instance.host_1]
  alarm_name                = "autorecovery_host_1"
  comparison_operator       = "GreaterThanThreshold"
  namespace                 = "AWS/EC2"
  statistic                 = "Minimum"
  alarm_actions             = ["arn:aws:automate:${var.region}:ec2:recover"]
  insufficient_data_actions = ["arn:aws:automate:${var.region}:ec2:recover"]
  metric_name               = "StatusCheckFailed_System"
  threshold                 = 0
  period                    = 60
  evaluation_periods        = 2
}

この設定でCloudWatchAlarmを作ると、EC2インスタンスのシステムチェックを1分に一回行い、2回失敗すると自動的にリカバリ(再起動ではなく、一度停止して起動。なので別ハードウェアで起動となる)
が、この中のこれ

 insufficient_data_actions = ["arn:aws:automate:${var.region}:ec2:recover"]

ここでああ、そういうことかとわかった。 insufficient_data_actions はメトリクスデータが無い場合の動作を設定する。
この設定でいくと、異常発生時だけでなく、その判定をするためのデータが無い場合にリカバリ(インスタンスを停止し、起動し直す)をする。
インスタンスの初回起動時には当然、ある程度時間が経つまでメトリクスデータはない。一方でCloudWatchの方はインスタンスが生成されると同時にメトリクスの収集を開始する。
結果としてデータが無いと判定され、初回起動時の cloud-init が走ってる途中にも関わらず再起動がかかる。
で、再起動のかかったタイミングが /etc/sudores.d/90-cloud-init-users に書き込み中だった場合は文字化けデータが書き込まれて強制終了となるわけだ。
当時は理解が足りてなくて、メトリクスが無いのは異常なので再起動すべきと思ってた。初回起動時がここにハマるとは...

というわけで

 insufficient_data_actions = []

に変更し、ついでにアラーム検知も2分から10分(1分おきに10回)に変更。

  period                    = 60
  evaluation_periods        = 10

以上でカスタムAMIから生成したインスタンスにSSHログインできないことは無くなった。

Discussion