それでは、ようやく Kubernetes on EKS を構築していこうと思います。一気に進めるとグチャグチャになりそうなので、こまめに分けていくよう意識していきます。
今回は、Kubernetes のクラスタを触れるようにするとこまでで、実際には何も動かないですが大事なベースです。
概要
Kubernetes on EKS を Terraform 一本勝負でゴリゴリ書いていきます。EKS も Kubernetes も Helm も全部です。あとは LaunchTemplate の user data などのいくつかのちょっとした処理でシェルスクリプトを使う程度です。Terraform のバージョンは v0.12.0 以降を想定しており、自分の環境だと terraform も provider も常に最新にしていくよう意識しています。特に kubernetes provider は発展途上なので、古いと使えない項目があったりするので、最新にするのを推奨です。
構築手順については、要所に説明は入れていきますが、基本的な手順や解説はちゃんと公式っぽいところを見たほうがよいので、リンク貼ってくので合わせて参考にお願いします。
開発準備
Terraform, AWS CLI
以前の下記記事を参考にしてください。Terraform のバージョンは、CHANGELOGをみて、Unreleased ではない最新のものに更新していくとよいです。
1 2 |
# 現時点で最新 tfenv install 0.12.5 |
provider は普段は aws とかしか使わないかもですが、kubernetes や helm なども出てくるので、ちょいちょい最新に更新していくのがよいです。
1 |
terraform init --upgrade |
kubectl
Kubernetesを操作するためのコマンドです。公式ドキュメントはまぁ上記リンク先に書いてあるのですが……私の場合はLinuxなのでこうじゃ。
1 2 3 4 5 |
curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl chmod +x kubectl sudo chown root:root kubectl sudo mv kubectl /usr/local/bin/ kubectl version |
helm
あまり直に操作することはないですが、あると安心するんじゃポルナレフ。こちらもこうじゃ。
1 2 3 4 5 6 7 8 9 |
curl -L https://git.io/get_helm.sh | bash # credentials がないとエラーになるけど初期化はできる helm init --service-account tiller --history-max 200 --upgrade # credentials があっても tiller-deploy という pod がないとエラーになるけどバージョンは確認できる helm version # いろいろリソースができた後に軽く使ってみましょう |
IAM Role 作成
ここからリソースを作成していくにあたって、基本的な公式手順は下記にあるのですが細かい説明はそちらを見てほしいものの、ぶっちゃけ管理画面とか eksctl からやっても理解しづらいです。Terraform などでリソース1つ1つを分解して関連付けを見ていった方が、圧倒的早さで身につくと思います。
──という感じで、早速2つRoleを作ります。
Control Plane用
まずはEKSそのものであるControl Plane用のRoleを作成します。まずはRole本体を作成し、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
resource "aws_iam_role" "eks_master" { count = local.on_common ? 1 : 0 name = "eks_master" assume_role_policy = <<JSON { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "eks.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } JSON } |
次に既存Policyを割り当てます。ちょいと local変数 使っちゃってますが、もう1つのNodeのポリシーが多くなったので、合わせてまとめてしまいました。
1 2 3 4 5 6 7 8 9 10 11 12 |
locals { eks_master_policy_arns = [ "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy", "arn:aws:iam::aws:policy/AmazonEKSServicePolicy", ] } resource "aws_iam_role_policy_attachment" "eks_master_policy" { count = local.on_common ? length(local.eks_master_policy_arns) : 0 policy_arn = element(local.eks_master_policy_arns, count.index) role = aws_iam_role.eks_master[0].name } |
最後にカスタムポリシーです。構築中になにか足りなくなって、既存ポリシーに無いものはココに入れていくことにしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
resource "aws_iam_role_policy" "eks_master_custom" { count = local.on_common ? 1 : 0 name = "eks_master_custom" role = aws_iam_role.eks_master[0].id policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ec2:CreateTags" ], "Resource": [ "*" ] } ] } EOF } |
Worker Node用
こちらは、Pod が載るノードである EC2インスタンスに割り当てる profile と、kubernetesの方で指定する role になります。Pod が持つ権限もこれと同等になります。まずはRole本体を作成し、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
resource "aws_iam_role" "eks_node" { count = local.on_common ? 1 : 0 name = "eks_node" assume_role_policy = <<JSON { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } JSON } |
EC2にも使うので profile を作り、
1 2 3 4 5 6 |
resource "aws_iam_instance_profile" "eks_node" { count = local.on_common ? 1 : 0 name = "eks_node" role = aws_iam_role.eks_node[0].name } |
既存ポリシーを割り当てるのですが、こちらは私がやりたいことを実現するためにいっぱい追加しちゃってるので、最終的には真面目に精査してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
locals { eks_node_policy_arns = [ "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy", "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy", "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly", "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess", "arn:aws:iam::aws:policy/AutoScalingFullAccess", "arn:aws:iam::aws:policy/service-role/AWSLambdaRole", ] } resource "aws_iam_role_policy_attachment" "eks_node_policy" { count = local.on_common ? length(local.eks_node_policy_arns) : 0 policy_arn = element(local.eks_node_policy_arns, count.index) role = aws_iam_role.eks_node[0].name } |
で、最後にお好みスパイスで、SSMなど。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
resource "aws_iam_role_policy" "eks_node_custom" { count = local.on_common ? 1 : 0 name = "eks_node_custom" role = aws_iam_role.eks_node[0].id policy = <<JSON { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ec2:CreateTags" ], "Resource": [ "*" ] }, { "Effect": "Allow", "Action": [ "ssm:GetParameter*", "secretsmanager:GetSecretValue", "kms:Decrypt" ], "Resource": [ "arn:aws:ssm:${local.region}:${local.service_account_id}:parameter${local.eks_ssm_prefix}/*", "arn:aws:kms:${local.region}:${local.service_account_id}:alias/aws/ssm" ] } ] } JSON } |
セキュリティグループ作成
Control Plane は開発者の kubectl や WorkerNode から注文を受けるし、WorkerNode はクラスタとしての通信や Pod間の通信が行き来するので色々開けることになります。非常に大雑把に開けていますが、実は公式+α したくらいの内容です。ポイントは、クラスタ用通信として443番と、Ephemeral port 範囲がTCP/UDP 全部、Private内で許容されてれば良いって感じです。もっとキッチリ締めたい場合でも、最初は大きくしておいた方が構築中に無駄に詰まらないので、精査するのは後回しにしましょう。
公式はこの辺ですね
Control Plane用
こちらは443番があればって感じですが、Private Network だけじゃなく、開発環境のために自分とこ管理のGlobal Networkも追加してます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
resource "aws_security_group" "eks_master" { count = local.on_service ? 1 : 0 vpc_id = aws_vpc.default[0].id name = "eks_master" description = "EKS master" ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = concat(local.local_networks, local.own_global_addresses) } ingress { from_port = 1025 to_port = 65535 protocol = "tcp" cidr_blocks = local.local_networks } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = format("EKS Master %s %s", local.service_name, local.env) service = local.service_name env = local.env } } |
WorkerNode用
こっち、色々開けてて気持ち悪いかもなので、言い訳しておきます。公式の推奨+独自判断の内容になっているのですが、重複を排除して記述すると公式部分がなにで独自がどれだかわかりづらくなるので、あえて重複無視してガツンと置いています。1つ重要な追加点としては、UDP を追加したところです。もっというと、UDP:53 だけでも良いです。これは、EKSクラスタを作成して、1つ目のNodeを起動した時点で、EKSが coredns というPodを2つ立ち上げるのですが、名前の通り普通にDNSサーバーとしてUDP:53 を使用します。
ということは、1Node内でのPod間通信ならセキュリティグループ関係なく通るのですが、複数NodeでのNode間Pod通信になると、Nodeのセキュリティグループの判定を通ることになるので、UDPがないと名前解決できない問題に引っかかることになります(引っかかった:-)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
locals { # EKSで使うリソースと識別するためのタグのキー eks_tag_key = "kubernetes.io/cluster/${local.eks_cluster_name}" } resource "aws_security_group" "eks_node" { count = local.on_service ? 1 : 0 vpc_id = aws_vpc.default[0].id name = "eks_node" description = "EKS node" ingress { from_port = 0 to_port = 65535 protocol = "udp" cidr_blocks = local.local_networks } ingress { from_port = 0 to_port = 65535 protocol = "tcp" cidr_blocks = local.local_networks } ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = local.local_networks security_groups = [ aws_security_group.eks_master[0].id, ] } ingress { from_port = 1025 to_port = 65535 protocol = "tcp" cidr_blocks = local.local_networks security_groups = [ aws_security_group.eks_master[0].id, ] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = { "Name" = format("EKS Node %s %s", local.service_name, local.env) "service" = local.service_name "env" = local.env "${local.eks_tag_key}" = "owned" } } |
EKS用タグ
↑のリソースで出てきましたが、怪しげなキーのタグがあります。これは他のリソースでもつけるモノがあるのですが、EKSがKubernetesのリソースとして使うというマーキング、もしくは使って良いと判断するための目印になります。
例えば、AutoscalingGroup 経由で EC2 にもつけるのですが、それによって EC2 リストから、このタグがついているインスタンスだけを Node リストに自動的に追加します。
他に、VPC や subnet はここでは作成用コードを記述していませんが、EKSクラスタを作成した時点で、指定したVPC と subnet には自動的に同じキーのタグが付与されます。
そのため、Terraform 外からリソース操作されたことになるので、クラスタ作成後に plan を打つと、そのEKS用タグを削除しようとします。それを回避するために、aws_vpc や aws_subnet に無視コードを入れるのですが、
1 2 3 |
lifecycle { ignore_changes = [tags] } |
残念ながら ignore_changes の配列内の値では変数を利用できないので、こういうコードが書けず、ちょっと tags の扱いがイマイチな雰囲気になります。
1 2 3 4 5 6 7 8 9 |
# これは無理 lifecycle { ignore_changes = [tags[local.eks_tag_key]] } # こーいうのもダメ lifecycle { ignore_changes = ["tags[${local.eks_tag_key}]"] } |
EKSクラスタ作成
ようやく、EKSリソースを作成します。いわゆる Control Plane という中心部ですが、EKSがなんとかしてくれるのはコレだけです。コレだけって言ったら怒られそうですが、それ以外は全部自分で頑張るのです、それがK8s。ロググループ
私は良い子ちゃんなので、CloudWatch LogGroupを先に作っちゃいます。EKSも自動で作ってくれますが、それだと無期限なので、その辺は Lambda の扱いと一緒。
1 2 3 4 5 6 |
resource "aws_cloudwatch_log_group" "eks_main" { count = local.on_eks ? 1 : 0 name = "/aws/eks/${local.eks_cluster_name}/cluster" retention_in_days = 7 } |
クラスタ
ここまで作成した地味リソースを指定して作成します。完了までに10分前後かかります。ログの種類はもっとあるのですが、あまりに意味ないログを吐き出す項目は削除しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
resource "aws_eks_cluster" "main" { count = local.on_eks ? 1 : 0 name = local.eks_cluster_name role_arn = data.terraform_remote_state.common.outputs.aws_iam_role_eks_master_arn vpc_config { security_group_ids = [aws_security_group.eks_master[0].id] subnet_ids = slice(aws_subnet.public.*.id, 0, local.eks_config["az_num"]) endpoint_public_access = true endpoint_private_access = true } enabled_cluster_log_types = ["authenticator", "controllerManager", "scheduler"] depends_on = [aws_cloudwatch_log_group.eks_main] } |
作成が完了したら、EKS管理画面でも確認したり、VPC や subnet に変なタグが付いたことを確認してきましょう。
Kubernetes に触る準備
Terraformでのリソース作成は今回はいったんこれで終わりにして、手動でKubernetesちゃんと戯れる準備をします。kubectl で作業するには、IAM User とクラスタ接続用の情報が必要になります。IAM User
ちょっとここ曲者なのですが、EKS Kubernetes を扱えるのは、最初はEKSクラスタを実際に作成した User のみとなっています。つまり今回の場合、terraform apply を実行した IAM User …… 例えば “terraform” とかですかね。おそらく全権限持っちゃってるでしょう。他の User を使いたい、とかいう話はこちらにあるのですが、
長くなるので今回は初期ユーザーでの実行ということでいきます。鍵を export するなり、aws configure で ~/.aws/credentials に書かれてるとして次へいきます。
クラスタ情報取得
クラスタから接続用の情報を引っ張ってきて保存します。eks:UpdateKubeconfig のような権限はなくて、eks:DescribeCluster があれば取得できます。
1 2 3 4 5 |
# 取得 aws eks update-kubeconfig --name ${CLUSTER_NAME} --region ${REGION} # 情報確認 less ~/.kube/config |
kubectl の始まりだ
リソースは全て Terraform で管理するとはいえ、開発にはゴリゴリ kubectl を使っていくことになります。まだNodeもPodもないので、クラスタの情報だけ確認してティータイムにしましょう。
1 2 3 4 5 6 |
$ kubectl cluster-info Kubernetes master is running at https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.sk1.ap-northeast-1.eks.amazonaws.com CoreDNS is running at https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.sk1.ap-northeast-1.eks.amazonaws.com/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'. |
もしエラーが出たら、IAM Userの権限や鍵、セキュリティグループあたりが原因なので、なんとかしてください。
ね、たいへんでしょ?