長生村本郷Engineers'Blog

千葉県長生村本郷育ちのエンジニアが書いているブログ

Rails (gem 'sendgrid-ruby') × SendGrid の Event Notification で API Key ごとの独自メタ情報を設定する

f:id:kenzo0107:20190313234822p:plain

SendGrid の Event Notification の使い所

SendGrid には Event Notification という Webhook を設定することでメールの送信状態をイベント情報として取得することができます。

メールを SendGrid が受信した、送信先に届いた、等の情報です。

sendgrid.kke.co.jp

例えば、未達だったメールの情報を取得したい場合等に、この Webhook を利用し、イベント情報を保存することで調査や集計が可能です。

AWS API Gateway + Lambda で構築したエンドポイントに投げ、S3 に保存し、送信失敗件数を Athena で検索集計する、ということができます。

何か問題でも?

SendGrid は 1アカウントで複数のプロジェクト毎の API Key を発行することができます。

ですが、 イベント情報にはどの API Key を利用してメール送信したか、の記録がありません。

複数の API Key がある場合に、どのプロジェクトのどの環境で送信したのか、調査や集計ができません。

これを解決する手段として、メール送信時にメタ情報を登録する方法があります。

Rails 5.2 で試してみました。

まずはセットアップ

  • Gemfile
gem 'sendgrid-ruby'
  • config/initializers/sendgrid.rb
sendgrid_api_key = Rails.application.credentials.dig(Rails.env.to_sym, :sendgrid_api_key)
ActionMailer::Base.add_delivery_method :sendgrid, Mail::SendGrid, api_key: sendgrid_api_key

api_key は credentials に設定し、そこから取得。*1

  • lib/mail/send_grid.rb
# frozen_string_literal: true

class Mail::SendGrid
  def initialize(settings)
    @settings = settings
  end

  def deliver!(mail)
    sg_mail = SendGrid::Mail.new
    sg_mail.from = SendGrid::Email.new(email: mail.from.first)
    sg_mail.subject = mail.subject
    personalization = SendGrid::Personalization.new
    personalization.add_to(SendGrid::Email.new(email: mail.to.first))
    personalization.subject = mail.subject
    sg_mail.add_personalization(personalization)
    sg_mail.add_content(SendGrid::Content.new(type: 'text/plain', value: mail.body.raw_source))
    sg_mail.add_content(SendGrid::Content.new(type: 'text/html', value: mail.body.raw_source))

    // ここでカテゴリー情報として登録
    sg_mail.add_category(SendGrid::Category.new(name: "#{Rails.env}-#{Settings.project_name}"))

    sg = SendGrid::API.new(api_key: @settings[:api_key])
    response = sg.client.mail._('send').post(request_body: sg_mail.to_json)
    Rails.logger.info response.status_code
  end
end

#{Rails.env}-#{Settings.project_name} の部分は適宜変更してください。

メール送信してみる

rails c して

ActionMailer::Base.mail(to: "nakayama.kinnikunn@hogehoge.jp", from: "info@<sender authentication で認証したドメイン>", subject: "メールタイトル", body: "すいません、テスト送信です").deliver_now

すると、Event Notification では以下の様なイベント情報が取得できます。

{"email":"nakayama.kinnikunn@hogehoge.jp","timestamp":1551964210,"ip":"12.345.67.89","sg_event_id":"xxxxxxxxxxxxxxxx","sg_message_id":"xxxxxxxxxxxxxxxxxxxx.yyyyyyyyyyyyyyyyyyyyyy","category":["staging-kenkoboys"],"useragent":"Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0 (via ggpht.com GoogleImageProxy)","event":"open"}

注目すべきは category":["staging-kenkoboys"] です。

add_category したカテゴリ情報が取得でき、これで どの API Key のイベント情報であるかの紐付けができます。

以上 参考になれば幸いです。

現場で使える Ruby on Rails 5速習実践ガイド

現場で使える Ruby on Rails 5速習実践ガイド

*1:RAILS_ENV=development でお試し可なので直に設定でも可。そこは自己責任で

Rails に reCAPTCHA v3 導入して bot 対策

概要

