6 minute read

1. 개요

Amazon EKS는 자체 Kubernetes 컨트롤 플레인이나 작업자 노드를 설치 및 운영할 필요 없이 AWS에서 Kubernetes를 손쉽게 실행할 수 있도록 지원하는 관리형 서비스이다.

EKS 서비스에 대해 알아가면서 2년전에 멋쟁이사자처럼 해커톤에서 개발했던 풀꽃길 api 서비스를 MSA 아키텍쳐의 형태로 바꾸어보면 좋을것 같다는 생각이 들었다. 그래서 관련자료들을 찾아보니 하나의 app을 통해 이루어지는 서비스에서 가장 먼저 인증 기능을 분리하는 예제가 많았다. 그래서 기존의 풀꽃길 api에서 인증 기능을 분리하고, 이를 EKS를 통해 배포해보면서 필요했던 내용을 위주로 글을 남겨보려 한다.

API github 주소

https://github.com/lunacircle4/floweryroad-api-v2

Infra github 주소

https://github.com/lunacircle4/infra-projects/tree/master/eks-example

2. 기존 프로젝트를 인증, 앱 프로젝트로 분리, 도커 이미지

기존에는 하나의 프로젝트에서 모든 요청을 처리했지만, 이제는 이를 인증, 앱 프로젝트로 분리하여 각각의 모듈에서 요청을 처리하도록 개발해야 한다. 따라서 가장 먼저 기존의 코드를 두개 프로젝트로 분리시키고 필요로 하는 파이썬 라이브러리를 각각 조사하여 새로 requirements.txt를 작성해주었다.

# APP image Dockerfile
FROM lunacircle4/python:3.8.1
WORKDIR /app

