社内開発環境を GKE から EKS に移行した話

この記事は Retty Advent Calender 2020 の16日目の記事です。

adventar.org

昨日は @pikatenor さんの ちょっと便利に使う CoreDNS でした。
Kubernetes を触っているとお馴染みの CoreDNS ですが、実は直接触ったことがなくて設定方法なんかを見てると色々便利に使えそうだなーと思いました。

こんにちは。Retty 技術部インフラチームの幸田です。
会社近くの好きなお店は SAVOY 麻布十番店 で、毎日行きたいくらいには大好きです。

数ヶ月前からアドベントカレンダーに合わせて何かを作ろうと思っていたのですが、案の定何も作らずに12月を迎えてしまいました。
何を書こうか迷いましたが、夏頃に GKE で稼働していた社内開発環境を EKS に移行したので、その話をしたいと思います。昨日 CoreDNS の話も出たので Kubernetes 繋がりでちょうどいいですね。

GKE から EKS に移行した理由

社内開発環境は Kubernetes で構築しており、以前まで GCP の GKE を使用していました。
しかし Retty は大半のシステムが AWS 上で稼働しており、例えば開発用の RDS に接続するために AWSGCPVPN 接続するなどしていました。

f:id:rettydev:20201216114607p:plain
AWSGCPVPN 接続していた図

GKE を使い続ける理由も特になく、運用負荷を減らしたかったので Amazon EKS へ移行することにしました。
なぜ最初から EKS を使っていないのかという話ですが、「そもそも今の開発環境ができた当初は Amazon EKS が存在しなかった」というのが理由です。正確には一部リージョンでプレビュー版が公開されていたのですが、今ほど機能も豊富でなかったので GKE を使用していました。

開発環境の構成

開発環境の簡単な構成は下図の通りです。

f:id:rettydev:20201214115450p:plain
開発環境の構成

このあたりの仕組みは見直す予定ですが、現在は専用の SSH サーバが用意されており、開発者は SSH サーバ上のファイルを変更すると、同じボリュームがマウントされている個人の Pod にも反映されるという仕組みです。

開発者ごとに専用の Pod / Service を用意し、Ingress を使用してホスト名ベースでそれぞれの環境に割り振っています。
具体的には下記のようなマニフェストをユーザ毎に作成することで、それぞれの開発環境にアクセスできるようにしています。
※ リソース名やホスト名は架空のものです

apiVersion: extensions/v1beta1
kind: Ingress
  name: development-user-ingress
  namespace: retty-development
spec:
  rules:
  - host: username-service1.dev.retty.me
    http:
      paths:
      - backend:
          serviceName: development-user-service1
          servicePort: 80
  - host: username-service2.dev.retty.me
    http:
      paths:
      - backend:
          serviceName: development-user-service2
          servicePort: 80
  - host: username-service3.dev.retty.me
    http:
      paths:
      - backend:
          serviceName: development-user-service3
          servicePort: 80

Ingress Controller について

Ingress Controller には kubernetes-sigs/aws-alb-ingress-controller(現: aws-load-balancer-controller) と nginxinc/kubernetes-ingress の2種類を採用しました。

github.com

github.com

本来であれば ALB を利用した Ingress のみで事足りるのですが、ALB に登録できるルールの上限数が100であり、今回のような使い方だとルール数に不安があったため、ホスト名ベースのルーティングは nginx にまかせて ALB は HTTPS の終端と外部のロードバランサーとしての役割のみを担わせています。

具体的には下記のような設定にしています。証明書の管理には ACM を利用しており ALB で HTTPS を終端した後、後段の Ingress である nginx に流しています。
※ リソース名やホスト名は架空のものです

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: alb-ingress
  namespace: nginx-ingress
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-1:1234567890:certificate/hoge-fuga-hoge-fuga-hogefuga
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
    alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/security-groups: sg-123456789
    alb.ingress.kubernetes.io/healthcheck-protocol: HTTP
    alb.ingress.kubernetes.io/healthcheck-path: "/nginx-health"
    alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=300
