Atrae Tech Blog

People Tech Companyの株式会社アトラエのテックブログです。

【EKS】ALBIngressControllerからAWSLoadBalancerControllerに移行する

f:id:atrae_tech:20201225155333j:plain

こんにちは、アトラエでインフラエンジニアをやっているくーまです。

今だとEKSを使っている人が多いと思いますが、AWSkubernetesを利用・運用する場合にALBIngressControllerを利用して、kubernetes側からALBを作成・管理している人は多いのではないでしょうか。

そんなALBIngressControllerですが、2020年10月23日にバージョンが2に上がるのを機に、名前すらAWSLoadBalancerControllerと変更になるなど、数々の変更が入りました。

変更の詳細については本題から逸れるので触れませんが、詳細はリリースページをご参照ください。

github.com

このバージョンアップですが、Imageのバージョンを上げれば移行おっけ〜という話ではなく、明確な移行作業を行わないといけない形になっています。

もちろんたちどころにALBIngressControllerが使えなくなるというわけではありませんが、バージョンアップ作業とダイエットは早い内に手をつけたほうが良いとよく言われますので、実際にやってみました。

※個人的に先日長らく放置していたRuby/Railsのバージョンアップに手をつけたら大変なことになった経験が効いています…w

そして実際にやった結果、大きな落とし穴が一つと、危険なポイントが一つあったので、そちらも合わせて記載します。

とりあえず抑えておきたいこと

下位互換性があるため、ビビる必要はない

後述しますが、現状動いているであろうALBIngressControllerはアンインストールすることになります。

そのため、「え?これ本当にやって大丈夫なの?」という気になるかもしれませんが、

  • 今動いているALBに影響無く
  • 今管理しているingressに影響無く

移行を完了することができるようになっています。

とはいえ、現状動いているALBIngressControllerのバージョンをv1.1.3以上に上げる、ということは必要なので、これだけは事前に確認しておきましょう。

ドキュメントに大きな落とし穴がある

ドキュメント通りにいくと、

  1. AWSLoadBalancerControllerを作る
  2. ALBIngressControllerからのmigrateに必要な権限を渡す

という順番で書いてありますが、この「migrateに必要な権限」が無い状態でAWSLoadBalancerControllerを起動すると、現在存在するALBに紐付いているセキュリティグループが、ノードグループのセキュリティグループのインバウンドから外れ、外部アクセスが遮断されます。

そのため、この順序は逆にして、予め権限を付与した状態で起動することをおすすめします。

ドキュメントに危険なポイントがある

こちらはしっかりドキュメント通りに対応すれば問題ない点ですが…

下記の文言が移行ドキュメントに記載されていると思います。

when security-groups annotation isn't used:

a managed SecurityGroup will be created and attached to ALB. This SecurityGroup will be preserved.
an inbound rule will be added to your worker node securityGroups which allow traffic from the above managed SecurityGroup for ALB.
The ALBIngressController didn't add any description for that inbound rule.
The AWSLoadBalancerController will use elbv2.k8s.aws/targetGroupBinding=shared for that inbound rule
You'll need to manually add elbv2.k8s.aws/targetGroupBinding=shared description to that inbound rule so that AWSLoadBalancerController can delete such rule when you delete your Ingress.

要は、「自分でセキュリティグループを作って、それをingressのannotationで渡している場合は何もやることはないけど、これまでALBIngressControllerにセキュリティグループを作らせていた場合は、ノードグループ側のセキュリティグループの説明欄にelbv2.k8s.aws/targetGroupBinding=sharedと記載してね」ということを言っています。

この「elbv2.k8s.aws/targetGroupBinding=shared」という説明文が書いてあるとAWSLoadBalancerControllerはセキュリティグループの操作をやるよ!ということなので、はいお願いしますということで説明欄に書かないといけません。

が、これを誤って自分で作ったセキュリティグループにつけてしまうと、ノードグループのセキュリティグループのインバウンドから外れ、外部アクセスが遮断されます。

これに関してはミスらなければOKという話ではあるのですが、ミスったときの罰が想像以上に重いので、気をつけたほうが良さそうです。

移行作業開始

基本的にはドキュメント通りで上手くいくと思われます。

こちらを軸に、自分の環境に合わせこんで移行を進めることになります。

本記事でも基本はそこをなぞりながら書きます。

ノードグループのセキュリティグループに説明文を追加する

これは上記で既に触れたところですが、ALBIngressControllerに作らせているセキュリティグループについては、説明文として「elbv2.k8s.aws/targetGroupBinding=shared」を追加します。

ALBIngressControllerのアンインストールは後に回す

初手からALBIngressControllerのアンインストールとなっていますが、なるべくALBがアンコントローラブルになる時間を減らしたいなと思ったので、ここは後回しにしました。

