DevOpsTerraformEKS02. Creating AWS EKS with RBAC configuration

02. Creating AWS EKS with RBAC configuration

기본 개념

Security를 위한 best practice 중 하나는, Admin user라고 무작정 admin privilege를 주지 않고 admin 이어도 kubectl과 같은 명령어로 system에 직접적으로 수정할 수 없게하고, kubectl로는 조회(describe, get)만 가능하게 하고 오직 Terraform과 같은 IaC로만 변경할 수 있게 한다.

center|640

또한 least priviledge rule에 충실하게 특정 유저에게는 K8s cluster 전체에서 하나의 namespace를 조회할 권한만 줄 수 있다. (→ 이름은 상관없지만 여기서는 이를 위해 developer라는 유저를 만들 것이다.)
특정 namespace에만 주는 역할은 Role이라는 K8s object가 있고, Admin user처럼 cluster 전체에 대한 권한(cluster-wide access)을 줄 때는 ClusterRole을 사용한다.
그리고 이 Role을 유저에게 부여하는 것을 RoleBinding이 하게 되고, ClusterRole을 유저에게 부여하는 것을 ClusterRoleBinding을 통해 하게 된다.

여기서는
→ Admin 한테도 cluster-wide read only access를 준다 → ClusterRole for admin을 생성
Developer는 Role (read only)만 부여 (혹은 특정 namespace) → then add config that maps Role with IAM

기존 방식 v.19

aws-auth ConfigMap

기존 방식 (terraform-aws-modules/eks/aws version 19까지)은 AWS EKS를 생성하는 순간
EKS가 master node의 API Server안에 aws-auth라는 ConfigMap을 박아놓는다.

K8s cluster의 모든 요청은 이 master node의 API Server로 들어가는데 AWS 계정에 대한 인증은 이 aws-auth에서 이루어진다. 과거에 생성했던 EKS에 조회를 해보면

 kubectl get cm -A | grep aws-auth
kube-system       aws-auth

존재한다. EKS를 생성한 유저 (즉, EKS creator)에게는 이 CM에 EKS에 대한 admin 권한이 자동적으로 주어졌다.

우리가 RBAC과 IAM principals를 추가하기 위해서는 이 CM(ConfigMap)을 수정해야한다.

kubectl edit cm aws-auth -n kube-system

해서 권한을 주고 싶은 IAM user의 ARN주소를 넣으면 admin 권한을 줄 수가 있다.