spec:
  rules:
    - host: "*.dev.retty.me"
    - http:
        paths:
         - path: /*
           backend:
             serviceName: ssl-redirect
             servicePort: use-annotation
    - http:
        paths: 
          - backend:
              serviceName: "nginx-ingress"
              servicePort: 80

nginx を type: LoadBalancer で公開することによって、L7 のルーティングは nginx に任せつつ、外部にロードバランサーを配置することが可能ですが、なるべく CLB を使用したくなかったのと、ALB の機能を使うこともできるという点からこのような構成にしました。

ちなみに ALB Ingress Controller は最近 kubernetes-sigs/aws-load-balancer-controller という新しい Controller に生まれ変わって、ALB でしか使用できなかった IP targets *1を NLB でも利用できるようになりました。

EKS クラスタの構築

EKS を構築する方法はいくつかありますが、最も簡単に構築できるのは eksctl を使った方法ではないでしょうか。

github.com

README にもある通り、下記のコマンド1行でコントロールプレーンはもちろんのこと、VPC 関連のリソースや IAM の権限設定まで全部やってくれます。とっても便利です。

eksctl create cluster

便利な一方で VPC などのリソースは EKS 以外にも使用します。例えば RDS を配置する場合などは、EKS クラスタが使用しているものと同じ VPC を使うと思います。
eksctl は裏側で CloudFormation を利用しているので、eksctl ですべてのリソースを作成してしまうと IaC による管理を前提とした時、自動生成された CloudFormation のテンプレートに手を入れる形になります。

自動生成されたものに手を入れるのはなんとなく嫌だったので、今回は下記のように Terraform と eksctl を使い分けました。

リソース ツール
VPC(VPC/Subnet/IGW/NGW/Route tables) Terraform
EC2(Security Group) Terraform
IAM eksctl(※)
EKS Control Plane eksctl
EKS Worker Node(ASG/EC2) eksctl

※ IRSA などで使用する IAM Role は Terraform で作成

Terraform と eksctl

上述のように使用する構成管理ツールを分けることで解決するかと思いきや、次はそれぞれを跨ぐようなリソースというのが出てきます。

例えば EKS のログを有効にしたい場合、eksctl でも設定可能ですが CloudWatch Logs の保持期間はデフォルトの無期限のままになっていたりします。現状 eksctl 側で保持期間の設定ができないため、これを変更したい場合は CloudFormation を触るか Terraform に import するかのどちらかになります。
今回は CloudFormation には手を入れないという方針だったので、terraform import で設定をインポートして、ロググループの保持期間の設定は Terraform で行いました。

このように複数の構成管理ツールを扱う場合に「どちらのツールにどこまで管理させるか」というのは非常に難しい問題だなと構築していて感じました。

余談ですが最近は eksctl そのものを Terraform で管理するための Terraform Provider もあるようです。

github.com

工夫した点

クラスタの構築方法などは至って普通なので、今回の開発環境の移行で工夫した点をいくつか紹介したいと思います。

スポットインスタンスの活用

社内でしか利用されない開発環境ということもあり、ワーカーノードにはスポットインスタンスを活用しました。
ここでは詳細は割愛しますが、AWS 内で使用されていない EC2 インスタンスを最大90% OFF で利用できるというものです。その代わり余剰のキャパシティがなくなった時に中断される可能性があります。

今回は社内利用であるため最悪途中で中断されてもそこまで問題ないと判断し、コスト面を意識してワーカーノードにはスポットインスタンスを採用しました。

上記の通りスポットインスタンスは途中で中断される可能性があります。
とはいえ黙って終了するわけではなく、終了の2分前に中断の通知を受け取ることができます。*2
aws-node-termination-handler を使うとこの通知をハンドリングしてくれて、対象のノードで稼働している Pod に対して SIGTERM を送信してくれます。これにより Pod を突然死させることなく Graceful に終了させることができます。

github.com

先日のリリース で通常の interruption notice よりも早く通知される可能性のある rebalance recommendation *3を受け取った時に対象のノードに Pod がスケジューリングされないように(cordon)してくれたり、Auto Scaling の lifecycle hooks *4を受け取ることもできるようになったようです。

これにより Auto Scaling 側でインスタンスを更新*5した際にも、同様にハンドリングすることが可能になったので嬉しいアップデートですね。

そして更に今月の re:Invent でマネージドノードグループによるスポットインスタンスのサポートが発表されました!

aws.amazon.com

マネージドノードグループでスポットインスタンスを使用すると、 rebalance recommendation による新しいスポットインスタンスの起動や、今まで node-termination-handler が行っていた drain などを全て ASG 側でイイ感じにやってくれます!node-termination-handler すらも不要になったのは嬉しいですね。

Ansible k8s モジュールによるマニフェストの管理

Kubernetesマニフェスト管理には Ansible の k8s モジュールを使用しました。
前半部分で紹介したように、開発者ごとに個別に Pod や Service を用意している関係で同じようなマニフェストを開発者毎に用意する必要があります。
ユーザ追加の度にコピーして使うのは効率が悪いため、下記のようなテンプレートファイルを用意して、Ansible の変数ファイルでユーザを管理できるようにしました。

apiVersion: v1
kind: Service
metadata:
  labels:
    run: "development-{{ user.name }}-service1"
  name: "development-{{ user.name }}-service1"
  namespace: retty-development
spec:
  ports:
  - port: 80
    targetPort: 80
  selector:
    run: "development-{{ user.name }}-service1"

{{ user.name }} に格納するための変数ファイルは下記のような形式です。他にも所属チーム毎にそれぞれ開発用の API Key を出し分けたり、services で必要なサービスを指定することで必要とするリソースのみを作成できるようにしています。

users:
  - name: alice
    mission_type: web
    services:
      - service1
      - service2
      - service3
    state: present
  - name: bob
    mission_type: sre
    services:
      - service1
    state: present
  - name: carol
    mission_type: app
    services:
      - service1
      - service3
    state: present

テンプレートと変数ファイルを使用して、Playbook を走らせるとそれぞれの情報に基づいて開発環境が作成されます。
上記の例だと web チームに所属している alice に対して、web チーム用の開発者 API Key を使用して service1, service2, service3 の開発環境が作られる。といったイメージです。

Secrets Manager を用いた秘匿情報管理

Kubernetes 上で稼働するコンテナに、秘匿情報を渡したい場合よく使用されるのが Secrets だと思います。

下記のような形式で秘匿情報を base64 エンコードした文字列を定義して Secrets リソースを作成すると、ボリュームマウントを経由してコンテナに対して情報を与えることができます。

apiVersion: v1
kind: Secret
metadata:
  name: my-secrets
type: Opaque
data:
  username: aG9nZWhvZ2UK
  password: ZnVnYWZ1Z2EK
---
apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
  - name: myapp-container
    image: busybox
    env:
    - name: USERNAME
       valueFrom:
         secretKeyRef:
           name: my-secrets
           value: username
    - name: PASSWORD
       valueFrom:
         secretKeyRef:
           name: my-secrets
           value: username

この Secrets のマニフェストの管理についてですが、デコードすることによって簡単に秘匿情報を確認することができます。よって GitHub などのリポジトリにこのまま保存するのは、望ましくないという考えが一般的かと思います。

類似の OSS がいくつか存在しますが、今回は Secrets の管理に kubernetes-external-secrets + AWS Secrets Manager を使用しました。

github.com

これは AWS Secrets Manager や Hashicorp Vault などに格納された秘匿情報を、Kubernetes クラスタにデプロイした external-secrets-controller が定期的に問い合わせることで、自クラスタの Secrets を自動的に更新してくれるというものです。これによって Secrets のマニフェストの管理が不要になります。

当然コントローラーに対して Secrets Manager に対する参照権限を付与する必要があるわけですが、その方法として下記の4つが README で挙げられています。

  • EKS のワーカーノードに付与された IAM ロールの使用
  • IAM roles for service accounts の使用
  • Pod 毎に IAM Role を設定する kaim や kube2iam の使用
  • 環境変数に設定した IAM アクセスキーの使用

最も手軽に試せるのは1つ目だと思いますが、README にもあるように非推奨となっています。これは IAM Role を付与したワーカーノード上で動くすべての Pod に対して Secrets Manager へのアクセス権限を付与してしまう形になるからです。

今回は簡単でかつ安全に使用できるように、2つ目の IAM Roles for service accounts を使用しました。
この方法は Kubernetes の Service Account に対して IAM Role を紐付けることができ、その Service Account を任意の Pod に紐付けるというものです。こうすることによって必要な Pod にだけ権限を付与することができます。

IAM Roles for service accounts の詳しい仕組みについては、下記のブログが参考になるかと思います。

aws.amazon.com

最後に

AWS に移行したことで使い慣れた AWS サービスとの連携も行うことができて、以前よりもすっきりとした構成になりました。

f:id:rettydev:20201216120730p:plain
移行後の構成図

環境の移行なので基本的な構成などは変わっていませんが、個人的に興味のある Kubernetes を業務で触れたのは楽しかったです。

実は環境の移行に伴って社内的な運用自体も弊チームに変わったのですが、やはりバージョンのアップデートへの追従は大変だなと改めて思いました。特に EKS の場合は各種アドオンのバージョンを個別にアップデートする必要があるので、先日のアップデートで発表された Amazon EKS add-ons の今後に期待したいですね。

aws.amazon.com

開発環境の移行は完了しましたが、課題や改善点はたくさんあるのでこれからも全社的に使われる基盤としてより良くしていきたいです!