AWS ALB Ingress Controllerを使ってEKSからALBを作成してみた

AWS上でKubernetesのLoadBalancer Serviceを利用すると、CLB/NLBを簡単に作成することができます。

Amazon Elastic Container Service for Kubernetes(EKS)でClassic Load Balancer(CLB)とNetwork Load Balancer(NLB)を使ってみる

ですが、特別な理由がない限りはL7の機能が強化されたALBを利用したい場面が多いのではないでしょうか。

ということで今回は、先日EKSでの利用が正式にサポートされたAWS ALB Ingress Controllerを使ってALBを作成する方法を紹介したいと思います。

今回構築する環境のイメージ

今回は以下の作業を実施します。

  1. EKSクラスタ構築
  2. AWS ALB Ingress Controllerのデプロイ
  3. サンプルアプリケーションのデプロイ
  4. ALB作成

AWS ALB Ingress ControllerによりALBを作成し、サンプルアプリケーションに対しパスベースルーティングができることを確認します。

では早速やっていきましょう!

EKSクラスタ構築

まずはEKSクラスタを構築します。今回はeksctlを利用しサクッと構築していきます。

$ eksctl create cluster --name=k8s-cluster --nodes=2 --node-type=t2.medium

2つのWorkerノードをクラスタ化した状態からスタートします。 kubectlでの実行結果は以下のようになります。

$ kubectl get nodes
NAME                                                STATUS   ROLES    AGE   VERSION
ip-192-168-21-187.ap-northeast-1.compute.internal   Ready    <none>   8m    v1.11.5
ip-192-168-57-228.ap-northeast-1.compute.internal   Ready    <none>   8m    v1.11.5

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

パブリックサブネットへのタグ追加

ALB Ingress ControllerがALBに使用されるサブネットを自動検出できるようにパブリックサブネットにタグkubernetes.io/role/elbを付与します。

AWS ALB Ingress Controllerのデプロイ

AWS ALB Ingress Controllerとは

IngressリソースがEKSに登録されたタイミングでALBを作成するモジュールです。

AWS ALB Ingress ControllerはEKSクラスタにPodとして起動させます。コントローラはAPIサーバーと通信し、要件を満たすIngressリソースを見つけるとALBの作成を開始します。

詳細は公式ドキュメント を参照してください。

Workerノードへのポリシー追加

WorkerノードにALBを作成するための権限を付与します。 IAMよりポリシーを作成し、WorkerノードのIAMロールに作成したポリシーをアタッチします。

ServiceAccount作成

AWS ALB Ingress Controllerで利用するServiceAccountを作成し、そのServiceAccountに対しRoleをバインドします。

$ kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.0.0/docs/examples/rbac-role.yaml
clusterrole.rbac.authorization.k8s.io/alb-ingress-controller created
clusterrolebinding.rbac.authorization.k8s.io/alb-ingress-controller created
serviceaccount/alb-ingress created

AWS ALB Ingress Controllerのデプロイ

ローカルにマニュフェストファイルをダウンロードします。

$ curl -sS "https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.0.0/docs/examples/alb-ingress-controller.yaml" > alb-ingress-controller.yaml

–cluster-nameの部分を今回作成したクラスタ名に変更した後、マニュフェストをapplyします。

$ kubectl apply -f alb-ingress-controller.yaml
deployment.apps/alb-ingress-controller created

デプロイが完了すると名前空間kube-systemにAWS ALB Ingress ControllerのPodが作成されます。

$ kubectl get pod --all-namespaces
NAMESPACE     NAME                                      READY   STATUS    RESTARTS   AGE
kube-system   alb-ingress-controller-77666996cf-ksv4b   1/1     Running   0          3m
kube-system   aws-node-nmwjv                            1/1     Running   1          24m
kube-system   aws-node-q97ss                            1/1     Running   1          24m
kube-system   coredns-7774b7957b-4kh8p                  1/1     Running   0          29m
kube-system   coredns-7774b7957b-rx8l4                  1/1     Running   0          29m
kube-system   kube-proxy-8gfjx                          1/1     Running   0          24m
kube-system   kube-proxy-kwwf7                          1/1     Running   0          24m

サンプルアプリケーションのデプロイ

