Closed17

EKS on FargateでAWS Load Balancer Controllerを動作させる(eksctlでやる⇒Terraform化する)

jimatomojimatomo

EKSをTerraformでクラスタやRDS含めて基盤を作っていろいろやろうとして敗北したので、eksctlでちゃんとチュートリアルをやってみたうえで再度Terraform化にチャレンジしたいと思います。

Terraform化したいモチベーションはIaCを使ってブラックボックス化せずに、Terraformのコードを見ればなんとなくそのインフラ構成がわかるようにして、CI/CDの仕組みに乗せて継続的な改善ができるようにしたいなという想いがあるからです。

とはいえ、AWS Load Balancer Controllerのところで敗北して、何が悪いのかわからなくなり、詰んでしまったので、ちゃんとeksctlで動くものを作ってから応用編に突入しようと思っています。

今回は以下のバージョンでやってみます。
・kubectl:1.24.7
・eksctl:0.134.0
・EKS:1.24
・Helm:3.11.2

環境はCloudShellです。
デフォルトでAWS CLIとかちゃんと入っていたり、IAMの権限はAdministratorの権限がついていてなんでもできる状態です。

参考にするチュートリアルは以下です。
https://aws.amazon.com/jp/premiumsupport/knowledge-center/eks-alb-ingress-controller-fargate/

jimatomojimatomo

いろいろ準備系のコマンド(各所から寄せ集めなので、ちゃんとしてなくてすみません)
※どうせ入れるのでTerraformも入れてます(なんか依存関係で入ってくるライブラリが必要だったりするので、入れておきます)

CloudShellで実行
#--------------------------
# Install Terraform
#--------------------------
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
sudo yum -y install terraform

#--------------------------
# Install kubectl
#--------------------------
# 永続化領域が${HOME}なのでちょっと特殊な位置にインストール
mkdir -p $HOME/.local/bin
cd $HOME/.local/bin

# download kubectl
curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl"

# add executable permission to kubectl
chmod +x ./kubectl

# add PATH
PATH=$PATH:$HOME/.local/bin:$HOME/bin
export PATH

#--------------------------
# Install Helm
#--------------------------
 # Cloudshellのインストール用ディレクトリを作成
mkdir ${HOME}/.local/helm
cd ${HOME}/.local/helm

# Download Install script
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh

# Install
cd ${HOME}/.local/helm
./get_helm.sh

#--------------------------
# Install eksctl
#--------------------------
# バイナリをダウンロードして解凍
curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp

# 移動
sudo mv /tmp/eksctl /usr/local/bin

#--------------------------
#確認
#--------------------------
terraform version

kubectl version

helm version

eksctl version
jimatomojimatomo

↑、初回だけホームディレクトリにダウンロード必要なやつと、2回目以降は単にコピーだけでよかったりする系を分けておいて、スクリプト化とかしたら、CloudShellのセットアップ楽になりそうなので、余裕があったらやりたい感ある

jimatomojimatomo

EKSクラスターを作る(ついでにVPCとかも)

CloudShellで実行
eksctl create cluster --name eksHandson --version 1.24 --fargate

お昼休憩をはさんでいる間に終わってました。
こんな感じでFargateのノードができています。(ちなみにこれはcoreDNSのPodみたいです)

kubectl get nodes
NAME                                                         STATUS   ROLES    AGE    VERSION
fargate-ip-192-168-172-98.ap-northeast-1.compute.internal    Ready    <none>   8m1s   v1.24.9-eks-300e41d
fargate-ip-192-168-181-237.ap-northeast-1.compute.internal   Ready    <none>   8m3s   v1.24.9-eks-300e41d

Fargateプロファイルは3AZにまたがっていて、Pod Selectorはdefaultとkube-systemの名前空間が割り当てられています。
Pod Selector
サブネットはプライベートサブネットが選択されています。

ちなみにサブネットはこんな感じでした

