AWS
route53
kubernetes
loadbalancer
eks

AWSのKubernetesでサービスを公開する最高の方法~ALB,ACM,Route53の自動作成~

Kubernetesが流行っているので、これからEKSを使ってサービスを公開していこうと考えている方の参考になれば嬉しいです。
AWSのEKSを使って構築しています。

はじめに

EKSでサービスを公開したいけど、ロードバランサとか証明書とかの設定面倒くさいと考えている方が多いと思います。
今回の記事のゴールはymlファイルをKubernetesにデプロイするだけで、ALB作成、Route53にレコードセット追加、ACMの証明書をALBに割当の作業を自動でできるようにします。

EKSのGetting StartではCLB(Classic Load Balancer)を使用していますが、L7ロードバランサがいいのでALBで作成します。ホストベースでもパスベースでもどちらでもルーティングしてくれるので。。

本記事丁寧に説明するのですごく長くなりそうです。。。

アーキテクト

やっていく

IAMロールの作成

3つのロールを準備する必要があります。

Worker Node用のIAMロール

まずはWorker Node用のIAMロールに対して権限を付与します。
eksctlを使ってクラスタを構築した場合、XXXXXXXX-NodeInstanceRole-YYYYYYYYYYYYという名前のロールが作成されます。

このロールに対してconfig/node-role-permission-policy.jsonのポリシーをアタッチします。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "*"
    }
  ]
}
#!/bin/sh

NODE_INSTANCE_POLICY_ARN=$(aws iam create-policy --policy-name sts-assumerole-policy --policy-document file://config/node-role-permission-policy.json --query "Policy.[Arn]" --output text)

NODE_INSTANCE_ROLE_NAME=$(aws iam list-roles --query "Roles[?contains(RoleName,\`NodeInstanceRole\`)].[RoleName][]" --output text)

aws iam attach-role-policy --role-name $NODE_INSTANCE_ROLE_NAME --policy-arn $NODE_INSTANCE_POLICY_ARN

ALB用のIAMロール

config/ingress-iam-policy.json公式手順にあるポリシー をダウンロードしています。

export NODE_INSTANCE_ROLE_ARN=$(aws iam list-roles --query "Roles[?contains(RoleName,\`NodeInstanceRole\`)].[Arn][]" --output text)

cat << EOF  | jq > config/pod-role-trust-policy.json
{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
          "Service": "ec2.amazonaws.com"
        },
        "Action": "sts:AssumeRole"
      },
      {
        "Effect": "Allow",
        "Principal": {
          "AWS": "${NODE_INSTANCE_ROLE_ARN}"
        },
        "Action": "sts:AssumeRole"
      }
    ]
  }
EOF

aws iam create-role \
  --role-name alb-ingress-controller \
  --assume-role-policy-document file://config/pod-role-trust-policy.json

INGRESS_POLICY_ARN=$(aws iam create-policy --policy-name ingressController-iam-policy --policy-document file://config/ingress-iam-policy.json --query "Policy.[Arn]" --output text)

aws iam attach-role-policy --role-name alb-ingress-controller --policy-arn $INGRESS_POLICY_ARN

external-dns用のIAMロール

ALB用のIAMロールと同じようにexternal-dns用のIAMロールも作成していきます。
config/route53-iam-policy.jsonファイルは次のようにしています。

{
 "Version": "2012-10-17",
 "Statement": [
   {
     "Effect": "Allow",
     "Action": [
       "route53:ChangeResourceRecordSets"
     ],
     "Resource": [
       "arn:aws:route53:::hostedzone/*"
     ]
   },
   {
     "Effect": "Allow",
     "Action": [
       "route53:ListHostedZones",
       "route53:ListResourceRecordSets"
     ],
     "Resource": [
       "*"
     ]
   }
 ]
}
export NODE_INSTANCE_ROLE_ARN=$(aws iam list-roles --query "Roles[?contains(RoleName,\`NodeInstanceRole\`)].[Arn][]" --output text)

cat << EOF  | jq > config/pod-role-trust-policy.json
{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
          "Service": "ec2.amazonaws.com"
        },
        "Action": "sts:AssumeRole"
      },
      {
        "Effect": "Allow",
        "Principal": {
          "AWS": "${NODE_INSTANCE_ROLE_ARN}"
        },
        "Action": "sts:AssumeRole"
      }
    ]
  }
EOF

aws iam create-role \
  --role-name route53-externaldns-controller \
  --assume-role-policy-document file://config/pod-role-trust-policy.json

