일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- jenkins
- 추가 보안 그룹
- instances failed to join cluster
- Gateway
- route53
- Service Account
- helm_release
- Terraform
- ingestion
- fruition
- node group
- Amazon CloudWatch
- kubernetes
- assumerole
- NAT
- docker
- 테라폼
- Docker0
- aws ses #aws lambda
- s3
- 에이전트 유형
- IRSA
- amazon sns
- 클러스터 보안 그룹
- 에이전트 구성
- 코드커버리지
- httpasswd
- aws-loadbalacner-controller
- Pipeline
- saa-c03 #saa #aws certified solutions architect - associate
- Today
- Total
cloudwithbass
[Terraform] Terraform 연습하기 8: alb-loadbalancer-controller와 ingress, OIDC, IRSA, cert-manager 본문
[Terraform] Terraform 연습하기 8: alb-loadbalancer-controller와 ingress, OIDC, IRSA, cert-manager
여영클 2024. 9. 11. 19:57
이전 포스팅까지의 인프라 구성도입니다.
이전 포스팅에선 private subnet을 노드 그룹으로 관리하도록 구성했었습니다.
이번 포스팅에선 이 노드 그룹에 안정적으로 트래픽을 전달하도록 로드 밸런서를 구성할 것입니다.
모든 소스 코드는 아래 주소에서 확인하실 수 있습니다.
https://github.com/Dminus251/practice-terraform/tree/main/demo08-ingress
목차
1. helm 차트 구성
AWS의 aws-loadbalancer-controller 문서와 Helm artifact hub 문서를 참고해서 helm chart 구성합니다.
Terraform에서는 Helm 차트를 사용할 때 helm_release 리소스를 사용합니다.
helm.tf
resource "helm_release" "alb-ingress-controller"{
depends_on = [module.eks-cluster, module.public_subnet, helm_release.cert-manager]
repository = "https://aws.github.io/eks-charts"
name = "aws-load-balancer-controller"
chart = "aws-load-balancer-controller"
version = "1.8.2"
namespace = "kube-system"
set {
name = "clusterName"
value = module.eks-cluster.cluster-name
}
set {
name = "region"
value = var.AWS_REGION
}
set {
name = "vpcId"
value = module.vpc.vpc-id
}
set {
name = "rbac.create"
value = "true" #if true, create and use RBAC resource
}
set {
name = "serviceAccount.create"
value = "false"
}
set {
name = "serviceAccount.name"
value = "aws-load-balancer-controller"
}
set {
name = "createIngressClassResource"
value = "true"
}
}
resource "helm_release" "cert-manager"{
repository = "https://charts.jetstack.io"
name = "jetpack"
chart = "cert-manager"
namespace = "cert-manager"
create_namespace = true # 네임스페이스가 없는 경우 생성
set {
name = "installCRDs"
value = "true" # Cert Manager 설치 시 CRDs도 함께 설치
}
}
helm.tf에서는 aws-loadbalancer-controller와 cert-manager를 배포합니다.
1-1. aws-loadbalacner-controller
저는 IAM Role과 Service Account를 이용할 것이므로 serviceAccount.create 속성을 false로, serviceAccount.name을 aws-load-balancer-controller로 설정했습니다.
왜냐하면 aws-loadbalancer-controller의 Artifact Hub 문서를 보면 다음과 같은 내용이 적혀 있기 때문입니다.
# If using IAM Roles for service account install as follows -
NOTE: you need to specify both of the chart values `serviceAccount.create=false`
and `serviceAccount.name=aws-load-balancer-controller`
aws-loadbalancer-controller 이전에 cert-manager를 구성해야 하므로 cert-manager를 depends_on에 추가합니다.
1-2. cert-manager
인증서 구성을 webhook에 주입하기 위해 배포합니다.
cert-manager는 TLS 인증서를 자동으로 발급하고 관리합니다.
aws-loadbalancer는 http와 https 프로토콜을 처리하는 alb를 프로비저닝하는데, 이 중 https를 위해 배포해야 합니다.
또한 cert-manager는 Custom resource를 사용하므로 installCRDs를 true로 설정해야 합니다. (cert-manager artifcat hub 참고)
Public Subnet 관련 주의사항
1. 인터넷이 연결된 public subnet에 kubernetes.io/role/elb 태그를 추가하고, 값을 1로 설정해야 합니다.
- 애플리케이션 및 ALB 를사용한 HTTP 트래픽 라우팅 문서에 따르면 annotation에 명시적으로 서브넷ID를 지정하지 않으면 프라이빗 서브넷과 퍼블릭 서브넷에 각각 태그를 추가해야 합니다.
- 태그를 추가하지 않으면 로드밸런서가 프로비저닝되지 않았던 것을 보아, 서브넷을 감지하지 못 하는 것 같습니다.
따라서 public subnet과 private subnet 모듈에 다음과 같이 태그를 추가합니다.
추가하지 않을 경우, ingress 리소스에서 아래와 같은 에러가 발생합니다.
modules/t-aws-public_subnet
resource "aws_subnet" "public_subnets"{
vpc_id = var.vpc-id
cidr_block = var.public_subnet-cidr
availability_zone = var.public_subnet-az
map_public_ip_on_launch = true
tags = {
Name = var.public_subnet-name
"kubernetes.io/role/elb" = "1"
}
}
modules/t-aws-private_subnet
resource "aws_subnet" "private_subnets"{
vpc_id = var.vpc-id
cidr_block = var.private_subnet-cidr
availability_zone = var.private_subnet-az
tags = {
Name = var.private_subnet-name
"kubernetes.io/role/internal-elb" = "1"
}
}
2. helm_release의 의존성에 public subnet이 있어야 합니다.
- 의존성을 추가하기 전에 로드밸런서가 프로비저닝되지 않던 문제가, 의존성을 추가하니 해결됐습니다.
- helm.tf 코드의 line 2를 참고해주세요.
- 아마 public subnet보다 aws-loadbalancer-controller가 먼저 프로비저닝되면 서브넷을 인식하지 못해 발생하는 것 같지만.. 확실하진 않습니다. 혹시나 로드밸런서가 프로비저닝되지 않는다면 이 방법도 시도해 보세요.
2. Service Account 구성
쿠버네티스 리소스가 AWS 리소스에 접근하기 위해선 Service Account가 필요합니다.
이전에 관련 내용을 자세히 포스팅했으니, Service Account에 대한 설명은 이 포스팅을 참조해주세요.
Service Account의 내용은 AWS 문서를 참고합니다.
아래 내용을 따라 저의 환경에 맞게 수정하겠습니다.
다음과 같이 service account 모듈을 작성합니다.
modules/t-k8s-sa/
#main.tf
resource "kubernetes_service_account" "example"{
metadata{
labels = var.sa-labels
name = var.sa-name
namespace = var.sa-namespace
annotations = var.sa-annotations
}
}
#vars.tf
variable "sa-labels"{
type = map(string)
}
variable "sa-name"{
type = string
}
variable "sa-namespace"{
type = string
}
variable "sa-annotations"{
type = map(string)
}
#outputs.tf
output "sa-namespace"{
value = kubernetes_service_account.example.metadata[0].namespace
}
output "sa-name"{
value = kubernetes_service_account.example.metadata[0].name
}
kubernetest_service_account 리소스의 attribute 중 metadata는 list(map)형태로 metadata들을 반환합니다.
인덱스는 0밖에 없으므로 0번째 인덱스를 참조하여 namespace와 name을 내보냅니다.
이렇게 얻은 sa-namespace와 sa-name은 이후 IAM Role을 구성할 때 사용됩니다.
main.tf (루트 모듈)
module "sa-alc"{
source = "./modules/t-k8s-sa"
sa-labels = {
"app.kubernetes.io/component" = "controller"
"app.kubernetes.io/name" = "aws-load-balacner-controller"
}
sa-name = "aws-load-balancer-controller"
sa-namespace = "kube-system"
sa-annotations = {
"eks.amazonaws.com/role-arn" = "arn:aws:iam::<my_aws_account_id>:role/alb-ingress-sa-role"
}
}
annotations에서 마지막에 있는 alb-ingress-sa-role은 이 SA와 연결할 IAM Role 이름입니다.
이 값을 변수를 이용해 사용하고 싶었지만 sa는 role을 참조하고, role은 sa를 참조하는 순환 참조 에러가 발생해서 일단은 하드코딩해놨습니다.
3. IAM Role 구성
AWS 문서에서는 aws-loadbalacner-controller에 다음과 같은 AssumeRole이 필요하다고 합니다.
cat >load-balancer-role-trust-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.eks.region-code.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.region-code.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud": "sts.amazonaws.com",
"oidc.eks.region-code.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub": "system:serviceaccount:kube-system:aws-load-balancer-controller"
}
}
}
]
}
EOF
이 Assume Role은 계정 ID(111122223333), OIDC, Service Account의 namespace와 이름을 필요로 합니다.
따라서 다음과 같이 모듈을 작성합니다.
modules/t-aws-eks/role/alc/
#main.tf
resource "aws_iam_role" "alb_ingress_sa_role" {
name = var.role-alc_role_name
assume_role_policy = jsonencode({
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<my_aws_account_id>:oidc-provider/${var.role-alc-oidc_without_https}"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"${var.role-alc-oidc_without_https}:aud": "sts.amazonaws.com", #인증 요청 대상
"${var.role-alc-oidc_without_https}:sub": "system:serviceaccount:${var.role-alc-namespace}:${var.role-alc-sa_name}"
}
}
}
]
})
}
resource "aws_iam_policy" "iam_policy-aws-loadbalancer-controller" {
name = "AWSLoadBalancerControllerIAMPolicy"
policy = file("AWSLoadBalancerControllerIAMPolicy.json")
}
resource "aws_iam_role_policy_attachment" "alb_ingress_policy_attach" {
policy_arn = aws_iam_policy.iam_policy-aws-loadbalancer-controller.arn
role = aws_iam_role.alb_ingress_sa_role.name
depends_on = [aws_iam_policy.iam_policy-aws-loadbalancer-controller, aws_iam_role.alb_ingress_sa_role]
}
우선 Role의 Statement 부분을 설명하겠습니다.
이에 앞서 Principal과 Condition에서 사용한 var.role-alc-oidc_without_https 변수에 대해서는 4. OIDC Connector 구성하기 부분에서 자세히 설명하겠습니다.
- Principal
- 역할의 주체를 말합니다.
- 이 역할의 주체는 oidc-provider입니다.
- Action
- Effect가 Allow이니 Action은 허용할 동작입니다.
- 이 역할에선 AssumeRoleWithWebIdentity 동작을 허용합니다.
- AssumeRoleWithWebIdentity는 AWS STS(Security Token Service)에서 제공하는 액션 중 하나인 OIDC를 통해 IAM Role을 맡을 수 있게 하는 동작입니다.
- aws 문서에선 다음과 같이 설명합니다: 웹 자격 증명 공급자로 모바일 또는 웹 애플리케이션에서 인증된 사용자에 대한 임시 보안 자격 증명 집합을 가져올 수 있는 권한을 부여합니다.
- Condition
- Action 동작을 허용할 조건입니다.
- 이 경우aud 값이 sts.amazonaws.com이여야 합니다.
- aud(audience)는 ' 토큰을 수신할 수 있는 대상'을 의미합니다.
- 따라서 aws sts만이 이 토큰을 처리할 수 있습니다.
- 또한 sub 클레임이 제가 지정한 네임스페이스와 서비스 어카운트에 해당해야 합니다.
- sub(subject)는 '토큰의 실제 소유자'를 의미합니다.
- 즉, 이 Role의 내용을 요약하면 sts.amazonaws.com이 처리하며, 지정한 namespace의 service account가 토큰을 소유한 경우 oidc provider에게 AssumeRoleWithWebIdentity 동작을 허용합니다.
또한 iam_role_policy 리소스에서 AWSLoadBalancerControllerIAMPolicy.json 파일의 내용을 생성하는데, 이 파일은 AWS 문서에서 가져온 파일입니다.
aws-loadbalancer-controller에게 적절한 IAM 권한을 주며, 해당 파일은 루트 디렉터리에 있어야 합니다. (어차피 이 리소스는 루트 모듈에서 module로 실행되기 때문에)
modules/t-aws-eks/role/alc/vars.tf
#vars.tf
variable "role-alc_role_name"{
type = string
}
variable "role-alc-oidc_without_https"{
type = string
}
variable "role-alc-namespace"{
type = string
}
variable "role-alc-sa_name"{
type = string
}
main.tf(루트 모듈)
#role for aws-loadbalacner-controller used in Service Account
module "role-alc-sa"{
source = "./modules/t-aws-eks/role/alc"
role-alc_role_name = "alb-ingress-sa-role"
role-alc-oidc_without_https = module.eks-cluster.oidc_url_without_https
role-alc-namespace = module.sa-alc.sa-namespace
role-alc-sa_name = module.sa-alc.sa-name
}
namespace와 name은 Service Account 모듈의 값을 사용합니다.
role-alc-oidc_without_https는 이 포스팅의 4번 항목에서 설명하겠습니다.
4. OIDC Provider 구성
OIDC(OpenID Connect)란?
OAuth 2.0을 기반으로, 사용자를 인증하기 위해 사용하는 표준 프로토콜입니다. OIDC는 사용자 인증을 위해 ID 토큰을 발급하고, 이 토큰을 통해 신원을 인증합니다.
aws-loadbalacner-controller는 pod로 실행되며, 기본적으로 pod는 AWS 리소스에 접근할 권한이 없습니다.
따라서 IRSA(IAM Role for Service Account)를 이용해 AWS 리소스에 접근해야 합니다.
이때 OIDC가 있어야만 pod가 IRSA를 통해 AWS 리소스에 접근할 수 있습니다.
다음 예시에서 그 과정을 확인하겠습니다.
Diving into IRSA 문서의 그림을 예로 들겠습니다.
이 그림은 Pod가 AWS의 리소스인 S3에 대해 list 동작을 수행하는 상황을 예시로 듭니다.
1. Pod는 AWS SDK를 이용해 JWT와 IAM Role ARN을 AWS IAM에게 제공하며 임시 자격 증명을 요청합니다.
- AWS SDK가 AssumeRoleWithWebIdentity API를 호출합니다.
- 이때 JWT는 SA에 대해 OIDC Provider에게 발급받은 것입니다.
- IAM Role ARN는 Service Account의 Manifast 파일에 선언한 값입니다.
2. STS는 AWS IAM에게 검증을 요청합니다.
3. AWS IAM은 OIDC Provider Pulic key로 JWT의 유효성을 검증합니다.
4. 검증됐다면, AWS IAM은 STS에게 정상적이라는 응답을 보냅니다.
5. 임시 자격 증명을 POD에게 발급해 줍니다.
6. 이제 pod는 AWS 리소스에 접근할 권한이 생겼습니다.
따라서 OIDC Provider는 반드시 필요하며, 이는 EKS cluster 생성 시 자동으로 함께 생성됩니다.
저희가 해야할 일은 IAM의 자격 증명 공급자에 이 OIDC 공급자 URL을 등록하는 것이며, 이는 테라폼의 aws_iam_openid_connect_provider 리소스를 통해 생성할 수 있습니다.
modules/t-aws-openid_connect_provider/main.tf
resource "aws_iam_openid_connect_provider" "eks_oidc_provider" {
client_id_list = var.client_id_list
url = var.url
thumbprint_list = ["55635cfea6a15f4770cc5ec0977492b318f9b0cc"] # AWS>의 OIDC thumbprint
}
여기서 thumbprinit_list 값은 AWS의 OIDC thumbprint 값입니다.
OpenID Connect ID 공급자에 대한 지문 얻기 문서에 따른 절차로 얻을 수 있으며, 이는 고정된 값이라고 합니다.
여담이지만 아래 블로그에 따르면, 어차피 내부적으로 인증이 이루어지기 때문에 이값은 아무 문자열이나 40자로 채워도 된다고 하던데요
https://popcorn-overflow.tistory.com/m/44
음.. 원래의 thumbprint_list 값을 주석 처리하고 임의의 값을 사용한 후 프로비저닝해봤는데 ingress가 로드밸런서를 생성하지 않네요
modules/t-aws-openid_connect_provider/vars.tf
variable "client_id_list"{
type = list(string)
}
variable "url"{
type = string
}
client_id_list는 ["sts.amazonaws.com"]과 같이 사용되기 때문에 list(string) 타입으로 설정합니다.
main.tf(루트 모듈)
module "openid_connect_provider"{
source ="./modules/t-aws-openid_connect_provider"
client_id_list = ["sts.amazonaws.com"]
url = module.eks-cluster.oidc_url
depends_on = [module.eks-cluster]
}
여기서 url은 위에서 설명드렸던 eks clsuter의 oidc provider url입니다.
테라폼 문서에 따르면 다음과 같이 oidc provider url 값을 사용할 수 있습니다.
eks cluster 모듈에서 다음 output을 추가해 줍니다.
output "oidc_url" {
value = aws_eks_cluster.example.identity[0].oidc[0].issuer
}
output "oidc_url_without_https" {
value = replace(aws_eks_cluster.example.identity[0].oidc[0].issuer, "https://", "")
}
이렇게 하면 oidc_url에는 https://oidc.eks.<aws-region>.amazonaws.com/id/*******와 같은 형식의 값이 나올 올 것이며, oidc_url_without_https에는 위 값에서 replace 함수를 사용해 https:// 문자열을 없앤 값이 나올 겁니다.
이 포스팅의 3번 항목에서 사용한 var.oidc_url_without_https 값이 이겁니다. role에서는 oidc_url에서 https를 없앤 값만을 요구하기 때문입니다. (이 포스팅의 3번 항목의 가장 위에 있는 AssumeRole 예제 코드를 참고해주세요)
만약 oidc provider를 자격 증명 공급자게 등록하지 않으면 ingress에서 아래와 같은 에러가 발생할 것입니다.
5. 트러블 슈팅
deployment, service, ingress를 배포한 결과 아래 에러가 발생했습니다.
Error from server (InternalError): error when creating "test-svc.yaml": Internal error occurred: failed calling webhook "mservice.elbv2.k8s.aws": failed to call webhook: Post "https://aws-load-balancer-webhook-service.kube-system.svc:443/mutate-v1-service?timeout=10s": context deadline exceeded
webhook 호출에 실패한 에러입니다.
kubectl get endpoint 명령을 확인해보니 aws-loadbalacner의 웹훅은 9443 포트로 설정되어 있었습니다.
따라서 노드 그룹과 클러스터 보안 그룹에서 9443 포트를 뚫어주면 됩니다.
#클러스터 보안 그룹
module "sg_rule-main_cluster-allow_webhook" {
source = "./modules/t-aws-sg_rule-sg"
description = "allow webhook"
sg_rule-type = "ingress"
sg_rule-from_port = 9443
sg_rule-to_port = 9443
sg_rule-protocol = "tcp"
sg_rule-sg_id = module.eks-cluster.cluster-sg #규칙을 적용할 sg
sg_rule-source_sg_id = module.sg-node_group.sg-id #허용할 sg
depends_on = [module.eks-cluster]
}
#노드 그룹
module "sg_rule-ng-allow_webhook" {
source = "./modules/t-aws-sg_rule-sg"
description = "allow webhook"
sg_rule-type = "ingress"
sg_rule-from_port = 9443
sg_rule-to_port = 9443
sg_rule-protocol = "tcp"
sg_rule-sg_id = module.sg-node_group.sg-id #규칙을 적용할 sg
sg_rule-source_sg_id = module.eks-cluster.cluster-sg #허용할 sg
}
이제 ingress를 배포하면 로드밸런서가 프로비저닝됩니다.
리스너 및 규칙을 확인하니 HTTP 프로토콜을 사용합니다.
저는 매일 블로그를 확인하기 때문에 혹시 궁금한 점, 이해되지 않는 점, 보완할 점이 있다면 댓글로 알려주세요. 읽어주셔서 감사합니다.
'Terraform' 카테고리의 다른 글
[Terraform] Terraform 연습하기 9: Prometheus, Grafana, PV와 PVC, Route53 (2) | 2024.09.21 |
---|---|
Terraform 연습하기 7: 'instances failed to join cluster' 트러블 슈팅 (3) | 2024.09.05 |
[Terraform] ★ 테라폼 연습하기 6: eks cluster 구성과 Assume Role, kubeconfig (0) | 2024.09.01 |
[Terraform] 테라폼 연습하기 5: security group (0) | 2024.08.31 |
[Terraform] 테라폼 연습하기 4: ec2 instance (0) | 2024.08.29 |