$ aws ec2 describe-subnets --query "Subnets[].[Tags[?Key=='Name'] | [0].Value,SubnetId,AvailabilityZone,CidrBlock]" --filter "Name=vpc-id,Values=${EKS_VPC_ID}" --output table
-----------------------------------------------------------------------------------------------------------------------------
|                                                      DescribeSubnets                                                      |
+-------------------------------------------------------+---------------------------+------------------+--------------------+
|  eksctl-eksHandson-cluster/SubnetPrivateAPNORTHEAST1C |  subnet-0e911c6dc138a100d |  ap-northeast-1c |  192.168.160.0/19  |
|  eksctl-eksHandson-cluster/SubnetPublicAPNORTHEAST1A  |  subnet-00a134258813a1f36 |  ap-northeast-1a |  192.168.32.0/19   |
|  eksctl-eksHandson-cluster/SubnetPublicAPNORTHEAST1C  |  subnet-0c004a333f85f4d9b |  ap-northeast-1c |  192.168.64.0/19   |
|  eksctl-eksHandson-cluster/SubnetPublicAPNORTHEAST1D  |  subnet-088318dd5d1cad36e |  ap-northeast-1d |  192.168.0.0/19    |
|  eksctl-eksHandson-cluster/SubnetPrivateAPNORTHEAST1A |  subnet-0610ba67feefa2591 |  ap-northeast-1a |  192.168.128.0/19  |
|  eksctl-eksHandson-cluster/SubnetPrivateAPNORTHEAST1D |  subnet-0df6ee8f2449e41c8 |  ap-northeast-1d |  192.168.96.0/19   |
+-------------------------------------------------------+---------------------------+------------------+--------------------+

※VPCは192.168.0.0/16で確保されていて、パブリックサブネットとプライベートサブネットとNATがついた状態でサブネットが作成されています。(/19のネットマスクで各AZごとに作成されていました)

jimatomojimatomo

OpenID ConnectorでサービスアカウントがIAMを使えるようにする

クラスターがサービスアカウントのために AWS Identity and Access Management (IAM) を使用することを許可します。

CloudShellで実行
eksctl utils associate-iam-oidc-provider --cluster eksHandson --approve

これで、サービスアカウントを見てみましょう。

CloudShellで実行
aws iam list-open-id-connect-providers
export OIDC_ARN=`aws iam list-open-id-connect-providers --output text --query "OpenIDConnectProviderList[0].Arn"`
aws iam get-open-id-connect-provider --open-id-connect-provider-arn $OIDC_ARN

コンソールで見てもいいです。

jimatomojimatomo

AWS Load Balancer Controller がユーザーに代わって AWS API を呼び出すことを許可する

IAMポリシーを作成

CloudShellで実行
# 一時ディレクトリに移動しておく
cd /tmp

# ポリシードキュメントのダウンロード
curl -o iam_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.4.4/docs/install/iam_policy.json

# IAMポリシーを作成する
aws iam create-policy \
   --policy-name AWSLoadBalancerControllerIAMPolicy \
   --policy-document file://iam_policy.json

サービスアカウントを作成

EKSのPodの利用するOIDCから利用可能なIAMロールを作っていきます。(という解釈であっているはず…)。そのIAMロールを利用できるService Accountも作成されています。

サービスアカウントとは正確に言うと「Kubernetes API を使用して作成、管理される Kubernetes リソースであり、Kubernetes API サーバーまたは外部サービスでの認証を行うことを目的として、クラスタ内で Kubernetes によって作成された Pod などのエンティティが使用するためのものです。」とGKEのドキュメントで紹介されています。
https://cloud.google.com/kubernetes-engine/docs/how-to/kubernetes-service-accounts?hl=ja

CloudShellで実行

# まずはAWSのアカウントIDを環境変数に取得しておきます
export AWS_ACCOUNT_ID=`aws sts get-caller-identity --output text --query "Account"`

# サービスアカウントを作成する
eksctl create iamserviceaccount \
  --cluster=eksHandson \
  --namespace=kube-system \
  --name=aws-load-balancer-controller \
  --attach-policy-arn=arn:aws:iam::${AWS_ACCOUNT_ID}:policy/AWSLoadBalancerControllerIAMPolicy \
  --override-existing-serviceaccounts \
  --approve

確認

EKSのコンソールで作成されたService Accountを見てみると
こんな感じでannotationsでロールを指定しています。
aws-load-balancer-controller

コマンドでも確認できます。

CloudShellで実行
eksctl get iamserviceaccount --cluster eksHandson --name aws-load-balancer-controller --namespace kube-system
jimatomojimatomo

Helm を使用して AWS Load Balancer Controller をインストール

注意点に関する記載について

