はじめに
最近マルチクラウドへの需要が増えている気がします。
パブリッククラウド毎のサービス特性やビジネス判断によって、その時々での最適解を選ぶ必要性に迫られている今日この頃。。
今の現場ではAWS ECS上でコンテナオーケストレーションを行っていますが、既存システムを他クラウドへ移行する際のポータビリティを考えると、ECSは完全にAWSロックインだし、Kubernetes Cluster管理だけ任せることができればEKS、GKE、AKSのように他クラウドへの移行も簡単になるのかなと漠然と考えていました。
個人的にもKubernetes Service系のマネージドサービスを使ってみたいということもあり、今回はEKSを使って簡単なオンラインシステムを構築してみたので、手順と所感を残していきます。
アーキテクチャ
- インターネットからの通信をALBで受けて、バックエンドのnginxに接続する。
- 名前解決にはexternal-dnsコンテナを利用する。
フォルダ構成
terraform
│ eks.tf
│ helm_external_dns.tf
│ helm_nginx.tf
│ iam_service_account.tf
│
└─charts
├─external_dns
│ │ Chart.yml
│ │ values.yml
│ │
│ └─templates
│ external-dns.yml
│
└─online_app
│ Chart.yml
│ values.yml
│
└─templates
nginx-deployment.yml
nginx-service.yml
構築手順
EKS ClusterとWorker Nodeのデプロイ
terraform公式の eksモジュールを利用。公式なだけあってよく作りこまれています。
work_groupsの設定だけカスタマイズするくらいで、あとは特に変更せずに使えました。
module "online_app" {
source = "terraform-aws-modules/eks/aws"
version = "v12.2.0"
cluster_name = "online-app"
cluster_version = "1.17"
// publicサブネットを含めないとexternal ALBが作成されないので注意
subnets = concat(data.aws_subnet.private_subnets.*.id, data.aws_subnet.public_subnets.*.id)
vpc_id = data.aws_vpc.vpc.id
worker_groups = [
{
instance_type = "t3.medium"
asg_max_size = 1
// worker nodeにssh接続したい場合に指定
key_name = "dummy"
// ssh接続元IPを絞りたいなど、特殊な通信要件がある場合に指定
additional_security_group_ids = [sg-xxxx]
subnets = data.aws_subnet.private_subnets.*.id
}
]
}
terraform applyを実行すればEKSクラスタ構築は完了です。構築に15分ほどかかるので気長に待ちます。
k8nのコード準備
ECSではterraformコードとして表現していたコンテナオーケストレーションのコードを、EKSではKubernetesのyamlファイルに記述する必要があります。
LoadBalancerのクラウド毎の個別設定はmetadata->annotationsで設定していきます。
LoadBalancer service
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
kubernetes単独だと変数化ができないので、helmを利用して変数化しています。
apiVersion: apps/v1
kind: Service
metadata:
name: nginx-service
labels:
app: nginx
annotations:
service.beta.kubernetes.io/aws-load-balancer-backend-protocol: http
service.beta.kubernetes.io/aws-load-balancer-ssl-cert: {{ .Values.loadBalancer.sslCert }}
service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "https"
external-dns.alpha.kubernetes.io/hostname: {{ .Values.loadBalancer.hostname }}
spec:
selector:
app: nginx
ports:
- name: https
port: 443
targetPort: 80
protocol: TCP
type: LoadBalancer
作成されたLoad Balancerの名前解決はどうすればいいのか悩んでいたところ、externalDNSというAuto Discovery用のサービスがあるとのこと。metadata->annotationsに external-dns.alpha.kubernetes.io/hostname
を指定することで、Serviceに到達するためのRoute 53 recordを自動で作成してくれます。Kubernetes yamlはexternalDNSのサイトに記載されているテンプレートを元に、変数化しました。
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
annotations:
eks.amazonaws.com/role-arn: {{ .Values.eks.serviceAccountRoleArn }}
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services","endpoints","pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list","watch"]
---
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: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
args:
- --source=service
- --source=ingress
- --domain-filter={{ .Values.externalDns.domainFilter }}
- --provider=aws
- --aws-zone-type={{ .Values.externalDns.zoneType }}
- --registry=txt
- --txt-owner-id=my-hostedzone-identifier
securityContext:
fsGroup: {{ .Values.externalDns.securityContext.fsGroup }}
externalDNSコンテナにRoute53の操作を許可するために、Service Accountとして利用するIAM Roleを作成して、EKSのOpenID Connect Providerと連携させます。
こちらのページを参考に、terraformコードを作成しました。
data "aws_iam_policy_document" "eks_assume_role_policy" {
statement {
effect = "Allow"
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.eks.arn]
}
actions = ["sts:AssumeRoleWithWebIdentity"]
condition {
test = "StringEquals"
variable = "${aws_iam_openid_connect_provider.eks.url}:aud"
values = ["sts.amazonaws.com"]
}
}
}
data aws_iam_policy_document AllowExternalDNSUpdates {
statement {
effect = "Allow"
actions = [
"route53:ChangeResourceRecordSets",
]
resources = [
"arn:aws:route53:::hostedzone/*",
]
}
statement {
effect = "Allow"
actions = [
"route53:ListHostedZones",
"route53:ListResourceRecordSets"
]
resources = [
"*",
]
}
}
resource aws_iam_role_policy cluster_AllowExternalDNSUpdates {
name = "AllowExternalDNSUpdates"
role = aws_iam_role.external_dns_cluster_AllowExternalDNSUpdates.id
policy = data.aws_iam_policy_document.AllowExternalDNSUpdates.json
}
resource aws_iam_role external_dns_cluster_AllowExternalDNSUpdates {
name = "${local.name}-external-dns"
assume_role_policy = data.aws_iam_policy_document.eks_assume_role_policy.json
}
resource aws_iam_openid_connect_provider eks {
url = module.online_app.cluster_oidc_issuer_url
client_id_list = [
"sts.amazonaws.com"
]
thumbprint_list = ["9e99a48a9960b14926bb7f3b02e22da2b0ab7280"]
}
helmコードの準備
AWSリソースとKubernetesコンテナの準備が整ったので、コンテナをデプロイするためのコードを作ります。helmをそのまま使ってもデプロイできますが、今回はterraform Helm providerを利用しました。
terraform Helm providerを使ってみて、下記の点で素晴らしいと感じました。
- terraformで構築したAWSの情報を共有できるため、helm実行時に渡す変数をaws-cliなどを使って取得する必要がない。
- helm chartsのデプロイ状態がterraform stateとして管理できる。
Helmのコードとterraform Helm Providerのコードは以下の通りです。
apiVersion: v1
appVersion: "1.0"
description: nginx helm chart for Kubernetes
name: nginx
version: 1.0.0
loadBalancer:
sslCert:
hostname:
provider "helm" {
version = "~> 1.3.0"
kubernetes {
config_path = module.online_app.kubeconfig_filename
}
}
resource "helm_release" "nginx" {
name = "nginx-chart"
chart = "./charts/nginx"
// force_updateをtrueにすることで、helm chartに差分があったら更新する。
force_update = true
set {
name = "loadBalancer.sslCert"
value = data.aws_acm_certificate.this.arn
}
set {
name = "loadBalancer.hostname"
value = "nginx.${data.aws_route53_zone.this.name}"
}
}
同様にexternalDNSのデプロイコードも作成していきます。
apiVersion: v1
appVersion: "1.0"
description: external-dns chart for Kubernetes
name: external-dns
version: 1.0.0
eks:
serviceAccountRoleArn:
externalDns:
domainFilter:
zoneType: public
securityContext:
fsGroup: 65534
resource "helm_release" "external_dns" {
name = "external-dns-chart"
chart = "./charts/external_dns"
force_update = true
set {
name = "eks.serviceAccountRoleArn"
value = aws_iam_role.external_dns_cluster_AllowExternalDNSUpdates.arn
}
set {
name = "externalDns.domainFilter"
value = data.aws_route53_zone.this.name
}
}
動作確認
コードの準備ができたら再度terraform applyを実行します。
うまく動いているようなら external-dns.alpha.kubernetes.io/hostname
で指定したホストに接続することで、nginxの画面が表示されます。
また、externalDNS podのログを見るとちゃんとRoute 53へのUPSERTに成功していることが確認できました。
kubectl logs external-dns-xxx # please use your pod id
EKSを使ってみて所感
-
EKSクラスタ作成に時間がかかる。
コスト節約のために毎回terraformでVPC周りから検証環境を作り直しているが、ECSと比べてEKSはクラスタ作成に時間がかかる。検証するたびに毎回15分以上待つのはツライ。。 -
ECSよりもポータビリティが上がった (気がする…)
Kubernetes yamlに付与するmetadataの種類によってマルチクラウドに対応可能。
ECSでは必要なAWSリソースを自分で構築しなければいけなかったが、EKSではmetadataを付与することでALBが自動で作成されたり、DNS recordが作られたりと、特定クラウドを意識させない点はよい。 -
リソースの統合管理
ECSはAWS管理コンソール上で、コンテナインスタンスのリソース使用状況、各タスクの実行状態やサービスタスク起動数など確認できる統合管理画面が提供されているが、それに比べるとEKSの管理画面は情報が不足していると感じた。Kubernetesの統合管理を実現するための仕組みを検討する必要がある。