기존(v.19)에도 Terraform으로 RBAC 설정을 위해서는 이 cm(ConfigMap)을 수정해야주어야했고, version 20에는 ConfigMap과 API 방식을 혼용해서 사용해도 되지만 version 21에서는 완전히 API방식으로 바뀐다고 하니 (참조: https://github.com/terraform-aws-modules/terraform-aws-eks/blob/master/docs/UPGRADE-20.0.md) API 방식을 이용해보자. Syntax도 version 19에 비해서 몇 가지 변경되었다. 기존 버전은 이제 알 필요없으니 바로 version 20의 API 방식으로 코딩해보자.

기존 RBAC 방식에서 ConfigMap이 Access entry API로 바뀐 점 하나밖에 없지만, RBAC을 사용해보지 않았던 개발자라면 우선 RBAC 방식에 대해 이해할 필요가 있다. 그래야지만 복잡한 RBAC을 헷깔리지 않는다.
가장 중요한 점은 어떤 것이 Kubernetes native 영역이고, 어디가 AWS EKS 고유의 영역이며, 어떤 영역이 AWS IAM 영역인지를 구분하는 일이다. 위 그림을 한 단계 더 자세히 보자면 (그리고 실제로 여기서 구현할 Terraform 코드의 계획) 다음과 같다.

center|640

Kubernetes cluster 내의 이름은 당연히 절대 IAM user의 이름이 아니다. 그래서 ‘admin’이라는 Kubernetes의 user_name에 IAM user yjkim을 K8s role mapping을 해주어야 한다. K8s role mapping 기존 ConfigMap 방식때 이름이었고, 개념은 똑같지만 새로운 공식명칭은 Access Entry이다. 이름이 변경된 이유는 K8s role mapping은 실제로 AWS IAM의 user 혹은 roled을 K8s의 role에 mapping하는 것이지만 너무 기니깐 줄인 것이 K8s role mapping인데 이 단어에는 IAM이 안 들어가니 의미가 왜곡되서 그런 것. 아무튼 중요하지 않으니 여기서 줄이고.

위에 그림처럼 ‘developer’는 K8s 내에서의 이름이다. 이것을 ‘hcche’라는 IAM user와 연결할 것이다. K8s group이라고 하는 것은 K8s cluster 내부의 group인데 큰 의미는 없다. 만약 여러 명을 같은 K8s group으로 놓는다면 (위 그림의 ‘cluster-viewer’ K8s group처럼 각각의 user_name이 아닌 그 group 자체에 ClusterRole 혹은 Role을 줄 수 있다.

Terraform code with eks ver.20

Access keys

우선 이 Terraform 코드에서 출발해본다.
01-main-eks.tf

provider "aws" {
  region     = var.aws_region
  access_key = var.aws_access_key_id
  secret_key = var.aws_secret_access_key
  
  default_tags {
    tags = var.tags
  }
}

위에 access_keysecret_key는 AWS console에서 Terraform과 같은 IaC가 전용으로 사용하는 user를 (예를들어 devops_admin이라는 계정을) 생성하고 거기서 key를 생성해와서 terraform.tfvars에 입력한다. (물론 variables.tf에 선언도 한다.)

EKS Module

그리고 다음 기본 EKS 생성 코드에서 출발하자.
01-main-eks.tf

# EKS Cluster
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"
 
  cluster_name                   = var.name
  cluster_version                = var.k8s_version
  cluster_endpoint_public_access = true
  
  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets
 
  create_cluster_security_group = false
  create_node_security_group    = false
  
  eks_managed_node_groups = {
    initial = {
      instance_types = ["t3.micro"]
      min_size     = 1
      max_size     = 10
      desired_size = 2
    }
  }
 
  tags = var.tags
}

위 코드를 보면 module로 시작을 한다.
어떤 테라폼 코드는 resource로 시작하는 것을 볼 수 있다.
이 둘의 차이는 resource는 일일이 개인이 수동으로 설정하는 방식이고, module은 이미 있는 라이브러리를 쓰는 개념이다. 즉, 코드의 길이 자체는 module을 쓰는 것이 훨씬 적지만 장단점이 있다. 마치 eksctl을 사용해서 자동으로 vpc, subnet 생성하는 것과 aws cli 명령어로 내가 원하는 리소스만 생성하는 것과 유사하다.

EKS Addons

EKS module 안에 설치할 addon을 추가해보자. kube-proxy, vpc-cni, coredns 3가지는 거의 필수 3총사라 할 수 있고, Pod Identity Agent는 추후 사용할 addon인데 미리 설치해두자. 그래서 다음 코드를 추가한다.

  cluster_addons = {
    kube-proxy = {
      most_recent = true
    }
    vpc-cni = {
      most_recent = true
    }
    coredns = {
      most_recent = true
      configuration_values = jsonencode({
        computeType = "EC2"
      })
    }
    eks-pod-identity-agent = {
      most_recent = true
    }
  }

위에 computeType = "EC2"는 자동으로 Fargate를 사용하지 않는다고 명시해주는 용도고 필수는 아니지만 넣어준다.

API mode

EKS module 안에 다음 두 줄 추가

  authentication_mode = "API"  
  enable_cluster_creator_admin_permissions = true

위에 언급했듯이 ConfigMap이 아닌 API only 모드로 명시하고
위에서 세팅한 devops_admin이라는 AWS IAM user의 access key를 이용하므로 admin permission을 true로 한다. → 사실 EKS 생성시 admin 권한이 없으면 생성조차 안 되니 당연히 true.

Access Entries

같은 파일 내에서 코딩을 해도 되지만 01-main-eks.tf에다가 다음 코드를 추가하고

access_entries = local.access_entries

local 변수를 이용해서 선언하자.
Terraform은 변수를 variables.tf에 정의하고 변수 값을 terraform.tfvars에 주입하는 방식도 있지만, Terraform 자체는 같은 폴더에 있는 모든 .tf파일을 하나의 .tf파일로 간주하므로 어떠한 .tf 파일에도 그냥 locals { (여기는 s가 들어감에 유의)에 선언하면 그냥 그 변수를 local.이라는 변수로 불러올 수 있다.

순서가 좀 헷깔리겠지만 저 local.access_entries는 나중에 선언해주기로 하고 우선 IAM roles를 만들어보자.

전체 RBAC 구성도

center

위 그림은 AWS 공식문서에 있는 그림이다. 우선 저 빨간 모자 모양의 Role부터 만들어줄 것이다.

IAM roles

03-iam-roles.tf 생성 → 공식문서: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role

중요한 것 - Kubernetes의 role이 아닌 AWS의 role이다.
eks-external-admin의 role 이름으로 만들어본다.

resource "aws_iam_role" "eks-external-admin" {
  name = "eks-external-admin"
 
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Sid    = ""
        Principal = {
          AWS = var.user_for_admin_role
        }
      },
    ]
  })
}

user의 arn 주소를 변수로 assume role을 해준다.
Assume Role이란 AWS에서 중요한 개념으로 코드에 Principal에 IAM user를 명시하고, 이 user가 이 IAM role을 assume(역할을 맡다)할 수 있도록 허용해주는 것.

variables.tf에 선언해준다.

variable user_for_admin_role {}

terraform.tfvars에 IAM user의 ARN 주소를 넣어준다.

user_for_admin_role="arn:aws:iam::11111111111111:user/yjkim"

그리고 위 role에 이어서 바로 inline policy를 붙여준다. Inline이라 함은 AWS console에서 policy를 검색해도 따로 보이지 않지만 role에 붙여주는 policy를 말한다.