何やらHelmだとcert-managerの問題があるらしいです。読んでもよくわからなかったので、Bingさんに聞いてみました。
「Cert-manager issues with fargate について教えてください」
↓回答です。

Bingさんに聞いてみた

(笑顔の絵文字がついていてかわいいですね。こんにちは)

これをベースに読んでいきます。

From their investigation, the issue mentioned is more related to cert-manager setup. The cert-manager-webhook deployment uses port 10250 which is also used for kubelet on the Fargate pods. Therefore when the connection was made to the cert-manager-webhook service it was reporting the error with ip address corresponding to pod cert-manager-webhook.
https://github.com/cert-manager/cert-manager/issues/3237

we tested the helm chart and I am aware that it's working on fargate as it's not using cert-manager and creating a self signed cert , we just need the documentation to reflect that it's the only way right now to deploy the controller on fargate pods to avoid confusion
https://github.com/kubernetes-sigs/aws-load-balancer-controller/issues/1606

DeepLさんもCopilotとして拡張機能として加えているので、訳してもらいました。

helmチャートをテストしたところ、cert-managerを使用せず、自己署名証明書を作成しているので、fargateで動作していることは認識しています。混乱を避けるために、fargate podsにコントローラーを展開する唯一の方法であることをドキュメントに反映させる必要があります。

とりあえず、Helmをちゃんと使うのが大事そうです。Helmは同じ轍を踏まないようにできるので本当にありがたいですね。

Helmでインストールする

Helmのリポジトリを追加

CloudShellで実行
helm repo add eks https://aws.github.io/eks-charts

TargetGroupBinding カスタムリソース定義 (CRD) をインストール

CloudShellで実行
kubectl apply -k "github.com/aws/eks-charts/stable/aws-load-balancer-controller//crds?ref=master"

Helm チャートをインストール

CloudShellで実行
# VPC IDを取得(仕様が変わるかもしれないので、ちゃんとechoで確認しましょう
export EKS_VPC_ID=`aws ec2 describe-vpcs --filters "Name=tag:Name,Values=eksctl-eksHandson-cluster/VPC" --query "Vpcs[0].VpcId" --output text`

echo $EKS_VPC_ID

helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
    --set clusterName=eksHandson \
    --set serviceAccount.create=false \
    --set region=ap-northeast-1 \
    --set vpcId=${EKS_VPC_ID} \
    --set serviceAccount.name=aws-load-balancer-controller \
    -n kube-system

こんな感じにあなれば成功。

$ kubectl get deployment -n kube-system aws-load-balancer-controller
NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
aws-load-balancer-controller   2/2     2            2           17m

※私はコマンドの環境変数(--set vpcId=${EKS_VPC_ID})の指定を失敗したので、helm upgrade ...で再度デプロイしなおしました。

トラブルシューティングはポッドを確認してログを見る感じです。

トラブルシューティングの流れ
# podを確認してみます
kubectl get pod -n kube-system 

# ↑のコマンドの実行結果から
# こんな感じのPodの名前を確認する(STATUSがCrashLoopBackOffになっていたら失敗している)
# aws-load-balancer-controller-5944755c59-n7ssp

# Podの情報を確認する
kubectl describe pod -n kube-system aws-load-balancer-controller-5944755c59-n7ssp

# ログを確認する
kubectl logs -n kube-system aws-load-balancer-controller-5944755c59-n7ssp
jimatomojimatomo

AWS Load Balancer Controller をテストする

ゲームのデプロイに必要な Fargate プロファイルを作成する

CloudShellで実行
eksctl create fargateprofile --cluster eksHandson --region ap-northeast-1 --name your-alb-sample-app --namespace game-2048

こんな感じです
新しいプロファイル

サンプルゲームをデプロイし、AWS Load Balancer Controller が ALB Ingress リソースを作成することを確認

CloudShellで実行
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.4.4/docs/examples/2048/2048_full.yaml

数分後、次のコマンドを実行することによって、Ingress リソースが作成されたことを検証

CloudShellで実行
kubectl get ingress/ingress-2048 -n game-2048

ログの確認方法

CloudShellで実行
kubectl logs -n kube-system deployment.apps/aws-load-balancer-controller
jimatomojimatomo

マネジメントコンソールを見てみたら、ALBがちゃんとできていました。
ちゃんとHTTPのエンドポイントでアクセスしたら接続できました。

2048には到達できませんでしたが…

