시작하며

개발자라면 누구나 CI/CD라는 용어를 들어봤을 것입니다. 저도 AWS 프리티어를 이용해 간단히 CI/CD 파이프라인을 구축해본 경험만 있고, 사내에서는 이미 구축된 시스템을 사용해왔기 때문에 직접적인 경험은 부족했습니다. 이 글에서는 사내에서 신규 프로젝트의 CI/CD 파이프라인을 구축한 경험을 공유하고자 합니다. 저처럼 경험이 부족한 분들에게 간접적인 경험을 통해 조금이나마 도움이 되길 바랍니다.

 

실제 구축하면서 여러 차례 Git Action Failure가 발생하여 구글링을 통해 해결하였으며, 모든 설정 파일을 완벽하게 외우지는 못하지만, 대략적인 그림이 그려진다면 다른 환경에서도 작업이 가능할 것이라 생각됩니다.

먼저 CI/CD란 무엇인가? 

대표적인 CI/CD 툴로는 Jenkins, GitHub Actions, Travis CI 등이 있으며, 필자는 Git Action을 선택하였습니다.

  • Git Action 장점
    • GitHub Actions는 GitHub와 통합되어 있어 별도의 서버 설치가 필요 없습니다.
    • GitHub Actions는 간편한 설정과 강력한 기능을 제공하여 빠르게 CI/CD 파이프라인을 구축할 수 있습니다.

그 외에 Docker 컨테이너 관리를 위해 AWS의 ECR을 선택하였고, 안전한 관리를 위해 Kubernetes 클러스터 서비스인 EKS를 활용하였습니다.

  • ECR의 장점
    • AWS 내에서 Docker 컨테이너 이미지를 저장하고 관리하기 위한 서비스로, AWS의 다른 서비스들과의 통합성이 뛰어납니다.
  • EKS의 장점
    • EKS는 고가용성, 확장성, 보안성 등의 특징을 제공하여 안정적인 운영 환경을 제공합니다.

필자는 위 장점들 때문에 해당 기술스택을 사용하였습니다.

1. Git Secret 설정하기

Git Repository의 Settings에서 GitHub Actions에 필요한 환경 변수를 설정할 수 있습니다. 민감한 정보를 보호하기 위해, 환경 변수는 Secret에 Key-Value 형태로 등록합니다.

  • 환경 변수 등록
    • 시크릿의 이름(Key)과 값을 입력합니다.
    • Secret을 등록후 Value 값을 확인할 수는 없다.

이렇게 등록된 시크릿은 GitHub Actions 워크플로우에서 참조하며, 민감한 정보가 노출되지 않도록 보호됩니다.

 

2. DockerFile 작성

Git Action을 통해 Docker Image를 만들 DockerFile을 작성합니다. 아래는 예제 Dockerfile입니다.

# 빌드 스테이지
FROM adoptopenjdk/openjdk8 AS build

# 애플리케이션 파일 복사
COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .
COPY lib/nsso-agent-15.jar .
COPY src src
COPY lib lib

# 권한 설정 및 빌드
RUN chmod +x ./gradlew
RUN ./gradlew bootJar

# 실행 스테이지
FROM adoptopenjdk/openjdk8:latest
LABEL maintainer="myProject"
VOLUME /tmp

# 환경 변수 설정
ARG SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-dev}
ENV SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-dev}
RUN echo ${SPRING_PROFILES_ACTIVE}