Rails で構築した Webサービスbot 攻撃を定期的に受けた為、問い合わせフォームに reCAPTCHA v3 を導入しました。

何故 v2 でなく、reCAPTCHA v3 ?

v2 は I'm not a robot チェックボックスにチェックを入れた後に画像選択させる仕様があります。

例えば、看板が写ってるのはどれ?と選ばせる問いが出てきた場合、
「どこまでが看板としたらいいの?」と心理的負担も高く、ユーザが離脱する可能性もあります。

f:id:kenzo0107:20190216210737p:plain

v3 だと嬉しいことは何?

v3 *1 は設置したページのユーザ行動をスコア化し bot か判断します。

アクセスが増えるとより精度が高まってくる、という仕様です。

bot ユーザへの負担は全くなく、 bot を遮断できる様になるという、世の中進んでるなぁ感満載です。

gigazine.net

gem ある?

今回 gem は使用しませんでした。

というのも、 以下理由からでした。

  • gem 'recaptcha' が v3 非対応。
  • gem 'new_google_recaptcha' は v3 対応してますが、スコアが返ってこないのでテストし辛い。

その他に既にあるのかもわかりませんが、記事執筆時には探し出すことはできませんでした。

まず reCAPTCHA v3 発行

以下 reCAPTCHA コンソールにアクセスし発行してください。

https://g.co/recaptcha/v3

v3 を選択し、今回導入するドメインを登録します。*2

f:id:kenzo0107:20190216214351p:plain

発行されたサイトキー・シークレットキーを保存しておきます。

  • サイトキー

    • ユーザがサイトにアクセスした際にトークンを取得する際に必要なキーです。こちらはユーザ公開して問題ありません。
  • シークレットキー

    • トークンを元に Google に問い合わせする際に必要なキーです。こちらは秘密情報として扱います。

f:id:kenzo0107:20190216214558p:plain

Rails 側実装

Rails >= 5.2 を想定しています。

config/credentials.yml.enc

recaptcha:
  secret_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

シークレットを秘密情報に保存します。

app/controllers/application_controller.rb

require 'net/http'
require 'uri'

class ApplicationController < ActionController::Base
...
  RECAPTCHA_MINIMUM_SCORE = 0.5
  RECAPTCHA_ACTION = 'homepage'
...
  def verify_recaptcha?(token)
    secret_key = Rails.application.credentials.recaptcha[:secret_key]
    uri = URI.parse("https://www.google.com/recaptcha/api/siteverify?secret=#{secret_key}&response=#{token}")
    r = Net::HTTP.get_response(uri)
    j = JSON.parse(r.body)
    j['success'] && j['score'] > RECAPTCHA_MINIMUM_SCORE && j['action'] == RECAPTCHA_ACTION
  end
end

共通メソッドとして、recaptcha の認証メソッド verify_recaptcha? を設定しています。

ここで、bot となるスコアを 0.5 以下としています。

通常通り操作していれば、十分超える数値です。

config/locales/en.yml

en:
  recaptcha:
    errors:
      verification_failed: 'reCAPTCHA Authorization Failed. Please try again later.'

local en 設定です。

config/locales/ja.yml

ja:
  recaptcha:
    errors:
      verification_failed: 'reCAPTCHA 認証失敗しました。しばらくしてからもう一度お試しください。'

local ja 設定です。

app/controllers/hoges_controller.rb

class HogesController < ApplicationController
  def new; end

  def create
    unless verify_recaptcha?(params[:recaptcha_token])
      flash.now[:recaptcha_error] = I18n.t('recaptcha.errors.verification_failed')
      return render action: :new
    end

    # something to do

    redirect_to hoge_finish_path
  end

  def finish; end
end

new から create に post して reCAPTCHA で bot 判定して

  • OK → finish へ進む
  • NG → new に戻る

という設計です。

app/views/hoges/new.html.erb

<% if flash[:recaptcha_error] %>
<div class="text">
  <p><spacn class="error"><%= flash[:recaptcha_error] %></span></p>
</div>
<% end %>