2048ゲームでも敗北…

jimatomojimatomo

ちゃんとうまく言った理由を確認していく

さて、ここからが本番です。
まずは、今回デプロイしたアプリケーションのマニュフェストを見てみましょう。

2048_full.yaml
---
apiVersion: v1
kind: Namespace
metadata:
  name: game-2048
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: game-2048
  name: deployment-2048
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: app-2048
  replicas: 5
  template:
    metadata:
      labels:
        app.kubernetes.io/name: app-2048
    spec:
      containers:
      - image: public.ecr.aws/l6m2t8p7/docker-2048:latest
        imagePullPolicy: Always
        name: app-2048
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  namespace: game-2048
  name: service-2048
spec:
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
  type: NodePort
  selector:
    app.kubernetes.io/name: app-2048
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: game-2048
  name: ingress-2048
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: service-2048
              port:
                number: 80
jimatomojimatomo

後で違いを見直せるように各サービスの状態を取得していきます。

デプロイメントについて

$ kubectl describe deployment -n game-2048
Name:                   deployment-2048
Namespace:              game-2048
CreationTimestamp:      Tue, 21 Mar 2023 05:51:16 +0000
Labels:                 <none>
Annotations:            deployment.kubernetes.io/revision: 1
Selector:               app.kubernetes.io/name=app-2048
Replicas:               5 desired | 5 updated | 5 total | 5 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
  Labels:  app.kubernetes.io/name=app-2048
  Containers:
   app-2048:
    Image:        public.ecr.aws/l6m2t8p7/docker-2048:latest
    Port:         80/TCP
    Host Port:    0/TCP
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    True    NewReplicaSetAvailable
OldReplicaSets:  <none>
NewReplicaSet:   deployment-2048-6bc9fd6bf5 (5/5 replicas created)
Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  30m   deployment-controller  Scaled up replica set deployment-2048-6bc9fd6bf5 to 5

次にServiceについてです

$ kubectl describe service -n game-2048
Name:                     service-2048
Namespace:                game-2048
Labels:                   <none>
Annotations:              <none>
Selector:                 app.kubernetes.io/name=app-2048
Type:                     NodePort
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.100.5.175
IPs:                      10.100.5.175
Port:                     <unset>  80/TCP
TargetPort:               80/TCP
NodePort:                 <unset>  32613/TCP
Endpoints:                192.168.113.22:80,192.168.115.156:80,192.168.125.159:80 + 2 more...
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

最後にIngressについて見ていきましょう

$ kubectl describe ingress -n game-2048
Name:             ingress-2048
Labels:           <none>
Namespace:        game-2048
Address:          k8s-game2048-ingress2-a36001719e-1225590979.ap-northeast-1.elb.amazonaws.com
Ingress Class:    alb
Default backend:  <default>
Rules:
  Host        Path  Backends
  ----        ----  --------
  *           
              /   service-2048:80 (192.168.113.22:80,192.168.115.156:80,192.168.125.159:80 + 2 more...)
Annotations:  alb.ingress.kubernetes.io/scheme: internet-facing
              alb.ingress.kubernetes.io/target-type: ip
Events:
  Type    Reason                  Age   From     Message
  ----    ------                  ----  ----     -------
  Normal  SuccessfullyReconciled  35m   ingress  Successfully reconciled
jimatomojimatomo

一旦サンプルアプリの2048ゲームは削除しておきます

CloudShellで実行
kubectl delete -f https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.4.4/docs/examples/2048/2048_full.yaml

