😋

EKSで作成したALBでgRPCのパスベースルーティングする

2021/12/10に公開

概要

  • IFにgRPCを採用したマイクロサービスをEKS化する手順
    • マイクロサービスは複数あり、ALBからパスベースルーティングでリクエスト
  • EKSの作成からgRPCクライアントによる疎通まで一気通貫でまとめ
  • 内容の勉強も兼ねてeksctl は使わずに可能な限りマネジメントコンソール上で作成します

参考にした記事

大まかな流れ

  • やることが多くて全体感が掴みにくいのでざっくりと、EKS準備、EKS作成、ALB準備、Service準備、Service作成、動作確認で分けて考えます

EKS準備

  • EKSを操作する(kubectlを実行する)EC2の準備
  • EKSを動作させるVPC、サブネット等の準備

EKS作成

  • EKSクラスターの作成
  • OIDCプロバイダーの作成
  • EKSクラスターへアドオンを追加
  • ノードグループの作成

ALB準備

  • EKSクラスターへAWS Load Balancer Controllerをインストール
  • ALB用のドメイン取得
  • ALBに登録するTLS証明書の作成
  • externalDNS設定

Service準備

  • サービスアカウントのIAMロールとポリシーの作成
  • DockerレジストリへマイクロサービスのコンテナイメージをPush

Service作成

  • EKSにマイクロサービスをデプロイ
  • Ingressをデプロイ
    • 自動的にALBが生成されます
    • Route53にDNS登録もされます

動作確認

  • gRPCクライアント(Python)からのポスト

EKS準備

EC2の作成

  • マシンパワーは不要なので t3.micro で作成
  • OSはAmazon Linux2
  • 用途はEKSクラスタの作成、kubectlによる操作、ECRレポジトリへのプッシュ

IAMロールの作成

  • 「IAM」→「ロール」→「ロールの作成」
    • ユースケースでEC2を選択
    • AWS管理のポリシーからAmazonEC2ContainerRegistryPowerUserを選択
      • このEC2でECRレポジトリへのプッシュをしないのであれば不要
    • インラインポリシーで以下を追加
    {
        "Version": "2012-10-17",
        "Statement": [
    	{
    	    "Effect": "Allow",
    	    "Action": [
    		"eks:DescribeCluster",
    		"eks:CreateCluster"
    	    ],
    	    "Resource": "*"
    	}
        ]
    }
    

必要なアプリのインストール

  • AWS公式によると This guide requires that you use version 2.3.7 or later or 1.22.8 or later.とのことなのでAWS CLIを更新

    curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
    unzip awscliv2.zip
    sudo ./aws/install
    aws --version
    
  • 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"
    chmod +x ./kubectl
    sudo mv ./kubectl /usr/local/bin/kubectl
    kubectl version --client --short
    
  • docker インストール

    sudo yum update -y
    sudo yum install -y docker
    sudo usermod -aG docker $USER
    sudo systemctl start docker.service
    sudo systemctl enable docker
    docker -v
    
  • (ここでは使いませんが必要になったときのために念のため)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
    eksctl version
    

VPC等の準備

  • VPC及び複数AZにサブネットを作成します
    • EKSクラスターを作成するには2つ以上のAZでサブネットを用意する必要があります
  • 注意するのは以下の点
    • パブリックサブネットのタグに以下を設定
      • Key: kubernetes.io/role/elb
      • Value: 1
    • プライベートサブネットのタグに以下を設定
      • Key: kubernetes.io/role/internal-elb
      • Value: 1
    • プライベートサブネットにNATゲートウェイへのルートを設定する

EKS作成

クラスター作成

クラスター用のロール作成

  • マネジメントコンソールで「IAM」→「ロール」→「ロールの作成」
    • ユースケースの選択で「EKS - Cluster」を選択
    • 自動で信頼関係の作成もAmazonEKSClusterPolicyのアタッチもされる