<%= form_tag({action: :create}, {method: :post}) do %>
...
  <input id="recaptcha_token" name="recaptcha_token" type="hidden"/>
  <%= submit_tag "送信する", :class => "submit-recaptcha btn", :disabled => true %>
<% end %>

<script src="https://www.google.com/recaptcha/api.js?render=<%= Settings.recaptcha.site_key %>&ver=3.0"></script>
<script>
grecaptcha.ready(function() {
  grecaptcha.execute('<%= Settings.recaptcha.site_key %>', {action: 'homepage'}).then(function(token) {
    $('#recaptcha_token').val(token);
    $('.submit-recaptcha').prop('disabled', false);
  });
});
</script>
エラーメッセージ表示
<% if flash[:recaptcha_error] %>
<div class="text">
  <p><spacn class="error"><%= flash[:recaptcha_error] %></span></p>
</div>
<% end %>
<form> ~ </form> 内に以下 name=recaptcha_token input タグを追加します。
<input id="recaptcha_token" name="recaptcha_token" type="hidden"/>
ページアクセス時に reCAPTCHA の token を取得すべく、スクリプトを仕込みます。
<script src="https://www.google.com/recaptcha/api.js?render=<%= Settings.recaptcha.site_key %>&ver=3.0"></script>
<script>
grecaptcha.ready(function() {
  grecaptcha.execute('<%= Settings.recaptcha.site_key %>', {action: 'homepage'}).then(function(token) {
    $('#recaptcha_token').val(token);
    $('.submit-recaptcha').prop('disabled', false);
  });
});
</script>

reCAPTCHA トークン取得が成功した場合に以下実行します。

  • id="recaptcha_token" input タグの valueトークンを設定
  • submit ボタンの有効化

<%= Settings.recaptcha.site_key %> について
gem 'settingslogic' をインストールしている前提で設定しています。

導入していない場合は、簡易的に処理を試す程度であれば、 <%= Settings.recaptcha.site_key %> を取得したサイトキーに置き換えて下さい。*3

以上で設定は完了です。

ページにアクセスしてみる

ページ右下に reCAPTCHA マークが常に表示される様になります。

f:id:kenzo0107:20190216234742p:plain

集計情報を見る

reCAPTCHA コンソールを見ると、以下の様な表示が出ていてすぐには集計情報が反映されていないと思います。

f:id:kenzo0107:20190216235044p:plain

しばらく経つと以下の様なグラフが表示される様になります。

f:id:kenzo0107:20190216235204p:plain

注意

例えば、社内 IP 等固定された IP からテストで頻繁にアクセスすると、 bot 扱いされます。

reCAPTCHA 側で IP のホワイトリストはないので、その場合、 Rails 側で許可 IP リストを作る必要があります。

以上
参考になれば幸いです。

*1:2019年2月現在最新バージョン

*2:ドメインは複数登録可能です。ドメイン毎に集計や、 bot 対策の傾向を変えたい場合は、個々に発行します。 また、 RAILS_ENV = production とそれ以外で発行する方が本番への影響がないので推奨されます。

*3:前にもお伝えしましたが、サイトキーの管理は直指定でなく、何かしら管理が推奨です。

AWS ECS トラブルシューティング

f:id:kenzo0107:20190208225212j:plain

ECS を利用していて幾つかはまったポイントがあったのでまとめました。

started 1 task が複数回実行されるが、コンテナが起動しない

$ ecs-cli compose service up ...

level=info msg="(service hogehoge) has started 1 tasks ..."
level=info msg="(service hogehoge) has started 1 tasks ..."
level=info msg="(service hogehoge) has started 1 tasks ..."

ecs-cli compose service up でデプロイ時にタスク起動を実行するものの、起動が正しくできていない状態です。
こちらはコンテナ起動時の処理に問題がある場合があります。

  • コンテナログを確認して、コンテナ起動失敗時のログを確認してください。
  • 例えば、Nginx の設定ファイル, Rails のコードに typo がある等です。

already using a port required by your task

service hogehoge was unable to place a task because no container instance met all of its requirements. 
The closest matching container-instance a1b2c3d4-e5f6-g7h8-j9k0-l1m2n3o4p5q6 is already using a port required by your task

