プロダクションで EKS on Fargate を(できるだけ)使うことを目標に EKS on Fargate に入門します。
Managed Node Groupとの使い分けなどについてもまとめます。
※ 本記事は 2019/12/14 時点の情報に基づいています。
Fargate、EKS on Fargateとは
FargateはAWS上で「仮想マシンの管理なしにコンテナをデプロイできるサービス」です。
元々はAWS独自のコンテナオーケストレーションシステムであるECSの一機能と思われていましたが、EKS on Fargateが登場した今では、「ECS・EKSどちらでも動く、コンテナ化されたワークロード向けのサーバレスコンピュートエンジン」という体でブランディングされているようです。Fargateの公式ページ冒頭でも以下のように紹介されています。
AWS Fargate is a serverless compute engine for containers that works with both Amazon Elastic Container Service (ECS) and Amazon Elastic Kubernetes Service (EKS). Fargate makes it easy for you to focus on building your applications. Fargate removes the need to provision and manage servers, lets you specify and pay for resources per application, and improves security through application isolation by design.
https://aws.amazon.com/fargate/?nc1=h_ls
特にEKS向けのFargateのことを公式には「Amazon EKS on AWS Fargate」、一般には「EKS on Fargate」と呼びます。本記事では後者を使います。
参考: Amazon EKS on AWS Fargate Now Generally Available
前提
EKS on Fargateに関しては既に著名な方々による解説記事が存在するため、それを参照ください。この記事では、他の記事を補足するような内容に集中します。
- EKSはわかるけど、EKS on Fargateはわからん、という方は @inductor さんのファーストインプレッション資料が参考になります。
- Kubernetesはわかるけど EKS on Fargateはわからん、という方は aoyama3 の EKS on Fargate:virtual-kubelet の違い + Network/LB 周りの調査 - @amsy810's Blog が参考になります。
EKSわからん、という方は手前味噌ですがAmazon Elastic Container Service for Kubernetes (EKS)の所感が参考になるかもしれません。
EKS on Fargateの特徴、通常のEKSとの差異
基本的には、以下を抑えておくと良いと思います。
- Podごとに仮想マシンがつくられる(ようにみえる)
- ノードやその裏側の仮想マシンへは一切アクセスできない
- 結果的に、ノード管理からは完全に解放される
- ノードがないぶんVPC内のIPアドレスが節約できる
使えるもの
- Init Container
- サイドカーコンテナ
- ConfigMap, Secretボリューム
- emptyDirボリューム(同じPod内コンテナ間でファイルを共有することはできる、ということです)
- ClusterIP Service
- AppMesh
- ALB (使い方に注意。後述)
クラスタ内で完結するようなアプリケーションは基本的に問題なく移行できると思います。
使えないもの
- emptyDir以外のボリューム。hostPath、EBSともに使えません。
- NodePort
- Service LoadBalancer
- DaemonSet
- NLB (技術的にはできるが、公式サポートされていない)
- Network Policy
K8sの第一級市民であったNode(多くのケースでは仮想マシン、物理マシンなどに対応していた)が利用者側の管理外となったことによって、Podから見て外(ホスト、他VPC、インターネット)と繋ぐ場合に制限がかかるケースがあります。
例えば、DaemonSet経由でそのノード上のPod・コンテナのメトリクスやログを収集していたようなケースだと、代替となる方法を考える必要があります。
Network PolicyはCNIを利用できないために一切使えなくなるので、Security Groupで賄うか、App MeshでEgressを絞ることで代替することになると思います。
使えないけど対応予定なもの
- EFSボリューム
- AppMesh以外のサービスメッシュ(NET_ADMIN依存のIstio、Linkerdなどは少なくとも公式な手順では利用できません)
このあたりは待つしか無いですね。必要な方はContainers Roadmap経由で要望を上げましょう。
Managed Node GroupとEKS on Fargateの使い分け
Fargateで動くものは可能な限りFargateを使うようにするとよいと思います。そうすることで、ノードを適切に更新し、守り続けることを含めた「ノードの運用」全般から解放されるためです。
どちらでも動くがFargateのほうが適切
- ClusterIP、Pod IP経由でアクセスされ、ステートレスで、負荷が変動するようなワークロード全般
どちらでも動くがManaged Node Groupのほうが適切
- 頻繁にローリングアップデートするPod
- 開発時に起動するシェル等
- FargateよりPodを速く増やしたい
- docker pullを予めしておく、低優先度のダミーPodによってノードを多めに確保しておく、などの方法がとれる
Managed Node Group必須
- NLB/ALBのバックエンド(NodePortやService LoadBalancer経由)
- Daemonset
- Fargateの最大Podサイズより大きなCPU・メモリがほしい
- DBなど(プロダクションでDB on K8s流行らないと思いますがテスト環境等ではありますよね)のステートフルワークロード
- その他様々な理由でemptyDir以外のボリュームを使う
プロダクション投入前に抑えておきたいポイント
基本的にはtwitterスレに書いてありますが、特に重要だと思われた点を個別に説明していきます。
Pod Specの変更
EKSを利用する場合、Pod(上で動いているアプリケーションの)ログやメトリクスはDaemonsetとしてデプロイした Datadog Agent や CloudWatch Agent、 Fluentd、 Fluentbit などで収集されていたかと思います。
EKS on FargateではDaemonsetが使えない(Node・仮想マシンには一切アクセスできない)ため、Daemonset以外の方法でログやメトリクスを収集する必要があります。
EKS on Fargateにメトリクスやログを取得するためのAWS APIがあればそれを使ってDatadogやCloud Watchにメトリクスやログを取り込むことができるだろうと思うのですが、そういった便利なAPIは今のところ存在しません。
そうすると、必然的に Sidecar コンテナでがんばるしかありません。
具体的には、各Deployment/Job/Podに以下のような変更が必要です。
- アプリケーションのログはstdout/stderrではなくemptyDir内のファイルに書き出し、それをサイドカーのfluentd, fluentbitなどから収集する
- コンテナのメトリクスは、コンテナ内から見えるものであればサイドカーにDatadog Agentなどを置くことで収集(基本的にコンテナではなく仮想・物理マシンと同じような見え方になります(参考: https://twitter.com/mumoshu/status/1204765309334245378)
例えばFluentbitでログを収集し、メトリクスをDatadogで収集する場合はこのようなマニフェストを書くことになります。
containers[]
の最初が皆さんのアプリケーションで、2つ目と3つ目がそれぞれFluentbitとDatadog Agentです。ログをアプリケーションからFluentbitにわたすために、emptyDirボリュームを作成しアプリとFluentbitコンテナの両方マウントしていますね。
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: testapp
name: testapp
spec:
replicas: 1
selector:
matchLabels:
app: testapp
strategy: {}
template:
metadata:
labels:
app: testapp
spec:
containers:
- image: alpine:3.10
name: alpine
command:
- /bin/sh
- -c
- sleep 100000
volumeMounts:
- name: fluent-bit-config
mountPath: /fluent-bit/etc/
- name: var-log
mountPath: /var/log
- image: fluent/fluent-bit:1.3.3
name: fluent-bit
volumeMounts:
- name: fluent-bit-config
mountPath: /fluent-bit/etc/
- name: var-log
mountPath: /var/log
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- image: datadog/agent:latest
name: datadog-test
env:
- name: DD_API_KEY
value: <YOUR DATADOG KEY>
volumes:
- name: fluent-bit-config
configMap:
name: fluent-bit-config
- name: var-log
emptyDir: {}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: fluent-bit-config
labels:
k8s-app: fluent-bit
data:
fluent-bit.conf: |
[SERVICE]
Flush 1
Log_Level info
Daemon off
Parsers_File parsers.conf
HTTP_Server On
HTTP_Listen 0.0.0.0
HTTP_Port 2020
@INCLUDE input-json-log.conf
@INCLUDE filter-modifier.conf
@INCLUDE output-datadog.conf
input-json-log.conf: |
[INPUT]
Name tail
Tag log.json.*
Path /var/log/*.json.log
Parser json
DB /var/log/flb_json_log.db
Mem_Buf_Limit 5MB
Skip_Long_Lines On
Refresh_Interval 10
filter-modifier.conf: |
[FILTER]
Name record_modifier
Match *
Record hostname ${HOSTNAME}
Record kube_pod ${POD_NAME}
Record kube_namespace ${POD_NAMESPACE}
output-datadog.conf: |
[OUTPUT]
Name datadog
Match *
apikey <YOUR DATADOG KEY>
parsers.conf: |
[PARSER]
Name apache
Format regex
Regex ^(?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER]
Name apache2
Format regex
Regex ^(?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^ ]*) +\S*)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER]
Name apache_error
Format regex
Regex ^\[[^ ]* (?<time>[^\]]*)\] \[(?<level>[^\]]*)\](?: \[pid (?<pid>[^\]]*)\])?( \[client (?<client>[^\]]*)\])? (?<message>.*)$
[PARSER]
Name nginx
Format regex
Regex ^(?<remote>[^ ]*) (?<host>[^ ]*) (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER]
Name json
Format json
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER]
Name docker
Format json
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L
Time_Keep On
[PARSER]
Name syslog
Format regex
Regex ^\<(?<pri>[0-9]+)\>(?<time>[^ ]* {1,2}[^ ]* [^ ]*) (?<host>[^ ]*) (?<ident>[a-zA-Z0-9_\/\.\-]*)(?:\[(?<pid>[0-9]+)\])?(?:[^\:]*\:)? *(?<message>.*)$
Time_Key time
Time_Format %b %d %H:%M:%S
なお、上記はDatadog公式のEKS on Fargate向け設定ではありません。公式な方法も試してみたので、詳細については下記Twitterスレを見てください。
いちいちマニフェストを書き直していくのが面倒な場合、サイドカーインジェクションをするという手はあると思います。OSSを利用するのであれば、tumblr/k8s-sidecar-injectorなどでしょうか。ただこれはこれで自分たちで運用しなければならないので、ただマニフェストを変更するだけ、というのと比較するとまた別の難しさがあるように思います。
アプリケーションのメトリクスは、これまで通り(?) Prometheus向けのメトリクスエンドポイントを生やしておいてPrometheusやDatadog AgentのPrometheus Integrationにスクレイプさせれば良いでしょう。
個人的にはこれが一番しんどい。
ログに関してはFirelens対応が待たれます。メトリクスに関しては最低限AWS APIで収集できるようになってほしいですね。そうすればDatadogなどがそのAPIを使ってメトリクスを収集するようにエージェントを改修してくれると思います。ゆくゆくは、Firelensのメトリクス版のようなものがEKS on Fargateに追加されるとうれしいところです。
ALB/NLB等について
alb-ingress-controllerはさけて、Managed Node Group + CloudFormation/Terraformなどを使いましょう。NLBはそもそも公式サポートされている方法がないので、同様です。
ALB/NLB ---> NodePort Services on Managed Node Group ---kube-proxy---> Pods on Fargate
元々EKSを使っていた方であれば、クラスタ再作成時の移行を簡単にするため、Service LoadBalancerは使わずTerraform等でASG・Target Group・ELB(v2)などを管理されていたかと思います。
EKS on Fargateの場合、NodePortが利用できないので、Service LoadBalancerはもちろん、Terraform等でASG・Target Groupなどを使ってNodePortをALB/NLBにつなぐ、ということもできなくなります。
EKS on Fargateは公式にはALBサポートを謳っていますが、それはaws-alb-ingress-controllerをIP Modeで利用することを意味しています。IP Modeとは、ALBからPod IP(Fargateの場合、Fargate上に起動したPodのIPアドレス)へ直接ルーティングするという方法です。ALBへのPod IPの登録・登録解除のためにalb-ingress-controllerを使うということですね。
そもそも alb-ingress-controller を使ってクラスタをVPC内/インターネットに公開すると、クラスタを作り直すときにALBも一緒に作り直すことになってしまいます。せっかくスケールアップして温まったALBをクラスタを作り直すたびに捨てるのか?トラフィックを新旧クラスタ間で切り替えるときにRoute 53に頼るのか?(Target Groupレベルでやりたくないですか?)といったあたりが気になります。かといって、従来の方法はEKS on Fargateで使えないので、次善の策としてManaged Node Groupを利用しましょう。
EC2 Metadata Service, ECS Task Metadata Endpoint
各言語向けのaws-sdkが、そのプログラムが稼働しているEC2インスタンスやECSタスクの情報(リージョンなども含みます)を取得するために利用している、169.254.169.254とか169.254.170.2などにあるHTTPサービスですね。これに依存しているアプリケーションはEKS on Fargate上では動きません。
アプリケーション側でメタデータではなくコマンドライン引数などで設定を受け取れるようにしてくれている場合はそれを利用すればいいのですが、そうなっていない場合はどうしようもありません。悔しいですが、そういったアプリケーションはManaged Node Groupにデプロイしましょう。
例えば、AppMeshのコントローラはこの制限によりFargateにデプロイできませんでした。
参考: https://twitter.com/mumoshu/status/1204715698842300422
eksctlでManaged Node GroupとFargateを併用する
$ eksctl create cluster --fargate
のようなコマンドでFargateを有効化したクラスタを作る方法がよく紹介されていると思います。
前述のポイントを踏まえると、現状EKS on Fargateが対応している機能を色々と試すためには以下のような cluster.yaml を書くことになります。これを必要に応じて書き換えてみてください。
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: mycluster
region: us-east-2
availabilityZones:
- us-east-2b
- us-east-2c
# Fargateで動かないもの・Fargateに適さないものはManaged Node Groupへ
managedNodeGroups:
- name: ctrl
minSize: 1
maxSize: 2
desiredCapacity: 1
volumeSize: 50
# Node Groupであればeksctlが各サービスの利用に必要な権限を持ったIAM Role作成してくれる
iam:
withAddonPolicies:
appMesh: true
albIngress: true
xRay: true
cloudWatch: true
certManager: true
autoScaler: true
# それ以外は基本的にFargateに
fargateProfiles:
- name: default
selectors:
# default, kube-system, demo nsのpodはすべてFargateに、それ以外はManaged Node Groupにスケジュールする
- namespace: default
- namespace: kube-system
- namespace: demo
iam:
withOIDC: true
serviceAccounts:
# Pod IAM Roleを利用したい場合、ここでService Accountを作成しておきます
- metadata:
name: myapp
namespace: demo
attachPolicyARNs:
- "arn:aws:iam::aws:policy/AWSAppMeshEnvoyAccess"
- "arn:aws:iam::aws:policy/AWSCloudMapDiscoverInstanceAccess"
- "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
- "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess"
これを以下のように指定してください。
$ eksctl cluster -f - <<EOF
<cluster.yaml内容>
EOF
OR
$ eksctl create -f cluster.yaml
その後必要なアプリケーションをインストールすることになります。上記の例だとFargateで動かすかどうかをNamespaceで決めるため、Namespaceを間違えないようにしましょう。
例えば、AppMeshコントローラはFargateで動かないので、 cluster.yaml
の fargateProfiles[].selectors[].namespace
で指定されていない Namespace (appmesh-system) にインストールすることで、結果的にManaged Node Groupにスケジュールされるようにします。
例えば helmfile を利用する場合は以下のような構成になります。
repositories:
- name: eks
url: https://aws.github.io/eks-charts
- name: flagger
url: https://flagger.app
releases:
- name: appmesh-controller
chart: eks/appmesh-controller
namespace: appmesh-system
- name: appmesh-inject
namespace: appmesh-system
chart: eks/appmesh-inject
- name: podinfo
chart: flagger/podinfo
namespace: "{{ $podinfo_ns }}"
needs:
- appmesh-system/appmesh-controller
- appmesh-system/appmesh-inject
$ helmfile apply
関連Issue
まとめ
プロダクション利用を目標に EKS on Fargateに入門してみました。
現時点ではManaged Node Groupと併用し、Kubernetesマニフェスト、Helm Chart、Kustomizationなどを修正してまわればプロダクション利用できそう、といったところです。ただし、「あらゆるケースでプロダクション利用すべき」とは言ってないことに注意してください。
ノードの管理から解放されるのは本当にうれしいのですが、そのためにユーザ側でいくつもサイドカーを仕込む対応をしなければならない点がネックです。K8s・EKSをヘヴィーに使っているところほどそのための作業量が多くなり、移行は大変になるのではないでしょうか。
また、その対応は今後FirelensがEKS on Fargate対応したり、EKS on Fargate上のPodのログやメトリクスを取得するAWS APIが追加されたりすると不要になる可能性が高いので、移行が難しいものに関してはそれを待つというのも手だと思います。待つ場合は、 Containers Roadmapに要望を上げるのをお忘れなく!
一方で、EKSを使い始めたばかりで2・3しかDeploymentやJobなどがない、というような方は、早めにEKS on Fargateに移行すると運用が楽になる可能性があります。
ただし、Managed Node Groupを併用する場合は注意してください。現状、WebアプリなどのワークロードにEKS on Fargateを利用するとしても、それをALB/NLBなどでインターネットやVPC内に公開する場合は、Managed Node Groupを併用したほうがいい、という状況です。そうなると、結局ノードの管理から完全には解放されません。管理対象のノードが減るので全く無駄というわけでもないのですが・・・それが気になる場合は、いっそALB/NLBがEKS on Fargateに対応するまで待って、今はManaged Node Groupに一本化する、というのも十分に考えられそうです。
ALB/NLBを使わない、Deploy/Jobの種類が少ないサービスであれば、現時点でEKS on Fargateに移行するメリットは大きいといえます。バッチワークロードなどにEKSを使っていたケースでしょうか。繰り返しになりますが、移行対象が少なく、余裕がある場合は、そういった限られた範囲から徐々にEKS on Fargateに移行し始めて、ログやメトリクスス・ALB・NLB周りの改善を待つと効率がよさそうですね。