パスベースルーティングを確認するため簡単なアプリケーションを作成します。作成したアプリケーションはECR経由でEKSにデプロイします。

ECR作成

ECRを作成しECRにログインします。

$ aws ecr create-repository --repository-name eks-test-app
{
    "repository": {
        "repositoryArn": "arn:aws:ecr:ap-northeast-1:XXXXXXXXXXXX:repository/eks-test-app",
        "registryId": "XXXXXXXXXXXX",
        "repositoryName": "eks-test-app",
        "repositoryUri": "XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/eks-test-app",
        "createdAt": 1546156701.0
    }
}

$ aws ecr get-login --no-include-email
$ docker login -u AWS -p XXXXXX

アプリケーション作成

httpレスポンスとしてPod名を返却するWebサーバーを作成します。まずは/target1のリクエストに対応するアプリケーションを作成します。

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
      fmt.Fprintf(w, "healthy!")
    })
    
    http.HandleFunc("/target1", func(w http.ResponseWriter, r *http.Request) {
      fmt.Fprintf(w, "/target1:" + os.Getenv("POD_NAME"))
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}

ビルド用のDockerfileを作成します。

FROM golang

ADD . /go/src/
EXPOSE 8080
CMD ["/usr/local/go/bin/go", "run", "/go/src/server.go"]

タグtarget1でビルドし、ECRにPushします。

$ docker build -t eks-test-app:target1 .
$ docker tag eks-test-app:target1 XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/eks-test-app:target1
$ docker push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/eks-test-app:target1

/target2のリクエストに対応するアプリケーションも同様に作成し、タグtarget2でビルド後、ECRにデプロイします。

マニュフェスト作成

アプリケーションをデプロイするためのマニュフェストを作成します。

マニュフェストにはtarget1,target2のリクエストに対応するDeploymentおよびServiceを定義します。また、Deploymentで指定するDockerImageには先ほど作成したアプリケーションを指定し、Podの環境変数にはPOD_NAMEを設定します。

apiVersion: v1
kind: Namespace
metadata:
  name: "test-app"

---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: "test-app-deployment-target1"
  namespace: "test-app"
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: "test-app-target1"
    spec:
      containers:
      - image: XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/eks-test-app:target1
        imagePullPolicy: Always
        name: "test-app-target1"
        ports:
        - containerPort: 8080
        env:
      	- name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name

---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: "test-app-deployment-target2"
  namespace: "test-app"
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: "test-app-target2"
    spec:
      containers:
      - image: XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/eks-test-app:target2
        imagePullPolicy: Always
        name: "test-app-target2"
        ports:
        - containerPort: 8080
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name

---
apiVersion: v1
kind: Service
metadata:
  name: "test-app-service-target1"
  namespace: "test-app"
spec:
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
  type: NodePort
  selector:
    app: "test-app-target1"

---
apiVersion: v1
kind: Service
metadata:
  name: "test-app-service-target2"
  namespace: "test-app"
spec:
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
  type: NodePort
  selector:
    app: "test-app-target2"

アプリケーションのデプロイ

アプリケーションをデプロイします。

$ kubectl apply -f test-app.yaml
namespace/test-app unchanged
deployment.extensions/test-app-deployment-target1 created
deployment.extensions/test-app-deployment-target2 created
service/test-app-service-target1 created
service/test-app-service-target2 created

$ kubectl get all --all-namespaces
NAMESPACE     NAME                                               READY   STATUS    RESTARTS   AGE
kube-system   pod/alb-ingress-controller-77666996cf-ksv4b        1/1     Running   0          1h
kube-system   pod/aws-node-nmwjv                                 1/1     Running   1          1h
kube-system   pod/aws-node-q97ss                                 1/1     Running   1          1h
kube-system   pod/coredns-7774b7957b-4kh8p                       1/1     Running   0          1h
kube-system   pod/coredns-7774b7957b-rx8l4                       1/1     Running   0          1h
kube-system   pod/kube-proxy-8gfjx                               1/1     Running   0          1h
kube-system   pod/kube-proxy-kwwf7                               1/1     Running   0          1h
test-app      pod/test-app-deployment-target1-778f6fddc7-p766m   1/1     Running   0          1m
test-app      pod/test-app-deployment-target1-778f6fddc7-txkb7   1/1     Running   0          1m
test-app      pod/test-app-deployment-target2-7589dcc48d-6ptwx   1/1     Running   0          1m
test-app      pod/test-app-deployment-target2-7589dcc48d-8n5qn   1/1     Running   0          1m

NAMESPACE     NAME                               TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)         AGE
default       service/kubernetes                 ClusterIP   10.100.0.1       <none>        443/TCP         1h
kube-system   service/kube-dns                   ClusterIP   10.100.0.10      <none>        53/UDP,53/TCP   1h
test-app      service/test-app-service-target1   NodePort    10.100.38.252    <none>        80:30583/TCP    1m
test-app      service/test-app-service-target2   NodePort    10.100.124.165   <none>        80:32110/TCP    1m