port mapping を以下の様に設定していた。

"portMappings": [
   {
     "hostPort": 0,
     "protocol": "tcp",
     "containerPort": 80
   }
 ],

新しいタスクでも 0:80 のポートを利用しようとする為、エラーとなります。 以下の様に設定することで回避できました。

"portMappings": [
   {
     "containerPort": 80
   }
 ],

insufficient memory available

INFO[0031] (service hogehoge) was unable to place a task because no container instance met all of its requirements. The closest matching (container-instance a1b2c3d4-e5f6-g7h8-j9k0-l1m2n3o4p5q6) has insufficient memory available. For more information, see the Troubleshooting section of the Amazon ECS Developer Guide.  timestamp=2018-03-09 15:45:24 +0000 UTC

タスク更新(ecs-cli compose service up)実行時、
上記の様なメモリ不足が出る場合はインスタンスタイプを上げるなどメモリーリソースを増やす対応が必要です。

Fargate - Port Mapping Error

level=error msg="Create task definition failed" error="ClientException: When networkMode=awsvpc, the host ports and container ports in port mappings must match.\n\tstatus code: 400, request id: a1b2c3d4-e5f6-g7h8-j9k0-l1m2n3o4p5q6"

起動タイプ Fargate で以下の様な設定だと、NG

    ports:
      - "80"

こちらだと OK。

    ports:
      - "80:80"

ホストポートとコンテナポートのマッピングが必要です。

Fargate volume_from は利用できない

volume_from は Fargate では使用できません。

level=error msg="Create task definition failed" error="ClientException: host.sourcePath should not be set for volumes in Fargate.\n\tstatus code: 400, request id: a1b2c3d4-e5f6-g7h8-j9k0-l1m2n3o4p5q6"

指定された IAM Role が適切なパーミッションを与えられていない

IAM Role に権限を適宜付与します。

level=info msg="(service hogehoge) failed to launch a task with (error ECS was unable to assume the role 'arn:aws:iam::123456789012:role/ecsTask
ExecutionRole' that was provided for this task. Please verify that the role being passed has the proper trust relationship and permissions and that your IAM user has permissions to pass this role.)." timestamp=2018-06-21 08:15:43 +0000 UTC

イメージ pull できないというエラーも権限を付与していないことに起因することが主です。

CannotPullContainerError: API error (500): Get https://123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/v2/: net/http: request canceled while waiting for connection"

現在稼働している ECS の IAM Role の権限を参考してください。変更される可能性があるのであくまで参考にし、適宜最新の情報を以ってご対応ください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "logs:PutLogEvents",
                "logs:CreateLogStream",
                "logs:CreateLogGroup",
                "elasticloadbalancing:RegisterTargets",
                "elasticloadbalancing:Describe*",
                "elasticloadbalancing:DeregisterTargets",
                "ecs:UpdateService",
                "ecs:Submit*",
                "ecs:StartTelemetrySession",
                "ecs:StartTask",
                "ecs:RunTask",
                "ecs:RegisterTaskDefinition",
                "ecs:RegisterContainerInstance",
                "ecs:Poll",
                "ecs:ListTasks",
                "ecs:DiscoverPollEndpoint",
                "ecs:DescribeTasks",
                "ecs:DescribeServices",
                "ecs:DescribeContainerInstances",
                "ecs:DeregisterContainerInstance",
                "ecs:CreateService",
                "ecr:UploadLayerPart",
                "ecr:PutImage",
                "ecr:InitiateLayerUpload",
                "ecr:GetDownloadUrlForLayer",
                "ecr:GetAuthorizationToken",
                "ecr:CompleteLayerUpload",
                "ecr:BatchGetImage",
                "ecr:BatchCheckLayerAvailability",
                "ec2:Describe*"
            ],
            "Resource": "*"
        }
    ]
}

以上です。

また何か発生したら追記していきたいと思います。

Reference

ECS EC2 上で起動する Datadog Agent コンテナが unhealthy になる時の処方箋

f:id:kenzo0107:20190110112818p:plain

概要

$ docker ps

