概要
AWSのマネージドKubernetesクラスタであるEKS上でRails製Webアプリケーションを動かし、クラスタの監視とアプリケーションのログをCloudWatchで一元管理できるようにします。
クラスタの監視にはContainer Insightsを利用し、アプリケーションのログ収集にはfluentd-kubernetes-daemonsetを利用します。
Kubernetesのバージョン1.12以上が必要です。
それより古いバージョンを使っていたとしても、EKSを使っていればボタンをポチポチするだけで簡単にアップデートができます。
手順
1. KubernetesワーカーノードのEC2インスタンスのポリシー設定
まずはKubernetesのワーカーからCloudWatchへログを送信できるようにIAMの設定を行います。
ワーカーのロールにCloudWatchAgentServerPolicyをアタッチするだけです。
2. Container Insightsの導入
まずはクラスタの監視を行うContainer Insightsを導入します。
手順としては、CloudWatch用のネームスペース・サービスアカウント・ConfigMapを作成し、Container Insightsのデーモンセットをデプロイします。
$ kubectl apply -f https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/master/k8s-yaml-templates/cloudwatch-namespace.yaml
$ kubectl apply -f https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/master/k8s-yaml-templates/cwagent-kubernetes-monitoring/cwagent-serviceaccount.yaml
$ curl -O https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/master/k8s-yaml-templates/cwagent-kubernetes-monitoring/cwagent-configmap.yaml
$ micro cwagent-configmap.yaml # エディタはなんでもいいが、cluster_namesをContainer Insightsを追加するクラスタ名で埋める
$ kubectl apply -f cwagent-configmap.yaml
これだけでCloudWatchにいろいろログが送られるようになります。また、自動ダッシュボードであらかた見たい指標は見られるようにもなります。
では次にRailsのアプリケーションログをCloudWatchに送れるようにしましょう。
3. Railsアプリ側のログ出力設定
fluentd-kubernetes-daemonsetは、各Podの標準出力を勝手にCloudwatchに送信してくれます。
というわけで、Railsアプリケーションのすべてのログをファイルではなく標準出力に出すように変更。 config/{{environment}}.rb と config/puma.rb を編集します。
Rails5だと、production.logに以下のような記述があるので環境変数をセットすればそれで終わりです。
if ENV["RAILS_LOG_TO_STDOUT"].present?
stdout_logger = ActiveSupport::Logger.new(STDOUT)
stdout_logger.formatter = config.log_formatter
multiple_loggers = ActiveSupport::Logger.broadcast(stdout_logger)
logger.extend(multiple_loggers)
end
pumaのほうも、stdout_redirectの設定があればその行を消すだけです。
4. fluentd-kubernetes-daemonsetのデプロイ
すでにCloudWatch用のネームスペースは追加してあるので、次のコマンドを実行するだけです。
$ curl -O https://s3.amazonaws.com/cloudwatch-agent-k8s-yamls/fluentd/fluentd.yml
// cluster_nameとregion_nameは適宜変える
$ kubectl create configmap cluster-info --from-literal=cluster.name=cluster_name --from-literal=logs.region=region_name -n amazon-cloudwatch
これだけでアプリケーションのログがCloudWatchに転送されます。
これだけでもいいという場合はここまででOKです。
5. ログのフォーマット設定
Railsの出力するログはそのままだと複数行で出力されるので、CloudWatch上では各行が別々のログとして扱われてしまいます。
そこで、複数行で出力するのではなくJSON形式で一行のログとして出力するようにします。
JSON形式になっていると、CloudWatch上でいい感じに検索ができるようになるなどいいことづくめです。
logrageを使いjson形式でリクエストログを出すように変更します。Gemfileにlogrageを追加して、bundle installしておきましょう。
Rails.application.configure do
config.lograge.enabled = true
config.lograge.formatter = Lograge::Formatters::Json.new
config.lograge.custom_options = Proc.new do |event|
exceptions = %w(controller action format id)
{
time: event.time,
host: event.payload[:host],
remote_ip: event.payload[:remote_ip],
params: event.payload[:params].except(*exceptions),
exception_object: event.payload[:exception_object],
exception: event.payload[:exception],
backtrace: event.payload[:exception_object].try(:backtrace),
}.compact
end
...
end
class ApplicationController < ActionController::Base
def append_info_to_payload(payload)
super
payload[:user_agent] ||= request.user_agent
payload[:request_id] ||= request.request_id
if @exception.present?
payload[:exception_object] ||= @exception
payload[:exception] ||= [@exception.class, @exception.message]
end
end
end
またこれだけだと、ActionControllerの外側、ActionDispatchレベルで起きるRoutingErrorに対応できないので次のようなモンキーパッチを追加します
if defined? Lograge
class ActionDispatch::DebugExceptions
alias_method :org_log_error, :log_error
def log_error(request, wrapper)
msg = {
exception: wrapper.exception,
backtrace: wrapper.exception.try(:backtrace),
raw_request: request,
raw_wrapper: wrapper
}
Rails.logger.fatal(msg.to_json)
end
end
end
しかし、このままだとせっかくJSON形式で出力したRails側のログはエスケープされたただの文字列になっています。
そこで、fluentdの設定も変えておきます。
さきほどの記事でダウンロードしたfluend.ymlに、以下の記述を追加して、json形式のログをパースするようにしておきます。こうすると、 parsed_log というキーでパースされたログが記録されるようになります。
<filter **>
@type parser
format json
key_name log
reserve_time true
reserve_data true
emit_invalid_record_to_error false
hash_value_field parsed_log
</filter>
追加する箇所は
の125行目あたり。そこに上記設定を追記して適用します。
また、ログストリーム名も標準だとポッド名になってしまい、ポッドが変わると別のログストリームになってしまいます。
個人的にはログストリームはポッドが変わっても同じログストリームにログが出てほしいので、ラベルから取るようにします。
<filter **>
@type record_transformer
@id filter_containers_stream_transformer
enable_ruby # 追加
<record>
stream_name ${record["kubernetes"]["labels"]["app"]}
</record>
</filter>
将来的には、このログが出力したアプリケーションの最終コミットIDもわかったりすると便利そうだなぁと思います。
まとめ
これでCloudWatchでクラスタの監視とアプリケーションログの集約ができるようになりました。