クラスター作成

  • EC2上で以下のように実行(clusterやroleの名称、subnetIdsは適宜変更)

    aws eks create-cluster \
    --region ap-northeast-1 \
    --name eks-cluster-name \
    --kubernetes-version 1.21 \
    --role-arn arn:aws:iam::xxxxxxxxxxxx:role/eks-cluster-role \
    --resources-vpc-config subnetIds=subnet-05bd5fc3f34a8e495,subnet-0c8c82ba3f745b7f5,subnet-0ce8461ce308040a0,subnet-091ee2c5a14b5713d,subnet-0382411db769d634e,subnet-0d1de987a451d32b8
    
  • マネジメントコンソールからEKSクラスターを作成すると、そのユーザーが作成者となるため操作用のEC2から手出しできなくなるのでEC2で作成

    • 操作用のEC2に同じユーザーを設定できるなら問題ないはずです
    • ConfigMapをちゃんとする場合はこちらを参考に
  • kubeconfigを作成し、EC2からEKSをコントロールできるようにします

    aws eks update-kubeconfig \
      --region ap-northeast-1 \
      --name eks-cluster-name
    
  • 状態が取得できることを確認

    $ kubectl get svc
    NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
    kubernetes   ClusterIP   10.100.0.1   <none>        443/TCP   10m
    

IAM OIDC プロバイダーを作成する

  • この後のCNIプラグインやAWS Load Balancer Controller、externalDNSといったサービスのIAMロールで使うため先に作成します

    • OpenIDを使ってIAMロールを引き受けて各種アクセス権を取得する感じ
  • マネジメントコンソールで「EKS」→「クラスター」→「作成したクラスター」を選択し、詳細画面からOpenID ConnectプロバイダーURLをコピーします

  • マネジメントコンソールで「IAM」→「IDプロバイダ」→「IDプロバイダを作成」を選択し、OpenID Connectを選び、上記のURLをコピーし、サムプリントを取得

    • 対象者に sts.amazonaws.com を入力

クラスターへアドオンを追加

CNIプラグイン追加

  • VPCからPodにIPアドレスを設定するため、EKS標準のamazon-vpc-cni-k8sを使います

  • ロールの作成

    • 「IAM」→「ロール」→「ロールの作成」で以下のようにする(IDプロバイダーはEKSクラスターのOIDCプロバイダーURL)
    • ポリシーはAmazonEKS_CNI_Policyをアタッチ
    • 名前をつけてロールを作成し、再度そのロールの画面を開きます
    • 「信頼関係」タブから「信頼関係の編集」をクリック
      • audsub に変更
      • sts.amazonaws.comsystem:serviceaccount:kube-system:aws-node に変更
  • マネジメントコンソールからEKSクラスターを開き、「設定」→「アドオン」→「新規を追加」をクリックし作成したロールを設定します

coredns と kube-proxy 追加

  • CNI同様にマネジメントコンソールから追加します
    • こちらはロールは不要

ノード作成

ノード用のロール作成

  • マネジメントコンソールにて「IAM」→「ロール」→「ロールの作成」
    • 信頼されたエンティティは「AWSサービス」のまま、ユースケースで「EC2」を選択
    • 次のステップでAWS管理ポリシーから以下をアタッチします
      • AmazonEKSWorkerNodePolicy
      • AmazonEC2ContainerRegistryReadOnly

ノードグループ作成

  • マネジメントコンソールにて「EKS」→「クラスター」→「作成したクラスター名」
    • 設定→コンピューティングから「ノードグループの追加」をクリック
    • ノードIAMロールに上で作成したノード用のロールを設定
    • インスタンスタイプやスケール設定はよしなに

確認

  • 以下コマンドで作成されたことを確認
    $ kubectl get nodes
    NAME                                              STATUS   ROLES    AGE   VERSION
    ip-172-31-127-7.ap-northeast-1.compute.internal   Ready    <none>   23s   v1.21.5-eks-bc4871b
    ip-172-31-46-89.ap-northeast-1.compute.internal   Ready    <none>   35s   v1.21.5-eks-bc4871b
    

ALB準備

AWS Load Balancer Controllerインストール

IAMポリシー作成

  • 「IAM」→「ポリシー」→「ポリシーを作成」
    • JSONタブを選択しこちらの内容をコピペ
    • 名前を付けて「ポリシーの作成」を押下