IAM Roleの準備

AWSLoadBalancerControllerはAWSリソースに対して様々な処理を行うので、IAMの設定は当然必要になります。

旧ALBIngressControllerの時代からも必要な権限は変わっているようなので、新規に作り直します。

ドキュメントではawsコマンドでIAM Roleを作成していますが、恐らくProduction環境前提の場合この形は取らないのではないかと思います。

アトラエではterraformを使って管理をしているためここはterraformに置き換える形で作成しました。

terraformのjsonドキュメント形式に変換するのはだいぶ面倒くさかったので、誰か使うかもということで載せておきます。

data "aws_iam_policy_document" "aws-lbc-sample" {
  statement {
    actions = ["iam:CreateServiceLinkedRole",
                "ec2:DescribeAccountAttributes",
                "ec2:DescribeAddresses",
                "ec2:DescribeInternetGateways",
                "ec2:DescribeVpcs",
                "ec2:DescribeSubnets",
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeInstances",
                "ec2:DescribeNetworkInterfaces",
                "ec2:DescribeTags",
                "elasticloadbalancing:DescribeLoadBalancers",
                "elasticloadbalancing:DescribeLoadBalancerAttributes",
                "elasticloadbalancing:DescribeListeners",
                "elasticloadbalancing:DescribeListenerCertificates",
                "elasticloadbalancing:DescribeSSLPolicies",
                "elasticloadbalancing:DescribeRules",
                "elasticloadbalancing:DescribeTargetGroups",
                "elasticloadbalancing:DescribeTargetGroupAttributes",
                "elasticloadbalancing:DescribeTargetHealth",
                "elasticloadbalancing:DescribeTags"]
    effect  = "Allow"
    resources = ["*"]
  }

  statement {
    actions = ["cognito-idp:DescribeUserPoolClient",
                "acm:ListCertificates",
                "acm:DescribeCertificate",
                "iam:ListServerCertificates",
                "iam:GetServerCertificate",
                "waf-regional:GetWebACL",
                "waf-regional:GetWebACLForResource",
                "waf-regional:AssociateWebACL",
                "waf-regional:DisassociateWebACL",
                "wafv2:GetWebACL",
                "wafv2:GetWebACLForResource",
                "wafv2:AssociateWebACL",
                "wafv2:DisassociateWebACL",
                "shield:GetSubscriptionState",
                "shield:DescribeProtection",
                "shield:CreateProtection",
                "shield:DeleteProtection"]
    effect  = "Allow"
    resources = ["*"]
  }

  statement {
    actions = ["ec2:AuthorizeSecurityGroupIngress",
               "ec2:RevokeSecurityGroupIngress"]
    effect  = "Allow"
    resources = ["*"]
  }

  statement {
    actions = ["ec2:CreateSecurityGroup"]
    effect  = "Allow"
    resources = ["*"]
  }

  statement {
    actions = ["ec2:CreateTags"]
    effect  = "Allow"
    resources = ["arn:aws:ec2:*:*:security-group/*"]
    condition {
      test     = "StringEquals"
      variable = "ec2:CreateAction"
      values   = ["CreateSecurityGroup"]
    }
    condition {
      test     = "Null"
      variable = "aws:RequestTag/elbv2.k8s.aws/cluster"
      values   = ["false"]
    }
  }

  statement {
    actions = ["ec2:CreateTags",
                "ec2:DeleteTags"]
    effect  = "Allow"
    resources = ["arn:aws:ec2:*:*:security-group/*"]
    condition {
      test     = "Null"
      variable = "aws:RequestTag/elbv2.k8s.aws/cluster"
      values   = ["true"]
    }
    condition {
      test     = "Null"
      variable = "aws:ResourceTag/elbv2.k8s.aws/cluster"
      values   = ["false"]
    }
  }

  statement {
    actions = ["ec2:AuthorizeSecurityGroupIngress",
               "ec2:RevokeSecurityGroupIngress",
               "ec2:DeleteSecurityGroup"]
    effect  = "Allow"
    resources = ["*"]
    condition {
      test     = "Null"
      variable = "aws:ResourceTag/elbv2.k8s.aws/cluster"
      values   = ["false"]
    }
  }

  statement {
    actions = ["elasticloadbalancing:CreateLoadBalancer",
                "elasticloadbalancing:CreateTargetGroup"]
    effect  = "Allow"
    resources = ["*"]
    condition {
      test     = "Null"
      variable = "aws:RequestTag/elbv2.k8s.aws/cluster"
      values   = ["false"]
    }
  }

  statement {
    actions = ["elasticloadbalancing:CreateListener",
                "elasticloadbalancing:DeleteListener",
                "elasticloadbalancing:CreateRule",
                "elasticloadbalancing:DeleteRule"]
    effect  = "Allow"
    resources = ["*"]
  }

  statement {
    actions = ["elasticloadbalancing:AddTags",
                "elasticloadbalancing:RemoveTags"]
    effect  = "Allow"
    resources = ["arn:aws:elasticloadbalancing:*:*:targetgroup/*/*",
                "arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*",
                "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*"]
    condition {
      test     = "Null"
      variable = "aws:RequestTag/elbv2.k8s.aws/cluster"
      values   = ["true"]
    }
    condition {
      test     = "Null"
      variable = "aws:ResourceTag/elbv2.k8s.aws/cluster"
      values   = ["false"]
    }
  }

  statement {
    actions = ["elasticloadbalancing:ModifyLoadBalancerAttributes",
                "elasticloadbalancing:SetIpAddressType",
                "elasticloadbalancing:SetSecurityGroups",
                "elasticloadbalancing:SetSubnets",
                "elasticloadbalancing:DeleteLoadBalancer",
                "elasticloadbalancing:ModifyTargetGroup",
                "elasticloadbalancing:ModifyTargetGroupAttributes",
                "elasticloadbalancing:DeleteTargetGroup"]
    effect  = "Allow"
    resources = ["*"]
    condition {
      test     = "Null"
      variable = "aws:ResourceTag/elbv2.k8s.aws/cluster"
      values   = ["false"]
    }
  }

  statement {
    actions = ["elasticloadbalancing:RegisterTargets",
                "elasticloadbalancing:DeregisterTargets"]
    effect  = "Allow"
    resources = ["arn:aws:elasticloadbalancing:*:*:targetgroup/*/*"]
  }

  statement {
    actions = ["elasticloadbalancing:SetWebAcl",
                "elasticloadbalancing:ModifyListener",
                "elasticloadbalancing:AddListenerCertificates",
                "elasticloadbalancing:RemoveListenerCertificates",
                "elasticloadbalancing:ModifyRule"]
    effect  = "Allow"
    resources = ["*"]
  }
}

