본문 바로가기
공부 및 정리/기타 정리

Github Actions를 사용한 꺼진 AWS Instance에도 CI/CD하기

by 스파이펭귄 2024. 7. 23.
728x90

프로젝트 중 AWS의 EC2가 꺼져있는 상태에서 Main에 Push를 하게 된다면 문제가 발생할 것입니다.

 

이를 멘토 성준님께서 말씀주셨고, CI/CD를 하기 전 먼저 Instance에 취할 적절한 작업을 생각해보라 말씀하셨습니다.

이를 위해서는 AWS에서 생성한 인스턴스에 대한 정보(public IP)를 얻고, 조작할 수 있어야 합니다.

https://repost.aws/ko/knowledge-center/start-stop-lambda-eventbridge

 

Lambda 함수를 사용하여 정기적으로 EC2 인스턴스 중지 및 시작

Amazon Elastic Compute Cloud(Amazon EC2) 인스턴스를 자동으로 중지 및 시작하여 Amazon EC2 사용량을 줄이려고 합니다.

repost.aws

 

먼저 다행이게도 위 AWS 블로그를 통하여 boto3를 통한 방식에 대해 알 수 있었습니다.

https://docs.aws.amazon.com/ko_kr/cli/latest/userguide/cli-services-ec2-instances.html

 

Amazon EC2 인스턴스 시작, 나열 및 종료 - AWS Command Line Interface

이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.

docs.aws.amazon.com

 

또한 boto3를 사용하지 않고, AWS CLI를 사용해 Instance의 정보를 얻는 방식도 존재해, 이 중 고민하다 Lambda는 비용이 더 드는 방식이기 때문에 AWS CLI를 Github Actions에서 사용하는 방식으로 선택하였습니다.

이때 우리에게 필요한 Instance에 대한 정보는 tag를 통하여 얻을 수 있습니다. AWS의 tag에 대해서는 추후 정리해보도록 하겠습니다.

간단히 tag에 대해 설명하자면 AWS에서 리소스를 구분하기 위해 사용자가 각 리소스마다 달아주는 key-value 쌍의 꼬리표 같은 것입니다.

 

https://github.com/de3-final-total-lecture/total-lecture-airflow/blob/07af6f81967c7ccc1f92773288702e7a98053a08/.github/workflows/aws_cicd.yml

 

total-lecture-airflow/.github/workflows/aws_cicd.yml at 07af6f81967c7ccc1f92773288702e7a98053a08 · de3-final-total-lecture/tota

강의 통합 서비스 레포지토리. Contribute to de3-final-total-lecture/total-lecture-airflow development by creating an account on GitHub.

github.com

전체 yaml 파일은 위와 같습니다.

 

각 부분에 대해 차근차근 살펴보도록 하겠습니다.

Name & 작동 조건

name: Deploy to EC2

on:
  push:
    branches: [ "main" ]

위 부분은 main 브랜치에 push 될 때마다 이 코드를 실행하겠다는 의미입니다.

 

환경 설정

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Configure AWS CLI
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ vars.AWS_REGION }}

위 코드는 Github Actions를 실행할 환경과 AWS CLI 자격 증명을 통하여 AWS CLI를 구성하는 과정입니다.

Repository secrets에 iam의 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY를 추가한 후 variables에 사용중인 AWS 리전을 추가해준 후 위 처럼 사용할 수 있습니다.

 

steps: Get instance ID by Tag

steps:
    ...
  - name: Get instance ID by Tag
    id: get-instance
    run: |
      BASTION_HOST_ID=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=${{ vars.BASTION_HOST_NAME_TAG }}" --query 'Reservations[0].Instances[0].InstanceId' --output text)
      PRIVATE_AIRFLOW_ID=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=${{ vars.AIRFLOW_NAME_TAG }}" --query 'Reservations[0].Instances[0].InstanceId' --output text)
      echo "Bastion Host ID: $BASTION_HOST_ID"
      echo "PRIVATE AIRFLOW ID: $PRIVATE_AIRFLOW_ID"
      echo "::set-output name=bastion_host_id::$BASTION_HOST_ID"
      echo "::set-output name=private_airflow_id::$PRIVATE_AIRFLOW_ID"

그 후 Tag를 통하여 Instance의 ID를 추출합니다.

aws ec2 describe-instances --filters "Name=tag:Name,Values=${{ vars.BASTION_HOST_NAME_TAG }}" --query 'Reservations[0].Instances[0].InstanceId' 

위 코드가 AWS CLI를 사용하는 부분으로 vars에 저장한 BASTION_HOST_NAME_TAG에 저장된 Bastion Host의 Tag를 사용하여 Instance의 ID를 추출합니다.

이를 echo를 통해 출력하고, output으로 변수로 저장해 추후 step들에서 사용할 수 있게 합니다.

 

steps: Check instance status

steps:
    ...
    - name: Check instance status
    id: check-status
    run: |
      BASTION_HOST_STATUS=$(aws ec2 describe-instances --instance-ids ${{ steps.get-instance.outputs.bastion_host_id }} --query 'Reservations[0].Instances[0].State.Name' --output text)
      PRIVATE_AIRFLOW_STATUS=$(aws ec2 describe-instances --instance-ids ${{ steps.get-instance.outputs.private_airflow_id }} --query 'Reservations[0].Instances[0].State.Name' --output text)
      echo "Bastion Host STATUS: $BASTION_HOST_STATUS"
      echo "PRIVATE AIRFLOW STATUS: $PRIVATE_AIRFLOW_STATUS"
      echo "::set-output name=bastion_host_status::$BASTION_HOST_STATUS"
      echo "::set-output name=private_airflow_status::$PRIVATE_AIRFLOW_STATUS"

