839の日記

業務とは関係ない趣味の話を書くブログです。

EKS on FargateでALBからアプリにアクセスする

f:id:husq:20191208153743p:plain

金額的に自分の趣味でEKSやFargateを使うことはないのですが、興味があったので少し触ってみました。
現時点ではドキュメントが揃っておらず、試行錯誤が必要だったので記事に残しておきます。

...と思ったのですが、AWS Advent Calendarが代わりに投稿できそうだったので7日目として投稿します。
qiita.com

この記事で紹介されていることは以下のリポジトリで簡潔に確認することができます。
GitHub - 8398a7/eks-on-fargate

書いてあること

今回紹介する内容のまとめです。
検証はmacOS上から行っています。

ツールの準備

下記のドキュメントを読んでaws-cli, kubectl, eksctlを利用できるようにしてください。
eksctl の開始方法 - Amazon EKS

eksctlが既に入っている場合でもfargateクラスタを作成するためには0.11以上が必要です。
必要に応じてbrew upgradeしてください。

$ > eksctl version 
[]  version.Info{BuiltAt:"", GitCommit:"", GitTag:"0.11.1"}

クラスタの準備

eksctl create cluster poc --fargate

eksctlで作成するとそのクラスタで利用するVPCやsubnetの設定を行ってくれます。
下記で触れるドキュメントでsubnet tagにannotationをする手順が書かれていますが、eksctl経由で作成した場合は既に付与されているため不要な手順です。

具体的には以下の部分です。

Key Value
kubernetes.io/role/elb 1
kubernetes.io/role/internal-elb 1

クラスタの作成は割と時間がかかるので、とりあえず叩いて他事をするのがお勧めです。

クラスタが作成された直後はcorednsのpodが動いており、これらもFargateで動作しています。

$ > kubectl get po -A
NAMESPACE     NAME                       READY   STATUS    RESTARTS   AGE
kube-system   coredns-6d75bbbf58-5vhpf   1/1     Running   0          5m18s
kube-system   coredns-6d75bbbf58-zttkt   1/1     Running   0          5m18s
$ > kubectl get node
NAME                                                         STATUS   ROLES    AGE     VERSION
fargate-ip-192-168-175-13.ap-northeast-1.compute.internal    Ready    <none>   5m1s    v1.14.8-eks
fargate-ip-192-168-179-161.ap-northeast-1.compute.internal   Ready    <none>   5m17s   v1.14.8-eks

Fargateで動かすためにはnamespace単位で設定する必要がありますが、eksctlで作成した場合はdefault, kube-systemのpodがFargateで起動するようになっています。

f:id:husq:20191207171347p:plain

上記以外のnamespaceでpodを動かす場合は現在はnodeが存在しないのでpendingのまま待たされる、といった挙動になります。

ALB Ingress Controllerのセットアップ

公式ドキュメントは ALB Ingress Controller on Amazon EKS - Amazon EKS ですが、EC2で動かす前提で書かれており、Fargateのみの構成では動きません。
具体的にはFargateのpodでIAMの権限を渡す方法がEC2の場合とFargateの場合で異なるのが原因です。

一旦簡易的な方法としてalb-ingress-controllerのDeploymentに直接key/secretを書く方法を紹介します。
prodでは推奨されていない方式なので、ちゃんと使う場合はiam-for-podsを利用しましょう。
refs: Authentication Issues On EKS Cluster with Fargate Policy · Issue #1092 · kubernetes-sigs/aws-alb-ingress-controller · GitHub
iam-for-podsを利用する方式は後述しますが、一旦key/secret方式で続けます。

以下の手順でkey/secretを取得します。
jqを利用しているので、予め brew install jq を済ませておいてください。