RUN apt update \
    && apt install -y postgresql postgresql-contrib libpq-dev supervisor zlib1g-dev libjpeg-dev       
    && rm -rf /var/lib/apt/lists/* 
    
COPY requirements.txt ./
RUN pip3 -v --no-cache-dir install -r requirements.txt && rm -rf requirements.txt

COPY . .
RUN cp ./supervisord.conf /etc/supervisor/conf.d

ENTRYPOINT ["supervisord"]
# Auth image Dockerfile
FROM lunacircle4/python:3.8.1
WORKDIR /app

RUN apt update \
    && apt install -y postgresql postgresql-contrib libpq-dev supervisor zlib1g-dev libjpeg-dev       
    && rm -rf /var/lib/apt/lists/* 
    
COPY requirements.txt ./
RUN pip3 -v --no-cache-dir install -r requirements.txt && rm -rf requirements.txt

COPY . .
RUN cp ./supervisord.conf /etc/supervisor/conf.d

ENTRYPOINT ["supervisord"]

프로젝트도 별개이므로 서비스를 위한 컨테이너 이미지도 따로 만들어 주어야 한다. 이를 위해 Auth, APP을 위한 Dockerfile을 따로 작성해주었다.

3. 데이터베이스 마이그레이션??

인증 서비스의 경우 필요한 데이터베이스 테이블을 그대로 옮겨도 상관 없지만, application 서비스의 경우 User 테이블을 가지고 있지 않기 때문에 문제가 발생하게 된다. 따라서 app 서비스의 테이블중 user을 foreignkey로 가지고 있는 테이블들에 한하여 참조 무결성 제약 조건을 해제할수 있는 방법을 찾아 적용해야한다. 이번에는 django가 제공하는 db_constraint 조건을 설정하여 적용했지만, 일반 integer type 을 사용해도 무방한것으로 보인다.

class Comment(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, db_constraint=False)
    flower = models.ForeignKey(Flower, on_delete=models.CASCADE, related_name='comments')
    content = models.CharField(max_length=200, blank=True)
    star = models.FloatField(validators=[MinValueValidator(0.0), MaxValueValidator(5.0)], default=0.0)
    created_at = models.DateTimeField(auto_now=True)

그리고 기존 데이터베이스에서 각각의 데이터베이스로 필요로 하는 테이블들 마이그레이션 해야 한다. 이를 위해 새로운 서비스들의 데이터베이스에 django migration 파일들을 적용하여 base 테이블들을 생성한후, 기존 데이터베이스의 테이블들의 컬럼들을 새로운 데이터베이스들에 복사하는 파이썬 코드를 작성하여 활용하였다. 새로운 데이터베아스로의 마이그레이션이 완료된후 덤프를 떠서 향후 배포에 활용할수 있도록 하였다.

import os
import psycopg2
from psycopg2 import OperationalError

def execute_read_query(connection, query):
    cursor = connection.cursor()
    result = None
    try:
        cursor.execute(query)
        result = cursor.fetchall()
        return result
    except Error as e:
        print(f"The error '{e}' occurred")

def create_connection(db_name, db_user, db_password, db_host, db_port):
    connection = None
    try:
        connection = psycopg2.connect(
            database=db_name,
            user=db_user,
            password=db_password,
            host=db_host,
            port=db_port,
        )
        print("Connection to PostgreSQL DB successful")
    except OperationalError as e:
        print(f"The error '{e}' occurred")
    return connection

# migration
def migration(table_con, new_table_con, tables, new_tables):
    for i in range(len(tables)):
        datas = read_query(table_con, tables[i])
        insert_query(new_table_con, new_tables[i], datas)

# read datas form table
def read_query(connection, table_name):
    select_query = f"SELECT * from {table_name}"
    return execute_read_query(connection, select_query)

# read datas form table
def insert_query(connection, table_name, datas):
    if len(datas) != 0:
        records = ", ".join(["%s"] * len(datas))
        insert_query = (
            f"INSERT INTO {table_name} VALUES {records}"
        )
        connection.autocommit = True
        cursor = connection.cursor()
        cursor.execute(insert_query, datas)

4. 네트워크 인프라 생성

EKS와 DB 생성하기 이전에 VPC를 생성해줘야 한다. EKS를 위해 subnet을 최소 2개, RDS를 위해 최소 2개를 생성해줘야 하고, EKS에서 필요로 하는 태그를 서브넷 생성시에 삽입해주는것이 포인트이다. 태그를 통해 향후 ELB생성시 항상 퍼블릭서브넷에 생성되도록 유도할수 있다. 만약 태그가 없다면 aws에서 kubernetes.io/cluster/${var.cluster_name} 의 경우 자동으로 삽입하고, elb의 경우 라우팅테이블을 확인하여 퍼블릭, 프라이빗 유무를 확인후 배포하게 된다.

# subnet
resource "aws_subnet" "public_1" {
  vpc_id = aws_vpc.eks.id
  cidr_block = "172.16.0.0/27"
  availability_zone = "ap-northeast-2a"
  tags = {
    "kubernetes.io/cluster/${var.cluster_name}" = "shared"
    "kubernetes.io/role/elb" = 1
  }
  map_public_ip_on_launch = true
}

resource "aws_subnet" "private_1" {
  vpc_id = aws_vpc.eks.id
  cidr_block = "172.16.0.96/27"
  availability_zone = "ap-northeast-2a"
  tags = {
    "kubernetes.io/cluster/${var.cluster_name}" = "shared"
  }
}

5. eks 클러스터 생성

네트워크 인프라가 준비되면, 클러스터를 위한 iam role을 생성해줘야 한다. 공식 문서를 통해 amazonEKSClusterPolicy, amazonEKSVPCResourceController 정책이 필요하다는것을 알았다. 이제 eks 서비스가 role을 assume 할수 있게 trust policy를 role에 할당하고, 앞에 언급한 2개의 policy를 role에 부착해준다.

resource "aws_iam_role" "cluster" {
  name = "eks-cluster-example"

  # trust policy
  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "eks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

resource "aws_iam_role_policy_attachment" "amazonEKSClusterPolicy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
  role       = aws_iam_role.cluster.name
}

# Optionally, enable Security Groups for Pods
# Reference: https://docs.aws.amazon.com/eks/latest/userguide/security-groups-for-pods.html
resource "aws_iam_role_policy_attachment" "amazonEKSVPCResourceController" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSVPCResourceController"
  role       = aws_iam_role.cluster.name
}

이후 cluster 구축시 이 rule을 인자로 넘겨주면 된다.

resource "aws_eks_cluster" "cluster" {
  name     = var.cluster_name
  role_arn = var.cluster_role_arn

  vpc_config {
    subnet_ids = var.cluster_subnets
  }

  depends_on = [
    var.cluster_attachment_1,
    var.cluster_attachment_2
  ]
}

6. eks node group 생성

클러스터가 생성되면, 노드 그룹을 위한 iam role을 생성해줘야 한다. 공식 문서를 통해 amazonEKSWorkerNodePolicy, amazonEKS_CNI_Policy 정책이 필요하다는것을 알았다. 이제 ec2 서비스가 role을 assume 할수 있게 trust policy를 role에 할당하고, 앞에 언급한 2개의 policy를 role에 부착해준다.

resource "aws_iam_role" "node_group" {
  name = "eks-cluster-example_node_group"

  # trust policy
  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

resource "aws_iam_role_policy_attachment" "amazonEKSWorkerNodePolicy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
  role       = aws_iam_role.node_group.name
}

# Amazon EKS 클러스터의 포드 네트워킹을 위한 네트워킹 플러그인입니다.
resource "aws_iam_role_policy_attachment" "amazonEKS_CNI_Policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
  role       = aws_iam_role.node_group.name
}

Node group을 구성할때 Node의 인스턴스 타입과 오토스케일링 정책을 설정해주고 아까 만든 role도 인자로 넣어 준다. 이때 인스턴스들이 private subnet에만 위치하도록 하고 싶다면 subnet에 private subnet들의 list만 할당하도록 한다.

resource "aws_eks_node_group" "node_group" {
  cluster_name    = aws_eks_cluster.cluster.name
  node_group_name = "node_group"
  node_role_arn   = var.node_group_role_arn
  subnet_ids      = var.cluster_subnets
  # 최소 t3.small 로 진행
  instance_types = ["t3.small"]
  scaling_config {
    desired_size = 1
    max_size     = 1
    min_size     = 1
  }

  depends_on = [
    var.node_group_attachment_1,
    var.node_group_attachment_2,
    var.node_group_attachment_3
  ]
}

7. Auth, APP 서비스를 EKS 클러스터에 배포

우선 서비스들을 위한 환경변수가 있는 시크릿 오브젝트를 생성해야 한다. 그후 시크릿을 참조하는 deployment 오브젝트를 생성하여 각 서비스들을 배포할수 있다.이서비스들을 쉽게 사용할수 있도록 DNS, 로드밸런싱 기능을 제공해주는 ClusterIP 서비스도 각각 구축해준다.

# secret 생성
kubectl create secret generic app --from-env-file=.envs/staging/app.env -n floweryroad
kubectl create secret generic auth --from-env-file=.envs/staging/auth.env -n floweryroad
# app deployment, service file, app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
  namespace: floweryroad
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
    spec:
      containers:
      - name: app
        image: lunacircle4/app.floweryroad:${TAG}
        imagePullPolicy: Always
        envFrom:
        - secretRef:
            name: app
---
apiVersion: v1
kind: Service
metadata:
  name: app
  namespace: floweryroad
spec:
  ports:
    - name: app
      port: 80
      targetPort: 80
  selector:
    app: app
  type: ClusterIP
# deployment, service 배포
export TAG=1.0.0
envsubst < app.yaml | kubectl apply -f -
envsubst < auth.yaml | kubectl delete -f -

8. ALB ingress 컨트롤러 배포를 위한 준비

서비스를 배포했다 하더라도, 프론트엔드 단에서 요청을 보내기 위해서는 서비스를 퍼블릭으로 노출시킬 필요성이 있다. 여러가지 방법중 ingress alb기법을 사용하여 배포해보았다. ALB ingress 배포를 위해서는 가장먼저 아까 만들었던 node group을 위한 role에 elb 관련 정책을 할당해야 하기 때문에 가장 먼저 base 정책을 다운로드 해준다.

https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.8/docs/examples/iam-policy.json

그리고 다운로드한 파일을 바탕으로 정책을 생성하고 node group을 위한 role에 할당해주면 된다. 이를 통해 ingress controller가 관리하는 pod에서 elb관련 요청시 인스턴스에 할당된 role을 상속받아 작업을 수행할수 있도록 해준다.

resource "aws_iam_policy" "node_group_elb_policy" {
  name        = "node_group_k8s_policy"
  description = "node_group_k8s_policy"

  policy = file("${path.module}/node_group_elb_policy.json")
}

resource "aws_iam_role_policy_attachment" "node_group_elb_policy" {
  policy_arn = aws_iam_policy.node_group_elb_policy.arn
  role       = aws_iam_role.node_group.name
}

그리고 마지막 과정으로 cert_manager를 k8s 클러스터에 배포해주도록 한다.

kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v1.0.2/cert-manager.yaml

9. ALB ingress 컨트롤러 배포

이제 준비가 되었으면, ingress 배포를 위한 컨트롤러 yaml을 다운로드 해준다.

curl -o ingress_base.yaml https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.2.0/docs/install/v2_2_0_full.yaml

아쉽게도 ingress 컨트롤러 파일을 그대로 바로 사용할수 없다. 파일에서 해당하는 부분을 찾아 –cluster-name 값을 내가 생성했던 cluster의 이름으로 변경해줘야 한다.

    spec:
      containers:
      - args:
        - --cluster-name=클러스터이름
        - --ingress-class=alb
        image: amazon/aws-alb-ingress-controller:v2.2.0
        livenessProbe:
          failureThreshold: 2
          httpGet:
            path: /healthz
            port: 61779
            scheme: HTTP
          initialDelaySeconds: 30
          timeoutSeconds: 10

클러스터이름을 수정했으면 kubectl apply를 통해 컨트롤러를 구축해준다. 그리고 /app, /auth 경로로 요청을 라우팅하기 위한 ingress 오브젝트 파일을 만들어 배포해주면, 이제 프론트엔드에서 els 주소 및 경로로 k8s로 요청, 응답을 받을수 있게 된다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-gateway
  namespace: floweryroad
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/scheme: internet-facing
    # rewrite 하면 full path가 전달되지 않으므로 주의
    # nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
    - http:
        paths:
        - path: /app
          pathType: Prefix
          backend:
            service:
              name: app
              port:
                number: 80
        - path: /auth
          pathType: Prefix
          backend:
            service:
              name: auth
              port:
                number: 80

10. 최종 AWS 아키텍처

Image Alt 텍스트

Categories: ,

Updated:

Leave a comment