EKSでAWS ALB Ingress ControllerでALBつくってRoute 53にレコード作ってさらにACMでhttps!!!

最近勉強がてら AWS EKS を使ってKubernetes をさわってます!
もし間違ってるところがあれば @hatappi まで教えていただけると嬉しいです。

今回はどんなことがしたかったか

  • ALBを使いたい!
    • 2ドメインを使ってホストベースでルーティングしたい
  • ALBは自動で作るけど Routes 53 のレコードは手動ですみたいなことはしたくない
  • やっぱり今の時代は https でしょ! (ACM)

事前準備

まずは検証環境がないといけないので、クラスターの作成をおこないます。
クラスターは以前書いた記事で使用した eksctl を使用して作成していきます。

blog.hatappi.me

例えばこんな感じでクラスターを作成します。

$ eksctl create cluster  --name test --region ap-northeast-1 --nodes 2 --nodes-min 1 --nodes-max 2 --node-type t2.small --version=1.11

これで kubectl でEKSで構築したクラスターを扱うための準備ができました。

ALB を良い感じに作ってくれるその名もAWS ALB Ingress Controller

CLBを作るのであれば LoadBalancer Service を使えば作ってくれる。
しかし今回やりたいことに書いたようにホストベースでルーティングをしていきたい。
なのでALBを使いたい!!!!

ということで使うのが ↓ AWS ALB Ingress Controller です。

kubernetes-sigs.github.io

AWS ALB Ingress Controller をpodで起動しておくことで特定のannotationがついた Ingress リソースをみつけるとそのannotationの情報に応じて ALB を作成してくれます。

詳しくは公式のドキュメントを見てください。

kubernetes-sigs.github.io

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

このポリシーをどこにアタッチするのかを考えないといけないです。
通常?の方法であればノード(EC2)に割り当てられてるロールに対して上記のポリシーをアタッチします。
しかしノードに付与するということはノード配下のpodに対しても同様の権限が与えられてしまいます。
このままいくと神ノードができてしまいますし、今後複数のサービスが混在することを考えるとこのポリシーって外して良いのかみたいなことに迷いそうですよね。。。

ということで使ったのが次に紹介する kube2iam です。

pod に IAM Role を付与できる! その名も kube2iam

神ノードができないための解決策の1つがこの kube2iam です。

github.com

これはノードではなく pod に対して権限を付与するので神ノードができることが防ぐことができます。
ポリシーなどもそれぞれの pod に対して付与するので、必要なくなったポリシーを外しやすいのではないでしょうか。

kube2iam の類似プロダクトとしては kiam というものがあります。
これもアプローチとしては kube2iam と同じく pod に対して IAM Role をアタッチします。

github.com

なぜ kube2iam を選んだかというと今回クラスタの作成の際に使った eksctl が今後出すバージョンで kube2iam をアドオンとしてサポートするようなことが書いてあったためです。 https://github.com/weaveworks/eksctl/issues/271

というのも kube2iam は使う際にノードに対して↓のようなポリシーを付与する必要があります。

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

eksctl ではクラスタに紐づくノードの作成も行うのですが、現状は kube2iam をサポートしていないので生成されたノードに後から kube2iam で必要なポリシーをアタッチする必要があります。
eksctl でアドオンとしてサポートされればきっと、この作業も必要がなくなるはずです!
あとkube2iamは割り当てたいロールの信頼関係の Principal にノードを追加してあげる必要があります。
僕はTerraform で作成してます。

demonset は↓な感じで設定しました。

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: kube2iam
  namespace: kube-system
  labels:
    app: kube2iam
spec:
  selector:
    matchLabels:
      name: kube2iam
  template:
    metadata:
      labels:
        name: kube2iam
    spec:
      serviceAccountName: kube2iam
      hostNetwork: true
      containers:
        - image: jtblin/kube2iam:latest
          name: kube2iam
          args:
            - "--auto-discover-base-arn"
            - "--iptables=true"
            - "--host-ip=$(HOST_IP)"
            - "--node=$(NODE_NAME)"
            - "--host-interface=eni+"
          env:
            - name: HOST_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
          ports:
            - containerPort: 8181
              hostPort: 8181
              name: http
          securityContext:
            privileged: true

あとは README 通り apply して任意のpodに割り当てたい Role を annotation として定義すればいけるはずです。

  template:
    metadata:
      labels:
        app: alb-ingress-controller
      annotations:
        iam.amazonaws.com/role: arn:aws:iam::1111:role/hoge/alb-ingress-controller
    spec:
      containers:

正しく実装されていることを確認するためにS3にアクセスするようなイメージでテストしてみます。
まずは S3 にアクセスができるようなロールを作成します。

$ trustPolicy=$(cat << EOS | jq
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    },
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::11111:role/node"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOS
)