NAMESPACE     NAME                        DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
kube-system   daemonset.apps/aws-node     2         2         2       2            2           <none>          1h
kube-system   daemonset.apps/kube-proxy   2         2         2       2            2           <none>          1h

NAMESPACE     NAME                                          DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
kube-system   deployment.apps/alb-ingress-controller        1         1         1            1           1h
kube-system   deployment.apps/coredns                       2         2         2            2           1h
test-app      deployment.apps/test-app-deployment-target1   2         2         2            2           1m
test-app      deployment.apps/test-app-deployment-target2   2         2         2            2           1m

NAMESPACE     NAME                                                     DESIRED   CURRENT   READY   AGE
kube-system   replicaset.apps/alb-ingress-controller-77666996cf        1         1         1       1h
kube-system   replicaset.apps/coredns-7774b7957b                       2         2         2       1h
test-app      replicaset.apps/test-app-deployment-target1-778f6fddc7   2         2         2       1m
test-app      replicaset.apps/test-app-deployment-target2-7589dcc48d   2         2         2       1m

ALB作成

ingressのマニュフェストを作成します。ALBはパスベースでターゲットを切り替える設定とします。

apiVersion: v1
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: "ingress"
  namespace: "test-app"
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
  labels:
    app: test-app
spec:
  rules:
    - http:
        paths:
          - path: /target1
            backend:
              serviceName: "test-app-service-target1"
              servicePort: 80
          - path: /target2
            backend:
              serviceName: "test-app-service-target2"
              servicePort: 80

マニュフェストをapplyします。

$ kubectl apply -f ingress.yaml

しばらくするとALBおよびターゲットグループが作成されます。

$ aws elbv2 describe-load-balancers
{
    "LoadBalancers": [
        {
            "LoadBalancerArn": "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:loadbalancer/app/b0a25c21-testapp-ingress-1d16/8e4a9e9c192eae33",
            "DNSName": "b0a25c21-testapp-ingress-1d16-2135925699.ap-northeast-1.elb.amazonaws.com",
            "CanonicalHostedZoneId": "Z14GRHDCWA56QT",
            "CreatedTime": "2018-12-30T08:54:13.480Z",
            "LoadBalancerName": "b0a25c21-testapp-ingress-1d16",
            "Scheme": "internet-facing",
            "VpcId": "vpc-02498dac27677a79a",
            "State": {
                "Code": "active"
            },
            "Type": "application",
            "AvailabilityZones": [
                {
                    "ZoneName": "ap-northeast-1c",
                    "SubnetId": "subnet-028193ac333ae6241"
                },
                {
                    "ZoneName": "ap-northeast-1d",
                    "SubnetId": "subnet-0aaf687b20bb26445"
                },
                {
                    "ZoneName": "ap-northeast-1a",
                    "SubnetId": "subnet-0c0df73a4671b923c"
                }
            ],
            "SecurityGroups": [
                "sg-0c25b1682e9fd7b72"
            ],
            "IpAddressType": "ipv4"
        }
    ]
}