이제 Instance ID를 가져왔으므로 해당 인스턴스가 켜져있는지 상태를 확인합니다.

aws ec2 describe-instances --instance-ids ${{ steps.get-instance.outputs.bastion_host_id }} --query 'Reservations[0].Instances[0].State.Name'

여기서 직전 step에서 output으로 넘겨준 Bastion Host와 Private Server의 Instance ID를 ${{ steps.get-instance.outputs.bastion_host_id }}과 같이 가져옵니다. (이 경우는 bastion host의 id)

이 정보를 describe-instances에 넘겨주어 State의 Name을 가져와서 현재 상태를 확인합니다. 상태는 다음과 같습니다.

  • stopped: 중지
  • pending: 실행 준비중
  • running: 실행 중

 

steps: Start Bastion Host & Private Airflow Server if stopped

steps:
  - name: Start Bastion Host if stopped
    if: steps.check-status.outputs.bastion_host_status != 'running'
    run: |
      aws ec2 start-instances --instance-ids ${{ steps.get-instance.outputs.bastion_host_id }}
      aws ec2 wait instance-running --instance-ids ${{ steps.get-instance.outputs.bastion_host_id }}


  - name: Start Private Airflow Server if stopped
    if: steps.check-status.outputs.private_airflow_status != 'running'
    run: |
      aws ec2 start-instances --instance-ids ${{ steps.get-instance.outputs.private_airflow_id }}
      aws ec2 wait instance-running --instance-ids ${{ steps.get-instance.outputs.private_airflow_id }}

start-instances를 통해 Bastion Host와 Private Airflow Sever의 상태가 running이 아닌 경우 실행하게 됩니다.

이후 wait instance-running을 통해 실행 될때까지 대기합니다.

 

steps: Get instance IPs

steps:
    ...
  - name: Get instance IPs
    id: get-instance-ips
    run: |
      BASTION_HOST_IP=$(aws ec2 describe-instances --instance-ids ${{ steps.get-instance.outputs.bastion_host_id }} --query 'Reservations[0].Instances[0].PublicIpAddress' --output text)
      PRIVATE_AIRFLOW_IP=$(aws ec2 describe-instances --instance-ids ${{ steps.get-instance.outputs.private_airflow_id }} --query 'Reservations[0].Instances[0].PrivateIpAddress' --output text)
      echo "Bastion Host IP: $BASTION_HOST_IP"
      echo "PRIVATE AIRFLOW IP: $PRIVATE_AIRFLOW_IP"
      echo "::set-output name=bastion_host_ip::$BASTION_HOST_IP"
      echo "::set-output name=private_airflow_ip::$PRIVATE_AIRFLOW_IP"

실행이 완료되었다면 Public IP가 생기게 됩니다. 그러므로 이전과 마찬가지로 describe-instances를 통해 인스턴스의 정보들로부터 public IP를 가져오고 이전에 Instance ID를 가져온것과 동일하게 다음 step들에서 사용할 수 있도록 output으로 넘겨줍니다.

 

steps: Create SSH key file

steps:
    ...
  - name: Create SSH key file
    run: |
      echo "${{ secrets.PRIVATE_KEY }}" > team_jun_1.pem
      chmod 600 team_jun_1.pem

이제 SSH 연결을 위해 secrets에 저장해둔 private 키 값으로 pem 파일을 생성하고, 권한을 수정해줍니다.

 

steps: Git Pull on Airflow Server

steps:
    ..
  - name: Git Pull on Airflow Server
    run: |
      sleep 10
      ssh -f -N -M -S my-cicd-socket -o StrictHostKeyChecking=no -i team_jun_1.pem -L 2222:${{ steps.get-instance-ips.outputs.private_airflow_ip }}:22 ubuntu@${{ steps.get-instance-ips.outputs.bastion_host_ip}}
      ssh -o StrictHostKeyChecking=no -i team_jun_1.pem -p 2222 ubuntu@localhost << 'EOF'
        echo "Connected to Private Subnet Airflow SAeerver via SSH Tunneling"
        cd ${{ vars.AIRFLOW_DIR }}
        git config --global credential.helper store
        echo "https://${{ secrets.GIT_AUTH_TOKEN }}:@github.com" > ~/.git-credentials
        git pull origin main
        rm ~/.git-credentials
      EOF
      ssh -S my-cicd-socket -O exit ubuntu@${{ steps.get-instance-ips.output.bastion_host_ip }}

먼저 실행한 서버들이 준비가 될 수 있게 10초간 대기합니다.

이후 SSH 터널링을 통해 Bastion Host를 통하여 Private Subnet에 있는 서버에 접근해 git pull 명령을 통해 main 브랜치의 새로운 데이터를 가져오게 하며 전체 과정이 끝납니다.

이때 Private Subnet에 CICD를 해야하기 때문에 Bastion Host를 통한 SSH 터널링을 수행합니다.

Private Subnet에 존재하는 서버의 22번 포트를 로컬 2222 포트로 연결한 후 2222포트로 ssh 연결을 한번 더 합니다.

이후 git pull 을 통하여 main 브랜치의 새로운 데이터를 가져오게 한 후 ssh 연결을 끝냅니다.

 

 

ps. 현재는 여기까지 작성했으나 비용 문제를 생각한다면 이후 켜져있던 상태라면 다시 종료하는 step을 하나 더 만들어도 좋을 것 같습니다.

728x90