“지금 테스트서버 쓰시는 분?” (GitOps로 브랜치별 배포 시스템 구축하기) (2/2)

Noah
레몬베이스 팀블로그
25 min readNov 23, 2022

--

안녕하세요. 레몬베이스에서 Backend Engineer로 일하고 있는 노아(Noah) 입니다. 😄

오늘은 GitOps로 배포 시스템 구축하기! 1편에 이어, 이후 과정과 결과를 이야기 해보려 합니다.

이 이야기를 시작하게 된 배경과 GitOps의 개념 및 기본 예제는 Charles가 작성해주신 1편에서 확인하실 수 있고요, 혹시 1편을 아직 읽지 않으신 분들은 먼저 보고 오시는 걸 추천합니다 😉

[1편 보러가기 - 클릭!]

구성할 인프라 구조

STEP

  1. Application 들과 ArgoCD가 배포되기 위한 Cluster를 구성
  2. 생성된 Cluster의 Pods에 ArgoCD app을 배포
  3. GithubAction에서 구성된 ArgoCD를 사용하기 위한 설정

그럼 이제부터 각 스텝별로 살펴보도록 할게요 😋

1. EKS (Elastic Kubernetes Service)

1.1 EKS 클러스터 생성

우선 ArgoCD와 어플리케이션 배포를 위해 EKS 클러스터를 생성했어요. Amazon EKS를 위한 cli tool 인 eksctl 을 이용했고, 콘솔로 했을 때보다 여러가지 설정들을 yaml 파일을 통해 한번에 확인하고 세팅할 수 있다는 장점이 있었어요.

node type은 앱을 빌드하는데 초점을 맞출 수 있도록 EC2에 비해 관리 포인트가 적은 Fargate로 했습니다.

eksctl create cluster --fargate --dry-run > cluster.yaml

생성하기 전에 dry-run option을 통해서 설정 파일을 받고, 기존에 사용 중인 VPC을 사용하기 위해서 VPC 정보를 다음과 같이 수정했어요.

참고 링크:

https://eksctl.io/usage/fargate-support/#managing-fargate-profiles

>> cluster.yaml

apiVersion: eksctl.io/v1alpha5
cloudWatch:
clusterLogging:
enableTypes: ["*"]
fargateProfiles:
- name: fp-default
selectors:
- namespace: default
- namespace: kube-system
status: ""
iam:
vpcResourceControllerPolicy: true
withOIDC: false
kind: ClusterConfig
kubernetesNetworkConfig:
ipFamily: IPv4
metadata:
name: lemonbase-test # 클러스터의 이름을 설정
region: ap-northeast-2
version: "1.22"
privateCluster:
enabled: false
skipEndpointCreation: false
vpc:
autoAllocateIPv6: false
cidr: ... # 기존 VPC에서 정보를 가져와 입력
clusterEndpoints:
privateAccess: true
publicAccess: true
id: vpc-... # 기존 VPC에서 정보를 가져와 입력
manageSharedNodeSecurityGroupRules: true
nat:
gateway: Single
subnets: # 기존 VPC와 관련된 서브넷 설정을 아래에 입력
private:
ap-northeast-2a:
az: ap-northeast-2a
cidr: ...
id: subnet-...
ap-northeast-2c:
az: ap-northeast-2c
cidr: ...
id: subnet-...
public:
...

TMI;

cluster를 생성하는 과정에서 CoreDNS의 이미지를 받아오지 못하는 오류(ImagePullBackOff)가 발생해 꽤 오랫동안 헤맸었는데, VPC에 Endpoint 설정이 되어 있기 때문이었어요.. 😢

그래서 private-link의 보안그룹에 클러스터의 보안그룹을 연결시켜 주어서 해결했습니다! 덕분에 PrivateLink의 개념에 대해 배울 수 있었죠 😋

eksctl create cluster -f cluster.yaml

위 yaml 로 eksctl 로 create 명령을 실행 하면 kube-system 이라는 namespace에 실행되고 있는 CoreDNS 네임서버, yaml 파일에서 추가해 준 명세에 따라 Fargate Profile 과 함께 클러스터가 생성됩니다.

