ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Github Actions, CodeDeploy로 Spring Application CI/CD 구축하기
    Project/우아한테크캠프 - 든든킷 2024. 1. 10. 13:57
    우아한테크캠프 6기를 진행하며 최종 프로젝트를 위해 CI/CD를 구축하며 정리한 문서입니다.

    LoadBalancer를 사용하지 않고 단일 EC2에 배포할 예정이라면 EC2, S3, CodeDeploy 설정, GitHub 설정 항목만 진행하셔도 됩니다. 이 경우 CodeDeploy 설정 - 배포 그룹 생성 시 로드 밸런싱 활성화 옵션을 Off 해주시면 됩니다.

    Actions + CodeDeploy

    Github Actions는 GitHub에서 제공하는 서비스로, 레포지토리마다 부여된 환경에서 특정 이벤트의 발생에 따라 작업을 실행하거나, 주기적으로 특정 작업을 실행할 수 있는 서비스입니다.

     

    이번 실습에서는 EC2 인스턴스는 private 서브넷에 두어 외부에서 접근할 수 없게 막고, public 서브넷에 로드밸런서를 두어 private 서브넷의 EC2 인스턴스들에게 요청을 분배하도록 설계하였습니다.


    Github Actions는 AWS서비스가 아니라 AWS 내 private한 인스턴스에 접근할 수 없기 때문에, AWS 내 배포를 지원하는 서비스인 CodeDeploy를 함께 사용해 CD 워크플로우를 구축해 보겠습니다.

    AWS 설정

    VPC

    VPC는 AWS 내에서 이용 가능한 사설 가상 네트워크입니다.
    VPC 서비스로 이동 후, `VPC 생성` 버튼을 클릭해 다음 절차를 따라 VPC를 생성합니다.

    • 생성할 리소스 VPC 등으로 선택해 VPC 생성 시점에 서브넷과 라우팅 테이블, 인터넷 게이트웨이를 한꺼번에 생성합니다.
    • 로드밸런서 생성 시 2개 이상의 AZ(Availability Zone, 가용 영역)를 요구하기 때문에, 가용영역 수2개로 설정하겠습니다.
    • NAT 게이트웨이1개 AZ에 위치하도록 하였습니다.
    • 퍼블릭 서브넷 수프라이빗 서브넷 수 모두 2로 설정하였습니다.
    • VPC 엔드포인트S3 게이트웨이로 설정해 CodeDeploy로 배포 시 S3에 접근할 수 있도록 했습니다.

    VPC 생성 후 결과는 다음과 같습니다.

    VPC 생성 결과

    S3

    애플리케이션 코드와 빌드 파일을 업로드하기 위한 S3를 준비해 줍니다.
    S3-버킷 만들기 메뉴에서 버킷 이름과 리전을 설정하고, 나머지 설정은 기본값으로 생성합니다.

    S3 생성

    EC2

    EC2 인스턴스 생성

    다음 설정으로 Spring Application을 배포할 EC2 인스턴스를 생성해 줍니다.

    실습을 위해 프리티어 인스턴스로 진행하였으며, 실제 배포 상황에 맞게 설정해 주시면 됩니다.

    • t2.micro Free tier
    • Ubuntu 20.04 LTS(x86 64bit)
    • 20GB Storage
    • 네트워크 설정에서 VPC를 위에서 생성한 VPC로 설정
    • 서브넷은 private subnet 중 하나로 설정
    • 퍼블릭 IP 자동할당 비활성화
    • 이 외 설정은 기본으로 설정

    IAM 권한 설정

    IAM(Identity and Access Management)는 AWS 리소스에 대한 엑세스를 안전하게 제어할 수 있도록 계정, 권한을 설정하는 서비스입니다.

    EC2에서 CodeDeploy 및, S3 저장소에 접근할 수 있도록 Role을 부여해 주어야 합니다.

    • 우측 상단 닉네임이 표시된 드롭다운 버튼을 클릭해 보안 자격 증명을 클릭합니다.
    • 좌측 역할 탭에서 역할 생성을 클릭합니다.
    • 엔터티 유형AWS 서비스, 사용 사례EC2를 선택해 줍니다.
    • 다음 정책을 추가하여 역할을 생성합니다. AmazonEC2RoleforAWSCodeDeploy

    생성 결과는 다음과 같습니다.


    생성한 인스턴스를 선택해서 작업 - 보안 - IAM 역할 수정을 클릭해 역할 수정 페이지에 접근합니다.

     

    이후 생성한 IAM 역할을 EC2에 부여합니다.


    역할을 부여한 이후에는 재부팅을 해 주어야 하기 때문에, 인스턴스 중지 - 인스턴스 시작을 차례로 실행해 인스턴스를 재부팅해 줍니다.

    CodeDeploy Agent 설치

    이제 CodeDeploy Agent를 설치할 차례입니다. 다음 절차를 따라 CodeDeploy Agent를 설치합니다.

    # apt-get update
    sudo apt-get update
    
    # ruby 설치
    sudo apt-get install ruby
    
    # wget 설치
    sudo apt-get install wget
    
    # wget 이용해 한국 리전 Codedeploy Agent 설치
    wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
    
    # 실행 권한 부여
    chmod +x ./install
    
    # agent 설치
    sudo ./install auto

     

    이후 다음 명령어들을 이용해 Agent를 관리합니다.

    # agent 상태 체크
    sudo service codedeploy-agent status
    
    # agent 시작
    sudo service codedeploy-agent start
    
    # agent 재시작
    sudo service codedeploy-agent restart

    Java 설치

    Spring Application 실행을 위한 Java를 설치해 줍니다. 이번 실습에서는 Amazon Corretto 17 JDK를 설치해 보겠습니다.

    # Corretto Apt 레포지터리를 추가합니다.
    wget -O- https://apt.corretto.aws/corretto.key | sudo apt-key add - 
    sudo add-apt-repository 'deb https://apt.corretto.aws stable main'
    
    # apt-get update
    sudo apt-get update
    
    # jdk 설치
    sudo apt-get install -y java-17-amazon-corretto-jdk


    이후 Java가 잘 설치되었는지 버전을 체크해 봅시다.

    java -version

    Load Balancer

    로드밸런서는 다중 애플리케이션 서버 환경에서 단일 경로로 들어오는 요청을 각각의 애플리케이션 서버로 요청을 분산하는 역할을 합니다. 하나의 서버에 장애가 발생하더라도 나머지 서버가 요청을 처리할 수 있어 가용성을 확보할 수 있습니다.

    Target Group 생성

    로드밸런서가 요청을 분배할 target instance들의 그룹입니다. 대상 그룹 메뉴에서 create target group 버튼을 클릭해 생성합니다.

    • target typeInstances로 선택해 EC2 인스턴스를 선택할 수 있게 합니다.
    • 프로토콜, 포트HTTP:8080으로 설정하였습니다. Spring에서 설정한 포트 번호에 맞게 설정하면 됩니다.
    • 프로토콜 버전은 상황에 맞게 설정합니다. 저는 HTTP1로 설정하였습니다.
    • 상태 검사 경로는 Spring Application이 항상 200 OK Status로 응답할 수 있는 엔드포인트를 제공합니다. Spring Actuator를 이용해 health check용 엔드포인트를 만들 수도 있습니다. 상태 검사 경로는 타겟 그룹 생성 이후에 다시 설정할 수 있습니다.

    Load Balancer 생성

    EC2 왼쪽 메뉴 로드밸런서에서 Create load balancer버튼을 클릭해 로드밸런서를 생성합니다. 저는 L7에서 동작하는 ALB(Application Load Balancer)를 이용하였습니다.

    • 체계는 로드밸런서가 퍼블릭 인터넷으로부터 오는 리퀘스트를 처리할지, 아니면 내부 네트워크에서만 private하게 사용할지 선택하는 옵션입니다. 저는 인터넷 경계로 설정하여 인터넷에서 오는 요청을 처리할 수 있도록 했습니다.
    • IP 주소 유형IPv4로 설정하였습니다.
    • VPC는 위에서 설정한 VPC를 이용합니다.

    • 네트워크 매핑은 로드밸런서가 트래픽을 라우팅할 서브넷을 설정하는 옵션입니다. 반드시 2개 이상의 서로 다른 Availability Zone과, 각 AZ당 하나 이상의 Subnet을 설정해야 합니다. 각 AZ의 Public Subnet을 설정해 두었습니다.

    • 리스너 및 라우팅에서는 HTTP:80으로부터 들어오는 요청을 위에서 생성한 타겟 그룹이 처리하도록 매핑했습니다. 
    • HTTPS 프로토콜을 이용하기 위해서는 SSL/TLS 인증서를 설정해야합니다.

    CodeDeploy

    CodeDeploy는 AWS 환경 내에 서비스를 배포하는 것을 돕습니다. S3에 소스 코드 혹은 빌드된 애플리케이션을 업로드하고 이후 EC2 혹은 Lambda 등의 컴퓨팅 자원에 배포하는 흐름으로 진행됩니다.

    IAM 역할 생성

    CodeDeploy에서 배포를 위해 필요한 Role을 부여해 주어야 합니다.
    보안 자격 증명 - 역할 메뉴에서 역할 만들기 버튼을 클릭해 역할을 생성합니다.

    • 신뢰할 수 있는 엔터티 유형을 AWS 서비스로 설정합니다.
    • 사용 사례를 CodeDeploy로 설정합니다.
    • 이후 다른 설정은 그대로 두고, 역할 이름을 설정하고 설정을 마무리합니다.

    애플리케이션 생성

    CodeDeploy는 Application 단위로 서비스를 배포하고 관리합니다.
    CodeDeploy 서비스에서 애플리케이션 생성 버튼을 클릭해 애플리케이션을 생성합니다.

    • 애플리케이션 이름을 설정합니다. 이 설정값은 이후 Actions 스크립트 작성 시 사용하게 됩니다.
    • 컴퓨팅 플랫폼EC2/온프레미스를 선택합니다.

    배포 그룹 생성

    애플리케이션을 선택하고 배포 그룹 생성 버튼을 클릭해 배포 대상 그룹을 생성합니다.

    • 배포 그룹 이름을 설정합니다. 배포 그룹 이름은 추후 Actions스크립트 작성 시 사용됩니다.
    • 서비스 역할은 위에서 생성한 CodeDeploy용 역할을 선택합니다.

    • - 배포 유형을 설정합니다. Autoscaling을 사용하지 않고, 인스턴스가 하나이기 때문에 무중단 배포를 포기하고 현재 위치를 사용하겠습니다. 
    • - 현재 위치 옵션을 사용하면 현재 동작 중인 EC2 인스턴스를 잠시 멈추고 업데이트를 진행합니다.
    • - 블루/그린 옵션은 blue/green 방식으로 무중단 배포를 할 수 있는 옵션입니다. AutoScaling을 사용하거나, green fleet에 미리 EC2를 설정해 두면 사용할 수 있습니다.
    • - 태그 그룹은 EC2에 설정된 키/값을 설정해 줍니다. 태그가 일치하는 모든 인스턴스에 배포를 시도합니다.

    • 배포 설정 옵션은 CodeDeployDefault.AllAtOnce로 설정했습니다.
      • AllAtOnce : 한번에 모든 인스턴스들에 배포를 시도합니다.
      • HalfAtATime : 50%의 인스턴스는 항상 동작 중인 상태로 조금씩 배포합니다.
      • OneAtATime : 한번에 하나씩의 인스턴스들에게 배포합니다.
    • 로드 밸런싱 활성화 옵션을 켜 배포 중인 인스턴스에게 트래픽이 전달되지 않도록 합니다.
    • 대상 그룹 선택 옵션에서 로드밸런서 대상 그룹을 설정합니다.

    GitHub Actions

    IAM for Actions

    GitHub Actions에서 Codedeploy 배포 프로세스를 invoke하기 위해 AWS에 접근할 수 있는 계정을 설정해 주겠습니다.
    루트 계정의 액세스 키를 사용해도 동작하지만, 보안상 새로운 계정을 생성하고, 해당 계정의 액세스 키를 사용하겠습니다.

    • 우측 상단 닉네임 드롭다운 버튼을 클릭해 보안 자격 증명 버튼을 클릭합니다.
    • 좌측 액세스 관리 메뉴에서 사용자 메뉴에 접근해 사용자 추가 버튼을 클릭해 사용자 생성을 시작합니다.
    • 사용자 이름을 입력합니다
    • 권한 설정 단계에서 직접 정책 연결을 선택하고 다음 두 정책을 검색해서 추가합니다.
      • AmazonS3FullAccess
      • AWSCodeDeployFullAccess

    생성 결과는 다음과 같습니다.

    • 이후 생성이 완료된 사용자의 보안 자격 증명 탭에서 액세스 키를 추가합니다. 생성된 액세스 키는 조심히 보관해 주세요. (한번 발급받으면 다시 참조할 수 없습니다)

    • 생성된 key는 repository의 Settings - Secrets and variables - Actions 에서 New repository secret버튼을 눌러 각 secret key들을 생성해 줍니다. 생성한 secret key는 이후 Actions yml 스크립트 작성 시 사용될 예정입니다.

    스크립트 작성

    deploy workflow

    .github/workflows/ 디렉터리 하위에  .yml 파일을 위치시키면 Actions가 yml 스크립트를 인식하여 설정에 따라 Actions 워크플로우가 동작하게 됩니다.

    아래 스크립트는 develop 브랜치에 push 이벤트가 발생하면 배포 과정을 실행하는 스크립트입니다.
    env 항목들을 적절히 수정하여 사용하면 됩니다.

    # .github/workflows/develop-deploy.yml
    
    name: Java CD with Gradle for Develop
    
    on:
      push:
        branches: [ "develop" ]
    
    env:
      AWS_REGION: <AWS 리전>
      S3_BUCKET_NAME: <S3 버킷 이름>
      CODE_DEPLOY_APPLICATION_NAME: <배포 애플리케이션 이름>
      CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: <배포 그룹 이름>
    
    jobs:
      deploy:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v3
          
          - name: Set up JDK 17
            uses: actions/setup-java@v3
            with:
              java-version: '17'
              distribution: 'corretto'
    
          - name: Build with Gradle
            uses: gradle/gradle-build-action@v2
            with:
              arguments: clean build -x test
    
          - name: Configure AWS credentials
            uses: aws-actions/configure-aws-credentials@v1
            with:
              aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
              aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
              aws-region: ${{ env.AWS_REGION }}
    
          - name: Upload to AWS S3
            run: |
              aws deploy push \
                --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
                --ignore-hidden-files \
                --s3-location s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip \
                --source .
    
          - name: Deploy to AWS EC2 from S3
            run: |
              aws deploy create-deployment \
                --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
                --deployment-config-name CodeDeployDefault.AllAtOnce \
                --deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
                --s3-location bucket=$S3_BUCKET_NAME,key=$GITHUB_SHA.zip,bundleType=zip
    • actions/checkout@v3 : Actions 환경에 코드를 가져오는 기능을 제공합니다.
    • JDK를 17 Corretto로 설정하였습니다. version, distribution은 프로젝트 상황에 맞게 설정해 주시면 됩니다.
    • gradle/gradle-build-action@v2 : gradle을 이용해 빌드하는 기능을 제공합니다. `-x test`옵션을 주어 항상 테스트를 실행하도록 설정했습니다.
    • Actions Secrets에 등록한 ACCESS KEY를 이용해 AWS credentials를 설정합니다.
    • 이후 S3에 빌드된 프로젝트 폴더 전체를 업로드합니다.
    • 업로드가 완료되면 CodeDeploy에 다음 과정을 위임합니다.

    start.sh, stop.sh

    CodeDeploy가 S3에 업로드 되어있던 코드를 EC2로 옮기는 작업이 완료된 후에 Java Spring Application을 실행, 중단하기 위한 스크립트가 필요합니다. 저는 프로젝트 루트에 `/script` 디렉터리를 만들고 `start.sh`, `stop.sh`를 작성하였습니다.


    start.sh

    #!/usr/bin/env bash
    
    PROJECT_ROOT="/home/ubuntu/app"
    JAR_FILE="$PROJECT_ROOT/build/libs/<프로젝트 jar 파일 이름>.jar"
    
    APP_LOG="$PROJECT_ROOT/application.log"
    ERROR_LOG="$PROJECT_ROOT/error.log"
    DEPLOY_LOG="$PROJECT_ROOT/deploy.log"
    
    TIME_NOW=$(date +%c)
    
    echo "[ $TIME_NOW ] Copy file $JAR_FILE to project root" >> $DEPLOY_LOG
    cp $PROJECT_ROOT/build/libs/*.jar $JAR_FILE
    
    echo "[ $TIME_NOW ] Run java application : $JAR_FILE" >> $DEPLOY_LOG
    nohup java -jar $JAR_FILE > $APP_LOG 2> $ERROR_LOG &
    
    CURRENT_PID=$(pgrep -f $JAR_FILE)
    echo "[ $TIME_NOW ] Application running PID : $CURRENT_PID" >> $DEPLOY_LOG

     

    stop.sh

    #!/usr/bin/env bash
    
    PROJECT_ROOT="/home/ubuntu/app"
    JAR_FILE="$PROJECT_ROOT/build/libs/<프로젝트 jar 파일 이름>.jar"
    
    DEPLOY_LOG="$PROJECT_ROOT/deploy.log"
    
    TIME_NOW=$(date +%c)
    
    CURRENT_PID=$(pgrep -f $JAR_FILE)
    
    if [ -z $CURRENT_PID ]; then
      echo "[ $TIME_NOW ] Running application not found." >> $DEPLOY_LOG
    else
      echo "$[ TIME_NOW ] Terminate application PID : $CURRENT_PID" >> $DEPLOY_LOG
      kill -15 $CURRENT_PID
    fi

    appspec.yml

    프로젝트 루트에 위에서 작성한 start.sh, stop.sh를 어느 시점에 실행할지에 대한 정보를 담은 appspec.yml 파일을 작성해야 합니다.

    version: 0.0
    os: linux
    
    files:
      - source:  /
        destination: /home/ubuntu/app
        overwrite: yes
    
    permissions:
      - object: /
        pattern: "**"
        owner: ubuntu
        group: ubuntu
    
    hooks:
      AfterInstall:
        - location: scripts/stop.sh
          timeout: 60
          runas: ubuntu
      ApplicationStart:
        - location: scripts/start.sh
          timeout: 60
          runas: ubuntu

    마무리

    배포 성공

    이제 모든 설정이 끝났습니다. 길고 복잡한 과정을 따라오시느라 고생 많으셨습니다.

    GitHub에 푸시해서 배포가 정상적으로 되는지 확인해 봅시다!

    Github Actions 동작 로그

     

    CodeDeploy 배포 로그

    TroubleShooting

    Q. AllowTraffic에서 배포가 더이상 진행되지 않습니다.
    A. 이 경우 대상 그룹(Target Group) 메뉴에서 Health Check가 성공하는지 확인해 봅시다. Health Check 설정을 다시 확인해 보고, 스프링 서버에서 Health Check Path에 대해 올바른 응답코드로 응답하는지 확인해 봅시다. Spring Actuator를 이용하면 Health Check용 엔드포인트를 쉽게 만들 수 있습니다.

    Q. BlockTraffic이 너무 느려요.
    A. 대상 그룹에서 배포에 사용하는 로드밸런서 Target Group을 선택하고, 속성 탭에서 대상 등록 해제 관리 옵션을 낮추면 BlockTraffic 단계에서 대상 등록 해제 지연시간(Deregistration Delay)을 짧게 설정해 줍니다. 이 옵션은 트래픽을 차단한 후에도 연결이 남아있는 경우, 연결이 자연스럽게 끊기도록 기다려주는 Delay입니다. 이 시간을 조절하면 BlockTraffic 단계의 시간을 빠르게 넘어갈 수 있습니다. 하지만 너무 짧게 설정하면 남은 커넥션이 강제로 종료되니 적절한 값을 찾아 튜닝하면 좋을 것 같습니다.

    Q. AllowTraffic이 너무 느려요.
    A. Health Check가 오래 걸리기 때문입니다. 대상이 Healthy한지 검증할 때 다음 옵션이 적용된 상태라면, 30초 간격의 5번의 Health Check를 성공해야하기 때문에, 150초 이상이 걸리는 것을 알 수 있습니다. 정상 임계값(Healthy threshhold)간격(interval)를 적절히 튜닝하여 속도를 개선할 수 있습니다.


    Q. GitHub에서는 문제가 없는데, Pending에서 배포가 멈춥니다.
    A. EC2에 IAM권한 설정을 나중에 한 경우, CodeDeploy Agent가 권한이 없는 상태로 동작하고 있을 수도 있습니다. EC2 인스턴스를 종료하고 다시 시작한 후, CodeDeploy Agent도 restart 해 보세요!

Designed by Tistory.