CONTAINER ID        IMAGE                            COMMAND                  CREATED             STATUS                    PORTS                NAMES
8baa0e2cff47        datadog/docker-dd-agent:latest   "/entrypoint.sh supe…"   31 hours ago        Up 31 hours (unhealthy)   8125/udp, 8126/tcp   ecs-dd-agent-task-1-dd-agent-f6d3d5eb9febcab9c601

ある日、ECS で起動させている Datadog Agent コンテナが unhealthy になってしまう事象が発生しました。 その原因と対応法をまとめました。

結論

Datadog Agent イメージを現時点の最新バージョン 6 系にすることで解決できました。

Datadog サポートに問い合わせた所、 今回のケースでは Datadog Agent イメージのバージョンが 5 系だったことに起因していました。

datadog/docker-dd-agent:latest は 5系の最新だった!

バージョン5が最新だった時には設定手続きは以下に沿って実施していました。 https://docs.datadoghq.com/integrations/faq/agent-5-amazon-ecs/

上記手順にて登場する datadog agent の ECS での起動用タスクが以下になります。 ここで指定しているイメージ (datadog/docker-dd-agent:latest) が 5系でした。 https://docs.datadoghq.com/json/dd-agent-ecs.json

datadog/docker-dd-agent:latest は 5系の最新だった!

datadog/agent:latest が 2019.01.10 時点最新の 6系 !

現最新バージョン 6系を扱うには以下設定手続きを参照します。 https://docs.datadoghq.com/integrations/amazon_ecs

手続きで変更点はタスク定義の変更くらいです。 https://docs.datadoghq.com/json/datadog-agent-ecs.json

今の所、datadog/agent:latest が6系の最新になっています。 7系になった際には是非とも互換維持してほしいです。

おまけ

サポートへの問い合わせ

サポートに問い合わせると、 caseID という問い合わせの ID をいただけます。 その後、caseID を設定し、起動時のログファイル (tar.gz) を取得し、サポート宛に添付しました。

ECS の管理下にある EC2 に ssh ログインし以下実行します。

$ docker run --rm -v /tmp:/tmp -e API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx datadog/docker-dd-agent:latest /etc/init.d/datadog-agent flare <caseID>

2019-01-03 12:27:44,472 | ERROR | dd.collector | utils.dockerutil(dockerutil.py:148) | Failed to initialize the docker client. Docker-related features will fail. Will retry 0 time(s). Error: Error while fetching server API version: ('Connection aborted.', error(2, 'No such file or directory'))
...
2019-01-03 12:27:45,807 | INFO | dd.collector | utils.flare(flare.py:161) | Saving all files to /tmp/datadog-agent-2019-01-03-12-27-44.tar.bz2
/tmp/datadog-agent-2019-01-03-12-27-44.tar.bz2 is going to be uploaded to Datadog.
...

EC2 ホスト上に /tmp/datadog-agent-2019-01-03-12-27-44.tar.bz2 ファイルが取得できるので、それをサポート宛にメール添付しました。

上記でログも含めサポートに連絡した所、API バージョンにより接続中止されている、という指摘を受け、バージョン上げて!という話になりました。

2019-01-03 12:27:44,472 | ERROR | dd.collector | utils.dockerutil(dockerutil.py:148) | Failed to initialize the docker client. Docker-related features will fail. Will retry 0 time(s). Error: Error while fetching server API version: ('Connection aborted.', error(2, 'No such file or directory'))

サポートさんありがとう♪ f:id:kenzo0107:20190110112453p:plain

以上です。 参考になれば幸いです。

ECS EC2 上で起動する Datadog Agent コンテナが unhealthy になる時の処方箋

f:id:kenzo0107:20190110112818p:plain

概要

$ docker ps

CONTAINER ID        IMAGE                            COMMAND                  CREATED             STATUS                    PORTS                NAMES
8baa0e2cff47        datadog/docker-dd-agent:latest   "/entrypoint.sh supe…"   31 hours ago        Up 31 hours (unhealthy)   8125/udp, 8126/tcp   ecs-dd-agent-task-1-dd-agent-f6d3d5eb9febcab9c601

