pod가 ECR의 이미지를 받아오는 방법은 여러가지가 있다.
1. EKS에서 보편적이고 권장되는 방법인, EKS Node Role의 권한으로 받아오는 것
2. ECR Token 값을 받아와 Docker Secret으로 저장 후 pod가 해당 Secret을 물고 올라감
나도 당연히 우리 회사의 새 아키텍처에서 1번으로 세팅해두고 싶었다.
그러나! 새 아키텍처에서는 "모든 계정의 클러스터" 에서 "공용 계정의 ECR" 에서 이미지를 받아오는 것으로 결정됐다.
뭐...그래도 Cross Account로 받아오는 방법이 당연 있을텐데, 역시 ECR Repository 단위로 타 계정을 허용해줄 수 있었다.
{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "AllowPushPull",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::{cross account id}:root"
},
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
]
}
]
}
그러나!!! 우리는 개발자들이 이미지 올릴 때 자동으로 생성하게 권한을 열어주고 있고, 그 자율성은 해칠 수 없을 것 같다... (Registry 단위 허용은 불가능했다)
그래서 결국 2번 방식을 선택하기로 했다.
(사실 ECR Repository를 계정별로 replication 뜨는 방법도 있다. 그러나 우리가 공용 ECR을 쓰기로 결정한 이유는 서비스 개발자들이 차트 구성 시 어떤 계정의 레포에서 받아와야 하는지를 신경쓰게 하고싶지 않아서 였기 때문에 포기하고 2번 방식으로 갔다.)
(물론 플랫폼 등을 직접 개발해서 더 나은 서비스와 개발 경험을 제공하고 싶지만, 아직은 리소스가 부족하고 당장 새로 구축하는게 바쁘기 때문에 추후 고도화 때 다시 논의해볼 것 같다.)
...그렇다면 2번 방식은 스무스하게 완성했는가? 그것도 아니었다. ㅋ..
우선 이 방식의 치명적인 단점은 "12시간마다 토큰값이 만료" 된다는 점이었다.
-> Cronjob을 만들어 주기적으로 업데이트를 쳐줘야겠다. 대략 6~10 시간마다 돌려주면 되겠지?
그러나 그 다음으로 발견한, 조금 덜 치명적인 단점은... "k8s Secret은 Namespace 종속 리소스" 라는 점이었다.
-> 네임스페이스별로 Secret을 만들고, 업데이트를 쳐줘야 한다.
이건 뭐 어렵지 않을 것 같은데? -> 아니다. 우리는 argocd를 통한 application 배포 또한 네임스페이스 자동 생성 옵션을 물릴 예정이다.
즉, 팀별/파트별 네임스페이스가 상대적으로 자유롭게 생길 수 있는데, 그 때마다 감지해서 시크릿을 만들어줘야 하는것일까...?
-> 고민 끝에, 이거는 아름다운 파이프라인을 일부 포기하기로 했다. 그냥 Cronjob 자체는 1시간에 한번씩 돌리고, 새로 네임스페이스가 생성되며 앱이 배포되어 이미지를 받아오지 못해 깨지는 경우, 한번씩 수동으로 Cronjob을 Trigger 시켜주기로 했다.
자, 그럼 왜 굳이 EKS와 ECS를 쓰면서 Docker Secret을 만들어 쓰는지에 대한 구구절절은 끝났다....
뭐든, 이제 만들어보자 🚀
# 0. 필요 권한 체크 및 SA 생성
이참에 k8s의 RBAC와 IRSA에 대해 빡세게 다져놓기나 해야겠다 ... 하고 마음을 먹었다.
그래서 이 Cronjob을 만들 때 필요한 권한이 뭐냐? 를 정리해본다면,
- k8s 내부 권한 : namespace 조회 권한 + 모든 namespace에서 secret을 생성하고 Patch할 수 있어야 함
- AWS 권한 : 중앙 계정의 ECR read 권한
k8s 내부 권한은 k8s RBAC으로, AWS 권한은 IRSA로 세팅한다.
그리고 RBAC이든 IRSA든 Pod가 사용하는 Service Account에 권한을 부여하게 된다.
따라서 Cronjob의 pod가 사용하게 될 Service Account를 미리 생성하고 시작하겠다.
apiVersion: v1
kind: ServiceAccount
metadata:
name: ecr-secret-udpater
namespace: kube-system
(크론잡 및 SA는 kube-system에 만든다)
# 1. RBAC : k8s 내부 권한 설정
RBAC (Role-Based Access Control) 을 통해 k8s api 서버에 접근하는 권한을 제어할 수 있다.
-> 즉, 역할 기반으로 쿠버네티스 내부 리소스들에 접근하는 권한을 제어할 수 있다.
부여할 수 있는 역할의 종류는 Role 과 ClusterRole이 존재한다. 이 둘의 차이는 Namespace 종속이냐 아니냐 정도로 이해하면 된다.
나는 전 namespace에 걸친 권한이 필요하므로, ClusterRole을 만들어야겠다.
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: ecr-secret-udpater
rules:
- apiGroups: [""] ## namespace 조회 권한
resources:
- namespaces
verbs:
- list
- apiGroups: [""] ## secret create / patch 권한
resources:
- secrets
verbs:
- get
- create
- patch
이제 ClusterRole과 ServiceAccount를 매핑시켜줘야 한다.
ClsuterRole을 사용할 유저에 매핑하기 위해선 ClusterRoleBinding이란 객체가 추가로 필요하다.
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: ecr-secret-udpater
subjects:
- kind: ServiceAccount
name: ecr-secret-udpater
namespace: kube-system
roleRef:
kind: ClusterRole
name: ecr-secret-udpater
apiGroup: "rbac.authorization.k8s.io"
쿠버 내부 권한 설정은 끝났다.
# 2. IRSA : AWS 권한 설정
모든 클러스터에서 중앙 계정의 ECR에 접근해야 된다. 즉,
모든 클러스터의 Cronjob에서 중앙 계정의 ECR을 읽을 수 있는 권한이 있어야 한다.
Cross Account의 권한을 받아오는 가장 쉬운 방법은 IAM User를 사용하는 것이다.
A 계정 User의 Access Key와 Secret Key를 박아 쓴다면 B 계정의 서버에서도 A 계정의 리소스를 사용할 수 있다.
그러나 난 모든 작업을 git에 올려 그 어떤 새로운 상황에 그대로 다시 돌릴 시에도 동일하게 작동하도록 설정하고 싶다.
-> 액세스키를 git에 올릴 순 없기도 하고, 보안상 바람직하지도 않기에 Role을 사용할 것이다.
그렇다면 k8s에서 aws의 role을 어떻게 받아오는가?
-> EKS에서는 AWS 의 IAM Role의 권한을 Pod에게 인가할 수 있는 IRSA (Iam Role for Service Account) 방식을 제공한다.
뭐...OIDC Provider를 만들고, STS로부터 임시 자격증명을 발급받아 사용할 수 있다...인데, 사실 원리를 파보려고 하면 복잡하고 본 글이 너무 길어질 것 같아서 생략하겠다. (자세한건 공식 문서 확인)
결론만 말하자면,
1) EKS별로 OIDC Provider를 만들면 해당 Provider를 Trust하는 IAM Role을 만들어 EKS 내부 pod에 권한을 전달할 수 있게 된다.
2) 이 때, OIDC Provider는 다른 계정에 만들 수 있다.
즉 모든 계정의 클러스터의 OIDC Provider들을 중앙 계정에 만들어둔다면,
-> 중앙 계정에선 모든 계정의 클러스터들을 신뢰할 수 있게 되고 -> 모든 클러스터에서 중앙 계정의 IAM Role을 인가받을 수 있게 된다.
그럼 이제 만들어보자 (w. Terraform)
(EKS Clsuter는 A 계정, ECR은 B 계정에 이미 만들어져 있다고 가정)
2-A) OIDC Provider
locals {
issuer = aws_eks_cluster.A.identity[0].oidc[0].issuer
}
data "tls_certificate" "this" {
url = local.issuer
}
data "aws_partition" "B" {
# 사실 어느 provider를 쓰던 상관없음
provider = aws.B
}
resource "aws_iam_openid_connect_provider" "B" {
provider = aws.B
client_id_list = ["sts.${data.aws_partition.B.dns_suffix}"]
thumbprint_list = [data.tls_certificate.this[each.key].certificates[0].sha1_fingerprint]
url = local.issuer
}
2-B) IAM Role & Policy Attach
locals {
oidc_arn = aws_iam_openid_connect_provider.B.arn
}
## A 계정 클러스터의 B 계정 OIDC Provider를 신뢰하는 Policy 생성
data "aws_iam_policy_document" "B" {
provider = aws.B
# Cluster별로 반복 - Dynamic Block을 이용하면 좋다.
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [local.oidc_arn]
}
condition {
test = "StringLike"
variable = "${split("oidc-provider/", local.oidc_arn)[1]}:sub"
# kube-system:ecr-secret-updater 부분은 실제 ECR Secret Updater SA의 namspace와 이름 작성
values = ["system:serviceaccount:kube-system:ecr-secret-updater"]
}
}
}
## 위 Policy를 사용하는 IAM Role 생성
resource "aws_iam_role" "B" {
provider = aws.B
name = "ecr-secret-updater"
description = "Managed By Terraform"
assume_role_policy = data.aws_iam_policy_document.B.json
}
## AmazonEC2ContainerRegistryReadOnly Policy ARN 검색
data "aws_iam_policy" "B" {
provider = aws.B
name = "AmazonEC2ContainerRegistryReadOnly"
}
## IAM Role에 AmazonEC2ContainerRegistryReadOnly Policy Attach
resource "aws_iam_role_policy_attachment" "B" {
provider = aws.B
role = aws_iam_role.B.name
policy_arn = data.aws_iam_policy.B.arn
}
2-C) Service Account에 권한 부여
resource "kubernetes_service_account" "ecr_secret_updater" {
metadata {
name = "ecr-secret-udpater"
namespace = "kube-system"
annotations = {
"eks.amazonaws.com/role-arn" = aws_iam_role.B.name
}
}
}
Service Account에 AWS IAM Role을 부여하기 위해서는 "eks.amazonaws.com/role-arn"
이라는 annotation을 활용해주면 된다.
어찌저찌 AWS 권한 세팅도 끝났다.
(사실 위 테라폼 코드들은 내가 실제로 사용한 코드들을 본 글의 목적에만 맞게 많이 변형한 것이라, 실제 돌아가는지 테스트해본적은 없다.
혹시라도 문제 있다면 알려주시면 감사하겠습니다...)
이제 찐 알맹이인 ECR Updater Cronjob을 만들어 보자.
# 3. Cronjob 생성
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: ecr-secret-update
namespace: kube-system
spec:
suspend: false
schedule: 0 * * * *
concurrencyPolicy: Replace
successfulJobsHistoryLimit: 0
failedJobsHistoryLimit: 0
jobTemplate:
spec:
ttlSecondsAfterFinished: 0
template:
spec:
serviceAccountName: ecr-secret-udpater
restartPolicy: Never
volumes:
- name: token
emptyDir:
medium: Memory
initContainers: # ECR Token 받아서 Volume에 저장
- image: amazon/aws-cli
name: get-token
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: /token
name: token
command:
- /bin/sh
- -ce
- aws ecr get-login-password --region ap-northeast-2 > /token/ecr-token
containers: # Volume에 저장된 토큰값을 가지고 Secret Patch - 존재하는 모든 네임스페이스마다 반복
- image: bitnami/kubectl:latest
name: create-secret
imagePullPolicy: IfNotPresent
env:
- name: SECRET_NAME
value: aws-ecr-creds
- name: ECR_REGISTRY
# ECR이 있는 계정의 Account ID
value: [B Account ID].dkr.ecr.ap-northeast-2.amazonaws.com
volumeMounts:
- mountPath: /token
name: token
command:
- /bin/sh
- -ce
- |-
for i in $(kubectl get ns -o name)
do
ns=${i#"namespace/"}
kubectl create secret docker-registry $SECRET_NAME -n $ns \
--docker-server="$ECR_REGISTRY" \
--docker-username=AWS \
--docker-password="$(cat /token/ecr-token)" \
--dry-run=client -o yaml | kubectl apply -f -
done
AWS CLI와 kubectl이 같이 깔린 경량 이미지는 공식으로 제공되는게 없는 것 같았다. (내가 못찾은걸지도..)
공식 말고, 누가 만들어둔것들이 있긴 하다만 이전 버전 (awscli 1..) 을 사용하고 있다던가 하는 이유로 awscli와 kubectl용 컨테이너를 나눈다.
awscli (ECR Token Get)이 꼭 선행되어야 하므로 initContainers로 사용한다.
---
이상으로 내가 세팅한 ECR Secret Updater 설명을 마친다.
EKS에서 ECR 사용하는게 굉장히 간단할 줄 알았지만 (아니 사실 간단한거지만) 생각지도 못한 제약조건들이 많이 추가되면서 의외로 애를 많이 먹었다.
그래도 이참에 IRSA와 RBAC을 제대로 이해하게 되었고, IRSA 를 Cross Account로 설정하는 방법에 대해서도 제대로 익히게 되어 좋았다...라고 생각한다.
끝!
---
# Ref
- https://fluxcd.io/flux/guides/cron-job-image-auth/
- https://channel.io/ko/blog/tech-aws-cross-accounts-irsa
- https://github.com/argoproj-labs/argocd-image-updater/issues/112
'공부 > Kubernetes' 카테고리의 다른 글
[Cluster-Autoscaler] Over Provisioning with Terraform (4) | 2023.11.26 |
---|---|
[kubernetes/GUI] OpenLens 사용하기 (MAC) (0) | 2023.11.05 |
[EKS/Java/IRSA] JAVA App에 IRSA 부여할 때 종속성 추가하기 (0) | 2023.09.15 |
[k8s/aws] 쿠버네티스에서 AWS EBS를 볼륨으로 사용할 수 있기까지 (0) | 2023.07.26 |
[kubernetes] Taints, Tolerations vs Node Affinity (요약) (0) | 2022.12.13 |
댓글