IAMロール作成

  • 「IAM」→「ロール」→「ロールの作成」で以下のようにする(IDプロバイダーはEKSクラスターのOIDCプロバイダーURL)

  • ポリシーは上記で作成したものをアタッチ

  • 名前をつけてロールを作成し、再度そのロールの画面を開きます

  • 「信頼関係」タブから「信頼関係の編集」をクリック

    • 信頼先に"oidc.eks.ap-northeast-1.amazonaws.com/id/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:sub": "system:serviceaccount:kube-system:aws-load-balancer-controller"を追加
      • IDは↑のOIDCプロバイダーURLのID

サービスアカウント作成

  • aws-load-balancer-controller-service-account.yamlを作成
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      labels:
        app.kubernetes.io/component: controller
        app.kubernetes.io/name: aws-load-balancer-controller
      name: aws-load-balancer-controller
      namespace: kube-system
      annotations:
          eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT-ID:role/AmazonEKSLoadBalancerControllerRole
    
  • 適用
    kubectl apply -f aws-load-balancer-controller-service-account.yaml
    

AWS Load Balancer Controllerインストール

  • cert-managerインストール

    kubectl apply \
    --validate=false \
    -f https://github.com/jetstack/cert-manager/releases/download/v1.5.4/cert-manager.yaml
    
  • コントローラのyaml取得

    curl -Lo v2_3_0_full.yaml https://github.com/kubernetes-sigs/aws-load-balancer-controller/releases/download/v2.3.0/v2_3_0_full.yaml
    
  • 上記yamlを修正

    • 480行目あたりにある下記のServiceAccount sectionを削除します(さきほど適用したサービスアカウントが上書きされてしまうため)
      apiVersion: v1
      kind: ServiceAccount
      metadata:
        labels:
          app.kubernetes.io/component: controller
          app.kubernetes.io/name: aws-load-balancer-controller
        name: aws-load-balancer-controller
        namespace: kube-system
      
    • 739行目のcluster-nameを今回作成したクラスター名にします
    • 下手にservice名(aws-load-balancer-controller)をいじらないほうがよいです(それで一度失敗しました)
  • 適用と確認

    kubectl apply -f v2_3_0_full.yaml
    kubectl get deployment -n kube-system aws-load-balancer-controller
    

ドメイン取得

  • ALBでgRPCを使う場合はリスナー側をHTTPSにする必要があるため、ドメインを取得して証明書を発行します。
  • freenomで無料のドメインを取得
    • ここでは albeksgrpc.tkというドメインを取得

TLS証明書の作成

  • Route53にパブリックホストゾーン登録
    • 「Route53」→「ホストゾーン」→「ホストゾーンの作成」
    • 作成されたホストゾーンを選択し、作成されたNSレコードを確認
  • freenomのページに戻りNameserversを追加
    • 右上のメニューの「Services」→「My Domains」
    • 作成したドメインの「Manage Domain」をクリック
    • 「Management Tools」→「Nameservers」
    • 「Use custom nameservers (enter below)」を選択
    • Route53で作成されたNSを登録
  • ACMで証明書をリクエスト
    • 「AWS Certificate Manager」→「証明書」→「証明書をリクエスト」→「パブリック証明書をリクエスト」
    • ドメイン名を設定しDNS検証を選択してリクエスト
    • 作成した証明書は検証の保留中になっているので、証明書の画面に入り「Route 53でのレコードの作成」をすることでRoute53にCNAMEが追加されます
    • 作成した証明書のARNは後ほど、Ingressを作成する際に必要になります

externalDNSのデプロイ

  • ALBのDNSとドメインをいい感じに関連付けてくれるサービスです
  • 公式の手順はこちら
    • yamlの書き方とか変わる可能性があるので注意

