入門 EKS on Fargate

プロダクションで 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](https://aws.amazon.com/jp/blogs/aws/amazon-eks-on-aws-fargate-now-generally-available/)

# 前提

EKS on Fargateに関しては既に著名な方々による解説記事が存在するため、それを参照ください。この記事では、他の記事を補足するような内容に集中します。

- EKSはわかるけど、EKS on Fargateはわからん、という方は @inductor さんの[ファーストインプレッション資料](https://inductor.hatenablog.com/entry/eks-for-fargate)が参考になります。 
- Kubernetesはわかるけど EKS on Fargateはわからん、という方は aoyama3 の [EKS on Fargate：virtual-kubelet の違い + Network/LB 周りの調査 - @amsy810's Blog](https://amsy810.hateblo.jp/entry/2019/12/04/151642) が参考になります。

EKSわからん、という方は手前味噌ですが[Amazon Elastic Container Service for Kubernetes (EKS)の所感](https://qiita.com/mumoshu/items/30ade68e8cb834b2e5c7)が参考になるかもしれません。

# 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](https://github.com/aws/containers-roadmap/issues)経由で要望を上げましょう。

# 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スレ](https://twitter.com/mumoshu/status/1202133251931832321)に書いてありますが、特に重要だと思われた点を個別に説明していきます。

## 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[]` の最初が皆さんのアプリケーションで、２つ目と３つ目がそれぞれFluentbitとDatadog Agentです。ログをアプリケーションからFluentbitにわたすために、emptyDirボリュームを作成しアプリとFluentbitコンテナの両方マウントしていますね。

```yaml
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スレを見てください。

https://twitter.com/mumoshu/status/1206820508190142464

いちいちマニフェストを書き直していくのが面倒な場合、サイドカーインジェクションをするという手はあると思います。OSSを利用するのであれば、[tumblr/k8s-sidecar-injector](https://github.com/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](https://aws.amazon.com/jp/blogs/containers/eks-managed-node-groups/) + 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](https://github.com/kubernetes-sigs/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のコントローラ](https://github.com/aws/aws-app-mesh-controller-for-k8s
)はこの制限によりFargateにデプロイできませんでした。

参考: https://twitter.com/mumoshu/status/1204715698842300422

# eksctlでManaged Node GroupとFargateを併用する

```console
$ eksctl create cluster --fargate
```

のようなコマンドでFargateを有効化したクラスタを作る方法がよく紹介されていると思います。

前述のポイントを踏まえると、現状EKS on Fargateが対応している機能を色々と試すためには以下のような cluster.yaml を書くことになります。これを必要に応じて書き換えてみてください。

```yaml: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"
```

これを以下のように指定してください。

```console
$ 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](https://github.com/roboll/helmfile/) を利用する場合は以下のような構成になります。

```yaml:helmfile.yaml
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
```

```console
$ helmfile apply
```

# 関連Issue

- [[EKS/Fargate] Automatic logging configuration for pods on Fargate · Issue #618 · aws/containers-roadmap](https://github.com/aws/containers-roadmap/issues/618)

# まとめ

プロダクション利用を目標に EKS on Fargateに入門してみました。

現時点ではManaged Node Groupと併用し、Kubernetesマニフェスト、Helm Chart、Kustomizationなどを修正してまわればプロダクション利用できそう、といったところです。ただし、「あらゆるケースでプロダクション利用すべき」とは言ってないことに注意してください。

ノードの管理から解放されるのは本当にうれしいのですが、そのためにユーザ側でいくつもサイドカーを仕込む対応をしなければならない点がネックです。K8s・EKSをヘヴィーに使っているところほどそのための作業量が多くなり、移行は大変になるのではないでしょうか。

また、その対応は今後FirelensがEKS on Fargate対応したり、EKS on Fargate上のPodのログやメトリクスを取得するAWS APIが追加されたりすると不要になる可能性が高いので、移行が難しいものに関してはそれを待つというのも手だと思います。待つ場合は、 [Containers Roadmap](https://github.com/aws/containers-roadmap)に要望を上げるのをお忘れなく！

一方で、EKSを使い始めたばかりで２・３しか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周りの改善を待つと効率がよさそうですね。