ROUTE53_POLICY_ARN=$(aws iam create-policy --policy-name route53Controller-iam-policy --policy-document file://config/route53-iam-policy.json --query "Policy.[Arn]" --output text)

aws iam attach-role-policy --role-name route53-externaldns-controller --policy-arn $ROUTE53_POLICY_ARN

以上で3つのIAMロールの準備ができました。

kube2iam

仕組み

ここであえて説明するとすれば AWS ALB Ingress Controller に付与するポリシーをどうするかです。
AWS ALB Ingress ControllerはALBの作成をしてくれるので、公式手順 だとこんな感じのポリシーが必要になります。

公式手順ではこのポリシーをノードのロールにアタッチしていますが、これをするとノード配下のPodに対しても同様の権限が与えられるというセキュリティ上恐ろしいことが起こってしまします。
そこでkube2iamを使ってノードに権限を与えるのではなくPodに権限を与えます。
類似プロダクトとしてkiamというものもあるみたいです。
Kubernetes(のアクセス管理機能)とIAMの間に入ってアクセス権限の管理を行ってくれるのが「kube2iam」や「kiam」です。

デプロイ

下記のmanufest.ymlファイルをデプロイするとDaemonsetでPodが各ノードで動きます。

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: kube2iam
  namespace: kube-system
---
apiVersion: v1
items:
  - apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: kube2iam
    rules:
      - apiGroups: [""]
        resources: ["namespaces","pods"]
        verbs: ["get","watch","list"]
  - apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: kube2iam
    subjects:
    - kind: ServiceAccount
      name: kube2iam
      namespace: kube-system
    roleRef:
      kind: ClusterRole
      name: kube2iam
      apiGroup: rbac.authorization.k8s.io
kind: List
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: kube2iam
  namespace: kube-system
  labels:
    app: kube2iam
spec:
  selector:
    matchLabels:
      app: kube2iam
  template:
    metadata:
      labels:
        app: kube2iam
    spec:
      serviceAccountName: kube2iam
      hostNetwork: true
      containers:
        - name: kube2iam
          image: jtblin/kube2iam:latest
          imagePullPolicy: Always
          args:
            - "--auto-discover-base-arn"
            - "--iptables=true"
            - "--host-ip=$(HOST_IP)"
            - "--host-interface=eni+"
            - "--verbose"
          env:
            - name: HOST_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
          ports:
            - containerPort: 8181
              hostPort: 8181
              name: http
          securityContext:
            privileged: true

デプロイコマンド

kubectl apply -f manufest.yml

kube2iamのオプションパラメーター(args:)の意味は以下の通りです。

--auto-discover-base-arn
ロールのARNのベース部分(arn:aws:iam::XXXXXXX:role/)を自動検出します。

--iptables=true
「true」を指定した場合、ホスト(EC2)のiptablesに必要な設定を自動登録します。

--host-ip=$(HOST_IP)
「--iptables」を指定する場合は「--host-ip」もセットで必ず指定する必要があります。

--host-interface=eni+
「Getting Started」の手順に従ってEKS環境を構築した場合はPod Networkingとして「amazon-vpc-cni-k8s」が採用されるため、このオプションは「eni+」を指定する必要があります。

--verbose
詳細なログを出力します。

以上でkube2iamの準備は完了です。

AWS ALB Ingress Controller

続いてAWS ALB Ingress Controllerをやっていきます。

仕組み

  • ALB Ingress Controllerは、KubernetesのAPI ServerからのEventを監視し、該当のEvent を検知したらAWSのリソースを作成し始めます。
  • annotationを指定することで、サブネットやインターネット向けか内部向けかも決めることもできます。
  • リスナーは、ingressのannotationで指定したポート用に作成されます。ポートが指定されていない場合、80または443を使用。ACMも使用することもできます。
  • 入力リソースで指定された各パスに対してルールが作成され、特定のパスへのトラフィックが正しい KubernetesのService にルーティングされる。

とまあ、めちゃくちゃいろいろできて超便利です!

デプロイ

  • ALB の向き先となるターゲットグループは、ingressに記述されたServiceごとにAWSで作成。 公式手順 通りにrbac-role.yamlファイルをデプロイします。
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.0.0/docs/examples/rbac-role.yaml

AWS ALB Ingress Controller本体をデプロイします。
公式のymlファイルをダウンロードして2箇所変更します。

--cluster-name=を自分のクラスタの名前に変更します。
alb-ingress-controller.yamlファイルの下記の部分のappの下に追記します。

  template:
    metadata:
      creationTimestamp: null
      labels:
        app: alb-ingress-controller
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: alb-ingress-controller
        # ここに追記
      annotations:
        iam.amazonaws.com/role: <ALB用のIAMロールのArn>
        # ここまで

ymlファイルを保存してデプロイします。

kubectl apply -f alb-ingress-controller.yaml

サブネットにタグを付与

AWS ALB Ingress Controllerは自動でクラスタのサブネットを見つけてくれるのですが、そのためにはPublicサブネットにタグを付与する必要があります。
Keyにkubernetes.io/role/alb-ingressを追加してValueは空。
Keyにkubernetes.io/role/elbを追加してValueは1にして保存。

これでAWS ALB Ingress Controllerの準備は完了です。

ACMの準備

example.comと*.example.comで証明書を取得してください。
手順はこちらを参考にしていただければと思います。
https://daichan.club/aws/78593

external-dns

仕組み

ExternalDNSを使用するとKubernetesのYAML内でドメイン名を指定するだけで、Route53のHosted Zoneにレコードが登録できて非常に便利です。

デプロイ

下記のmanufest.ymlファイルのexample.comと<external-dns用のIAMロールのArn>をご自身の環境に変更してください。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-dns
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: external-dns
rules:
  - apiGroups:
      - ""
    resources:
      - "services"
    verbs:
      - "get"
      - "watch"
      - "list"
  - apiGroups:
      - ""
    resources:
      - "pods"
    verbs:
      - "get"
      - "watch"
      - "list"
  - apiGroups:
      - "extensions"
    resources:
      - "ingresses"
    verbs:
      - "get"
      - "watch"
      - "list"
  - apiGroups:
      - ""
    resources:
      - "nodes"
    verbs:
      - "list"
---
apiVersion: rbac.authorization.k8s.io/v1beta1
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: kube-system
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: external-dns
  namespace: kube-system
spec:
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: external-dns
      annotations:
        iam.amazonaws.com/role: <external-dns用のIAMロールのArn>
    spec:
      serviceAccountName: external-dns
      containers:
      - name: external-dns
        image: registry.opensource.zalan.do/teapot/external-dns:v0.5.9
        args:
        - --source=service
        - --source=ingress
        - --domain-filter=example.com
        - --provider=aws
        - --policy=sync
        - --registry=txt
        - --txt-owner-id=hoge

デプロイコマンド

kubectl apply -f manufest.yml

external-dnsのオプションパラメーター(args:)の意味は以下の通りです。

--source
どのリソースをもとにroute53のレコードセットを作成するかを決めます。今回はServiceとIngressを指定しています。

--domain-filter
HostedZoneに対応するドメイン名に置き換えます。

--provider
AWSを使うのでAWSで大丈夫です。他にもGoogleやAzureなどが指定できるようです。

--policy
syncにすると削除も反映されます。作成のみにしたい場合はupsert-onlyにしてください。

--registry
txtレコードの登録を行います。

--txt-owner-id
ここは適当にしましたが問題なく動きました。

サンプルアプリケーション

すべての準備ができましたので、サンプルアプリケーションをデプロイしてみます。
サンプルアプリケーションはClassMethodさんのサイトのものを使っています。

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
# コマンドが出力されるのですべてコピーして貼り付け

repositoryUriは後で使うのでコピーしておいてください。

Dockerの準備

アクセスしたらPod名を返却するWebサーバを作成します。

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))
}
FROM golang

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