$ aws iam create-role --role-name s3-test-role --assume-role-policy-document ${trustPolicy}
$ aws iam attach-role-policy --role-name s3-test-role --policy-arn "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"

このロールを使用してS3のバケット一覧を表示するイメージを次のように定義して apply します。

apiVersion: v1
kind: Pod
metadata:
  name: aws-cli-sample
  labels:
    name: aws-cli-sample
  annotations:
    iam.amazonaws.com/role: arn:aws:iam::11111111:role/s3-test-role
spec:
  containers:
  - image: fstab/aws-cli
    command:
      - "/home/aws/aws/env/bin/aws"
      - "s3"
      - "ls"
    name: aws-cli-sample

次のコマンドでログを出して実行してうまくkube2iamが動いていれば、S3のバケットのリストが表示されるはずです。

$ kubectl log pod/aws-cli-sample

これで ALB を作る準備ができました。
ただこれだと ALB は作ったはいいけど 自分が使いたいドメインに設定するときは Route 53 を手動もしくは ALB のドメインを使って Terraform なりスクリプトを実行しないといけないです。
それはやりたくなかったので使ったのが次に紹介する external-dns です。

良い感じに各プロパイダのDNSレコードを作ってくれる! その名もexternal-dns

github.com

externa-dnsKubernetes Incubatorの1つです。
条件にあった Ingress や Service リソースが作成された時に AWS であれば AWS Route53, GCP であれば Google CloudDNS で DNS レコードを作成してくれます。

なので ALB Ingress Controller と連携できるように設定すれば ALB が作られてそのドメインを ALIAS とした Route 53 のレコードを作成してくれます!!
最高だ
マニフェストは↓のように定義しました。

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: arn:aws:iam::1111111:role/hoge/external-dns
    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

サンプルアプリケーションをデプロイする

最後に今まで説明したものをあわせてアプリケーションをデプロイしていく。

今回は Ruby で書いた簡単な http server を使います。

Dockerfile

FROM ruby:2.6

EXPOSE 8080

COPY server.rb .

CMD ruby server.rb

プログラムはこちら
中身はシンプルでアクセスした際にどのホストでのアクセスかを表示するだけ!

require 'webrick'

s = WEBrick::HTTPServer.new(Port: 8080)

s.mount_proc('/') do |req, res|
  res.status = 200
  res.body = "Hello!!!\nhost is #{req.host}"
end

Signal.trap('INT') { s.shutdown }
s.start

同じものは Docker Hub にあがっています。
https://cloud.docker.com/u/hatappi/repository/docker/hatappi/sample-ruby-http-server

アプリケーションのnamespaceを作成してデプロイまでは次のマニフェストで行いました。

apiVersion: v1
kind: Namespace
metadata:
  name: sample-app
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: sample-app
  namespace: sample-app
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - image: hatappi/sample-ruby-http-server
          imagePullPolicy: Always
          name: sample-app
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: sample-app
  namespace: sample-app
spec:
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
  type: NodePort
  selector:
    app: sample-app

Ingress はこちら

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: sample-app
  namespace: sample-app
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/subnets: subnet-000000,subnet-111111,subnet-22222
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-1:1111111111:certificate/111111-111111-111111-22222
    alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
spec:
  rules:
    - host: hoge.example.com
      http:
        paths:
          - path: /*
            backend:
              serviceName: ssl-redirect
              servicePort: use-annotation
          - path: /
            backend:
              serviceName: sample-app
              servicePort: 80
    - host: fuga.example.com
      http:
        paths:
          - path: /*
            backend:
              serviceName: ssl-redirect
              servicePort: use-annotation
          - path: /
            backend:
              serviceName: sample-app
              servicePort: 80

https化を行うために alb.ingress.kubernetes.io/certificate-arnACMのARNを指定します。
ACMは事前にDNS認証を使用して Terraform で作成しました。

resource "aws_acm_certificate" "cert" {
  domain_name               = "example.com"
  subject_alternative_names = ["*.example.com"]
  validation_method         = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_route53_record" "cert_validation" {
  name    = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_name}"
  type    = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_type}"
  zone_id = "Z111111111"
  records = ["${aws_acm_certificate.cert.domain_validation_options.0.resource_record_value}"]
  ttl     = 60
}

他には annotation に alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}' と記載されてますが、これは http -> https のリダイレクトを行うもので、公式にも記載されています。

kubernetes-sigs.github.io

これらを apply すれば https://hoge.example.com にアクセスした際には Hello!!!\nhost is hoge.example.comhttps://fuga.example.com にアクセスした際には Hello!!!\nhoge is fuga.example.com がレスポンスとしてかえってきます。

最後に

今回は EKS 上で ALB を作って Route 53 のレコードを自動生成して https でアクセスできるようにしました〜