IAMポリシー作成

  • externalDNSサービスがRoute53を更新できるようにポリシーを作成します
  • 「IAM」→「ポリシー」→「ポリシーを作成」で以下のJSONをコピペ
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "route53:ChangeResourceRecordSets"
      ],
      "Resource": [
        "arn:aws:route53:::hostedzone/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "route53:ListHostedZones",
        "route53:ListResourceRecordSets"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

サービスアカウントのIAMロール作成

  • externalDNSサービス用のロールを作成します
  • 「IAM」→「ロール」→「ロールの作成」で以下のようにする(IDプロバイダーはEKSクラスターのOIDCプロバイダーURL)
  • ポリシーは上記で作成したものをアタッチ
  • 名前をつけてロールを作成し、再度そのロールの画面を開きます
  • 「信頼関係」タブから「信頼関係の編集」をクリック
    • 信頼先に"oidc.eks.ap-northeast-1.amazonaws.com/id/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:sub": "system:serviceaccount:default:external-dns"を追加
      • IDは↑のOIDCプロバイダーURLのID
      • namespaceを変える場合は、defaultの部分を変更

externalDNSインストール

  • external_dns.yamlを作成
    • RBACありの想定の書き方
      • 念のため kubectl api-versions | grep rbac.authorization.k8s.io で確認するとよい
    • IAM-SERVICE-ROLE-NAMEは一つ前の手順で作成したロールの名前
    • domain-filterは取得したドメイン名
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: external-dns
    annotations:
        # Substitute your account ID and IAM service role name below.
        eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT-ID:role/IAM-SERVICE-ROLE-NAME
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: external-dns
    rules:
    - apiGroups: [""]
      resources: ["services","endpoints","pods"]
      verbs: ["get","watch","list"]
    - apiGroups: ["extensions","networking.k8s.io"]
      resources: ["ingresses"]
      verbs: ["get","watch","list"]
    - apiGroups: [""]
      resources: ["nodes"]
      verbs: ["list","watch"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: external-dns-viewer
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: external-dns
    subjects:
    - kind: ServiceAccount
      name: external-dns
      namespace: default
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: external-dns
    spec:
      strategy:
        type: Recreate
      selector:
        matchLabels:
          app: external-dns
      template:
        metadata:
          labels:
    	app: external-dns
        spec:
          serviceAccountName: external-dns
          containers:
          - name: external-dns
    	image: k8s.gcr.io/external-dns/external-dns:v0.7.6
    	args:
    	- --source=service
    	- --source=ingress
    	- --domain-filter=albeksgrpc.tk # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
    	- --provider=aws
    	- --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
    	- --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
    	- --registry=txt
    	- --txt-owner-id=my-hostedzone-identifier
          securityContext:
    	fsGroup: 65534 # For ExternalDNS to be able to read 
    
  • 反映する
    kubectl apply -f external_dns.yaml
    

Service準備

サービスアカウントのIAMロール作成

  • Pod内のコンテナ用のIAMロールを作成します
  • 今回のマイクロサービスでは特に権限は不要だったのでスキップします
    • やる場合はAWS Load Balancer ControllerやexternalDNSのIAMロール作成と同じ手順

コンテナイメージ作成

  • gRPCサーバ側の処理を作成します

    git clone https://github.com/grpc/grpc.git
    cd grpc/examples/python/route_guide
    
    • TLSはALBで終端され、マイクロサービス側はHTTPになるのでroute_guide_server.pyは下記のようにinsecureのままです(なのでソース修正はありません)
    server.add_insecure_port('[::]:50051')
    
  • Dockerfile を作成して上記のポートをEXPOSEします

    FROM ubuntu:18.04
    
    RUN apt-get update && \
      apt-get install -y \
      python3-pip
    
    RUN pip3 install -U pip
    
    RUN pip3 install \
      grpcio \
      grpcio-tools
    
    RUN mkdir /usr/local/route_guide
    COPY ./ /usr/local/route_guide
    
    EXPOSE 50051
    
    CMD ["bash", "-c", "cd /usr/local/route_guide; python3 route_guide_server.py"]
    
  • ECRのリポジトリを作成

    • 「ECR」→「リポジトリ」→「リポジトリを作成」
  • コンテナイメージを作成し、ECRへプッシュする

    • コマンドは先ほど作成したリポジトリを選択し「プッシュコマンドの表示」から確認できるのでそちらをコピペして使います
    aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin XXXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com
    docker build -t sample/route_guide .
    docker tag sample/route_guide:latest XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/sample/route_guide:latest
    docker push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/sample/route_guide:latest
    

Service作成

マイクロサービスをデプロイ

  • マニフェスト作成(イメージのURLは上記で作成したECRリポジトリのURL)
    • 注意するのはコンテナでEXPOSEしたポート番号と合わせること
    • (nameに _ が使えないのも注意)
    apiVersion: v1
    kind: Service
    metadata:
      name: route-guide-service
      labels:
        app: route-guide
    spec:
      selector:
        app: route-guide
      ports:
        - port: 50051
          targetPort: 50051
      type: NodePort
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: route-guide
      labels:
        app: route-guide
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: route-guide
      template:
        metadata:
          labels:
    	app: route-guide
        spec:
          containers:
    	- name: route-guide
    	  image: XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/sample/route_guide:latest
    	  imagePullPolicy: Always
    	  resources:
    	    limits:
    	      memory: "1024Mi"
    	      cpu: "500m"
    	  ports:
    	    - containerPort: 50051
          nodeSelector:
    	kubernetes.io/os: linux
    
  • 適用
    kubectl apply -f route_guide.yaml
    

Ingressのデプロイ

  • ingress.yaml を作成します
  • external-dns.alpha.kubernetes.io/hostname: 取得したドメイン名
  • alb.ingress.kubernetes.io/certificate-arn: ACMで作成した証明書のARN
  • path: /routeguide.RouteGuide/protopackage名.service名 と完全一致させます
    • Prefixなのでrouteguide とかで十分だろうなどと思っていたらうまくいきませんでした
  • 複数のマイクロサービスを登録する場合は、同じ手順でポート番号を変えてbackendに追加していきます(以下は3つ登録する場合)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: alb-name
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/backend-protocol-version: GRPC
    external-dns.alpha.kubernetes.io/hostname: albeksgrpc.tk
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-1:XXXXXXXXXXXX:certificate/12345678-96c7-431a-b781-361a23c48d87
spec:
  rules:
    - http:
        paths:
	  - backend:
	      service:
	        name: route-guide-service
	        port:
                  number: 50051
	    path: /routeguide.RouteGuide/
	    pathType: Prefix
	  - backend:
	      service:
		name: keyvaluestore-service
		port:
		  number: 50052
	    path: /keyvaluestore.KeyValueStore/
	    pathType: Prefix
	  - backend:
	      service:
		name: helloworld-service
		port:
		  number: 50053
	    path: /helloworld.Greeter/
	    pathType: Prefix
  • 適用
kubectl apply -f ingress.yaml

ALBの確認

  • ingressをデプロイするとALBが自動で作成されます
    • 「EC2」→「ロードバランサー」で確認
      • 作成されない場合は kubectl describe ingress alb-name などで確認
    • 「リスナー」タブから「ルールの表示/編集」でルーティングの設定を確認

DNSの確認

  • external-dnsのPod名を確認
    $ kubectl get all | grep pod/external-dns
    pod/external-dns-845fd6cbbc-82f4w   1/1     Running   0          20h
    
  • 上記PodのlogからDNSの更新がされたことを確認
    $ kubectl logs external-dns-845fd6cbbc-82f4w
    
    • 以下のようなメッセージがあればOK
    time="2021-12-10T09:12:14Z" level=info msg="2 record(s) in zone albeksgrpc.tk. [Id: /hostedzone/Z093644125CXTO2QHYRBL] were successfully updated"
    
  • 「Route53」→「ホストゾーン」でALBのDNSが登録されています

動作確認

クライアント

  • route_guide_client.pyrun関数を例にします
    • オリジナル
        def run():
            with grpc.insecure_channel('localhost:50051') as channel:
                stub = route_guide_pb2_grpc.RouteGuideStub(channel)
    
    • HTTPSなのでセキュアに変更します
        def run():
            cred = grpc.ssl_channel_credentials(root_certificates=None, private_key=None, certificate_chain=None) 
            with grpc.secure_channel('albeksgrpc.tk:443', cred) as channel:
                stub = route_guide_pb2_grpc.RouteGuideStub(channel)
    
    • あとはこのstubに対して関数コールすれば疎通できます
    • (補足)その他の50052や50053ポートのマイクロサービスに対してもこのchannelを使い回せます

あとがき

Discussion