본문 바로가기
공부/Kubernetes

[RBAC & IRSA] ECR Secret Updater Cronjob 구성

by haejang 2023. 9. 4.
728x90
728x90

 

 

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

 

 

728x90
728x90

댓글