ある日、ECS で起動させている Datadog Agent コンテナが unhealthy になってしまう事象が発生しました。 その原因と対応法をまとめました。

結論

Datadog Agent イメージを現時点の最新バージョン 6 系にすることで解決できました。

Datadog サポートに問い合わせた所、 今回のケースでは Datadog Agent イメージのバージョンが 5 系だったことに起因していました。

datadog/docker-dd-agent:latest は 5系の最新だった!

バージョン5が最新だった時には設定手続きは以下に沿って実施していました。 https://docs.datadoghq.com/integrations/faq/agent-5-amazon-ecs/

上記手順にて登場する datadog agent の ECS での起動用タスクが以下になります。 ここで指定しているイメージ (datadog/docker-dd-agent:latest) が 5系でした。 https://docs.datadoghq.com/json/dd-agent-ecs.json

datadog/docker-dd-agent:latest は 5系の最新だった!

datadog/agent:latest が 2019.01.10 時点最新の 6系 !

現最新バージョン 6系を扱うには以下設定手続きを参照します。 https://docs.datadoghq.com/integrations/amazon_ecs

手続きで変更点はタスク定義の変更くらいです。 https://docs.datadoghq.com/json/datadog-agent-ecs.json

今の所、datadog/agent:latest が6系の最新になっています。 7系になった際には是非とも互換維持してほしいです。

おまけ

サポートへの問い合わせ

サポートに問い合わせると、 caseID という問い合わせの ID をいただけます。 その後、caseID を設定し、起動時のログファイル (tar.gz) を取得し、サポート宛に添付しました。

ECS の管理下にある EC2 に ssh ログインし以下実行します。

$ docker run --rm -v /tmp:/tmp -e API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx datadog/docker-dd-agent:latest /etc/init.d/datadog-agent flare <caseID>

2019-01-03 12:27:44,472 | ERROR | dd.collector | utils.dockerutil(dockerutil.py:148) | Failed to initialize the docker client. Docker-related features will fail. Will retry 0 time(s). Error: Error while fetching server API version: ('Connection aborted.', error(2, 'No such file or directory'))
...
2019-01-03 12:27:45,807 | INFO | dd.collector | utils.flare(flare.py:161) | Saving all files to /tmp/datadog-agent-2019-01-03-12-27-44.tar.bz2
/tmp/datadog-agent-2019-01-03-12-27-44.tar.bz2 is going to be uploaded to Datadog.
...

EC2 ホスト上に /tmp/datadog-agent-2019-01-03-12-27-44.tar.bz2 ファイルが取得できるので、それをサポート宛にメール添付しました。

上記でログも含めサポートに連絡した所、API バージョンにより接続中止されている、という指摘を受け、バージョン上げて!という話になりました。

2019-01-03 12:27:44,472 | ERROR | dd.collector | utils.dockerutil(dockerutil.py:148) | Failed to initialize the docker client. Docker-related features will fail. Will retry 0 time(s). Error: Error while fetching server API version: ('Connection aborted.', error(2, 'No such file or directory'))

サポートさんありがとう♪ f:id:kenzo0107:20190110112453p:plain

以上です。 参考になれば幸いです。

boto3 の AssumeRole をしたアカウントスイッチ credentials 利用時の MFA 突破対応

f:id:kenzo0107:20181206121637p:plain

概要

備忘録です。

AssumeRole でのアカウントスイッチで credentials 情報を持っている場合に対応した boto3.Session での認証の仕方です。

MFA 設定してる場合も付けときました。

実装

# MFA 入力待ち
mfa_TOTP = raw_input("Enter the MFA code: ")

# sts クライアント
client=boto3.client( 'sts' )

# 認証
response = client.assume_role(
    RoleArn='arn:aws:iam::123456789:role/admin_full',
    RoleSessionName='mysession',
    DurationSeconds=3600,
    SerialNumber='arn:aws:iam::987654321:mfa/myaccount',
    TokenCode=mfa_TOTP,
)

# 認証情報
credentials = response['Credentials']

