EKS on FargateでのDatadog Cluster Agentの動かし方
概要
Fargate上でEKSのPodを動かしている場合に、Datadog Cluster Agentのマニフェストはどう定義すればいいか分かりづらかったのでメモとして残しておきます。
設定例
Datadog Cluster Agentをhelmでインストールします。 valuesファイルは以下のようにする。
# see: https://github.com/DataDog/helm-charts/tree/main/charts/datadog#values datadog: apiKeyExistingSecret: datadog agents: enabled: false clusterAgent: enabled: true replicas: 2 shareProcessNamespace: true tokenExistingSecret: datadog-cluster-agent env: - name: DD_EKS_FARGATE value: "true" - name: DD_KUBERNETES_KUBELET_NODENAME valueFrom: fieldRef: apiVersion: v1 fieldPath: spec.nodeName - name: DD_CLUSTER_NAME valueFrom: configMapKeyRef: key: DD_CLUSTER_NAME name: datadog resources: requests: cpu: 200m memory: 256M limits: cpu: 200m memory: 256M
datadog ConfigMap
apiVersion: v1 kind: ConfigMap metadata: name: datadog data: DD_CLUSTER_NAME: my-cluster-name
datadog Secret
apiVersion: v1 kind: Secret metadata: name: datadog data: api-key: my-api-key
datadog-cluster-agent Secret
apiVersion: v1 kind: Secret metadata: name: datadog-cluster-agent data: token: my-datadog-cluster-agent-token # 32文字以上にすること。じゃないとエラーになる
Fargate上でPodを動かす場合、Datadog AgentコンテナはSidecarとして動かすことになります。
そのDatadog AgentコンテナにDatadog Cluster Agentコンテナと通信するための環境変数を渡してあげます。
必要な環境変数はDD_CLUSTER_AGENT_ENABLED、DD_CLUSTER_AGENT_URL、DD_CLUSTER_AGENT_AUTH_TOKEN、DD_ORCHESTRATOR_EXPLORER_ENABLEDです。
Sidecarの定義
spec: containers: - image: public.ecr.aws/datadog/agent:7.49.1 name: datadog-agent env: - name: DD_API_KEY valueFrom: secretKeyRef: key: api-key name: datadog - name: DD_SITE value: datadoghq.com - name: DD_EKS_FARGATE value: "true" - name: DD_PROCESS_AGENT_ENABLED value: "true" - name: DD_KUBERNETES_KUBELET_NODENAME valueFrom: fieldRef: fieldPath: spec.nodeName - name: DD_CLUSTER_NAME valueFrom: configMapKeyRef: key: DD_CLUSTER_NAME name: datadog - name: DD_CLUSTER_AGENT_ENABLED value: "true" - name: DD_CLUSTER_AGENT_URL value: https://<CLUSTER_AGENT_SERVICE_NAME>.<CLUSTER_AGENT_SERVICE_NAMESPACE>.svc.cluster.local:5005 - name: DD_CLUSTER_AGENT_AUTH_TOKEN valueFrom: secretKeyRef: key: token name: datadog-cluster-agent - name: DD_ORCHESTRATOR_EXPLORER_ENABLED # Kubernetes リソースビューを取得するために必要です value: "true"
うまくいかない場合
1, ログを確認
Datadog Cluster Agentコンテナのログ、Sidecarとして動かしているDatadog Agentコンテナのログを確認します。 INFOレベルログでうまくいかない原因が出力されていることもあります…。(DD_CLUSTER_AGENT_AUTH_TOKENの長さは32文字以上じゃないといけないとか)
2, agent statusで確認
Sidecarとして動かしているDatadog Agentコンテナにはいり、agent statusを実行し、 Datadog Cluster Agent欄を確認する。
Successfully Connected to the Datadog Cluster Agent.と出ていれば正常に設定がされています。
3, Sidecarとして動かしているDatadog AgentコンテナのPodのdnsPolicyがDefaultになっていないか確認
DD_CLUSTER_AGENT_URLにDatadog Cluster Agent ServiceへURLを設定していますが、dnsPolicyがDefaultでは名前解決できずエラーになります。
CoreDNSなどはdnsPolicyがDefaultなのでそういったPodはDD_CLUSTER_AGENT_ENABLEDをfalseにするとよいでしょう。(他に良い解決方法があれば教えてください)
`RUN --mount=type=cache`のキャッシュをGitHub Actionsのキャッシュとして保存しイメージビルドを速くする
背景
CI環境でのDockerイメージビルドの速度向上は、多くの開発者にとって切実な課題です。
簡単な対策は
- CircleCIでは Docker レイヤー キャッシュ (DLC)を有効にする。
- GitHub Actionsでは
docker/build-push-actionでcache-from: type=gha cache-to: type=gha,mode=maxを使う https://docs.docker.com/build/cache/backends/gha/#using-dockerbuild-push-action
しかしこれらのキャッシュはCI側で有効期限があり、キャッシュ保存量制限もあり、CircleCIの場合はコスト(料金)もかかります。
なので次によく取られる対策は、キャッシュをイメージレジストリに保存するやり方になります。
参考: https://shepherdmaster.hateblo.jp/entry/2022/06/11/123419
これによりCI環境でもレイヤーごとのキャッシュは可能になりました。 しかしパッケージが変更された場合(例えばpackage.jsonやgo.modやGemfileが変更になった場合)はパッケージのインストールはフルで実行されてしまいます。 ローカルで実行するのであれば追加/変更したパッケージだけインストールすればすむのにCI環境では少しでも変更があると毎回フルインストールが走ってビルド時間が延びるのが悩みです。
さて、このイメージビルド時のパッケージのインストールにキャッシュを効かせて速くする方法があります。
RUN --mount=type=cacheです。
https://docs.docker.com/build/guide/mounts/
これはローカルマシンでは効果がありますが、同一マシンじゃないとキャッシュが効かないため、マシンが毎回変わるCIでは効果がありません。またキャッシュ保存先がDockerが管理するディレクトリに保存されるためキャッシュが取り出しづらいという問題があります。
reproducible-containers/buildkit-cache-danceという救世主
RUN --mount=type=cacheのキャッシュをGitHub Actions上で保存できるようにするためのActionがreproducible-containers/buildkit-cache-danceです。
これを使えば、apt-getやパッケージ(npm, Goライブラリ, Gem等)のインストール結果をキャッシュすることができるようになります。
このActionやってることはシンプル(だが泥臭い)で、
- 前処理として専用のイメージをビルドしてGitHub Actionsに保存されたキャッシュをDocker cacheにコピー
- Post Actionで専用のイメージをビルドしそのビルド内で
RUN --mount=type=cacheで使ったキャッシュを取り出しホストマシン側に保存。それがGitHub Actionsのキャッシュとして保存される https://github.com/reproducible-containers/buildkit-cache-dance/blob/main/post
という流れになっています。
apt-getのキャッシュの仕方
https://github.com/reproducible-containers/buildkit-cache-dance/tree/main#examples を参考
bundle installのキャッシュの仕方
Dockerfile:
WORKDIR /app/
COPY Gemfile Gemfile.lock ./
RUN bundle config set path .cache/bundle
RUN --mount=type=cache,sharing=locked,target=/app/.cache/bundle \
bundle install && \
cp -ar .cache/bundle .bundle && \
bundle config set path .bundle
Action:
jobs: build: name: container build runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Cache bundle-install uses: actions/cache@v3 with: path: bundle-install-cache key: bundle-cache-${{ hashFiles('Dockerfile') }} - name: inject bundle-install-cache into docker uses: reproducible-containers/buildkit-cache-dance@v2.1.3 with: cache-source: bundle-install-cache cache-target: /app/.cache/bundle - uses: docker/build-push-action@v5 with: context: . tags: user/app:latest
その他
試してないですが、npmやGoライブラリなどのキャッシュも同様に出来るはずです。
注意点
CI上でのキャッシュのrestore、save、そしてイメージへのキャッシュのinject、イメージからのキャッシュのextractはキャッシュサイズが大きくなればなるほど時間がかかるようになります。
そのためRUN --mount=type=cacheのキャッシュが効くことによる削減時間よりも先述したオーバーヘッドのほうが大きくなる場合があるので注意が必要です。
まとめ
RUN --mount=type=cacheのキャッシュをGitHub Actionsのキャッシュとして保存し、イメージビルドを速くする方法を紹介しました。
キャッシュのサイズによりますがDependabotやRenovateでパッケージのアップデートを頻繁に行う場合でもイメージビルドが速くなるのは嬉しいですね。
Github Actionsのdocker/build-push-actionのcache-toにECRを指定する
TL;DR
- uses: docker/build-push-action@v3 with: context: . tags: ${{ steps.login-ecr.outputs.registry }}/${{ env.MY_REPOSITORY }}:${{ env.MY_TAG }},${{ steps.login-ecr.outputs.registry }}/${{ env.MY_REPOSITORY }}:${{ env.MY_CACHE_TAG }} push: true cache-from: type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ env.MY_REPOSITORY }}:${{ env.MY_CACHE_TAG }} cache-to: type=inline,ref=${{ steps.login-ecr.outputs.registry }}/${{ env.MY_REPOSITORY }}:${{ env.MY_CACHE_TAG }} outputs: type=registry
説明
まずdocker/build-push-actionではcache-toにtype=ghaを使うのが簡単で一般的だと思う。
しかしこれは内部でGithub Actions Cache APIを使っているのでキャッシュ先はactions/cacheと同様で、actions/cacheのキャッシュ制限事項が適用されると思われる。
そうだとすると、7日間アクセスがなければキャッシュが破棄され、またリポジトリごとの10G制限も発生する。actions/cacheのキャッシュサイズが大きい場合キャッシュが効きづらくなりそうだ。
そこで、cache-toにtype=registryを使いたい。
これならキャッシュ保存先がimageレジストリになるのでキャッシュ制限を考えなくてよくなる。
type=registry はimageとcache manifestを別々にレジストリにpushするが、ECRはcache manifestをサポートしていない(2022/06/11現在)
ただ2022/5/7にサポートを検討するコメントがついているのでしばらくしたらtype=registryが使えるようになりそう。
で、type=registryが使えないのでtype=inlineを使うことになる。
type=inlineはimage内にキャッシュ用メタデータを埋め込む方法。
type=inlineのデメリットとしてはmode=maxが使えないので、マルチステージビルドを使っている場合、中間レイヤーイメージがキャッシュされない。
もし中間レイヤーもキャッシュしたいのであれば「キャッシュのためにDockerビルドで中間イメージをタグ付けしレジストリにPushする - 🤖」を参考にするとよさそう。
また、docker/build-push-actionを使う際はtagsにメインのimageだけじゃなくcache用のimageも指定しないと、cache用のimageがpushされないので注意。
Podに指定したserviceAccountNameを消したいけど消せない件
Podのservice accountを指定する場合、マニフェストファイルのspec.serviceAccountNameに書きますが、service accountを使わなくなったのでファイルから行ごと消してapplyしたけど反映されない事象に遭遇しました。
調べると、以下が理由。 github.com
つまり、applyしたリソースには、下位互換性のためにserviceAccountNameだけではなくserviceAccountも同じ値が設定される。フィールドを削除する場合は、両方を明示的に空に設定する必要がある。serviceAccountNameだけを未設定にすることはできない。
解決策:
1, kubectl editでserviceAccountNameとserviceAccountの両方を消す
2, マニフェストファイルにserviceAccountNameとserviceAccountを指定してapply後、serviceAccountNameとserviceAccountの両方を消して再度apply
面倒ですね。。 以下のissueで対応されるかもです。 github.com
Kubernetesで動くSidekiqへのヘルスチェック方法
Kubernetesクラスタ上で動かすSidekiqに対してヘルスチェックを導入した話 | Money Forward Engineers' Blog を参考にさせていただきましたが、調べるとヘルスチェックスクリプトを自作しなくても済むことが分かりました。
TL;DR
livenessProbe: exec: command: ["sh", "-c", "REDIS_URL=$SIDEKIQ_REDIS_URI sidekiqmon processes | grep $HOSTNAME"] startupProbe: exec: command: ["sh", "-c", "REDIS_URL=$SIDEKIQ_REDIS_URI sidekiqmon processes | grep $HOSTNAME"]
sidekiqmonとは
Sidekiq 6.0で導入されたモニタリング用のコマンドです。
sidekiq gemを入れていれば使えます。
ソースはこちら
sidekiq/sidekiqmon at main · mperham/sidekiq · GitHub
マネーフォワードさんのブログにあったスクリプト内でSidekiq::ProcessSetの中身を見てましたが、このsidekiqmonでもSidekiq::ProcessSetを見てます
またsidekiqmonはversion、overview、processes、queuesなどの引数をとれます。念の為ヘルスチェック対象の情報であるprocessesを指定します。
またコンテナの$HOSTNAMEはpod名になるので sidekiqmon processes結果を$HOSTNAMEをgrepしてあげます。
最終的には TL;DR に書いたようになります。
システムメンテナンスを事故なく終わらせるためのTips
システム運営をやっていると、システムメンテナンスをやらないといけない場面が出てきます。 ミドルウェアのバージョンアップ、データのマイグレーション、大きな機能リリースなどなど。 そこで事故なくメンテナンスを終わらせるためのTipsを書き残しておきます。
メンテナンス前
- メンテナンス数週間前に関係各所(主に社内)に連絡をする
- メンテナンス当日に大きな機能リリースやキャンペーンなどが無いかを確認する。あればメンテナンス日をずらす。
- ユーザへのメンテナンス通知
- アプリやWeb、SNS上でユーザに対しメンテナンスのお知らせを出す
- メンテナンス手順書を作る
- これが最も肝心。必ず作成すること。一度作成すれば次回のメンテナンス時にも手順書を流用できる。そして毎回手順書をブラッシュアップしていくことでどんどんミスが少ないメンテナンスができるようになる。
- 必ず手順書はレビューしてもらう。
- メンテナンス実施時には、手順をなぞるだけでメンテが完了するように、手順書は詳細かつ分かりやすく書く。特に深夜メンテナンス時は眠気と緊張で頭が普段の半分以下しか働かないものと想定する。
- 手順書について
- メンテ前に事前に行うことのチェックリストを設けておく(次回に使える)
- たとえば関係各所への連絡、作成すべきプルリクエストや、止めておくバッチの確認など
- 文章だけでは分かりにくい部分は画像で伝える。
- サーバーに入ってなにかコマンドを実行する場合は具体的なコマンドを載せる
- 例えば「Webサーバーを止める」という文章だけではなく止める際のコマンドを正確に書く。
- メンテナンスの目的がきちんと達成されたかを確認する項目を設ける
- メンテナンスを開けたあとに実はバージョンが上がってなかったとか、一部DBマイグレーションが失敗していたとかそういうことが稀に発生します。なので必ずメンテナンス中に目的が達成されたか確認をする。
- 行うべき動作確認をリストアップしておくこと
- メンテナンス後、システム影響(エラー等)が出ていないかを確認しますが、その際確認すべき項目をリストアップしておく。例えばよくあるのは会員登録からコンバージョンまでの一連のフロー。
- 切り戻し手順を書いておく。切り戻しを行うデッドラインの時間を決めておく。
- メンテナンス中に思わぬシステム影響が発生することを想定する
- 細かいが重要なこと
- メンテナンス中に止めるべきバッチ、メンテナンス後手動で再開すべきバッチを確認しておく
- メンテ前に事前に行うことのチェックリストを設けておく(次回に使える)
- メンテナンスのリハーサルをする
- これもかなり重要。一度リハーサルをしておくとメンテナンス当日の安心感が違う。
- リハーサルをやると手順書通りにうまくいかないことが判明したりする。またステージング環境などできるだけ本番に近い環境でリハーサルを行っておく。この際、メンテナンスにかかった時間を測っておく。
- メンテナンス当日に社内にリマインドをする
- 忘れている人が多いので、今日この時間帯にメンテナンスをやりますと周知する。
メンテ時
- Zoomをつなぎつつ、各作業開始/終了の報告はSlackで行う
- 各作業というのは、「ユーザのアクセスを止める」とか「Webサーバーを止める」とか「DBのスナップショットを取得する」とか「動作確認をする」とかそういう細かい作業
- これは結構個人的には重要だと思っている。各作業は平行して行うことが多いので自分の作業中に相手から口頭で各作業の開始終了を伝えられても覚えてられないんですよね。お互い、え、いまどこやってるんだっけ?みたいことになる。そうすると事故につながりかねない。
- Slackで各作業開始/終了を書き残しておくと、メンテナンス作業に伴うシステム障害やメンテナンス時にうまくいかなかった場合の調査に役立つ。この作業とこの作業の間に問題が起きたからこの作業が影響しているのではみたいなことがすぐに分かる。これが口頭だと残らない(レコーディングしてても時間を割り出すのは面倒)。
- 監視を止める
- メンテナンス中にオンコールが発生してメンテナンス対応者以外のエンジニアが起こされることがないようにしましょう。自分は面倒なので全部の監視を止めてますがそれだと気づくべきアラートに気づけないという問題もあるので理想的には最低限の監視だけ止められるとよい。
- SentryやRollbarを確認する
- メンテナンス中に動作確認をすると思いますが、その際実は裏でエラーが発生していたということもあります。またメンテナンスを開けたあと、一般のユーザがアクセスしだした後も一応確認すると良い。リアルアクセスに勝る動作確認はない。
- メンテが終わったらSlackで無事終わった報告をする
- メンテナンス後他のエンジニアがメンテナンスは無事に終わったのか心配にならないように報告しておきましょう。
- もし切り戻しをした場合や、なにか引き継ぎが必要な場合はSlackに細かく書いてきちんと引き継ぐこと。
メンテ後
- メンテナンス振り返り(KPT)をする
- 多くの場合、メンテナンスは改善の余地がたくさんあるものだと思います。手順書のブラッシュアップや一部作業の自動化など。
- ポストモーテムと同じで振り返りで出たアクションアイテムは必ず対応すること
その他
- インフラ/SRE以外のエンジニアにも時々参加してもらう
- メンテナンス作業はインフラ/SREが行うことが多いと思いますが、インフラ/SRE以外のエンジニアに参加してもらうことで、システムの理解が深まったり、いざというときにメンテナンス時の知見が役立ったりします。
【解決編】EKS Cluster Autoscalerとログ保存用Daemonsetの組み合わせでスケールイン時にログが失われる問題
shepherdmaster.hateblo.jp の続き&解決編です。
add DaemonSet eviction option for empty nodes by yaroslava-serdiuk · Pull Request #3824 · kubernetes/autoscaler · GitHub
がリリースされたので、--daemonset-eviction-for-empty-nodes=trueオプションをつけることで、アプリケーションpodのpreStop内のsleep時間を短くし、代わりにfluentd podのsleep時間を長くすることで、ログが正常に保存されることが可能になりました。
つまり流れを以下にできる。
- NodeのScale inが開始される
- アプリケーションpod と fluentd pod のターミネート処理が始まる
- fluentd podのpreStop内で長いsleepを実行
- 先にアプリケーションpodのターミネート処理が終わる
- fluentd podがログを保存する処理(flush)を実行(プロセスID 1にSIGTERMを送るとかで)
- fluentd podのターミネート処理が終わる アプリケーションPodとfluentd Podが終了したのでNodeインスタンスの終了処理が実行される
図にするとこんな感じ

以前と比べるとだいぶ自然な流れにできましたね。