(Fargate Profile은 Pod을 실행시키기 위해 필요한 권한과 서브넷에 대한 정보, Fargate 에 어떤 Pod들을 실행시킬지 결정하는 selector에 대한 정보를 제공해요.)

참고 링크:

https://eksctl.io/usage/fargate-support/

kubectl get pods -n kube-system
생성된 pods

⚠️ 기본적으로 클러스터 생성자가 아닌 유저는 kubectl을 사용해서 클러스터를 조작할 수 없기 때문에, 문서의 설명을 따라 권한을 부여해야 합니다.

다음으로, EKS에서 ALB를 Ingress로 사용하기 위해서 aws-load-balancer-controller 설치가 필요한데요.

⚠️ 이때, IAM 정책이 필요하니 정책을 추가하고 연결해 주는 작업이 선행되어야 해요.

(참고 링크에 설치 가이드가 자세히 나와있으니 읽어 보시는 것을 권장합니다🙂)

참고 링크:

https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/iam-roles-for-service-accounts.html

https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.4/deploy/installation/

NAME: aws-load-balancer-controller
LAST DEPLOYED: ...
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
AWS Load Balancer controller installed!

정상적으로 설치가 완료되었다면 실행중인 Pod을 확인할 수 있어요.

kubectl get pods -n kube-system

aws-load-balancer-controller-859c9bfbf7-cr2ms 1/1 Running 0 95s
aws-load-balancer-controller-859c9bfbf7-sj6sh 1/1 Running 0 2m32s

⚠️ Subnet에 태그가 추가되지 않으면 aws-load-balancer-controller가 EKS에서 지정한 Subnet을 찾을 수 없기 때문에, 아래와 같은 태그를 추가해 주어야 합니다.

#private subnet
kubernetes.io/role/internal-elb

#public subnet
kubernetes.io/role/elb

참고 링크:

https://aws.amazon.com/ko/premiumsupport/knowledge-center/eks-load-balancer-controller-subnets/

여기까지 클러스터와 관련된 기본 설정들을 마쳤으나, 아직 ArgoCD 을 클러스터에 올리기 위한 작업이 남아있습니다.

2. ArgoCD

앞서 Fargate 타입의 노드로 선택했기 때문에, workloads를 배포하기 위해서는 Fargate profile(fp)을 설정해 주어야 하는데요.

eksctl을 통해 cluster를 생성할 때, kube-system을 위한 Fargate만 생성되었으므로, ArgoCD가 올라갈 Fargate Profile을 ArgoCD라는 namespace로 추가 생성해 줍니다.

참고 링크:

https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/fargate-profile.html

2.1 ArgoCD 설치 및 환경 설정

ArgoCD 설치 와 app 생성은 공식 문서에 자세히 나와 있어서 링크를 첨부해두도록 하겠습니다. 🙇

다음으로, Ingress를 통해 ArgoCD 접속을 하기 위해서 ALB Ingress를 설정해야 하기에,

Route53에 등록한 도메인 argocd.example.com 을 host로 작성합니다.

따라서, 아래 예시와 같이 argocd_ingress.yml 파일을 적용 시켰습니다.

>> argocd_ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
# Ingress Core Settings
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/healthcheck-path: /health-check/
alb.ingress.kubernetes.io/healthcheck-protocol: HTTP
alb.ingress.kubernetes.io/scheme: internal
alb.ingress.kubernetes.io/target-type: ip
# SSL Settings
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:...
alb.ingress.kubernetes.io/ssl-redirect: "443"
alb.ingress.kubernetes.io/backend-protocol: HTTPS
# Customized
alb.ingress.kubernetes.io/group.name: "lemonbase-test"
alb.ingress.kubernetes.io/inbound-cidrs: ... # 인터널 접속을 허용하는 VPC CIDR