イメージを作成してECRにプッシュします。XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.comはECRのrepositoryUriに変更してください。

$ 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も同じように作成してECRにプッシュしてください。

パスベースのルーティング

test.example.com/target1にアクセスしたらtarget1のPodに、test.example.com/target2にアクセスしたらtarget2のPodにアクセスするmanufest.ymlファイルを作成します。

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: 155526509481.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: 155526509481.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"
---
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
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
    alb.ingress.kubernetes.io/certificate-arn: <ACMのArn>
    alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
  labels:
    app: test-app
spec:
  rules:
    - host: test.example.com
      http:
        paths:
          - path: /target1
            backend:
              serviceName: "test-app-service-target1"
              servicePort: 80
          - path: /target2
            backend:
              serviceName: "test-app-service-target2"
              servicePort: 80

デプロイコマンド

kubectl apply -f manufest.yml

test.example.com/target1とtest.example.com/target2にアクセスして挙動を確かめてください。

ホストベースでのルーティング

先ほど作成したmanufest.ymlファイルのingressのspecの部分のみ変更しています。

spec:
  rules:
    - host: target1.example.com
      http:
        paths:
          - path: /target1
            backend:
              serviceName: "test-app-service-target1"
              servicePort: 80
    - host: target2.example.com
      http:
        paths:
          - path: /target2
            backend:
              serviceName: "test-app-service-target2"
              servicePort: 80

これをデプロイすることでホストベースでもアクセス振り分けができます。

ymlファイルを書いたら長くなったので、あとでGithubにソース上げておきます。

参考サイト

https://qiita.com/koudaiii/items/2031d67c715b5bb50357
https://kubernetes-sigs.github.io/aws-alb-ingress-controller/guide/walkthrough/echoserver/#kube2iam-setup
https://qiita.com/mumoshu/items/bd82bd69525bc8feb4da