# ALBを操作するためのpolicyを作成
curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.4/docs/examples/iam-policy.json
policyArn=$(aws iam create-policy \
  --policy-name ALBIngressControllerIAMPolicy \
  --policy-document file://iam-policy.json | jq -r .Policy.Arn)
rm iam-policy.json

# ALBを操作するためのkey/secret払い出しユーザの作成
aws iam create-user --user-name pocUser
# 先程作成したユーザにALBのpolicyを紐付け
aws iam attach-user-policy --user-name pocUser --policy-arn $policyArn
# ユーザのkey/secretを払い出す
aws iam create-access-key --user-name pocUser

最後のコマンドで以下のような出力が得られるのでAccessKeyIdとSecretAccessKeyをメモしておいてください。

{
    "AccessKey": {
        "UserName": "pocUser",
        "AccessKeyId": "key",
        "Status": "Active",
        "SecretAccessKey": "secret",
        "CreateDate": "2019-12-07T08:30:11Z"
    }
}

ここまで整ったらrbac-roleとalb-ingress-controllerをdeployしていきます。
rbac-roleに関してはドキュメント通りの手順でdeployします。

kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.4/docs/examples/rbac-role.yaml

alb-ingress-controllerに関しては以下のような修正をしたyamlを手元に用意しdeployしてください。 修正点は3点です。

  • vpc_idの指定
  • keyの指定
  • secretの指定
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/name: alb-ingress-controller
  name: alb-ingress-controller
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: alb-ingress-controller
  template:
    metadata:
      labels:
        app.kubernetes.io/name: alb-ingress-controller
    spec:
      containers:
        - name: alb-ingress-controller
          image: docker.io/amazon/aws-alb-ingress-controller:v1.1.4
          args:
            - --ingress-class=alb
            - --cluster-name=poc
            - --aws-region=ap-northeast-1
            - --aws-vpc-id=vpc-xxxx # eksctlで作成されたVPCのid
          env:
            - name: AWS_ACCESS_KEY_ID
              value: # さっきメモしたkey
            - name: AWS_SECRET_ACCESS_KEY
              value: # さっきメモしたsecret
          resources: {}
      serviceAccountName: alb-ingress-controller

GitHub上ではv1.1.4タグでもv1.1.3のイメージを利用するようになっているので、v1.1.4に書き換えてあります。
修正できたら以下のコマンドでdeployし、alb-ingress-controllerがRunningになるまで待ちましょう。

kubectl apply -f alb-ingress-controller.yaml
watch -n1 kubectl get po -n kube-system

次にnginxをALBからアクセスするためのyamlを記述します。

---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: nginx
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
  labels:
    app: nginx
spec:
  rules:
    - http:
        paths:
          - path: /*
            backend:
              serviceName: nginx
              servicePort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 1
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - image: nginx
          name: nginx
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
  type: ClusterIP
  selector:
    app: nginx

ポイントは alb.ingress.kubernetes.io/target-type: ip のannotationsをつけることです。
ingressリソースが作られるとalb-ingress-controllerに以下のようなログが出ます。
ログは kubectl logs -n kube-system $(kubectl get po -n kube-system -o name | grep alb | cut -d/ -f2) -f で確認してください。

default/nginx: granting inbound permissions to securityGroup sg-0c4ff2ae847363695: [{    FromPort: 80,    IpProtocol: "tcp",    IpRanges: [{        CidrIp: "0.0.0.0/0",        Description: "Allow ingress on port 80 from 0.0.0.0/0"      }],    ToPort: 80  }]
default/nginx: creating LoadBalancer 0d836fa6-default-nginx-ef8b

ingressの結果を確認してブラウザでアクセスしてみましょう。

$ > kubectl get ing
NAME    HOSTS   ADDRESS                                                                 PORTS   AGE
nginx   *       0d836fa6-default-nginx-ef8b-28953798.ap-northeast-1.elb.amazonaws.com   80      4m2s
$ > open http://$(kubectl get ing -o jsonpath='{.items[].status.loadBalancer.ingress[].hostname}')

nginxのページが表示できたら成功です。
ここまでで起動されたhost数は4台でした。

$ > kubectl get node
NAME                                                         STATUS   ROLES    AGE     VERSION
fargate-ip-192-168-111-46.ap-northeast-1.compute.internal    Ready    <none>   9m26s   v1.14.8-eks
fargate-ip-192-168-126-238.ap-northeast-1.compute.internal   Ready    <none>   8m4s    v1.14.8-eks
fargate-ip-192-168-151-139.ap-northeast-1.compute.internal   Ready    <none>   16m     v1.14.8-eks
fargate-ip-192-168-170-198.ap-northeast-1.compute.internal   Ready    <none>   16m     v1.14.8-eks

以下の手順でリソースを掃除してください。

kubectl delete -f app.yaml # nginxのサンプルアプリ
kubectl delete -f https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.4/docs/examples/rbac-role.yaml
kubectl delete -f https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.4/docs/examples/alb-ingress-controller.yaml
eksctl delete cluster poc
userId=$(aws sts get-caller-identity | jq -r .UserId)
aws iam delete-policy --policy-arn arn:aws:iam::${userId}:policy/ALBIngressControllerIAMPolicy

pocUserに関してはaws consoleから手動で削除をお願いします。

target-type: ipの挙動

alb.ingress.kubernetes.io/target-type: ip というannotationが初見だったので、どういう挙動なのか見てみました。
LBが作られた際にターゲットグループが種類: ipで作られてFargate hostのprivate ipがroutingされていました。

f:id:husq:20191208145857p:plain

試しにdeploymentのreplicasを1->2に更新してみると以下のようにログが出て登録済みターゲットが自動的に更新されていました。

Adding targets to arn:aws:elasticloadbalancing:ap-northeast-1:xxx:targetgroup/yyy/zzz
modifying rule 1 on arn:aws:elasticloadbalancing:ap-northeast-1:xxx:listener/app/yyy/zzz
default/nginx: rule 1 modified with conditions [{    Field: "path-pattern",    Values: ["/*"]  }]

replicasを1に戻すとちゃんと外してくれます。

default/nginx: Removing targets from arn:aws:elasticloadbalancing:ap-northeast-1:xxx:targetgroup/yyy/zzz: 192.168.147.236:80, 192.168.106.177:80
default/nginx: modifying rule 1 on arn:aws:elasticloadbalancing:ap-northeast-1:xxx:listener/app/yyy/zzz
default/nginx: rule 1 modified with conditions [{    Field: "path-pattern",    Values: ["/*"]  }]

この辺は当たり前とはいえば当たり前ですが、ちゃんとintegrationされてて良いですね。
実装を読んでいないですが、target-type: ipのannotationがついたingresswatchしてsvc->deployまで辿ってreplicasを監視しているのでしょうか。

ターゲットグループ設定を見てみると、登録解除の遅延がデフォルト300秒で作られており、外されるまでの時間差が少し気になりました。
annotationで指定できるのかな、と調べてみたところドキュメントにちゃんと載っており、試してみると反映されていました。

default/nginx: Modifying TargetGroup arn:aws:elasticloadbalancing:ap-northeast-1:xxx:targetgroup/yyy/zzz attributes to [{    Key: "deregistration_delay.timeout_seconds",    Value: "30"  }].
default/nginx: modifying rule 1 on arn:aws:elasticloadbalancing:ap-northeast-1:xxx:listener/app/yyy/zzz
default/nginx: rule 1 modified with conditions [{    Field: "path-pattern",    Values: ["/*"]  }]

alb.ingress.kubernetes.io/target-group-attributes: deregistration_delay.timeout_seconds=30
refs: Annotation - AWS ALB Ingress Controller
300秒は長すぎ、短すぎという場合もちゃんと設定できるようになってて良いですね。

ここでpodが消えるタイミングとLBから外されるタイミングは「いい感じ」になっているのかと思って調べてみました。
ここで言う「いい感じ」とは

  1. ターゲットグループから外されるまでpodのterminateが待機される
  2. ターゲットグループから外されたことを確認してpodのterminateが実行される

ということです。
実際試してみると、podが削除と同時にターゲットグループの削除処理が走るようになっていました。
これでは外されるまでデフォルトの300秒delayのときに正常に動作しないのでは?と確かめてみたところ、一瞬504 gateway timeoutが出る挙動を確認しました。
ただし一瞬だけですぐに200しか返ってこなくなりました。

ターゲットグループをあまり使ったことがないので推測になりますが、deregistrationが走るとアクセスが新規に来ることはないものの、
podの削除が先行して走り、LB側で外す前にアクセスを流されてしまうと504が出るケースがあるのではないかと思います。
HPAをしっかり使っているアクセスが多いサービスのケースだと504はそこそこ出てしまうかもしれません。

これは「ing->svc->deployのreplicas数が変更していたらターゲットグループ変更」という検知パターンと相性が悪そうだと思います。
検知パターンの周期の合間に入ってpodが削除される方が先になるケースは多々あると思うので、504は避けにくいです。
deployment定義の preStop でalb-ingress-controller側が十分検知できる時間のsleepを挟んでpodを止めるようにする、などがワークアラウンドになるかもしれません。
十分に検証できていないポイントなのでもっと良い方法があればコメント等をもらえると嬉しいです。

hostの割当時間

大体30秒弱でステータスがPendingからContainerCreatingになります。
そこからイメージのpullが始まり、Runningになるまで大体1分弱といった感じでした。
今回はnginxイメージで非常に小さいサイズなのでpull時間があまり影響しませんでしたが、1GB+のイメージpullは少し時間がかかるかもしれません。
とはいっても、EKSというよりFargateの特性に由来するものなのでECSを使われている場合は既知かもしれませんが…。

iam-for-pods

先にkey/secretを使った方法を紹介しましたが、iam-for-podsを使った方法も紹介します。
もしもkey/secretを使う方式で試していたら一旦alb-ingress-controllerのリソースを念の為削除してください。

kubectl delete -f https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.4/docs/examples/rbac-role.yaml
kubectl delete -f https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.4/docs/examples/alb-ingress-controller.yaml

クラスタに対してoidc providerを紐付けてからALBのIAM権限を付与します。
policyArnは上で作成したALBIngressControllerIAMPolicyが存在していればそれを流用してください。

eksctl utils associate-iam-oidc-provider --region=ap-northeast-1 --cluster=poc --approve
curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.4/docs/examples/iam-policy.json
policyArn=$(aws iam create-policy \
  --policy-name ALBIngressControllerIAMPolicy \
  --policy-document file://iam-policy.json | jq -r .Policy.Arn)
eksctl create iamserviceaccount --name alb-ingress-controller \
  --namespace kube-system \
  --cluster poc \
  --attach-policy-arn ${policyArn}  \
  --approve --override-existing-serviceaccounts

作成すると、以下のような情報が取れます。

$ > kubectl get sa -n kube-system alb-ingress-controller -o jsonpath="{.metadata.annotations['eks\.amazonaws\.com/role-arn']}"
arn:aws:iam::xxx:role/eksctl-poc-addon-iamserviceaccount-ku-Role1-CBGA2Q1975Q9

次にalb-ingress-controller.yamlをデプロイします。
先ほどと違ってenvでkey/secretの指定を行いません。

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/name: alb-ingress-controller
  name: alb-ingress-controller
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: alb-ingress-controller
  template:
    metadata:
      labels:
        app.kubernetes.io/name: alb-ingress-controller
    spec:
      serviceAccountName: alb-ingress-controller
      containers:
        - name: alb-ingress-controller
          image: docker.io/amazon/aws-alb-ingress-controller:v1.1.4
          args:
            - --ingress-class=alb
            - --cluster-name=poc
            - --aws-region=ap-northeast-1
            - --aws-vpc-id=vpc-xxxx # eksctlで作成されたVPCのid
          resources: {}
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.4/docs/examples/rbac-role.yaml
kubectl apply -f alb-ingress-controller.yaml

alb-ingress-controllerのpodが作成されたら上記で記載したnginxを動かすためのリソースをapplyしてください。
envでkey/secretを指定しない場合と同様にLBが作られ、アクセスできるようになっています。

最初はoidc id providerの概念がややこしそうだったので敬遠していたのですが、このあたりも割と楽に設定できるようになっていますね。
eksctlでiamserviceaccountを作ると裏側でCloudFormationが動いているのですが、中ではoidc id providerへの紐付けをやってくれているようです。

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "IAM role for serviceaccount \"kube-system/alb-ingress-controller\" [created and managed by eksctl]",
  "Resources": {
    "Role1": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Statement": [
            {
              "Action": [
                "sts:AssumeRoleWithWebIdentity"
              ],
              "Condition": {
                "StringEquals": {
                  "oidc.eks.ap-northeast-1.amazonaws.com/id/yyy:aud": "sts.amazonaws.com",
                  "oidc.eks.ap-northeast-1.amazonaws.com/id/yyy:sub": "system:serviceaccount:kube-system:alb-ingress-controller"
                }
              },
              "Effect": "Allow",
              "Principal": {
                "Federated": "arn:aws:iam::xxx:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/yyy
              }
            }
          ],
          "Version": "2012-10-17"
        },
        "ManagedPolicyArns": [
          "arn:aws:iam::xxx:policy/ALBIngressControllerIAMPolicy"
        ]
      }
    }
  },
  "Outputs": {
    "Role1": {
      "Value": {
        "Fn::GetAtt": "Role1.Arn"
      }
    }
  }
}

eksctl経由で作成したSAはrbac-role.yamlをapplyすることによりannotationが消えるのではないか、と思っていましたが残っていました。

ハマったポイント

所感

EC2を管理したくないという観点ではFargateで完結できるようになっているので良さそうです。
LBのターゲットグループからの削除処理とpodが消えるタイミングでエラーが出るのは現状ワークアラウンドが必要そうだと感じました。

実際全部Fargateにするかと言われるとコスパはあまり良くないように感じました。
alb ingress controllerを始めとしたk8sの中を整えるようなpodは1host 1podがオーバースペックなので、そういったpodはまとめて1つのnodeで動かしたいように感じます。
他の記事でも言及されていますが、Daemonsetでfluentdを動かしておく、みたいなこともできないのでログ収集をsidecarで配置する必要があるというのも実運用では手間になるかもしれません。