久しぶりに2048ゲームやりましたが、思ったよりも楽しかったですね。あんまり考えすぎないで頭空っぽにできるのでたまにやるにはちょうどいいゲームです(^^♪

名残惜しいですが、お金もかかるので退場してもらいましょう。

jimatomojimatomo

一旦ちゃんと動く構成も確認できたので、敗北した時の構成との違いに関してメモにしたためた後にクラスタとかを削除しちゃいます。

とりあえず気づいたこと
クラスターのサブネットにパブリックサブネットも含まれるようになっていました。

量が多いのでこっちにはのせずにローカルに管理していきます。

CloudShellで実行
# サービスアカウントの削除
eksctl delete iamserviceaccount \
  --cluster=eksHandson \
  --namespace=kube-system \
  --name=aws-load-balancer-controller

# IAMポリシーの削除
aws iam delete-policy \
   --policy-arn arn:aws:iam::${AWS_ACCOUNT_ID}:policy/AWSLoadBalancerControllerIAMPolicy

# 一応念のためEXTERNAL-IPを持つサービスがいないことを確認
kubectl get svc --all-namespaces

# クラスターの削除
eksctl delete cluster --name eksHandson
jimatomojimatomo

Terraform化に向けた準備

ちゃんと座学しましょう。
ちょっと古いのですが、Classmethodさんの記事をみてみます。
https://dev.classmethod.jp/articles/getting-started-amazon-eks-with-eksctl/

Terraform化の範囲

ちょっと時間かかりそうですが、terraformのコードを作成する戦略は以下です。
・VPCを作る
https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest

・EKSを作る
https://registry.terraform.io/modules/terraform-aws-modules/eks/aws/latest

・サービスアカウント(IRSA: IAM Role for Service Accounts)
https://registry.terraform.io/modules/terraform-aws-modules/iam/aws/latest/submodules/iam-role-for-service-accounts-eks?tab=resources

※いまさらですが、そもそもサービスアカウントとかIRSAって?という話はこちら
https://aws.amazon.com/jp/blogs/news/diving-into-iam-roles-for-service-accounts/

・Kubernetes Provider
https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs

Terraform以外のところ

HelmでAWS Load Balancer ControllerをデプロイするところからはTerraformの管理外(EKSで完結させる範囲)とします。

jimatomojimatomo

Terraform開発環境

CloudShellだと開発環境としてはしんどいので、Terraformの開発環境はローカルのVSCode + devcontainerを作って開発環境を整えます。

私のマシンはWindows 11 Home 22H2です。
WSL2はセットアップ済み
Docker Desktopもセットアップ済み
VSCodeもインストール済み

https://qiita.com/yoshii0110/items/c480e98cfe981e36dd56

Dockerfileはこちら

Dockerfile
FROM amazonlinux:2023

# Install tar, unzip, git for install tools
RUN yum install -y tar unzip git

# Install Terraform
RUN yum install -y yum-utils
RUN yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
RUN yum -y install terraform

# Install kubectl
WORKDIR /usr/local/bin

RUN curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl"
RUN chmod +x ./kubectl

# Install Helm
WORKDIR /tmp
ENV VERIFY_CHECKSUM=false

RUN curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
RUN chmod 700 get_helm.sh
RUN ./get_helm.sh

# Install eksctl
RUN curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
RUN mv /tmp/eksctl /usr/local/bin

# Install AWS CLI v2
WORKDIR /tmp

RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
RUN unzip awscliv2.zip
RUN ./aws/install
RUN rm awscliv2.zip

【2023/5/8追記】
※新しいバージョンのget_helm.shでチェックサムをするようになった影響でこけるようになりました。checksumを実行するためのものを入れるか、RUN ./get_helm.shの前で、ENV VERIFY_CHECKSUM=falseを書いておくようにしておく必要があります。上は修正しています。

AWS CLIはv2を導入しているのでAWS Identity Center (旧SSO) を利用しています。
そろそろIdentity Centerに寄せていこうかしらと思っている今日この頃です。
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/sso-configure-profile-token.html#sso-configure-profile-token-auto-sso

jimatomojimatomo

TerraformでAWS SSOの権限で実行するときにはちょっと癖がある

aws configure ssoでSSOを実行するときにセッション名を入れたりするのですが、これがTerraformだとサポートされていないようでした。

Initializing modules...
╷
│ Error: error configuring S3 Backend: Error creating AWS session: profile "default" is configured to use SSO but is missing required configuration: sso_region, sso_start_url
│ 
│ 

以下の形式にconfigファイルを修正して再度aws sso loginをし直したらいけました。

~/.aws/config
[default]
sso_account_id = <account_id>
sso_role_name = <sso_role_name>
region = ap-northeast-1
output = json
sso_start_url = https://<your_sso_id>.awsapps.com/start#
sso_region = ap-northeast-1
jimatomojimatomo

Terraform化したコードはこちら

動作確認まで完了しました。
https://github.com/jimatomo/eks-handson

これで敗北したAWS Load Balancer Controllerがちゃんと動作する環境まで作れるようになったので、このEKSの環境をベースに検証していきたいと思います。

このスクラップは2023/03/26にクローズされました