# 빌드된 JAR 파일 복사
COPY --from=build build/libs/*.jar app.jar

# 타임존 설정
ENV TZ=Asia/Seoul
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

# 사용자 및 권한 설정
RUN groupadd -r springboot && useradd -r -g springboot springboot
RUN mkdir -p /logs/sso && chown -R springboot:springboot /logs/sso

# 사용자 변경
USER springboot

# 포트 설정
EXPOSE 17010

# 애플리케이션 실행
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "app.jar"]
  • 실행 스테이지
    • 빌드된 JAR 파일을 실행 환경으로 복사하고, 환경 변수 및 타임존 설정을 수행합니다.
    • 애플리케이션을 실행할 사용자와 권한을 설정한 후, JAR 파일을 실행합니다.

이 Dockerfile을 사용하여 GitHub Actions에서 Docker 이미지를 빌드하고, 이를 Kubernetes 클러스터에 배포할 수 있습니다.

3. Kubernetes File 작성

애플리케이션을 Kubernetes 클러스터에 배포하기 위한 YAML 파일을 작성합니다. 

Deployment 파일 (deployment.yaml)

apiVersion: v1
kind: Namespace
metadata:
  namespace: myProject
  name: myProject
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: myProject
  name: myProject
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myProject
  template:
    metadata:
      labels:
        app: myProject
    spec:
      containers:
        - name: myProject
          image: kustomization-eks-repository
          imagePullPolicy: Always
          ports:
            - containerPort: 17010
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: dev
            - name: AWS_REGION
              value: ap-northeast-2

Kustomization 파일 (kustomization.yaml)

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml
images:
  - name: kustomization-eks-repository

 

Service 파일 (service.yaml)

apiVersion: v1
kind: Service
metadata:
  namespace: myProject
  name: myProject
spec:
  selector:
    app: myProject
  ports:
    - port: 80
      targetPort: 17010
      name: http
  • Namespace
    • Namespace 리소스를 정의하여 myProject라는 네임스페이스를 생성합니다.
  • Deployment
    • 애플리케이션이 실행될 컨테이너 이미지를 지정하고, 환경 변수를 설정합니다.
    • 클러스터 내에서 애플리케이션의 복제본 수(replicas)를 정의합니다.
  • Kustomization
    • deployment.yaml과 service.yaml 파일을 리소스로 포함하고, 이미지 태그를 latest로 설정합니다.
  • Service:
    • service.yaml 파일에서 Service 리소스를 정의하여 클러스터 외부에서 애플리케이션에 접근할 수 있도록 합니다.
    • selector를 사용하여 Deployment와 연결하고, 포트 매핑을 설정합니다.

이렇게 작성된 Kubernetes 파일들을 사용하여, 애플리케이션을 클러스터에 배포하고 관리할 수 있습니다.

4. Git Action 파일작성

├── .github
│   └── workflows
│       └── pipeline-push-dev.yaml
name: DEV - Deploy to Amazon EKS

on:
  push:
    branches: [ dev ]
    paths-ignore:
      - ".github/**"

env:
  AWS_ACCESS_KEY_ID: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}
  EKS_CLUSTER: ${{ secrets.DEV_EKS_CLUSTER_NAME }}
  ECR_REPOSITORY: ${{ secrets.DEV_ECR_REPOSITORY }}
  APP_NAME:  myProject  # Application 이름. Image TAG Prefix로 사용 됨
  AWS_REGION: ap-northeast-2 # AWS EKS & ECR이 위치한 AWS Region
  DEPLOYMENT_NAME:  myProject # Kubernetes Deployment 명
  EKS_NAMESPACE: myProject
  YAML_ENV: kustomize/dev

jobs:
  build:
    name: Build and Push Docker Image
    runs-on: ubuntu-latest

    steps:
      # 소스 가져오기
      - name: Checkout
        uses: actions/checkout@v2

      # AWS credentials 설정
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ env.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ env.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      # AWS ECR 로그인
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      # sha 난수 생성
      - name: Short sha
        run: echo "short_sha=`echo ${{ github.sha }} | cut -c1-8`" >> $GITHUB_ENV

      # Docker 빌드 및 ECR로 Push 진행
      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${{ env.ECR_REPOSITORY }}
          IMAGE_TAG: DEV_${{ env.APP_NAME }}
          SHORT_SHA: ${{ env.short_sha }}
        run: |-
          pwd
          ls -al
          docker build -t ${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}_${SHORT_SHA} -f Dockerfile_DEV .
          docker push ${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}_${SHORT_SHA}
          docker tag ${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}_${SHORT_SHA} ${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}_latest
          docker push ${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}_latest
          echo "::set-output name=image::${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}_${SHORT_SHA}"

  deploy:
    needs: build
    name: Deploy to DEV Environment
    runs-on: ubuntu-latest

    steps:
      # 소스 가져오기
      - name: Checkout
        uses: actions/checkout@v2

      # AWS credentials 설정
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ env.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ env.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      # AWS ECR 로그인
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      # sha 난수 생성
      - name: Short sha
        run: echo "short_sha=`echo ${{ github.sha }} | cut -c1-8`" >> $GITHUB_ENV

      # EKS 배포를 위한 Kubeconfig 설정
      - name: Setup kubeconfig
        id: setup-kubeconfig
        env:
          AWS_REGION: ${{ env.AWS_REGION }}
          EKS_CLUSTER: ${{ env.EKS_CLUSTER }}
        run: |-
          aws eks --region ${AWS_REGION} update-kubeconfig --name ${EKS_CLUSTER}

      # EKS로 배포
      - name: Deploy to DEV Environment
        id: deploy-eks
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${{ env.ECR_REPOSITORY }}
          IMAGE_TAG: DEV_${{ env.APP_NAME }}
          SHORT_SHA: ${{ env.short_sha }}
          EKS_NAMESPACE: ${{ env.EKS_NAMESPACE }}
        run: |-
          cd $YAML_ENV

 

GitHub Actions 파일을 작성하여 CI/CD 파이프라인을 자동화합니다. 필자는 GitHub Actions 템플릿이 아닌 직접 작성한 Git Action 파일을 사용하여 배포를 수행하는 작업을 구성했습니다. dev 브랜치에 푸시될 때 작업이 수행되며, Docker 이미지를 빌드하고 Amazon ECR에 푸시한 후 Kubernetes 클러스터에 배포합니다.

AWS 기초개념

리전

  • 전세계의 지리적으로 분리된 물리적 위치 단위
  • 1개 리전은 최소 2개이상의 가용영역을 보유

리전 선택시 고려사항

  • 지연시간
  • 법률 및 규제 준수
  • 서비스 가용성
  • 비용

가용영역(Availability Zones)

  • 1개 AZ는 완전히 격리된 1개 이상의 데이터 센터(DC) 모음으로 구성된다.
  • 각 AZ는 지진 등 자연재해를 고려하여 물리적으로 떨어진 위치에 존재

 

POP(Point of Presence Location)

  • 최종 사용자에게 빠른 컨텐츠 배포를 위한 캐시 서비스
  • 컨텐츠를 제공시 지연시간 감소를 위한 글로번 CDN인 AWS CloudFront 서비스를 호스팅
  • 엣지 로케이션 : AWS CDN서비스 CloudFront를 위한 캐시 서버들의 모음
  • 리전 엣지 캐시 : 엣지 로케이션에 컨텐츠가 없는경우 리전별 엣지 캐시에서 검색 시도

* CDN : 사용자가 콘텐츠(HTML, 이미지, 동영상, 기타 파일)를 빠르게 받을 수 있도록 전세계 곳곳에 위치한 캐시 서버(Cache Server)로 복제해주는 서비스

 

AWS 제공 컴퓨팅 서비스와 주요특징

EC2

  • AMI기반 가상머신(Iaas) 제공
  • 사용자가 OS 패치등 관리

빈스톡

  • 애플리케이션 작성에만 집중
  • 플랫폼(Paas) 제공

Fargate

  • 컨테이너용 서버리스 컴퓨터 엔진으로 EKS, ECS에 대한 컨테이너 제공

Lambda

  • 서버리스, 코드 실행 시간만 지불(100밀리초 단위, 최대 15분)

라이트 세일(Lightsail)

  • 원클리으로 실행가능한 애플리케이션(Saas)
  • 원클릭으로 시작가능한 개발자 스택(Paas)
  • 원클릭으로 시작가능한 OS(Iaas)
  1. SasS(software as a Service)
    - 구글의  Gmail,MS Office 365,더존 ERP등과 같이 응용 프로그램(Application)을 인터넷 및 웹 브라우저를 통해 제공하는 서비스
  2. PasS(Platform as a Service)
    - 운영체제 및 미들웨어, SW 개발 혹은 데이터 분석을 위한 도구 등 웹 기반 서비스, 애플리케이션 혹은 SW 등의 개발을 위한 표준 플랫폼 환경을 제공하는 서비스
    - AWS Elastic Beanstalk 등
  3. IaaS(Infrastructure as a Service)
    - 물리적 서버(CPU, Memory 등), 네트워크, 스토리지 등을 가상화하여 다수의 고객을 대상으로 유연하게 제공하는 인프라 서비스
    - AWS EC2 등

시작하며

프로젝트 개발 테스트시 발생된 에러와 대처방법을 기록하였다.

인프라 영역을 누군가가 조작하고, 정보교환이 되지않아 발생하였다.

 

프로젝트에서 서버와 서버간의 통신을 할때 RestTemple을 사용하였다. 서버간의 통신구간에 try-catch문으로 예외처리를 한 부분에서 발생된 에러이다.

로그상 403 Forbidden이 발생하였고, 해당 403 에러의 경우 접근권한과 맞지 않는 접근 요청이 왔을 경우 발생하는 에러라고한다.

해당 부분은 대량의 트래픽이 서버로 전송되는 부분이였고, 관련되어 이전에 상용 서버에서 비슷한 경험이 있었다.

 

이전 경험을 토대로 수정하려했지만, AWS를 잘 몰라 이거 저거 만지고 찾아보며 삽질하며 해결한 방법이다.

 

AWS의 WAF & Shield 설정으로 이동

좌측 메뉴의 Web ACLs 선택 및 리전을 선택하면 ACL(Access Control List)가 나온다.

ACL의 Rules에서 문제가 발생하였던것으로 보인다.

 

1. 해결방법

운영설정과 비교해보니, 해당 설정에 특정IP의 대량 트래픽을 허용하는 룰이 삭제되어있어, 

IP sets에서 특정 IP에 대한 설정을 추가하고, 추가한 IP의 트래픽을 허용하는 룰을 만들었다.

 

해당 IP에 대한 트래픽을 허용하였지만, 아직 403 에러가 해결되지는 않았다.

 

이후 조치한 방법으로,

2. 해결방법

A라는 대량의 트래픽을 차단하는 룰 (Action 결과를 Block으로 차단)과,

B라는 특정 IP의 대량 트래픽을 허용하는 룰(Action 결과를 Allow으로 허용) 이 있을경우

 

순서가 적용이 되는것이였다.

 

이경우 대량의 특정 허용된 IP가 전송되었지만 A가 상단부분에 있었기 때문에 403 에러를 내렸던것이였다.

생각해보면 JAVA에서도

if(대량 트래픽 차단){
	return false;
}

if(특정아이피에 대해서는 허용){
	retrun true;
}

해당 설정이라 생각했을 경우 모두 첫번째 if문에서 걸렸을 것이다.

 

그래서 해당 설정 룰의 순서를 수정하니 해결되었다.👊🏻

+ Recent posts