# session に 認証情報付加
session = boto3.Session(profile_name=session_name, 
    aws_access_key_id = credentials['AccessKeyId'],
    aws_secret_access_key = credentials['SecretAccessKey'],
    aws_session_token = credentials['SessionToken'],
)

ec2Client = session.client('ec2', region_name='ap-north-east1')
resources = ec2.describe_instances()

boto3.Session に sts で AssumeRole で得た credentials 情報を渡してます。

以上です。

No space left on device が発生して i-node 枯渇してた時の原因調査法

f:id:kenzo0107:20181015001102j:plain

Linux Server で No space left on device が発生した時の対処まとめです。

とりあえず df -h してみる

df -h しても 最大で 77% no space left on device が発生することでもなさそう

$ df -h 

Filesystem      Size  Used Avail Use% Mounted on
udev            1.9G     0  1.9G   0% /dev
tmpfs           385M   40M  346M  11% /run
/dev/nvme0n1p1   15G   11G  3.3G  77% /
tmpfs           1.9G     0  1.9G   0% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs           1.9G     0  1.9G   0% /sys/fs/cgroup
tmpfs           385M     0  385M   0% /run/user/1022
tmpfs           385M     0  385M   0% /run/user/1128
tmpfs           385M     0  385M   0% /run/user/1098
tmpfs           385M     0  385M   0% /run/user/6096

-h = --human-readable 読みやすいサイズ表示をしてます。

df -i してみる

df -i で i-node 情報表示。最大 95%
これでした。

$ df -i

Filesystem     Inodes  IUsed  IFree IUse% Mounted on
udev           490419    351 490068    1% /dev
tmpfs          492742    521 492221    1% /run
/dev/nvme0n1p1 983040 927212  55828   95% /
tmpfs          492742      1 492741    1% /dev/shm
tmpfs          492742      3 492739    1% /run/lock
tmpfs          492742     16 492726    1% /sys/fs/cgroup
tmpfs          492742      4 492738    1% /run/user/1022
tmpfs          492742      4 492738    1% /run/user/1128
tmpfs          492742      4 492738    1% /run/user/1098
tmpfs          492742      4 492738    1% /run/user/1142

i-node とは?と思ったら、 「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典 i-node編 辺りを見てみてください。

簡単に言うと、ファイルの属性情報を管理しているデータです。

要は、ファイル数が増えると、ファイルを管理するデータが増え、 i-node はどんどん増えていきます。

その調査法をまとめました。

どのディレクトリのファイル数が多いか調査

以下は「現ディレクトリでのファイル数多い順ランキング」です。

sudo find . -xdev -type f | cut -d "/" -f 2 | sort | uniq -c | sort -r

※ find の -xdev オプションはマウント先のファイルシステムを検索しない様にしてます。-type f はファイルのみ検索。

このワンライナーで原因となるファイル数の多いディレクトリを探索します。

当たりが付いている場合はそのディレクトリで実行

例えば、ユーザ毎にディレクトリが用意されている場合等に、個々人が home directory で git clone してるとか、
個々人が bundle install してて vendor ディレクトリ以下がファイル数が激増してたとか。

そういった事象があり得そうなら、 /home/ ディレクトリ以下でワンライナー実行して原因調査をするのが良いです。

各ユーザ毎が原因なら相談して消して良いかも確認できるし!

一番手っ取り早いのは、root path 「/」 で実行

どのディレクトリのファイル数が多いのかを探るのなら、一番上位階層の「/」(root) から実行した方が特定しやすいです。

但し、root から全てのディレクトリ内のファイルを検索するとなると非常に cpu を食います。
実行してしばらくレスポンスが返ってこなくてドキドキします。

top コマンド等で cpu 状況を監視しつつ、実行することをオススメします。

本番環境の web サーバで直ちにユーザ影響が出そうな場合は、LBから一旦外して、とか、ユーザアクセスの少ない時間に実行する様に影響範囲を最小限にしたい所。

状況見た上で進めましょう。

実際にあった i-node 枯渇原因

/usr ディレクトリ以下に linux-headers-*** ファイルが溜まっており、30% 近く食ってました。

以下記事に救われました。ありがとうございます。 古いカーネルの削除方法メモ