アトラエではIAM Roleをkubernetesのserviceaccountに紐付ける形で権限の適応をやっていますが、そうでなくても上記のjsonドキュメントは使えると思うので、適宜ご利用下さい。

ここで合わせて、移行用のIAM ポリシーを作り、追加します。

本来はterraformで適用するところですが、移行が終わったら用無しになるポリシーなので、今回は手で作ってRoleに適用しました。

cert-managerのインストール

クラスターにcert-managerを入れる必要があります。

既に何らかの理由で入れてる人はスキップしていい(バージョンだけは要確認)項目ですが、アトラエでは入れてなかったので、入れました。

kubernetesクラスターのバージョンが1.16以上か未満かによってcert-managerのインストール方法が異なるので、注意しましょう。

記事公開時点での最新バージョンは1.1.0だったので、こちらをインストールします。

github.com

下記コマンド一発でインストールできますが、アトラエではYAMLGithubで管理しつつGitOpsで運用しているので、実際にはファイルを保存してから利用しています。

$ kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.1.0/cert-manager.yaml

ALBIngressControllerのアンインストール

先程後回しにしたので、ここでALBIngressControllerをアンインストールします。

ここは完全に各々の運用方法によって手段は変わると思いますので、適切な方法で実施して頂ければと思います。

アトラエの場合はkustomizeをつかってYAMLを管理しているので、コマンド一発でした。

当然ですが、アンインストールしてもサービスはそのまま利用できているはずです。

AWSLoadBalancerControllerのインストール

ドキュメント通り、まずはYAMLをダウンロードします。

そして、Deploymentにクラスター名を入れる必要があるので、忘れず対応しましょう。

apiVersion: apps/v1
kind: Deployment
. . . 
name: aws-load-balancer-controller
namespace: kube-system
spec:
    . . . 
    template:
        spec:
            containers:
                - args:
                    - --cluster-name=<YOUR_CLUSTER_NAME>

また、前述の通りIAM Roleをkubernetesのserviceaccountに紐付ける形で権限適応しているため、そこも修正します。

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: <YOUR_ROLE_ARN>

後はこれをapplyすると、先程作成したIAM Roleを利用してAWSLoadBalancerControllerが稼働します。

移行して思ったこと

今後もお世話になるAWSLoadBalancerController君なので移行をためらうことはありませんでしたが、途中でノードグループのセキュリティグループからALBのセキュリティグループが外れアクセスが遮断されたときは、「あ、これPRD適用やめようかな…」と思ったりもしました。w

移行時の落とし穴にさえ気をつければ、下位互換性もしっかりあるアップデートなので、早めにやっておいて損はないなという感じです。

今回は移行の話だけになりましたが、気が向いたら実際に様々な機能を使ってみてどうだったかみたいな話も書こうかなと思います。