finalizers:
- ingress.k8s.aws/resources
labels:
app: lemonbase-test
tier: backend
name: lemonbase-test-ingress # 인그레스 이름
namespace: argocd # 설치할 네임스페이스
spec:
rules:
- host: "argocd.example.com"
http:
paths:
- path: /*
pathType: ImplementationSpecific
backend:
service:
name: argocd-server
port:
name: https
kubectl apply -f ./argocd_ingress.yml -n argocd

참고 링크:

https://aws.amazon.com/ko/blogs/containers/introducing-aws-load-balancer-controller/

https://medium.com/@philipdam8/using-a-single-alb-ingress-across-multiple-kubernetes-namespaces-556df9b51f80

이제 실제로 ArgoCD가 배포하기 위해 필요한 리소스들을 정의해야 합니다.

2.2 Helm Chart 생성

Helm chart를 통해 CRD(Custom Resource Definition)를 정의하여 원하는 환경 변수를 주입받아 여러 어플리케이션들을 찍어 낼 수 있도록 했습니다.

메니페스트들에 대한 설명은 아래 참고 링크로 갈음하도록 할게요 🙂

참고 링크:

https://kubernetes.io/ko/docs/concepts/overview/working-with-objects/kubernetes-objects/#kubernetes-objects

각 브랜치에서 빌드한 image를 unique 한 domainName을 갖게 하기 위해서 아래와 같이 구성했습니다.

>> values.yaml

domainName: master
imageUrl: ""
imageTag: 1.0.0

values.yaml 에서 내부에서 변수로 사용될 domainName, imageUrl, imageTag를 선언했고, 이 변수들이 {{ .Values.~~}} 와 같이 templates 내부 메니페스트들에서 사용됩니다.

처음에 트래픽이 닿는 곳인 Ingress 에서 domainName을 받아서 service로 트래픽을 전달해요.

>> ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
namespace: lemonbase
name: lemonbase-test-{{ .Values.domainName }}-ingress
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/certificate-arn: ...
alb.ingress.kubernetes.io/healthcheck-path: /health-check/
alb.ingress.kubernetes.io/healthcheck-protocol: HTTP
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/ssl-redirect: "443"
alb.ingress.kubernetes.io/group.name: "lemonbase-test"
alb.ingress.kubernetes.io/inbound-cidrs: ... # 인터널 접속을 허용하는 VPC CIDR
spec:
rules:
- host: "{{ .Values.domainName }}.example.com"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: lemonbase-{{ .Values.domainName }}
port:
number: 80

group-name 어노테이션을 추가하여 배포할때마다 로드밸런서를 새로 만들지 않고, 각 도메인별로 트래픽을 타겟 그룹으로 보내주도록 했습니다.

참고링크:

https://medium.com/@philipdam8/using-a-single-alb-ingress-across-multiple-kubernetes-namespaces-556df9b51f80

https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.2/guide/ingress/annotations/#ingress-annotations

>> service.yaml

apiVersion: v1
kind: Service
metadata:
namespace: lemonbase
name: lemonbase-{{ .Values.domainName }}
spec:
ports:
- port: 80
targetPort: 80
selector:
app: lemonbase-{{ .Values.domainName }}

다음으로 어플리케이션을 배포하기 위해 deployment.yaml을 정의했습니다.

>> deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
namespace: lemonbase
name: lemonbase-{{ .Values.domainName }}
spec:
replicas: 1
revisionHistoryLimit: 3
selector:
matchLabels:
app: lemonbase-{{ .Values.domainName }}
template:
metadata:
labels:
app: lemonbase-{{ .Values.domainName }}
spec:
containers:
- image: {{ .Values.imageUrl }}/development/lemonbase/service:{{ .Values.imageTag }}
name: lemonbase-service-{{ .Values.domainName }}
...
ports:
- containerPort: 80
...

ArgoCD Application 생성을 위한 정의 (⚠️ Application 생성 시에 띄어쓰기가 허용되지 않아요.)

>> application.yaml

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: $DOMAIN_NAME
annotations:
...
spec:
destination:
name: ""
namespace: lemonbase
server: "<https://kubernetes.default.svc>"
source:
path: ./docs/argocd/chart/
repoURL: "git@github.com:example-repository.git"
targetRevision: $BRANCH_NAME
helm:
parameters:
- name: imageTag
value: $IMAGE_TAG
- name: imageUrl
value: $ECR_URL
- name: domainName
value: $DOMAIN_NAME
project: lemonbase
syncPolicy:
syncOptions:
- CreateNamespace=true
automated:
prune: true
selfHeal: false

위와 같이 application.yaml을 정의하고 아래의 커맨드를 실행하면 application을 생성할 수 있습니다.

(이후 GitHub Actions 에서 ArgoCD app을 생성하기 위해 사용될 커맨드입니다.)

export BRANCH_NAME=example-1234
envsubst < ./argocd/application.yaml | argocd app create $BRANCH_NAME -f -

여기까지 하면 Helm Chart를 정의를 하여 [STEP.2] 까지의 과정을 마치게 돼요 🙂

이제 [STEP.3] 위에서 정의한 Chart를 GitHub Actions를 이용해서 어플리케이션을 생성하고 브랜치별로 서버를 생성, 수정하는 과정이 시작됩니다!

위와 같이 원하는 브랜치와 데이터베이스로 테스트 서버를 띄우기 위해서 Workflow를 생성합니다.

3. GitHub Actions workflow 생성

도메인 이름을 결정하기 위해서 브랜치명에서 마지막 경로 2개를 가져와서 아래 예시와 같이 처리했어요. (eg. example/ex-1234/main > ex-1304-main)

- name: Check ArgoCD Application
id: argocd_check
shell: bash
continue-on-error: true
env:
CI_COMMIT_REF_NAME: ${{ github.ref }}
ARGOCD_URL: ${{ secrets.ARGOCD_URL }}
ARGOCD_ADMIN_USERNAME: ${{ secrets.ARGOCD_ADMIN_USERNAME }}
ARGOCD_ADMIN_PASSWORD: ${{ secrets.ARGOCD_ADMIN_PASSWORD }}
run: |
export DOMAIN_NAME=${{ steps.build_name.outputs.domain }}

curl -sSL -o /usr/local/bin/argocd <https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64>
chmod +x /usr/local/bin/argocd

argocd login $ARGOCD_URL --username $ARGOCD_ADMIN_USERNAME --password $ARGOCD_ADMIN_PASSWORD
argocd app get $DOMAIN_NAME

위 코드에서 runner에 argocd cli 를 설치하고, 배포하려는 도메인네임의 어플리케이션이 존재하는지 체크합니다.

cli로 app을 조회할 때, 존재하지 않는 경우 에러를 발생시키고 job이 종료되어서 continue-on-error 옵션을 이용해서 다음 step 으로 넘어갈 수 있도록 처리했어요.

- name: Create Argocd Application or Update Helm Parameters on Argocd Application
shell: bash
env:
ECR_URL: ${{ secrets.DEV_ECR_URL }}
run: |
export BRANCH_NAME=${{ steps.build_name.outputs.branch }}
export DOMAIN_NAME=${{ steps.build_name.outputs.domain }}
export IMAGE_TAG=${{ steps.build_image.outputs.tag }}

if [ "${{ steps.argocd_check.outcome }}" = "success" ]
then
argocd app set $DOMAIN_NAME --helm-set imageTag=$IMAGE_TAG
else
envsubst < ./docs/argocd/application.yaml | argocd app create $DOMAIN_NAME -f -
fi

argocd_check 스텝에서 해당 app 이 존재하는 경우라면 새로 build 된 이미지 tag로 update 하고, 존재하지 않는 경우에는 새로 생성해 줍니다 🙂

그리고 서버를 올려 테스트를 마치고 나서 그냥 두면 소중한 자원이 낭비될 수 있으니 PR이 close 될 때, 자동으로 서버가 내려가도록 규칙을 추가해주었어요.

name: "[Test] merge된 브랜치 기반 테스트 서버 제거"
...
jobs:
clean-up:
if: github.event.pull_request.merged
runs-on: self-hosted
...
steps:
...
- name: Extract branch and domain name
...
- name: Check ArgoCD Application
...
- name: Delete Argocd Application
shell: bash
...

if [ "${{ steps.argocd_check.outcome }}" = "success" ]
then
argocd app delete $DOMAIN_NAME --cascade
fi

결론

요약하면, ArgoCD와 여러 컨테이너를 실행/관리하기 위해서 EKS 클러스터를 생성하고, 생성한 클러스터에 ArgoCD를 설치 후 Github Actions를 통해서 ArgoCD가 어플리케이션을 생성할 수 있도록 workflow를 설정해 주었습니다.

as-is

  1. 기능 개발 후 테스트 해 보기 위해 4개의 테스트 서버 중에서 선택해서 배포
  2. 개발 중인 기능을 테스트하기 위해 4개의 서버 중 사용 중이지 않은 테스트 서버를 수소문 😓
  3. 사용중이지 않은 도메인의 테스트 서버에 테스트하고자 하는 기능을 배포
  4. 배포된 서버를 이용하여 테스트
  5. 기능 테스트가 완료되고 누군가 사용 중인지 물어보면 써도 좋다고 소통 🗣️

to-be

  1. 개발한 기능을 테스트할 테스트 서버를 즉시 생성
  2. 배포된 서버에서 테스트
  3. 테스트 완료 시 개발 중인 브랜치의 PR을 머지시켜, 자동으로 해당 서버를 삭제 혹은 ArgoCD 콘솔에서 직접삭제

이로써 그룹 내 여러 기능이 동시에 개발되는 상황에서도 누가 사용 중인지 서로 소통하지 않고, 원하는 만큼 만들거나 삭제할 수 있게 되었어요. 또한, 기능을 개발 중인 개발자는 인프라 구성에 대한 지식이 없는 상태에서도 선언된 인프라 설정대로 직접 테스트 서버를 개설하고 그 위에서 해당 기능을 테스트하고 QA를 진행할 수 있게 되었습니다. 😆

(여전히 한정된 개수의 DB를 공유하고 있다는 고민이 남아있긴 하지만, 이것은 다음 단계로 두고 조만간 이를 해결하기 위한 별동대가 출발할 예정입니다.. ☺️☺️)

위 과정들을 구성하면서 생각만큼 안 되는 것들도 정말 많고 😢, 고민되는 점들도 있었지만

“GitOps를 이용해서 브랜치별 배포를 자동화한 과정을 설명” 이라는 주제에서 벗어나지 않고 적으려다 보니, 생략된 설명들과 Trouble Shooting 과정이 있는 점 양해 부탁드립니다 🙇.

보너스..

(추가로 편의를 위한 몇 가지 기능을 추가했어요.☺️ 본편의 주제가 아니기 때문에 참조한 링크를 첨부하여 간략히 이야기 하겠습니다.)

Google SSO

내부 크루들이 자유롭게 ArgoCD에 workspace 계정으로 로그인 할 수 있도록 처리했습니다.

참고링크를 따라서 구글클라우드에서 프로젝트 및 OAuth 동의화면 을 구성후, OpenID 를 이용하여 ArgoCD의 설정을 수정해줍니다.

kubectl -n argocd edit configmaps argocd-cm

위 커맨드를 통해서 argocd의 ConfigureMaps에 dex.config를 추가해 주었다면, RBAC Configuration 을 통해 각 계정별로 역할 기반으로 권한관리를 할수 있습니다.

ArgoCD Notifications 설정

서버가 배포되었을 때, 슬랙으로 알림을 받아 볼 수 있도록 했습니다.

공식문서를 따라 ArgoCD Notification 관련 메니페스트들을 설치하고 트리거와 템플릿을 통해서 언제 알림을 받을 지 설정합니다.

kubectl edit -n argocd configmaps argocd-notifications-cm

위 커맨드를 통해서 ConfigMaps 설정을 수정해서 템플릿을 설정 해 주었어요

참고 링크:

https://wookiist.dev/154

Terminal 접속 기능 활성화

참고 링크:

https://argo-cd.readthedocs.io/en/stable/operator-manual/web_based_terminal/

마치며..

저는 GitOps와 쿠버네티스의 개념을 거의 모르는 상태였는데, 함께 매일 30분~1시간씩 작업하며 모르는 부분을 처음부터 끝까지 친절하게 설명해 주신 동료 크루 제이미(Jamie), 찰스(Charles) 에게 감사를 전하며 이 글을 마칩니다.

이상 주니어 개발자라도 관심만 있다면 어떤 것이든 제안하고 함께 만들어 갈 수 있다는 자신감과 심리적 안전감이 가득한 레몬베이스에서, 노아(Noah)였습니다. 😆

레몬베이스 팀에 관심이 있으시다면,
lemonbase.team 을 살펴봐주세요:)

--

--