03-iam-role.tf 코드

resource "aws_iam_role_policy" "eks-external-admin-policy" {
  name = "eks-external-admin-policy"
  role = aws_iam_role.eks-external-admin.id
 
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action   = ["eks:DescribeCluster"] 
        Effect   = "Allow"
        Resource = "*"
      },
    ]
  })
}

위에 보이듯이 DescribeCluster (Read 권한)만 admin에게 준다. eks:에서 보이듯이 이 또한 Kubernetes 자체의 권한이 아닌 AWS EKS에 대한 권한이다.

Mapping to K8s

이제 IAM user의 role에 Kubernetes user에 mapping 시켜보자. → 위에 local 변수로 만들었던 것을 선언해본다.
04-kube-resources.tf를 생성

locals {
	access_entries = {
		# Admin user access entry
		admin = {
			principal_arn = aws_iam_role.eks-external-admin.arn
			
			kubernetes_groups = ["cluster-viewer"]  # system:masters는 제외하고 명명
			
			user_name = "admin"
			
			# Type of access entry
			type = "STANDARD"
		}
	}
}

kubernetes_groups는 자유롭게 이름을 설정할 수 있지만 system:masters라고 쓰면 K8s root 권한이 주어진다.
위의 user_name은 K8s의 user로 설정하는 값.

Create IAM Policy

AWS console → IAM 내 아이디에 → 다음과 같이 create policy

center|400

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "Statement1",
			"Effect": "Allow",
			"Action": [
			        "sts:AssumeRole"
			    ],
			"Resource": "*"
		}
	]
}

→ name : ‘assume-role-policy’으로 만들었다.
더 정확하게 하기 위해서는 위의 Resource*가 아닌 eks-external-admin role의 ARN 주소를 넣으면 되지만 내 아이디는 Admin이니 굳이 국한하지 않는다. 중요한 것은 아까 생성한 eks-external-admin의 inline policy는 내 아이디를 허용하는 것이고, 위의 policy의 AssumeRole을 설정해서 서로 더블체크를 하는 개념이다. 단지 AdministratorAccess가 있다고 이 계정의 모든 EKS에 접속할 수 있는 것이 아니라는 점이 중요하다.

terraform.tfvars에 추가

Providers

eksctl version 20의 공식문서를 보면 테라폼에 대한 최소 요구사항이 나온다. 이걸 참조해서 provider를 작성.
04-kube-resources.tf에서는 Kubernetes를 사용할 것이기 때문에 Provider에 Kubernetes를 아래와 같이 추가한다.
Kubernetes의 테라폼 버전은 여기서 확인 → 공식문서: https://registry.terraform.io/providers/hashicorp/kubernetes/latest

terraform {
  required_version = ">= 1.3"  # Updated minimum version for v20
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.34"  # Updated minimum version for v20
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.37"
    }
  }
}

certificate

테라폼의 코드 실행 단계에서는 아직 EKS가 만들어진 상태가 아닐 것이기에, .kube/config도 처음부터는 없다. 만들어진 후에 eks의 enpoint를 가져오고, 동적으로 이 인증 configuration을 가져오자.

04-kube-resources.tf

provider "kubernetes" {
  host = module.eks.cluster_endpoint  # output에서 가져옴
	cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)  # terraform 공식문서에 있는 attribute "https://registry.terraform.io/modules/terraform-aws-modules/eks/aws/latest"
	# base64decode는 테라폼 명령어
	# EKS에 접속하기 위해 eks에서 발행하는 client token도 필요 -> 여기서 생성
	exec {  # execution block -> executed locally so AWS CLI is required
		api_version = "client.authentication.k8s.io/v1beta1"
		command = "aws"
		args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]
	}
}

cluster_ca_certificate은 K8s의 인증서

ClusterRole and ClusterRoleBinding

03-iam-roles.tf는 AWS resources를 생성했고
https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/cluster_role
위 페이지를 참조해서 04-kube-resources.tf에서 코딩

# create Cluster Role
resource "kubernetes_cluster_role" "cluster-viewer" {
  metadata {
    name = "cluster-viewer"
  }
 
  rule {
    api_groups = [""]
    resources  = ["*"]
    verbs      = ["get", "list", "watch", "describe"]
  }
}
 
# create Cluster Role Binding
resource "kubernetes_cluster_role_binding" "cluster-viewer-group" {
  metadata {
    name = "cluster-viewer-group"
  }
  role_ref {
    api_group = "rbac.authorization.k8s.io"
    kind      = "ClusterRole"
    name      = "cluster-viewer"
  }
  subject {
    kind      = "Group"
    name      = "cluster-viewer"
    api_group = "rbac.authorization.k8s.io"
  }
}

modify, update 권한은 주지 않았다.

Role과 RoleBinding도 위와 마찬가지로 하면 되고, Kubernetes Group이 아닌 user를 사용하려면 위 kindGroup에서 User로 변경하면 된다.