AWS EKS를 활용한 CI/CD 구축 - 2
2023-07-04 08:08
시리즈물 첫 번째 게시글에서, AWS EKS를 활용하여 Jenkins, ArgoCD 등 CI/CD 파이프라인을 구축하는 데 필요한 서비스를 배포하고, 이 과정에서 맞닥뜨린 각양 각색의 문제들과 문제를 해결한 방법에 대해서 공유하였습니다. 이번에는 구축된 서비스를 통해 실제 애플리케이션을 배포하는 과정을 공유합니다.
개요
두 번째 장에서는, 앞선 글에서 구축한 EKS 클러스터와 Jenkins, ArgoCD를 활용하여 Git을 통해 트리거되는 CI/CD 파이프라인을 생성하고, 실제 애플리케이션을 배포해보도록 한다.
다음 그림의 구조를 완성할 것이다.
Jenkins
첫 번째 장에서 Jenkins를 배포하고 Ingress를 통해 호스트와 연결되었으므로 jenkins에 접속할 수 있을 것이다. 간단한 Pipeline Job을 하나 생성해 보자.
System
- Jenkins 관리 -> System에 접근하여 # of executor를 1 이상으로 설정한다.
Credentials
- Jenkins 관리 -> Credentials -> (global) Domains를 클릭하고 Add Credential로 자격증명을 설정해야 한다.
- Git 자격증명 추가
- Kind: Username with password
- Scope: Global
- Username: token
- Password: Git 레포지토리에서 생성한 Personal Access Token
- ID: Jenkins 내부 파이프라인 등에서 사용할 ID
- Description: 설명
- ArgoCD 자격증명 추가
- ArgoCD에서 Token 발급
- Settings -> Accounts -> admin -> Generate New를 클릭하여 토큰을 생성
- 자격증명 추가
- Kind: Secret Text
- Scope: Global
- Secret: ArgoCD Token
- ID: Jenkins 내부 파이프라인 등에서 사용할 ID
- Description: 설명
- ArgoCD에서 Token 발급
- Amazon ECR AWS Credential 추가
- AWS Credentials 플러그인이 설치되었다면, Kind에 AWS Credentials가 추가되었을 것이다.
- IAM의 Access Key, Secret Access Key가 필요하다.
- Kind: AWS Credentials
- Scope: Global
- Access Key ID: AWS IAM 계정의 Access Key
- Secret Access Key: AWS IAM 계정의 Secret Access Key
- Git 자격증명 추가
Plugin
- 좌측 메뉴 Jenkins 관리 → Plugins → Available plugins
- Docker로 검색해서 나오는 Plugin들 설치
- Docker API Plugin
- Docker Commons Plugin
- Docker Compose Build Step Plugin
- Docker Pipeline
- Docker plugin
- docker-build-step
- Amazon EC2 Plugin, AWS Credentials Plugin, AWS Steps 설치
- 그 외에 필요하다고 생각되는 Plugin을 자유롭게 사용하도록 하자. 단 버전 및 호환성은 주의해야 한다.
- Docker로 검색해서 나오는 Plugin들 설치
Build Job
- 대시보드에서 새로운 Item → Pipeline 선택 → 이름 넣고 생성
- Pipeline Script: 다음과 같은 예시로 간단히 생성해 보자.
pipeline {
options {
timeout(time: 1, unit: 'HOURS')
}
agent any
environment {
ECR_REPOSITORY = 'my.ecr.repo.url'
IMAGE_NAME = 'my-api-server'
}
stages {
stage('Checkout') {
steps {
git credentialsId: 'my-git-credential', url: 'https://github.com/myrepo/MyApiServer.git', branch: 'develop'
}
}
stage('Build and Push image using Jib') {
steps {
script {
withCredentials([[
$class: 'AmazonWebServicesCredentialsBinding',
credentialsId: 'my-aws-credential'
]]) {
env.AWS_REGION = 'my-region'
sh 'echo Check ECR Private Repository'
def ecrRepoExists = sh(script: "aws ecr describe-repositories --repository-names ${env.IMAGE_NAME} --region ${env.AWS_REGION} || true", returnStdout: true).trim()
if (ecrRepoExists == '') {
sh 'echo ECR Repository does not exist, create it'
sh "aws ecr create-repository --repository-name ${env.IMAGE_NAME} --region ${env.AWS_REGION}"
}
sh 'echo jib build and push'
def ecrLoginResult = ecrLogin()
env.ECR_PASSWORD = ecrLoginResult.split("-p ")[1].split(" ")[0]
sh "./gradlew jib -Pprofile=prod -Djib.to.image=${ECR_REPOSITORY}/${IMAGE_NAME}:latest -Djib.to.auth.username=AWS -Djib.to.auth.password=${ECR_PASSWORD}"
}
}
}
}
}
}
- Dockerize 툴로 jib을 사용하였다. jib에 대한 사용 예시는 다음 링크를 참조하자: https://blog.leaphop.co.kr/blogs/32
- Dockerfile을 사용할 경우, k8s 환경에서는 전통적인 Dind, Dood 방식을 쓰기 까다롭다. 따라서 호스트의 도커 데몬을 쓰기 어려우므로 kaniko를 사용해 보자.
- Kaniko란 Google에서 만든 오픈소스 이미지 빌드 도구이며(https://github.com/GoogleContainerTools/kaniko), 다음과 같은 장점을 가진다.
- Docker 데몬이 필요 없음: Docker 데몬을 실행할 수 없는 환경에서도 이미지 빌드가 가능
- 컨테이너화된 환경에서 실행: Kaniko는 컨테이너 내에서 실행되므로, 쿠버네티스와 같은 컨테이너 오케스트레이션 플랫폼에서 실행하는 데 이상적
- 표준 Dockerfile 지원: Kaniko는 표준 Dockerfile을 사용하여 이미지를 빌드함
- 다양한 레지스트리 지원: Docker Hub, Google Container Registry, AWS ECR 등 대부분의 주요 컨테이너 레지스트리에 이미지 푸시 가능
- 다음과 같은 예시로 kaniko dockerfile build pipeline script를 간단히 작성해 보자.
- 정의된 대로 빌드를 수행할 임시 Pod이 Jenkins와 동일한 네임스페이스에 생성된다.
- 빌드를 위한 Pod이므로, CPU, Memory등의 리소스에 크게 의존적이다. 모자라면 Pod이 죽는다.
- Kaniko란 Google에서 만든 오픈소스 이미지 빌드 도구이며(https://github.com/GoogleContainerTools/kaniko), 다음과 같은 장점을 가진다.
pipeline {
options {
timeout(time: 1, unit: 'HOURS')
}
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: maven
image: maven:3-openjdk-8
command: ['sleep']
args: ['infinity']
- name: kaniko
image: gcr.io/kaniko-project/executor:debug
command: ['sleep']
args: ['infinity']
volumeMounts:
- name: registry-credentials
mountPath: /kaniko/.docker
volumes:
- name: registry-credentials
secret:
secretName: docker-registry-secret
items:
- key: .dockerconfigjson
path: config.json
'''
}
}
environment {
DOCKER_REPOSITORY = 'docker.my-docker-repository.co.kr'
IMAGE_NAME = 'my-api-server'
}
stages {
stage('Checkout') {
steps {
git credentialsId: 'my-git-credential', url: 'https://github.com/myrepo/MyApiServer.git', branch: 'develop'
}
}
stage('Build and Push image') {
steps {
container('kaniko') {
script {
sh '''#!/busybox/sh
/kaniko/executor --context dir://$(pwd) --dockerfile=$(pwd)/Dockerfile --destination=docker.my-docker-repository.co.kr/my-api-server:latest
'''
}
}
}
}
}
}
Deploy Job
- 대시보드에서 새로운 Item → Pipeline 선택 → 이름 넣고 생성
- Pipeline Script: 다음과 같은 예시로 간단히 생성해 보자.
pipeline {
options {
timeout(time: 1, unit: 'HOURS')
}
agent any
environment {
ARGOCD_SERVER = 'my-host/argocd'
APP_NAME = 'my-api-server'
}
stages {
stage('Sync ArgoCD Application') {
steps {
script {
withCredentials([string(credentialsId: 'argocd-api-token', variable: 'ARGOCD_AUTH_TOKEN')]) {
echo "Syncing ArgoCD application..."
sh '''
curl -s -k -H 'Content-Type: application/json' -H "Authorization: Bearer ${ARGOCD_AUTH_TOKEN}" -X POST \
--data '{}' \
https://${ARGOCD_SERVER}/api/v1/applications/${APP_NAME}/sync
'''
}
}
}
}
}
}
ArgoCD
프로젝트, 레포지토리, 애플리케이션 생성이 필요하다. 매뉴얼 Sync를 통해 K8s 환경에 앱을 배포할 수 있는 간단한 설정을 해볼 것이다.
Project
- 프로젝트를 생성한다.
- 좌측 메뉴 Settings -> Projects -> + NEW PROJECT
- SOURCE REPOSITORIES -> EDIT -> 아래에서 생성할 레포지토리 선택
- DESTINATIONS -> EDIT
- Server: https://kubernetes.default.svc
- Name: in-cluster
- Namespace: 애플리케이션을 배포할 네임스페이스. 없을 경우 생성해 두자.
kubectl create namespace my-app --kubeconfig="./kubeconfig.yaml"
- 좌측 메뉴 Settings -> Projects -> + NEW PROJECT
Repositories
- 소스 레포지토리를 연동한다. Git을 지원한다.
- 좌측 메뉴 Settings -> Repositories -> + CONNECT REPO
- VIA HTTPS
- Type: git
- Project: 위에서 생성한 프로젝트를 선택
- Repository URL: Git 레포지토리 URL
- Username: token
- Password: Git에서 발급산 Personal Access Token
- CONNECT를 클릭하여 생성
- 좌측 메뉴 Settings -> Repositories -> + CONNECT REPO
Application
- Sync 배포할 애플리케이션을 설정한다.
- 좌측 메뉴 Applications -> + NEW APP
- GENERAL
- Application Name: 애플리케이션 이름. API를 통해 호출할 때 이 이름을 사용함.
- Project Name: 위에서 만든 프로젝트 선택
- SYNC POLICY: Manual
- SOURCE
- Repository URL: 위의 Repositories에서 생성한 Git 레포지토리 선택
- Revision: HEAD라고 되어 있는데, 지우면 실제 레포지토리의 브랜치명이 뜬다. 배포할 브랜치 선택.
- Path: k8s 매니페스트 파일이 위치한 경로. 이 경로에 k8s 배포를 위한 yaml 파일이 정의되어 있어야 한다.
- DESTINATION
- Cluster URL: https://kubernetes.default.svc 선택
- Namespace: 애플리케이션을 배포할 네임스페이스. Project에서 설정한 네임스페이스와 같아야 함.
- GENERAL
- 좌측 메뉴 Applications -> + NEW APP
Git
GItlab에서는 gitlab-ci로, Github에서는 github action이라고 부르는 CI/CD 파이프라인을 지원한다. 필자는 gitlab으로 예시를 들도록 한다. gitlab runner 등의 설정은 모두 되어있다고 가정한다.
.gitlab-ci.yaml
프로젝트 루트 디렉토리의 .gitlab-ci.yaml
파일을 통해 CI/CD 스크립트를 제어하게 된다.
다음은 간략한 예제로, curl을 통해 Jenkins에 정의되어 있는 Build와 Deploy Job을 실행하는 예시이다.
stages:
- build
- deploy
variables:
JENKINS_URL: "$JENKINS_URL"
JENKINS_USER: "$JENKINS_USER"
JENKINS_API_TOKEN: "$JENKINS_API_TOKEN"
build:
stage: build
image: node:12.21.0-alpine
when: manual
script:
- apk --update --no-cache add curl jq
- echo "Stage build"
- CRUMB=$(curl -k -u ${JENKINS_USER}:${JENKINS_API_TOKEN} ${JENKINS_URL}/crumbIssuer/api/json | jq -r '.crumb')
- CRUMB_FIELD=$(curl -k -u ${JENKINS_USER}:${JENKINS_API_TOKEN} ${JENKINS_URL}/crumbIssuer/api/json | jq -r '.crumbRequestField')
- curl -k -u "${JENKINS_USER}:${JENKINS_API_TOKEN}" -X POST "${JENKINS_URL}/job/my-app-build" -H "$CRUMB_FIELD:$CRUMB" || exit 1
- |-
while true
do
sleep 5
result=$(curl -k -u ${JENKINS_USER}:${JENKINS_API_TOKEN} ${JENKINS_URL}/job/my-app-build/lastBuild/api/json | jq -r '.result')
if [ "$result" == "SUCCESS" ]; then
echo "Jenkins job succeeded"
echo $result > SUCCESS
break
elif [ "$result" == "FAILURE" ] || [ "$result" == "ABORTED" ] || [ "$result" == "UNSTABLE" ] || [ "$result" == "NOT_BUILT" ]; then
echo "Jenkins job ended with status $result"
exit 1
else
echo "Jenkins job is still running..."
fi
done
deploy:
stage: deploy
image: node:12.21.0-alpine
when: manual
script:
- apk --update --no-cache add curl jq
- echo "Stage deploy"
- CRUMB=$(curl -k -u ${JENKINS_USER}:${JENKINS_API_TOKEN} ${JENKINS_URL}/crumbIssuer/api/json | jq -r '.crumb')
- CRUMB_FIELD=$(curl -k -u ${JENKINS_USER}:${JENKINS_API_TOKEN} ${JENKINS_URL}/crumbIssuer/api/json | jq -r '.crumbRequestField')
- curl -k -u "${JENKINS_USER}:${JENKINS_API_TOKEN}" -X POST "${JENKINS_URL}/job/my-app-deploy/build" -H "$CRUMB_FIELD:$CRUMB" || exit 1
- |-
while true
do
sleep 5
result=$(curl -k -u ${JENKINS_USER}:${JENKINS_API_TOKEN} ${JENKINS_URL}/job/my-app-deploy/lastBuild/api/json | jq -r '.result')
if [ "$result" == "SUCCESS" ]; then
echo "Jenkins job succeeded"
echo $result > SUCCESS
break
elif [ "$result" == "FAILURE" ] || [ "$result" == "ABORTED" ] || [ "$result" == "UNSTABLE" ] || [ "$result" == "NOT_BUILT" ]; then
echo "Jenkins job ended with status $result"
exit 1
else
echo "Jenkins job is still running..."
fi
done
needs: ["build"]
dependencies:
- build
- Build와 Deploy 스테이지로 나누어져 있으며, JENKINS_URL과 JENKINS_USER, JENKINS_API_TOKEN은 gitlab CI/CD Variable로 등록해 두었다.
- Jenkins의 특정 버전 이후부터는 외부 URL을 통해 API 호출할 경우 토큰 뿐 아니라 CRUMB라는 헤더를 요구한다. 강화된 CSRF 보안 정책 때문이며, Disable 시키기가 까다롭다.
- Jenkins Job을 실행한 후 5초 간격으로 체크한다.
Application
배포될 애플리케이션의 경우 ArgoCD에서 설정할 특정 디렉토리에 k8s 배포용 매니페스트 파일을 만들어두어야 한다. 기초적으로 다음 세 가지 매니페스트 파일을 생성하여 배포해보자.
- deployment.yaml
- service.yaml
- ingress.yaml
deployment
k8s에서 애플리케이션의 배포를 정의한다. 다음과 같은 예시로 작성해보자.
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-api-server
namespace: my-app
spec:
replicas: 1
selector:
matchLabels:
app: my-api-server
template:
metadata:
labels:
app: my-api-server
spec:
containers:
- name: my-api-server
image: docker.my-docker-repository.co.kr/my-api-server:latest
ports:
- containerPort: 8080
resources:
limits:
cpu: 1
memory: 512Mi
requests:
cpu: 500m
memory: 256Mi
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
imagePullSecrets:
- name: docker-registry-secret
- 이미지가 ECR에 push되어 있다면 ECR에 대한 정보를 K8s secret으로 추가해야 한다. 다음 단계를 거쳐 추가해보자. 앱이 배포될 네임스페이스에 Secret을 생성해야 적용된다.
- ECR 토큰 가져오기
aws ecr get-authorization-token --profile myprofile --region ap-northeast-2 --output text --query 'authorizationData[].authorizationToken'
- Secret 생성
kubectl create secret docker-registry ecr-registry-secret --docker-server={my_ecr_url} --docker-username=AWS --docker-password={ecr_token} -n my-app --kubeconfig="./kubeconfig.yaml"
- 만약 ECR이 아닌 사설 Docker Registry를 사용할 경우에도 추가해야 한다. ECR과 동일하다.
kubectl create secret docker-registry docker-registry-secret --docker-server=docker.my-docker-registry.co.kr --docker-username=my --docker-password=password -n my-app --kubeconfig="./kubeconfig.yaml"
service
k8s에 생성된 애플리케이션의 Service를 정의한다. 다음과 같은 예시로 작성해보자.
apiVersion: v1
kind: Service
metadata:
name: my-api-server
namespace: my-app
spec:
selector:
app: my-api-server
ports:
- protocol: TCP
port: 8080
targetPort: 8080
nodePort: xxxxxx // 포트 번호
type: NodePort
- Ingress가 있으므로 Service 단계에서 internet-facing하게 Load Balancer 타입의 서비스를 생성할 필요는 없다. 단 Nodeport 번호는 겹치면 안된다.
Ingress
k8s에 생성된 애플리케이션의 Ingress를 정의한다. 다음과 같은 예시로 작성해보자. 예시에서는 internet-facing용도로 외부 노출된 Application Load Balancer를 생성하고, ACM 인증서와 연결된 특정 Host를 세팅한다. 첫번째 포스팅에서 설치한 aws-load-balancer-controller에 의해 컨트롤되며 80, 443 포트를 리스닝한다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-api-server-ingress
namespace: my-app
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]' alb.ingress.kubernetes.io/certificate-arn: {acm_인증서_arn}
alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'spec:
rules:
- host: my-host.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: my-api-server
port:
number: 8080
- API Call의 Path나 서비스명, 네임스페이스 등을 유의하자.
배포
Git -> Jenkins -> ECR -> ArgoCD -> K8s로 이어지는 배포 파이프라인을 완성하였다. 첫번째 포스팅에서 다음과 같은 그림을 공유했었다.
사용자는 다음과 같은 과정을 거쳐 배포를 수행하게 된다.
- Git 코드 Merge
- GItlab CI/CD Build -> Jenkins Build Job -> ECR Image Push
- Gitlab CI/CD Deploy -> Jenkins Deploy Job -> ArgoCD Sync -> EKS 배포
맺음말
짧지만, 두 개의 연재 포스팅을 통해 Amazon EKS와 ECR, Git, Jenkins, ArgoCD를 이용하여 CI/CD 파이프라인을 함께 생성해 보며 많은 예제와 놓칠 수 있는 부분들, 유의해야 할 부분들에 대해 공유하였다. 필자 또한 K8s나 EKS의 문외한으로 시작하여 수많은 시행착오와 더불어 성장할 수 있었던 소중한 시간과 기회였다. 이 경험들을 공유할 수 있게 되어 기쁘게 생각한다.
이어지는 마지막 글에서는 앞선 글에서 미처 담지 못한 각종 팁들을 공유하며 끝마치도록 할 예정이다.
Ref
- https://docs.aws.amazon.com/eks/latest/userguide/install-kubectl.html
- https://docs.aws.amazon.com/ko\_kr/eks/latest/userguide/getting-started.html
- https://github.com/GoogleContainerTools/kaniko
- https://s-core.co.kr/insight/view/kaniko%EB%A1%9C-docker-%EC%97%86%EC%9D%B4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0/