$ aws elbv2 describe-target-groups
{
    "TargetGroups": [
        {
            "TargetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:targetgroup/b0a25c21-ce379bdf1fac9fac389/f0458909d46b1a19",
            "TargetGroupName": "b0a25c21-ce379bdf1fac9fac389",
            "Protocol": "HTTP",
            "Port": 1,
            "VpcId": "vpc-02498dac27677a79a",
            "HealthCheckProtocol": "HTTP",
            "HealthCheckPort": "traffic-port",
            "HealthCheckEnabled": true,
            "HealthCheckIntervalSeconds": 15,
            "HealthCheckTimeoutSeconds": 5,
            "HealthyThresholdCount": 2,
            "UnhealthyThresholdCount": 2,
            "HealthCheckPath": "/",
            "Matcher": {
                "HttpCode": "200"
            },
            "LoadBalancerArns": [
                "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:loadbalancer/app/b0a25c21-testapp-ingress-1d16/8e4a9e9c192eae33"
            ],
            "TargetType": "instance"
        },
        {
            "TargetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:targetgroup/b0a25c21-daec225fa0154bfbe17/55129d7eec6da764",
            "TargetGroupName": "b0a25c21-daec225fa0154bfbe17",
            "Protocol": "HTTP",
            "Port": 1,
            "VpcId": "vpc-02498dac27677a79a",
            "HealthCheckProtocol": "HTTP",
            "HealthCheckPort": "traffic-port",
            "HealthCheckEnabled": true,
            "HealthCheckIntervalSeconds": 15,
            "HealthCheckTimeoutSeconds": 5,
            "HealthyThresholdCount": 2,
            "UnhealthyThresholdCount": 2,
            "HealthCheckPath": "/",
            "Matcher": {
                "HttpCode": "200"
            },
            "LoadBalancerArns": [
                "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:loadbalancer/app/b0a25c21-testapp-ingress-1d16/8e4a9e9c192eae33"
            ],
            "TargetType": "instance"
        }
    ]
}

最後に正しくルーティングができているか確認してみましょう。

$ kubectl get pod --all-namespaces
NAMESPACE     NAME                                           READY   STATUS    RESTARTS   AGE
kube-system   alb-ingress-controller-77666996cf-ksv4b        1/1     Running   0          1h
kube-system   aws-node-nmwjv                                 1/1     Running   1          1h
kube-system   aws-node-q97ss                                 1/1     Running   1          1h
kube-system   coredns-7774b7957b-4kh8p                       1/1     Running   0          2h
kube-system   coredns-7774b7957b-rx8l4                       1/1     Running   0          2h
kube-system   kube-proxy-8gfjx                               1/1     Running   0          1h
kube-system   kube-proxy-kwwf7                               1/1     Running   0          1h
test-app      test-app-deployment-target1-778f6fddc7-p766m   1/1     Running   0          27m
test-app      test-app-deployment-target1-778f6fddc7-txkb7   1/1     Running   0          27m
test-app      test-app-deployment-target2-7589dcc48d-6ptwx   1/1     Running   0          27m
test-app      test-app-deployment-target2-7589dcc48d-8n5qn   1/1     Running   0          27m

# /target1に対するリクエストはtarget1のpodにルーティングされることを確認
$ curl http://b0a25c21-testapp-ingress-1d16-2135925699.ap-northeast-1.elb.amazonaws.com/target1
/target1:test-app-deployment-target1-778f6fddc7-p766m
$ curl http://b0a25c21-testapp-ingress-1d16-2135925699.ap-northeast-1.elb.amazonaws.com/target1
/target1:test-app-deployment-target1-778f6fddc7-txkb7

# /target2に対するリクエストはtarget2のpodにルーティングされることを確認
$ curl http://b0a25c21-testapp-ingress-1d16-2135925699.ap-northeast-1.elb.amazonaws.com/target2
/target2:test-app-deployment-target2-7589dcc48d-8n5qn
$ curl http://b0a25c21-testapp-ingress-1d16-2135925699.ap-northeast-1.elb.amazonaws.com/target2
/target2:test-app-deployment-target2-7589dcc48d-6ptwx

リクエストがパス毎に正しくルーティングされることが確認できました!!

さいごに

AWS ALB Ingress Controllerを使ってEKSからALBを作成する方法を紹介しました。

AWS ALB Ingress Controllerを利用するとCFnやTerraformを使わずALBを作成することができます。Kubernetesのマニュフェストという統一されたフォーマットでリソースを作成することで、リソースの管理/運用もしやすくなるのではないでしょうか。

Service Brokerあたりもうまく利用することで、初期構築以外ではCFnやTerraformを利用しない!そんな時代がくるかもしれませんね。

参考