AWS EKS에서 CodePipeline을 활용한 스프링부트 서비스 배포
이번 글에서는 AWS EKS에 CodePipeline(CodeCommit & CodeBuild)을 활용하여 스프링부트 서비스를 배포해보도록 하겠다.
이 글의 순서는 다음과 같다.
1. AWS CodeCommit 생성
2. AWS CodeBuild 생성
3. AWS CodePipeline 생성
4. 스프링부트 서비스 배포
추가로 실습하기 전에 AWS EKS가 이미 구축되어 있어야 한다. AWS EKS는 퍼블릭 서브넷과 프라이빗 서브넷을 각각 2개씩 가진 VPC위에서 동작하며, EC2 인스턴스는 프라이빗 서브넷에서만 구동되게끔 셋팅되어 있어야 한다(AWS EKS를 활용한 쿠버네티스 클러스터 구축 참고)
1. AWS CodeCommit 생성
우선 AWS CodeCommit을 생성해보자.
CodeCommit을 생성하는 방법은 해당 글을 참고하면 된다
(AWS CodeCommit으로 소스코드 관리하기 참고)
2. AWS CodeBuild 생성
다음으로 AWS CodeBuild를 생성해보자.
[AWS Management Console] -> [Codebuild] -> [빌드 프로젝트 생성]을 선택한 후,
프로젝트 구성의 [프로젝트 이름]에 원하는 Codebuild 프로젝트명을 입력한다.
다음으로 [소스 공급자]는 CodeCommit, 리포지토리는 조금 전 생성한 CodeCommit 레파지토리를 입력한다. 브랜치는 빌드를 진행할 브랜치를 선택한다. 이 글에서는 master를 선택한다.
다음으로 [환경]에 대한 정보는 아래와 같이 입력한다. 이 Codebuild에서는 도커 빌드를 진행할 것이기 때문에, [도커 이미지를 빌드하거나 빌드의 권한을 승격하려면 이 플래그를 활성화 합니다.]를 체크한다. [서비스 역할]은 새 서비스 역할을 선택한다.
그 다음 [추가 구성]을 선택한 후, [VPC]는 AWS EKS가 적용된 VPC를 선택한다. 그러면 [서브넷] 정보를 입력할 수 있는데, 지난 AWS EKS 구축 시 EC2 인스턴스는 프라이빗 서브넷에서만 구동되게끔 설정해 놨기 때문에, CodeBuild의 서브넷도 프라이빗 서브넷만 선택한다. [보안그룹]은 선택한 VPC의 default 보안그룹을 선택한다. 설정이 완료되었으면 [VPC 설정 검증]을 클릭하여 인터넷에 연결되었는지 확인한다.
다음으로 [환경 변수]에서는 CodeBuild가 빌드 시 참고하는 buildspec.yml에서 사용할 수 있는 환경변수를 등록한다.
[Buildspec]에서는 Codebuild가 빌드 시 빌드Step을 참조할 스크립트 파일을 기술한다. 필자는 buildspec.yml이라고 작성했다. 이렇게 작성하고 [빌드 프로젝트 생성]을 클릭해서 빌드 프로젝트를 만든다.
3. AWS CodePipeline 생성
지금까지 생성한 CodeCommit과 CodeBuild를 활용하여 CodePipeline을 생성해보자.
우선 [AWS Management Console] -> [CodePipeline] -> [파이프라인 생성] 선택한다.
[파이프라인 이름]에는 원하는 이름을 넣고 [다음을 선택한다]
그 다음 [소스 스테이지] 페이지에서는 이전에 생성했던 AWS CodeCommit 정보를 입력해준다.
그 다음 [빌드 스테이지] 페이지에서는 이전에 생성했던 AWS CodeBuild 정보를 입력해준다.
그 다음 [배포 스테이지]는 건너뛰고 파이프라인을 생성하자.
4. 스프링부트 서비스 배포
다음으로 스프링부트 서비스를 배포해보자.
AWS CodeCommit 레파지토리에 스프링부트 소스가 존재한다는 가정하에 실습을 진행하도록 하겠다.
우선 스프링부트 소스를 쿠버네티스 클러스터에 배포하기 위해서는 4가지 파일이 필요하다
1. Dockerfile
2. Deployment.yaml
3. Service.yaml
4. buildspec.yml
우선 Dockerfile을 작성해보자. Dockerfile은 Gradle build에 의해서 생성된 jar파일을 도커이미지로 만들어 주는 역할을 한다.
Dockerfile :
FROM openjdk:11.0-jdk
VOLUME /tmp
ADD ./build/libs/cafe-svc-0.0.1-SNAPSHOT.jar app.jar
ENV JAVA_OPTS=""
ENTRYPOINT ["java","-jar","/app.jar"]
다음으로 deployment.yaml파일을 작성해보자. 이 글에서는 deployment.yaml파일을 root 디렉토리 밑에 k8s 디렉토리를 만든 후 그 아래에 작성해 놨다. deployment.yaml파일은 도커 레파지토리에 올라간 도커 이미지를 받아온 후 쿠버네티스 클러스터에 Deployment로 올려주는 역할을 한다. 디플로이먼트에 대한 설명은 다음 글을 참고하자(쿠버네티스 시작하기(3) - 쿠버네티스 구성요소(2/2)).
./k8s/deployment.yaml :
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloud-l3-project-deployment
spec:
replicas: 1
selector:
matchLabels:
app: cloud-l3-project
template:
metadata:
labels:
app: cloud-l3-project
spec:
containers:
- name: cloud-l3-project
image: AWS_ECR_URI #buildspec.yml에서 ECR의 URI:TAG 로 치환해줌
ports:
- containerPort: 8080
imagePullPolicy: Always
env:
- name: DATE
value: 'DATE_STRING'
다음으로 Service.yaml을 작성해보자. Service.yaml도 deployment.yaml과 마찬가지로 k8s 디렉토리 밑에 작성하였다. Service.yaml은 쿠버네티스 클러스터에 서비스를 등록해주는 기능을 하며, 서비스를 통해서 pod와의 통신이 가능하다. type은 외부에서 접속할 수 있게 LoadBalancer로 작성했다. 자세한 설명은 다음 글을 참고하자(쿠버네티스 시작하기(2) - 쿠버네티스 구성요소(1/2))
./k8s/service.yaml :
apiVersion: v1
kind: Service
metadata:
name: cloud-l3-service1
spec:
ports:
- name: "8080"
port: 8081
targetPort: 8080
selector:
app: cloud-l3-project
type: LoadBalancer
# type: ClusterIP
마지막으로 프로젝트 root 디렉토리에 buildspec.yml파일을 만들어보자. buildspec.yml 파일은 조금 전 CodeBuild 프로젝트 생성 시 [BuildSpec] 페이지에서 입력했던 파일이며, Codebuild로 빌드 수행 시 빌드STEP을 참조하는 스크립트 파일이다. buildspec.yml에 명시된 STEP을 통해 CodeBuild가 실행되는 서버에 kubectl을 설치하고, kubeconfig를 생성하여 AWS EKS 클러스터와 동기화한다. 또한 Dockerfile을 활용한 도커라이징을 한 후 도커 레파지토리로 도커 이미지를 업로드하고, 업로드 한 이미지를 내려받아 쿠버네티스 클러스터에 디플로이먼트 및 서비스를 배포하게 된다.
buildspec.yml :
version: 0.2
phases:
install:
runtime-versions:
docker: 18
commands:
- curl -o kubectl https://amazon-eks.s3.us-west-2.amazonaws.com/1.16.12/2020-07-08/bin/linux/amd64/kubectl
- chmod +x ./kubectl
- mv ./kubectl /usr/local/bin/kubectl
- mkdir ~/.kube
- aws eks --region ap-northeast-2 update-kubeconfig --name cloud-l3-eks
pre_build:
commands:
- echo Logging in to Amazon ECR...
- $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION)
build:
commands:
- echo Build Starting on `date`
- echo Building with gradle...
- chmod +x ./gradlew
- ./gradlew build
- echo Building the Docker image...
- docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
- docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
- docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
post_build:
commands:
- AWS_ECR_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
- DATE=`date`
- echo Build completed on $DATE
- sed -i.bak 's#AWS_ECR_URI#'"$AWS_ECR_URI"'#' ./k8s/deployment.yaml
- sed -i.bak 's#DATE_STRING#'"$DATE"'#' ./k8s/deployment.yaml
- kubectl apply -f ./k8s/deployment.yaml
- kubectl apply -f ./k8s/service.yaml
위 코드에 대해 간략히 설명하자면
- install.commands : curl -o kubectl 을 활용하여 Codebuild가 수행되는 서버에 kubectl을 설치하고, chmod로 kubectl의 실행권한을 부여한 후 bin 디렉토리로 이동시킨다. 그 다음 kubeconfig 파일이 생성될 .kube 디렉토리를 만든 후, aws eks 명령어를 활용하여 .kube 디렉토리 밑에 kubeconfig 파일을 생성한다.
- pre_build.commands : 도커 이미지파일을 업로드하고 다운받을 수 있는 AWS ECR에 로그인한다.
- build.commands : gradlew을 활용하여 스프링부트 서비스를 빌드한 후, docker build 명령어를 활용하여 도커라이징(도커 이미지 만들기)을 한 후 docker push 명령어로 AWS ECR로 도커 이미지를 업로드한다.
- post_build.commands : kubectl 명령어를 활용하여 deployment.yaml 과 service.yaml 에 정의된 deployment와 service를 쿠버네티스 클러스터에 배포한다. deployment 배포 시에는 AWS ECR에 업로드된 도커 이미지를 내려받아서 쿠버네티스 클러스터에 배포하게 된다.
모든 파일이 생성되었으면 소스를 AWS CodeCommit 레파지토리에 PUSH해보도록 하자.
AWS CodeCommit 레파지토리에 소스가 PUSH되면 자동적으로 쿠버네티스 클러스터에 서비스가 배포되게 된다(이 글에서는 git-bash를 화용해서 소스를 push 시켰다). PUSH할 때 ssh-keygen을 통해 생성했던 RSA 키페어 비밀번호를 입력해서 전송하자.
$ cd [서비스 루트 디렉토리로 이동]
$ git add .
$ git commit -m "[주석]"
$ git push origin master
Enter passphrase for key '/c/Users/minkyu/.ssh/codecommit.pem':
Counting objects: 5, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 462 bytes | 0 bytes/s, done.
Total 5 (delta 4), reused 0 (delta 0)
To ssh://git-codecommit.ap-northeast-2.amazonaws.com/v1/repos/cloud-l3-repository
b883c1c..f77f855 master -> master
PUSH를 완료한 후 CodePipeline을 보면 CodeCommit은 성공했지만, CodeBuild가 실패한 것을 확인할 수 있다.
An error occurred (AccessDeniedException) when calling the DescribeCluster operation: User: arn:aws:sts::003144000000:assumed-role/codebuild-cloud-l3-build-service-role/AWSCodeBuild-6890217d-049f-410e-b2ee-7c8f26a980bc is not authorized to perform: eks:DescribeCluster on resource: arn:aws:eks:ap-northeast-2:003144000000:cluster/cloud-l3-eks
[Container] 2020/08/21 17:25:24 Command did not exit successfully aws eks --region ap-northeast-2 update-kubeconfig --name cloud-l3-eks exit status 255
[Container] 2020/08/21 17:25:24 Phase complete: INSTALL State: FAILED
에러메시지를 잘 읽어보면, aws eks 명령어로 kubeconfig 파일을 생성하다가 에러가 발생했고, CodeBuild 의 역할(role)이 eks:DescribeCluster를 수행하기에 권한이 불충분한 것으로 보인다.
그러면 eks:DescribeCluster 권한을 주도록 하자.
[AWS Management Console] -> [IAM] -> [역할]로 이동한 후, CodeBuild에 부여했던 역할을 선택하자. 그리고 [인라인 정책 추가]를 선택한다.
그 다음 [서비스]는 EKS, [작업]은 읽기를 선택한 후 DescribeCluster, [리소스]는 모든 리소스를 선택하고 [정책 검토]->[정책 생성]을 하자.
정책 생성이 완료되었으면 CodePipeline으로 이동해서 [변경사항 릴리즈]를 선택하여 다시 배포해보자.
이번에도 에러가 발생해서 배포에 실패하는 것을 확인할 수 있다.
[Container] 2020/08/21 17:47:44 Running command kubectl apply -f ./k8s/deployment.yaml
error: unable to recognize "./k8s/deployment.yaml": Unauthorized
[Container] 2020/08/21 17:47:45 Command did not exit successfully kubectl apply -f ./k8s/deployment.yaml exit status 1
[Container] 2020/08/21 17:47:45 Phase complete: POST_BUILD State: FAILED
조금 전 발생했던 eks:DescribeCluster는 해결되었지만 또다른 에러가 발생하였다. 에러메시지를 확인해 보면 kubectl apply -f 로 deployment를 반영할 때 권한이 없어서 에러가 난 것으로 보인다. 왜 그럴까?
그 이유는 바로 CodeBuild가 EKS 쿠버네티스 클러스터에 접근 권한이 없기 때문이다. 때문에 EKS 쿠버네티스 클러스터에 CodeBuild가 접근할 수 있게 권한을 넣어줘야 한다.
EKS 쿠버네티스 클러스터에 CodeBuild가 접근할 수 있게 하는 방법은 간단하다. 우선 EKS 쿠버네티스 클러스터의 aws-auth라는 configmap을 추출한 후, 해당 configmap에 Codebuild의 role 값만 추가해주면 된다.
우선 로컬PC의 git-bash에서 쿠버네티스 클러스터에 아래와 같이 명령어를 수행하여 aws-auth라는 configmap을 저장해보자.
$ kubectl get configmaps aws-auth -n kube-system -o yaml > aws-auth.yaml
그리고 aws-auth.yaml을 열어보면 다음과 같다.
aws-auth.yaml :
apiVersion: v1
data:
mapRoles: |
- groups:
- system:bootstrappers
- system:nodes
rolearn: arn:aws:iam::003144000000:role/cloud-l3-worker-node-role
username: system:node:{{EC2PrivateDNSName}}
kind: ConfigMap
metadata:
creationTimestamp: "2020-07-29T14:22:33Z"
name: aws-auth
namespace: kube-system
resourceVersion: "1047"
selfLink: /api/v1/namespaces/kube-system/configmaps/aws-auth
uid: xx5cxxf5-ae27-xxbd-a706-3xa9xxx76eca
aws-auth.yaml의 ConfigMap 윗부분에 아래 내용을 추가해주자(CodeBuild Role 추가). CodeBuild의 Role 이름이 안되면 [CodeBuild] -> [빌드 세부 정보] 탭에서 확인 가능하다.
- rolearn: arn:aws:iam::003144000000:role/[CodeBuild Role 이름]
username: [CodeBuild Role 이름]
groups:
- system:masters
CodeBuild Role 추가 후 aws-auth.yaml :
apiVersion: v1
data:
mapRoles: |
- groups:
- system:bootstrappers
- system:nodes
rolearn: arn:aws:iam::003144222222:role/cloud-l3-worker-node-role
username: system:node:{{EC2PrivateDNSName}}
- rolearn: arn:aws:iam::003144222222:role/codebuild-cloud-l3-build-service-role
username: codebuild-cloud-l3-build-service-role
groups:
- system:masters
kind: ConfigMap
metadata:
creationTimestamp: "2020-07-29T14:22:33Z"
name: aws-auth
namespace: kube-system
resourceVersion: "1047"
selfLink: /api/v1/namespaces/kube-system/configmaps/aws-auth
uid: xx5cxxf5-ae27-xxbd-a706-3xa9xxx76eca
추가가 완료되었으면 해당 configmap을 쿠버네티스 클러스터에 적용시켜주자.
$ kubectl apply -f aws-auth.yaml --force
적용이 완료되었으면 CodePipeline에서 [변경사항 릴리즈] 버튼을 클릭하여 다시 배포해보자.
CodeBuild에서 확인해보면 정상적으로 배포된 것을 확인할 수 있다.
그럼 이제 kubectl로 쿠버네티스 클러스터에 접속하여 스프링부트 서비스가 정상적으로 떠 있는지 확인해보자.
우선 POD가 떠있는지 확인해보니 아래와 같이 정상적으로 떠있는 것을 확인할 수 있다.
$ kubectl get pod --all-namespaces
NAMESPACE NAME READY STATUS RESTARTS AGE
default cloud-l3-project-deployment-84658f486d-qtz7s 1/1 Running 0 13m
kube-system aws-node-77f92 1/1 Running 0 78m
kube-system aws-node-v6rf8 1/1 Running 0 78m
kube-system coredns-7dd7f84d9-47hpz 1/1 Running 0 91m
kube-system coredns-7dd7f84d9-82hnq 1/1 Running 0 91m
kube-system kube-proxy-4f6ld 1/1 Running 0 78m
kube-system kube-proxy-qjg2x 1/1 Running 0 78m
다음으로 서비스가 정상적으로 떠있는지 확인해보니 아래와 같이 Pending 상태인 것을 확인할 수 있다.
$ kubectl get svc --all-namespaces
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
default cloud-l3-service1 LoadBalancer 10.100.135.132 <pending> 8081:32291/TCP 69m
default kubernetes ClusterIP 10.100.0.1 <none> 443/TCP 92m
kube-system kube-dns ClusterIP 10.100.0.10 <none> 53/UDP,53/TCP 92m
서비스의 상세 정보를 보면 ELB(로드밸런서)를 생성하기에 적합한 서브넷이 없다고 나온다. 왜그럴까?
$ kubectl describe svc cloud-l3-service1
Name: cloud-l3-service1
Namespace: default
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"cloud-l3-service1","namespace":"default"},"spec":{"ports":[{"name...
Selector: app=cloud-l3-project
Type: LoadBalancer
IP: 10.100.135.132
Port: 8080 8081/TCP
TargetPort: 8080/TCP
NodePort: 8080 32291/TCP
Endpoints: 192.168.138.93:8080
Session Affinity: None
External Traffic Policy: Cluster
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning SyncLoadBalancerFailed 10m (x19 over 76m) service-controller Error syncing load balancer: failed to ensure load balancer: could not find any suitable subnets for creating the ELB
Normal EnsuringLoadBalancer 56s (x21 over 76m) service-controller Ensuring load balancer
우선 맨 처음에 AWS EKS를 구축할 때 어떻게 구축했는지 생각해보자(AWS EKS를 활용한 쿠버네티스 클러스터 구축 참고).
AWS EKS를 최초로 구성할 때 VPC를 만들고 VPC안에 퍼블릭 서브넷 2개와 프라이빗 서브넷 2개를 만들었다. 그리고 AWS EKS의 ENI(네트워크 인터페이스)를 프라이빗 서브넷 2개에만 배치해놓고, 워커 노드 그룹(EC2 인스턴스)도 프라이빗 서브넷 2개에만 배치해 놓았다. 그렇게 하면 외부에서 워커 노드 그룹으로 접근이 불가능하고 오직 퍼블릭 서브넷에 올라가 있는 로드밸런서를 통해서만 접근할 수 있다(또한 로드밸런서는 퍼블릭 서브넷에만 올라갈 수 있다).
그런데 로드밸런서가 퍼블릭 서브넷에 올라가질 못하고 pending 상태인 것이다.
서브넷에 문제가 있는 것으로 보이니 서브넷 정보를 확인해보자.
[AWS Management Console] -> [VPC] -> [서브넷]으로 들어가서 [태그] 정보를 확인해보자.
AWS EKS의 프라이빗 서브넷의 태그정보를 확인해보면 [kubernetes.io/cluster/cloud-l3-eks] 라는 키에 [shared]라는 값이 셋팅되어 있다. 우리가 지난번 AWS EKS 구축하는 글에서 만든 AWS EKS의 이름이 [cloud-l3-eks]였으며, 이 태그정보의 의미는 쿠버네티스 클러스터가 해당 서브넷을 사용할 수 있다는 의미이다.
Key: kubernetes.io/cluster/[cluster-name]
Value: shared
다음으로 퍼블릭 서브넷을 확인해보자. 퍼블릭 서브넷에는 해당 태그정보가 없다. 그렇기 때문에 쿠버네티스 클러스터가 퍼블릭 서브넷을 사용할 수 없는 것이고, 그래서 로드밸런서가 퍼블릭 서브넷에 올라가질 못하는 것이다.
그러면 [태그 추가/편집]을 통해 2개의 퍼블릭 서브넷에도 프라이빗 서브넷과 동일하게 태그정보를 입력해주자.
추가가 완료되었으면 기존에 올라간 서비스는 삭제한 후 다시 올려보도록 하자.
그러면 EXTERNAL-IP와 포트정보가 표시되는 것을 확인할 수 있다.
$ kubectl delete -f service.yaml
$ kubectl apply -f service.yaml
$ kubectl get svc --all-namespaces
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
default cloud-l3-service1 LoadBalancer 10.100.132.165 adf64d69f7e5247a18bcbc6795d58591-1718063616.ap-northeast-2.elb.amazonaws.com 8081:31834/TCP 4s
default kubernetes ClusterIP 10.100.0.1 <none> 443/TCP 127m
kube-system kube-dns ClusterIP 10.100.0.10 <none> 53/UDP,53/TCP 127m
그러면 이제 서비스에 접속해보자. EXTERNAL-IP와 포트정보를 넣고 접속해보면 서비스가 정상적으로 수행되는 것을 확인할 수 있다.
참고
https://aws.amazon.com/ko/premiumsupport/knowledge-center/eks-vpc-subnet-discovery/
https://aws.amazon.com/ko/premiumsupport/knowledge-center/eks-api-server-unauthorized-error/