EKS on FargateでArgoWorkflowを利用したJob実行基盤の構築
通常、EKS上でジョブを実行する場合、Webサービス等の実行ノードと同じかもしくは分けてバッチジョブ用のノードを用意することになるかと思いますが、ノードを分けない場合は、バッチ処理による負荷がサービスへ影響を与える可能性がありますし、ノードを分ける場合は、利用しない間も起動しておく必要があることからコスト面が課題になってきます。
そのため、バッチ処理をFargate上で実行することで、処理の負荷の影響を切り離してかつ必要な時に必要なリソースだけ利用することでコストの最適化も行えるのではないかと考え検証を行いました。今回は検証過程で得た知見を紹介したいと思います。
バッチ処理でEKS on Fargateを使う理由
同一ノード上でバッチ処理を行うとどうしても一定時間、CPUやメモリを占有するということが起きてしまいます。そのためどうにかして負荷を下げようとすると、まずはバッチ専用ノードを立てるという案が出てきますが、これはバッチ処理が1日に数回程度の利用状況だとコスト的に見合わないことになります。
次に考えられる案として、AWS前提の話となりますが、LambdaやAWS Batchの利用かと思います。現状、弊社では開発プロセスをkubernetesに寄せた形にしているため、それに関する知見も多く蓄積されているので、ここであえて開発者にLambdaやAWS Batchを学習してもらう必要もないと考えました。
これを検討し始めた時点ではLambdaのコンテナイメージ利用やAWS BatchのFargate利用などの機能がまだ出ていなかったので上記のような結論になりましたが、こうした機能が使えるのであれば、社内にある知見を活用しつつうまく利用できる方法を検討していきたいと考えています。
こうした状況を踏まえて、kubernetesの知見を活かしつつバッチ処理の負荷を切り離す方法としてFargateの利用が最も効果的ではないかと考えました。
EKS on FargateでのJobの実行方法
Fargate上でPodを実行するための設定はそれほど難しくありません。Fargate Profileを設定し、そこで指定したNamespaceでJobをapplyするだけでFargateノードでJobが実行されます。
また、FargateProfileは既存のクラスタに対して後から追加可能となっており、Fargateノードで起動するかどうかはNamespaceだけでなくlabelで制御することも可能です。
以下はeksctlのcluster.yamlへの設定方法になります。対象のnamespaceがfargate-testとなり、labelにfargate: onが指定されたJobのみFargateノードで起動される設定になります。
: fargateProfiles: - name: fargate-profile selectors: - namespace: fargate-test labels: fargate: 'on'
FargateProfileの作成
eksctl create fargatargateprofile -f cluster.yaml
EKS on FargateでJobを実行する時の問題点
テスト用のJobをFargate上で動かしたところ、本番運用にあたっていくつかの問題に気づきました。
ジョブの実行完了後に誰がジョブを削除するか?
Jobの実行後にステータスがCompletedとなったPodは意図的に削除しない限り残り続けることとなります。EC2ノード上だと実行が完了しているので、それでリソースが使われるということはなく単にゴミが残るだけとなりますが、Fargateの場合、Fargateノードも残ることとなりその分課金され続けてしまいます。EKSではPodの終了後に自動で削除してくれるttlSecondsAfterFinishedを使うことはできないので、Podの削除方法については自前で仕組みを検討する必要があります。
モニタリング、ログの取得
EKSの監視はDatadogなどのagentをDaemonsetで起動させてモニタリングしてるケースが多いかと思いますが、Fargateではコンテナのみの提供となるためDaemonsetは使えません。そのため、sidecar方式でPodにagentを同居させる必要があります。しかしながら、こうしたagent系のコンテナをsidecarとして実行させると、メインのJobの実行は完了したもののagentが終了しないのでJobがいつまでも起動したままとなります。そのため、どうにかしてagentをJobの完了に合わせて終了させる必要があります。
以下のブログはアプリの終了時にsidecarをkillするということをやっているようです。
tech.recruit-mp.co.jp
そしてこのブログを書いている間に、reinvent2020が始まり以下の機能がリリースされました。ログに関してはsidecarで取得することなくcloudwatch logsに送ることが可能になりました。
aws.amazon.com
起動時間の考慮
Fargateノードで起動する場合、ノードを起動するまで思ったよりも時間がかかりました。検証時には毎回2-3分かかりました。調べたところ以下のブログで検証されているのですが、起動時間の大半はイメージのpullにかかっているため、主にイメージサイズに依存します。そのため、時間どおり正確に処理を実行したいなどの要件がある場合は、実行基盤としてはそぐわない場合もあると思います。
qiita.com
DaemonsetでデプロイされたPodが起動しようとする
検証時のクラスターにはEFS CNIドライバーが起動されていました。テスト用のJobを実行すると、Fargateノードの起動を検知して同様にEFS CNDドライバもFargateノードで起動しようとする動きが見られました。EFS CNIドライバーはFargateノードではエラーで起動できなかったのでメインのPodの削除とともに消えましたが、ものによっては起動が成功するかもしれないので、環境によっては注意が必要になります。
開発者とインフラ担当者の責任分界点
弊社では、モニタリングやログ取得については、インフラ担当の責任範囲としており、アプリケーションに関するManifestは開発者の責任範囲としています。しかし、Fargateを利用するとモニタリングやログの取得はsidecar方式での利用となるため、設定を開発者へ委ねることになります。また、Fargateノードで起動するしないの設定も開発者に意識してもらう必要がありますので、こうした点を意識して設定してもらう必要があります。
その他
他にも、現状では、SecurityGroupが任意のものを設定できないといった問題がありました。このあたりは、今後の改善に期待ですね。
Argo Workflowの利用
EKS on FargateでJobを実行する場合の大きな問題点は、上記で書いたようにJob実行後のPodをどう削除するかとsidecarとなるコンテナをどのようにkillするかという点になるかと思います。これらを解決できないとJob実行後もFargateノードが残り続けることとなるためFargateノードを使う意味がなくなります。何か良い方法がないか調べていたところ、Argo Workflowを利用することで解決できそうなことがわかりました。
Argo WorkflowとはオープンソースのJobのワークフローエンジンとなり、UIでジョブ管理ができるツールになります。詳細は以下のリンクを参照してください。Argo WorkflowでJobを管理、実行することで完了後のPodも自動で削除してくれますし、メインのコンテナ終了後にsidecarコンテナもあわせてkillしてくれるので、Jobを実行するにあたっての大きな懸念点が解消されました。
argoproj.github.io
インストール及び設定
インストール方法
インストール方法については、cluster-installとnamespace-installの大きく2種類があります。
cluster-installとはクラスタのどのnamespaceでもJobを実行できるようにするインストール方式です。一方でnamespace-installは、argo workflowをインストールしたnamespaceと同じnamespaceでしかJobを実行できません。
現状ではargoworkflowはユーザーごとの権限制御があまり細かく設定できなさそうなので、namespaceごとにユーザーを限定したいのであればnamespace-installを利用すればいいかと思います。今回はnamespaceごとにインストールしたくはないのとあまり細かい制御は必要ないのでcluster-installとしました。
gitからargo workflowの取得
git clone https://github.com/argoproj
configmapを編集
Fargate上で動かすためには、ランタイムの設定であるcontainerRuntimeExecutorをk8sapiに変更する必要があります。ここが少しハマったポイントなのですが、デフォルト設定のdockerのままだと実行したJobのステータスがPendingのまま実行されませんでした。
また、workflowのデフォルト設定もできるので、デフォルトの値を設定しておくことで実行時のミスを減らせるかと思います。他の項目については必要に応じて編集してください。
cd argo/manifests/ vim base/workflow-controller/workflow-controller-configmap.yaml
workflow-controller-configmap.yaml
apiVersion: v1 kind: ConfigMap metadata: name: workflow-controller-configmap data: config: | : : containerRuntimeExecutor: k8sapi : :
kustomizeでビルドし、applyしてください。
cd cluster-install kustomize build . > install.yaml kubectl apply -f install.yaml -n argo
ポートフォワードします。必要に応じてALBなどを設定してください。
kubectl -n argo port-forward deployment/argo-server 2746:2746
以下のURLにアクセスするとArgo WorkflowのUIにアクセスできます。本番用途ではALBの利用やSSOなどの利用を検討してもいいかと思います。
http://localhost:2746
サービスアカウント
Argo WorkflowでJobを実行するには、以下の権限を持ったサービスアカウントが必要になります。以下の内容を保存してapplyしてください。
AWSのリソースにアクセスする必要がある場合は、eksctlなどでサービスアカウントを作成しているかと思いますが、そこで作成したサービスアカウントに以下のロールをバインドする必要があります。このあたりはeksctlで一括管理したいですが、今のところいい方法はなさそうです。
vim fargate-serviceaccount.yaml
apiVersion: v1 kind: ServiceAccount metadata: name: fargate-serviceaccount namespace: fargate-test --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: workflow-role rules: - apiGroups: - "" resources: - pods verbs: - get - watch - patch - apiGroups: - "" resources: - pods/log verbs: - get - watch --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: argo-batch-binding namespace: fargate-test subjects: - kind: ServiceAccount name: fargate-serviceaccount namespace: fargate-test roleRef: kind: Role name: workflow-role apiGroup: rbac.authorization.k8s.io
fluentbitの設定
Jobを実行するnamespaceでfluentbit用のConfigMapを設定しておきます。
以下はログをdatadogに送る場合のサンプルになります。DatadogのAPIキーが必要になります。
apiVersion: v1 kind: ConfigMap metadata: name: fluentbit-config namespace: fargate-test data: # Configuration files: server, input, filters and output # ====================================================== fluent-bit.conf: | [INPUT] Name tail Tag *.logs Path /var/log/*.log DB /var/log/logs.db Mem_Buf_Limit 5MB Skip_Long_Lines On Refresh_Interval 10 [OUTPUT] Name datadog Match * compress gzip apikey [your datadog api key] dd_service hello-world dd_source job dd_tags vendor:sample,env:stg
Jobの実行
以下はJobのサンプルになります。5秒ごとに「hello world」を出力するだけのサンプルです。
apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: generateName: hello-world- spec: entrypoint: helloworld templates: - name: helloworld serviceAccountName: fargate-serviceaccount metadata: labels: fargate: "on" container: image: busybox args: - /bin/sh - -c - > for i in 1 2 3 4 5; do echo hello world >> /var/log/app.log; sleep 5; done volumeMounts: - name: varlog mountPath: /var/log resources: requests: memory: 512Mi cpu: 500m sidecars: - name: fluentbit-agent image: amazon/aws-for-fluent-bit:latest ports: - containerPort: 2020 env: - name: FLUENTD_HOST value: "fluentd" - name: FLUENTD_PORT value: "24224" - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace volumeMounts: - name: varlog mountPath: /var/log - name: fluentbit-config mountPath: /fluent-bit/etc/ volumes: - name: varlog emptyDir: {} - name: fluentbit-config configMap: name: fluentbit-config ttlStrategy: secondsAfterCompletion: 30 secondsAfterSuccess: 30 secondsAfterFailure: 30
以下設定ポイントになります。
- namesapceとlabelsの[fargate: 'on']設定がFargateで起動するかどうかを制御します。
- アプリケーションからログを/var/log/app.logにリダイレクトすることでfluentbitがログを収集します。
- resourcesでのリソース要求設定で必要なFargateのスペックが割り当てられます。
- ttlStrategyでJobが完了した場合などで、何秒後にPodを削除するか設定します。
fargateラベルやttlStrategyなどは設定が漏れると困るのでデフォルト値として設定しておいてもいいかもしれません。
上記のマニフェストファイルを適当な名前で保存してUIから実行するか以下のコマンドを実行するとジョブが実行されます。
argo submit helloworld.yaml
ArgoWorkflowによるsidecarのkill制御
ArgoWorkflowでは、mainコンテナが終了するとsidecarコンテナもkillしてJobを終了させてくれますが、sidecarコンテナが複数あるとtimeoutが発生し、一つしかkillされませんでした。コントローラ用のコンテナからはシグナルを全てのコンテナに送っているものの片方しか終了させれない状況となっていました。そのため、当初はfluent-bitとdatadog-agentをsidecarとして配置し、ログとメトリクスを取得する予定でしたが、メトリクスの取得は断念しました。
しかし、上記でも紹介しましたがEKS on Fargateで直接Fargateのログをcloudwatch logsなどに送ってくれる機能追加が行われたため、これを利用しつつdatadog-agentをsidecarとして配置することでメトリクスを取得できそうです。ただし、現時点では利用中の環境のEKSプラットフォームバージョンが上がってないためこの機能を利用できないので、プラットフォームバージョンが上がるまで待つこととしました。ちなみにEKSのプラットフォームバージョンは利用者が意図的にあげることはできないようです。AWS側で定期的にプラットフォームのバージョンを更新しているためそれを待つ必要があります。どうしてもアップさせたい場合は、新しくクラスタを作り直すかkubernetesのメジャーバージョンをあげる必要があります。
まとめ
EKS on Fargateでのバッチ処理については、ある程度のデータ量を集計する必要があるなど、まとまったリソースが必要な処理には適していると思います。
向いていない処理としては、起動時間に数分必要となるので、正確な時間に実行する必要のある処理や毎分実行する処理、またイベント駆動的な処理などにもあまり適していないと感じました。
また、ログはEKSの標準機能でCloudWatchlogsから取得できるようになりましたが、メトリクスを取得するにはsidecarで取得せざるを得ない状況です。sidecarをArgoWorkflowで制御できるとはいえ、Jobごとに書く必要があるため冗長になってしまいます。できればメトリクスについてもcloudwatchから取得できるようにして欲しいところです。AWSコンソールでもkubernetesのワークロードが見れるようになったので、近いうちに対応してくれるのではないかと期待しています。
バッチ処理のようなバックグラウンドで行われる処理の負荷をどう下げるかというのは頭を悩ませていましたが、Fargateをうまく活用することで一時的な負荷を切り離しつつかつコストも抑えれることが分かりましたので、今後は色々な環境で利用していきたいと思います。