最近KubernetesクラスタをAWS上に作っているのだが,EKSは結構お高い.
これはどうしても削れない,EKSの利用料金として,$0.20/hour持っていかれるためである. EKSだけでだいたい$144/monthかかることになる.
これは流石に痛いので,kopsでKubernetesクラスタを作ることにした.
kopsコマンドで簡単にクラスタが作れるよ!という記事は結構あるのだが,実運用していく上で結構カスタマイズしたい部分があったので,書き残しておく.
VPC, IAM, SGあたりはterraform管理したい
とりあえず一度kopsで適当にクラスタを建ててみて,自動生成されるリソースのうち,こちら側で予め用意できるものがどの程度あるか確認した. これは,IAMやSecurityGroup等はできるだけterraform管理したいからだ.
kopsはVPCを始めとして,IAMやSecurityGroupを自動で作ってくれてしまう.たしかに初心者には優しいのだが,これをやられると,「別サーバからのアクセスを受け付けたいからSecurityGroupに穴あけなきゃ」というようなものをコードで管理する方法がなくなってしまう.
で,確認したところ
についてはこちらで用意したものを付与できる.
これならばkopsで問題なくいけそう. ただし,これらのリソースに加えて,KeyPair等までこちらで指定のものを使ってほしい場合には,kube-awsを検討したほうが良い.基本的に使用するAWSリソースのカスタマイズ性に関してはkube-awsのほうが多機能な気がしている. 俺が今回kopsを使っているのは,単純にkube-awsの裏にいるCloudFormationが嫌いというだけのことだ.
予め必要なリソースをterraformで作る
先にterraformで必要なものを作っておく. kopsは,あとから設定変更に応じてクラスタをアップデートできるのだが,インスタンス再生成が発生してしまい,結構時間を取られるので先に埋めてしまったほうが楽だ.
IAM Role
各インスタンスに付与するIAM Role,Instance Profileを作っておく.
IAM Role:
resource "aws_iam_role" "k8s_master_role" { name = "k8s-master-role" path = "/" assume_role_policy = "${file("aws_iam_role_policies/ec2_assume_role_policy.json")}" } resource "aws_iam_role" "k8s_node_role" { name = "k8s-node-role" path = "/" assume_role_policy = "${file("aws_iam_role_policies/ec2_assume_role_policy.json")}" }
こいつはEC2インスタンスにつけるRoleなので,EC2へのassumeが必要になる.
Assume Role Policy:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }
次に,先のRoleを使ってInstance Profileを作っておく.
Instance Profile:
resource "aws_iam_instance_profile" "k8s_master_profile" { name = "k8s-master-profile" role = "${aws_iam_role.k8s_master_role.name}" } resource "aws_iam_instance_profile" "k8s_node_profile" { name = "k8s-node-profile" role = "${aws_iam_role.k8s_node_role.name}" }
そして,これが k8s-master-role
につけるPolicy.
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ec2:DescribeInstances", "ec2:DescribeRegions", "ec2:DescribeRouteTables", "ec2:DescribeSecurityGroups", "ec2:DescribeSubnets", "ec2:DescribeVolumes" ], "Resource": [ "*" ] }, { "Effect": "Allow", "Action": [ "ec2:CreateSecurityGroup", "ec2:CreateTags", "ec2:CreateVolume", "ec2:DescribeVolumesModifications", "ec2:ModifyInstanceAttribute", "ec2:ModifyVolume" ], "Resource": [ "*" ] }, { "Effect": "Allow", "Action": [ "ec2:AttachVolume", "ec2:AuthorizeSecurityGroupIngress", "ec2:CreateRoute", "ec2:DeleteRoute", "ec2:DeleteSecurityGroup", "ec2:DeleteVolume", "ec2:DetachVolume", "ec2:RevokeSecurityGroupIngress" ], "Resource": [ "*" ] }, { "Effect": "Allow", "Action": [ "autoscaling:DescribeAutoScalingGroups", "autoscaling:DescribeLaunchConfigurations", "autoscaling:DescribeTags" ], "Resource": [ "*" ] }, { "Effect": "Allow", "Action": [ "autoscaling:SetDesiredCapacity", "autoscaling:TerminateInstanceInAutoScalingGroup", "autoscaling:UpdateAutoScalingGroup" ], "Resource": [ "*" ] }, { "Effect": "Allow", "Action": [ "elasticloadbalancing:AddTags", "elasticloadbalancing:AttachLoadBalancerToSubnets", "elasticloadbalancing:ApplySecurityGroupsToLoadBalancer", "elasticloadbalancing:CreateLoadBalancer", "elasticloadbalancing:CreateLoadBalancerPolicy", "elasticloadbalancing:CreateLoadBalancerListeners", "elasticloadbalancing:ConfigureHealthCheck", "elasticloadbalancing:DeleteLoadBalancer", "elasticloadbalancing:DeleteLoadBalancerListeners", "elasticloadbalancing:DescribeLoadBalancers", "elasticloadbalancing:DescribeLoadBalancerAttributes", "elasticloadbalancing:DetachLoadBalancerFromSubnets", "elasticloadbalancing:DeregisterInstancesFromLoadBalancer", "elasticloadbalancing:ModifyLoadBalancerAttributes", "elasticloadbalancing:RegisterInstancesWithLoadBalancer", "elasticloadbalancing:SetLoadBalancerPoliciesForBackendServer" ], "Resource": [ "*" ] }, { "Effect": "Allow", "Action": [ "ec2:DescribeVpcs", "elasticloadbalancing:AddTags", "elasticloadbalancing:CreateListener", "elasticloadbalancing:CreateTargetGroup", "elasticloadbalancing:DeleteListener", "elasticloadbalancing:DeleteTargetGroup", "elasticloadbalancing:DeregisterTargets", "elasticloadbalancing:DescribeListeners", "elasticloadbalancing:DescribeLoadBalancerPolicies", "elasticloadbalancing:DescribeTargetGroups", "elasticloadbalancing:DescribeTargetHealth", "elasticloadbalancing:ModifyListener", "elasticloadbalancing:ModifyTargetGroup", "elasticloadbalancing:RegisterTargets", "elasticloadbalancing:SetLoadBalancerPoliciesOfListener" ], "Resource": [ "*" ] }, { "Effect": "Allow", "Action": [ "iam:ListServerCertificates", "iam:GetServerCertificate" ], "Resource": [ "*" ] }, { "Effect": "Allow", "Action": [ "s3:Get*", "s3:ListBucket" ], "Resource": [ "*" ] }, { "Effect": "Allow", "Action": [ "ecr:GetAuthorizationToken", "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:GetRepositoryPolicy", "ecr:DescribeRepositories", "ecr:ListImages", "ecr:BatchGetImage" ], "Resource": [ "*" ] }, { "Effect": "Allow", "Action": [ "ec2:CreateNetworkInterface", "ec2:AttachNetworkInterface", "ec2:DeleteNetworkInterface", "ec2:DetachNetworkInterface", "ec2:DescribeNetworkInterfaces", "ec2:DescribeInstances", "ec2:ModifyNetworkInterfaceAttribute", "ec2:AssignPrivateIpAddresses", "tag:TagResources" ], "Resource": [ "*" ] } ] }
次にnodeに付与するPolicy:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:Get*", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::kops_storage_bucket", "arn:aws:s3:::kops_storage_bucket/*" ] }, { "Effect": "Allow", "Action": [ "ecr:GetAuthorizationToken", "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:GetRepositoryPolicy", "ecr:DescribeRepositories", "ecr:ListImages", "ecr:BatchGetImage" ], "Resource": [ "*" ] }, { "Effect": "Allow", "Action": [ "ec2:DescribeInstances", "ec2:DescribeRegions", "ec2:CreateNetworkInterface", "ec2:AttachNetworkInterface", "ec2:DeleteNetworkInterface", "ec2:DetachNetworkInterface", "ec2:DescribeNetworkInterfaces", "ec2:DescribeInstances", "ec2:ModifyNetworkInterfaceAttribute", "ec2:AssignPrivateIpAddresses", "tag:TagResources" ], "Resource": [ "*" ] } ] }
S3のBucket名は,kopsの構成ファイルを格納するS3のBucket名だ.これは初回に kops create
するときに入力するものと同じである.
resource "aws_iam_policy" "k8s_cluster" { name = "k8s-cluster-policy" path = "/" description = "" policy = "${file("aws_iam_policies/k8s_cluster_policy.json")}" } resource "aws_iam_policy" "k8s_node" { name = "k8s-node-policy" path = "/" description = "" policy = "${file("aws_iam_policies/k8s_node_policy.json")}" }
でこれらを紐付ける.
resource "aws_iam_policy_attachment" "k8s_cluster" { name = "k8s-cluster" roles = [ "${aws_iam_role.k8s_master_role.name}", ] policy_arn = "${aws_iam_policy.k8s_cluster.arn}" } resource "aws_iam_policy_attachment" "k8s_node" { name = "k8s-node" roles = [ "${aws_iam_role.k8s_node_role.name}", ] policy_arn = "${aws_iam_policy.k8s_node.arn}" }
Security Group
masterに付与するSecurityGroup:
resource "aws_security_group" "master_instance" { name = "${var.namespace}-master-instance-${var.env}" description = "Kubernetes master instance" vpc_id = "${data.terraform_remote_state.aws_vpc_tokyo.vpc_id}" # For SSH ingress { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # Myself ingress { from_port = 0 to_port = 0 protocol = "-1" self = true } # For node ingress { from_port = 0 to_port = 0 protocol = "-1" security_groups = [ "${aws_security_group.node_instance.id}", ] } # For API ELB ingress { from_port = 443 to_port = 443 protocol = "tcp" security_groups = [ "${aws_security_group.api_lb.id}", ] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags { Name = "${var.namespace}-master-instance-${var.env}" tfstate = "${var.tfstate}" } }
nodeに付与するSecurityGroup.ただし,これに関しては,SecurityGroupだけ定義して,Ruleを別定義にしている. これは,同時定義すると,master <-> nodeが相互依存になってしまって,terraform的に同時作成ができないためである.
resource "aws_security_group" "node_instance" { name = "${var.namespace}-node-instance-${var.env}" description = "Kubernetes node instance" vpc_id = "${data.terraform_remote_state.aws_vpc_tokyo.vpc_id}" tags { Name = "${var.namespace}-node-instance-${var.env}" tfstate = "${var.tfstate}" } } resource "aws_security_group_rule" "node_ssh" { type = "ingress" from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] security_group_id = "${aws_security_group.node_instance.id}" } resource "aws_security_group_rule" "node_myself" { type = "ingress" from_port = 0 to_port = 0 protocol = "-1" self = true security_group_id = "${aws_security_group.node_instance.id}" } resource "aws_security_group_rule" "node_from_master" { type = "ingress" from_port = 0 to_port = 0 protocol = "-1" source_security_group_id = "${aws_security_group.master_instance.id}" security_group_id = "${aws_security_group.node_instance.id}" } resource "aws_security_group_rule" "node_from_service" { type = "ingress" from_port = 0 to_port = 0 protocol = "-1" source_security_group_id = "${aws_security_group.service_lb.id}" security_group_id = "${aws_security_group.node_instance.id}" } resource "aws_security_group_rule" "node_egress" { type = "egress" from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] security_group_id = "${aws_security_group.node_instance.id}" }
kopsはmasterインスタンスを作る際にELBも一緒に作る.これは kubectl
コマンド等により外部からクラスタのAPIを叩く際に,そのリクエストを受け付けるELBである.
で,こいつにもSecurityGroupをつける必要がある.
resource "aws_security_group" "api_lb" { name = "${var.namespace}-api-lb-${var.env}" description = "Kubernetes API LB" vpc_id = "${data.terraform_remote_state.aws_vpc_tokyo.vpc_id}" ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags { Name = "${var.namespace}-api-lb-${var.env}" } }
これでterraformで作成しておくリソースはすべて作った.
Clusterを編集する
一番最初は kops create
する.ただし --yes
をつけてはいけない.これをつけると,いきなりクラスタを作り始めてしまう.
クラスタを作る前に,構成情報だけ生成し,中身を編集しよう.
なお,このときに先程ポリシー内に記載した kops_storage_bucket
を指定する必要がある.詳しくはkopsのガイドを見ると良い.
kops create
したら早速 kops edit cluster
をやろう.
ここで上書きできる項目は,
である.
apiVersion: kops/v1alpha2 kind: Cluster metadata: creationTimestamp: 2019-02-19T12:59:21Z name: hogehoge spec: api: loadBalancer: securityGroupOverride: <aws_security_group.api_lbのID> type: Public authorization: rbac: {} channel: stable cloudProvider: aws configBase: s3://hogehoge etcdClusters: - etcdMembers: - instanceGroup: master-ap-northeast-1a name: a name: main - etcdMembers: - instanceGroup: master-ap-northeast-1a name: a name: events iam: allowContainerRegistry: true legacy: false kubelet: anonymousAuth: false kubernetesApiAccess: - 0.0.0.0/0 kubernetesVersion: 1.11.6 masterInternalName: api.internal.hogehoge masterPublicName: api.hogehoge networkCIDR: 10.0.0.0/16 networkID: <VPCのID> networking: amazonvpc: {} nonMasqueradeCIDR: <VPCのcidr block> sshAccess: - 0.0.0.0/0 subnets: - cidr: <subnetのcidr block> id: <subnetのID> name: ap-northeast-1a type: Public zone: ap-northeast-1a - cidr: <subnetのcidr block> id: <subnetのID> name: ap-northeast-1c type: Public zone: ap-northeast-1c - cidr: <subnetのcidr block> id: <subnetのID> name: ap-northeast-1d type: Public zone: ap-northeast-1d topology: dns: type: Public masters: public nodes: public
こんな感じにしておく.
Instancegroupを編集する
次に kops edit ig
する.
ここで上書きできるのが,
- master/nodeのInstance Profile
- master/nodeに付与するSecurityGroup
である.
apiVersion: kops/v1alpha2 kind: InstanceGroup metadata: creationTimestamp: 2019-02-19T12:59:21Z labels: kops.k8s.io/cluster: hogehoge name: master-ap-northeast-1a spec: iam: profile: <aws_iam_profile.k8s_master_profileのARN> image: kope.io/k8s-1.11-debian-stretch-amd64-hvm-ebs-2018-08-17 machineType: m5.large maxPrice: "0.1" maxSize: 1 minSize: 1 nodeLabels: kops.k8s.io/instancegroup: master-ap-northeast-1a role: Master securityGroupOverride: <aws_security_group.master_instanceのID> subnets: - ap-northeast-1a --- apiVersion: kops/v1alpha2 kind: InstanceGroup metadata: creationTimestamp: 2019-02-19T12:59:21Z labels: kops.k8s.io/cluster: hogehoge name: nodes spec: iam: profile: <aws_iam_profile.k8s_node_profileのARN image: kope.io/k8s-1.11-debian-stretch-amd64-hvm-ebs-2018-08-17 machineType: t3.medium maxPrice: "0.05" maxSize: 2 minSize: 2 nodeLabels: kops.k8s.io/instancegroup: nodes role: Node securityGroupOverride: <aws_security_group.node_instanceのID> subnets: - ap-northeast-1a - ap-northeast-1c - ap-northeast-1d
これで良い.
あとは, kops update --yes
とかして,実際のクラスタを作成しよう.
terraformで作成したリソースが紐付いたクラスタが作成されるはずである.
aws-iam-authenticatorによる認証を行いたい
kopsでクラスタを作成すると .kube/config
に kubectl
で使う認証情報が吐き出される.ここにはCertificate等が書かれている.
しかし,kopsを使ったマシン以外のマシンで kubectl
を使いたい場合や,別のメンバーに kubectl
による認証をさせるためには,結構めんどくさい.
EKSを使っていた際は, aws-iam-authenticatorを使っていたので,これで解決できると非常に楽になる.
というわけでやってみる. もちろんサポートはしている.
ただし,このガイドの通りにやると
error building loader: certificate "aws-iam-authenticator" not found
となってしまう.
そこで,
この通りにやるとうまく行く.
kubernetes admin roleを作る
まず,AdminのRoleを作る必要がある.再びterraformで,
resource "aws_iam_role" "kubernetes_admin_role" { name = "kubernetes-admin-role" path = "/" assume_role_policy = "${data.template_file.account_assume_role_policy.rendered}" }
data "template_file" "account_assume_role_policy" { template = "${file("${path.module}/aws_iam_role_policies/sts_assume_role_policy.json.tpl")}" vars { account_id = "${var.account_id}" } }
{ "Version":"2012-10-17", "Statement": [ { "Effect":"Allow", "Principal": { "AWS": "${account_id}" }, "Action": "sts:AssumeRole", "Condition": {} } ] }
こんなのを作ってやる. こいつはassumeさせるだけで,特にPolicyを付与させる必要はない.
ConfigMapを作る
次にkubernetes内にConfigMapを作る必要がある.
kubectlが使える状況で,
apiVersion: v1 kind: ConfigMap metadata: namespace: kube-system name: aws-iam-authenticator labels: k8s-app: aws-iam-authenticator data: config.yaml: | clusterID: hogehoge server: # each mapRoles entry maps an IAM role to a username and set of groups # Each username and group can optionally contain template parameters: # 1) "{{AccountID}}" is the 12 digit AWS ID. # 2) "{{SessionName}}" is the role session name. mapRoles: - roleARN: arn:aws:iam::12345678:role/kubernetes-admin-role username: kubernetes-admin groups: - system:masters # map EC2 instances in my "KubernetesNode" role to users like # "aws:000000000000:instance:i-0123456789abcdef0". Only use this if you # trust that the role can only be assumed by EC2 instances. If an IAM user # can assume this role directly (with sts:AssumeRole) they can control # SessionName. - roleARN: arn:aws:iam::12345678:role/k8s-node-role username: aws:{{AccountID}}:instance:{{SessionName}} groups: - system:bootstrappers - aws:instances # map federated users in my "KubernetesAdmin" role to users like # "admin:alice-example.com". The SessionName is an arbitrary role name # like an e-mail address passed by the identity provider. Note that if this # role is assumed directly by an IAM User (not via federation), the user # can control the SessionName. - roleARN: arn:aws:iam::12345678:role/kubernetes-admin-role username: admin:{{SessionName}} groups: - system:masters # each mapUsers entry maps an IAM role to a static username and set of groups mapUsers: - userARN: arn:aws:iam::12345678:user/h3poteto username: h3poteto groups: - system:masters
これを kubectl apply
してしまう.
clusterを更新する
次に kops edit cluster
して,
apiVersion: kops/v1alpha2 kind: Cluster metadata: creationTimestamp: 2019-02-19T12:59:21Z name: hogehoge spec: authentication: aws: {}
というように authenticationの行を追加し,awsを指定する
これで,
$ kops update cluster $NAME --yes $ kops rolling-update cluster ${NAME} --instance-group-roles=Master --cloudonly --force --yes $ kops validate cluster
という順でやっていくと,見事aws-iam-authenticatorが起動する.
認証する
最後に, .kube/config-hogehoge
を作り,
apiVersion: v1 clusters: - cluster: certificate-authority-data: <ここは ~/.kube/configからコピーする> server: <API ELBのendpoint> name: hogehoge contexts: - context: cluster: hogehoge user: aws name: aws current-context: aws kind: Config preferences: {} users: - name: aws user: exec: apiVersion: client.authentication.k8s.io/v1alpha1 command: aws-iam-authenticator args: - "token" - "-i" - "hogehoge
こんなのを用意する.
で,
$ export KUBECONFIG=$HOME/.kube/config-hogehoge $ kubectl version
とすると無事認証が通る.
これでaws-iam-authenticatorによる認証ができた.
費用をケチる
kopsでクラスタを構成するとEC2インスタンスはオンデマンドのものを使うことになる. これをASGから起動しているのだが,最近のASGはSpotPriceを設定することができる. これを設定すると,ASGでインスタンスを管理しつつ,単発のSpotInstanceを指定の価格で購入することになる.
こうすることで,見た目はASGだけど,実質SpotInstanceを利用することができる.
これはSpotFleetとは少し違う.残念ながら現状kopsはSpotFleetには対応していない.
というわけで kops edit ig
する.
apiVersion: kops/v1alpha2 kind: InstanceGroup metadata: creationTimestamp: 2019-02-19T12:59:21Z labels: kops.k8s.io/cluster: hogehoge name: master-ap-northeast-1a spec: image: kope.io/k8s-1.11-debian-stretch-amd64-hvm-ebs-2018-08-17 machineType: m5.large maxPrice: "0.1" maxSize: 1 minSize: 1
ここの maxPrice
を設定すると,これがSpotのMaxPriceとして設定され,SpotInstanceを使うようになる.
こうすることでnodeやmasterのインスタンス料金をケチれる.
まとめ
というわけで無事,terraformでリソースを管理しつつKubernetesクラスタを作ることができた. 欲を言うなら,インスタンスが使うKeyPairあたりも別で管理させてほしいのだが…….この辺はkube-awsだとできるらしい.
あと,SGやIAMで,どんなものが必要になるかがどこにも書いてなくて,ソースを見るか,実際に一度クラスタを作って眺めるしかなかった. このあたりはもう少し丁寧にドキュメントになっていると嬉しい.
今回はSpotInstanceも使っているし,masterは1台だけだし,そこまで強いクラスタを作ったわけではない.どちらかというと安く済ませたいというのが大きい. しかし最終的に,高可用性や耐障害性を考えると,masterを複数台構成にしたり(これ自体はkopsでもそんなに難しいわけではない),etcdを冗長化したりする必要がある. ましてやKubernetesのmasterは,それなりにでかいインスタンスを使う必要があり,それを複数台+APIエンドポイント用のELBとかを考え始めると,おそらくEKSと値段的にはいい勝負なのではないだろうか. なので,そういうしっかりしたクラスタを作りたいのであれば,kopsでmulti master構成を考えるよりはEKSを考えたほうが良いとは思う.