이번 포스팅은 Github Actions를 통한 자동화 구성이다

서버 안에서 코드 수정도 해 보고 Github Actions 몇몇 기능도 사용해 볼 것이다.

1. 직접 개발 후 실행

자동화의 필요성을 알기 위해 먼저 서버에서 직접 github에 코드 반영을 진행해 볼 것이다

Python 3.10.12이 설치된 서버 하나를 띄우고 아래 코드를 실행한다.

cat > server.py <<EOF
*from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from datetime import datetime

class RequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/plain')
        self.end_headers()
        now = datetime.now()
        response_string = now.strftime("The time is %-I:%M:%S %p, CloudNeta Study.\n")
        self.wfile.write(bytes(response_string, "utf-8")) 

def startServer():
    try:
        server = ThreadingHTTPServer(('', 80), RequestHandler)
        print("Listening on " + ":".join(map(str, server.server_address)))
        server.serve_forever()
    except KeyboardInterrupt:
        server.shutdown()

if __name__== "__main__":
    startServer()*
EOF

코드 실행 후 다른 터미널에서 접속해 보았다

image

이제 서버 세팅은 끝났고 git 세팅해야 한다

먼저 Settings > Developer Settings > Personal access tokens > Tokens 에서 토큰을 발급하자

권한은 repo, workflow만 주면 된다

private repo으로 파이썬 .gitignore 파일 생성되게 하나 만들었다

image 1

서버로 돌아와서 직접 깃을 사용해 보자!

우선 매번 자격증명을 물어볼 때마다 입력하기 번거로우니 global 옵션으로 저장한다

git config --global user.name [username]
git config --global user.email [email]
git config --global credential.helper store

git clone https://github.com/$GITUSER/cicd-2w.git으로 소스코드를 땡겨오면 서버에 레포를 가져올 수 있다

image 2

위에서 만든 server.py를 해당 디렉토리로 옮긴 후 레포에 push해 주면 실제 레포에도 올라간 것을 확인할 수 있다.

image 3

image 4

여기서 .gitignore 파일은 git 저장소에 안올라가게 무시할 파일을 명시할 수 있는데 그 중 로그파일이 있다.

image 5

정말 실제로 안올라 가는지 테스트해 보자!

일단 서버 로그를 찍어서 파일을 생성하고

image 6

깃에 올려보면 낫띵투커밋 어쩌고가 뜬다. 올릴게 없다는 뜻!

image 7

위에 띄워둔 서버를 sudo fuser -k -n tcp 80로 죽이고 (80포트 열려있는 프로세스는 다 kill한다는 명령어이다)

소스코드를 바꾸고 다시 git 저장소에 push를 해 본다

image 8

image 9

흠… “소스코드 변경 → commit & push → 대상서버에 반영” 이 과정이 생각보다 번거롭다

지금은 대상서버에서 바로 작업했기 때문에 “소스코드 변경 → commit & push” 이 부분만 진행한거지만 모든 개발자가 다 이렇게 작업할 수도 없고 일반적으로 로컬PC에 IDE 툴을 사용하기 때문에 훨씬 더 번거로워 진다.

그래서 이 때! Github Actions를 사용하여 일련의 과정을 자동화해 볼 것이다!

2. GitHub Actions -1-

먼저 세팅해 줄 것이 있다.

Github Actions의 서버가 내 대상서버 EC2에 접근하기 위해 EC2의 Public IP와 SSH 프라이빗키를 Secret 키로 등록해 줘야 한다.

레포의 Settings > Secrets and variables > Actions 에서 등록하면 된다.

타인에게 노출되면 안되는 값은 Secrets 값으로 등록해 주고 민감하지 않은 정보는 variables에 등록할 수 있다. Secrets은 데이터가 암호화되어 저장되기 때문에 안전하게 사용할 수 있다.

image 10

이제 로컬PC로 돌아와 작업을 진행한다. git레포를 먼저 clone해 오고

.github/workflows/deploy.yaml 파일을 생성한다. Github Actions이 동작할 때 이 경로에 있는 yaml 파일 기반으로 동작하게 된다.

name: CICD1
on:
  workflow_dispatch:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Configure the SSH Private Key Secret
        run: |
          mkdir -p ~/.ssh/
          echo "$" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa

      - name: Set Strict Host Key Checking
        run: echo "StrictHostKeyChecking=no" > ~/.ssh/config

      - name: Git Pull
        run: |
          export MY_HOST="$"
          ssh ubuntu@$MY_HOST << EOF
            cd /home/ubuntu/cicd-2w || exit 1
            git pull origin main || exit 1
          EOF

      - name: Run service
        run: |
          export MY_HOST="$"
          ssh ubuntu@$MY_HOST sudo fuser -k -n tcp 80 || true
          ssh ubuntu@$MY_HOST "nohup sudo -E python3 /home/ubuntu/cicd-2w/server.py > /home/ubuntu/cicd-2w/server.log 2>&1 &"
  • on: workflow_dispatch: 워크플로우가 트리거되는 이벤트를 정의하는건데 수동으로 push할 때 트리거된다. 워크플로우에서 Run workflow 버튼을 클릭해서 수동 배포 가능하다.

    image 11

  • on: push: 조건은 main 브랜치에 push할 때 자동실행된다
  • job은 워크플로우에서 수행할 작업을 의미하는데 steps로 정의해서 파이프라인을 구성할 수 있다.
  • Github Actions의 가상컴퓨터환경이 대상 서버에 접근하기 위해 ssh 설정을 해 주고 StrictHostKeyChecking=no 옵션을 통해 ssh 접근할 때 뜨는 Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 이 부분을 생략할 수 있다.

    그 다음 서버에 접근해서 git pull을 진행해 서버에 변경된 코드를 반영해 주고 기존 프로세스를 죽인 후에 서버를 재실행하게 해 준다

push하고 레포에 들어가 보면 Actions 탭에 새로운 workflow가 생겨 있다

image 12

그럼 deploy.yaml에 정의한 대로 워크플로우가 실행된다.

Ubuntu 환경의 서버가 job을 실행하는 과정이다.

image 13

ssh 키를 설정하는 데에 사전에 설정한 Secrets 값을 가져와서 진행한다. 이 때 암호화되어 노출되지 않는 부분을 확인할 수 있다.

image 14

대상 서버에서 git pull 과정을 거치고 코드를 실행하고 나서 job을 Complete한다

image 15

image 16

직접 서버에 들어가서 확인해 보면 코드 반영 및 서버 실행이 정상 동작중인 것을 알 수 있다.

image 17

image 18

소스코드를 변경하고 deploy.yaml에 workflow 이름과 job 이름만 수정해서 push해 보았다.

name: CICD1 End
on:
  workflow_dispatch:
  push:
    branches:
      - main

jobs:
  deployfinal:
    runs-on: ubuntu-latest
    steps:
.....
.....

github에 들어가서 확인해 보면 workflow와 job 이름이 변경된 걸 확인할 수 있었다

image 19

하지만 매번 스크립트를 짜기에는 좀 번거로움이 있다.

그래서 Github Actions 기능을 좀 더 활용해 보고자 한다!

아래 실습에서 Secrets과 Extentions을 활용해 볼 것이다.

3. GitHub Actions -2-

우선 Github Actions에 job이 실행되는 환경에서 사용하는 파이썬 버전 정보나 env를 확인해 보자

name: CICD2
on:
  workflow_dispatch:
  push:
    branches:
      - main

jobs:
  deployfinal:
    runs-on: ubuntu-latest
    steps:
      - name: Test
        run: |
          python -V || true
          python3 -V || true
          which python || true
          which python3 || true
          env

파이썬 3.10.12를 사용하고 여러 환경변수를 확인할 수 있었다

image 20

GitHub Actions에서 .gitignore에 제외된 민감파일 내용을 안전하게 가져오기

.env 파일이 포함되어 있다

image 21

역시나 .env 파일 푸시는 무시된다.

image 22

이 때 Github Actions에서 Secrets을 생성해서 사용하면 안전하게 가져올 수 있다

image 23

이제 Extentions을 활용해 볼건데,

Github에는 Marketplace에서 사용자가 스크립트를 간편하게 사용할 수 있게 커스텀해서 만들어진 extentions을 사용할 수 있다

참고 링크 ) Github Marketplace

deploy.yml을 수정해서 appleboy/ssh-action@v1.2.0를 사용해 볼 것이다!

name: CICD2
on:
  workflow_dispatch:
  push:
    branches:
      - main

jobs:
  ssh-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Github Repository Checkout
        uses: actions/checkout@v4

      - name: executing remote ssh commands
        uses: appleboy/ssh-action@v1.2.0
        env:
          AWS_KEYS: $
        with:
          host: $
          username: ubuntu
          key: $
          envs: AWS_KEYS
          script_stop: true
          script: |
             cd /home/ubuntu/cicd-2w
             echo "$AWS_KEYS" > .env
  • actions/checkout@v4: GitHub의 기본 액션이며 현재 워크플로우가 실행되는 레포의 코드를 체크아웃할 수 있다.
  • appleboy/ssh-action@v1.2.0: Github Actions에 저장된 Secrets 값을 변수로 가져와서 ssh 접속 후에 스크립트를 실행할 수 있다
  • echo "$AWS_KEYS" > .env: Secrets 값을 통해 민감한 정보를 서버에 안전하게 전달할 수 있다

image 24

번거롭긴 하지만 .env 파일을 업데이트하려면 Github Actions Secrets 값을 업데이트해 주어야 한다!

SCP 사용하여 서버에 코드 배포하기

스크립트를 통해 매번 git pull을 하지 않고 SCP 파일 전송 프로토콜로 서버에 직접 배포가 가능하다

이전에는 “로컬PC에서 코드변경 & push → Github Actions 트리거 → 대상서버 ssh 접속 → git pull → 서비스중지 후 실행” 방식으로 이루어 졌다면

지금은 ssh, scp 익스텐션을 사용해서 “로컬PC에서 코드변경 & push → Github Actions 트리거 → git checkout → 대상서버 ssh 접속 → 서비스 중지 → 파일 복사” 방식으로 바꿀 수 있다 (서비스 실행까지는 바로 다음 실습에 이어서!)

소스코드를 살짝 변경하고 deploy.yml도 아래처럼 수정했다

name: CICD2
on:
  workflow_dispatch:
  push:
    branches:
      - main

jobs:
  scp-ssh-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Github Repository Checkout
        uses: actions/checkout@v4

      - name: executing remote ssh commands
        uses: appleboy/ssh-action@v1.2.0
        env:
          AWS_KEYS: $
        with:
          host: $
          username: ubuntu
          key: $
          envs: AWS_KEYS
          script_stop: true
          script: |
             cd /home/ubuntu/cicd-2w
             echo "$AWS_KEYS" > .env
             sudo fuser -k -n tcp 80 || true

      - name: copy file via ssh
        uses: appleboy/scp-action@v0.1.7
        with:
          host: $
          username: ubuntu
          key: $
          source: server.py
          target: /home/ubuntu/cicd-2w
  • appleboy/scp-action@v0.1.7: ssh 접속하고 나서 source에 지정된 파일을 target 경로에 복사해 줄 수 있다

image 25

이제 최종적으로 변경된 코드로 서비스 재기동까지 해 보겠다

로컬PC에서 코드변경 & push → Github Actions 트리거 → git checkout → 대상서버 ssh 접속 후 파일 복사 → ssh 접속 후 서비스 중지 → 복사한 파일 이동 → 서비스재기동” 순서로 이루어 진다.

name: CICD2
on:
  workflow_dispatch:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Github Repository Checkout
        uses: actions/checkout@v4

      - name: copy file via ssh
        uses: appleboy/scp-action@v0.1.7
        with:
          host: $
          username: ubuntu
          key: $
          source: server.py
          target: /home/ubuntu

      - name: executing remote ssh commands 
        uses: appleboy/ssh-action@v1.2.0
        env:
          AWS_KEYS: $
        with:
          host: $
          username: ubuntu
          key: $
          envs: AWS_KEYS
          script_stop: true
          script: |
             cd /home/ubuntu/cicd-2w
             echo "$AWS_KEYS" > .env
             sudo fuser -k -n tcp 80 || true
             rm server.py
             cp /home/ubuntu/server.py ./
             nohup sudo -E python3 /home/ubuntu/cicd-2w/server.py > /home/ubuntu/cicd-2w/server.log 2>&1 &
             echo "test" >> /home/ubuntu/text.txt

image 26

아무것도 설치되어 있지 않은 신규 서버에 배포한다고 가정해 보고 다른 서버 생성해서 배포해 보겠다

우선 Github Actions Secrets 값에 신규 서버 ip로 변경하고 시작한다!

처음 배포한 서버에는 cicd-2w 디렉토리가 생성되어 있지 않으니 mkdir로 생성해 줘야 하고 기존에 디렉토리가 있을 경우 에러가 나지 않게 || true 옵션을 줘야 한다. rm server.py도 마찬가지다

script: |
             mkdir -p /home/ubuntu/cicd-2w || true
             cd /home/ubuntu/cicd-2w
             echo "$AWS_KEYS" > .env
             sudo fuser -k -n tcp 80 || true
             rm server.py || true
             cp /home/ubuntu/server.py ./
             nohup sudo -E python3 /home/ubuntu/cicd-2w/server.py > /home/ubuntu/cicd-2w/server.log 2>&1 &
             echo "test" >> /home/ubuntu/text.txt

image 27

근데 마지막 echo "test" >> /home/ubuntu/text.txt을 실행할 때마다, 즉 배포할 때마다 test가 배포 수만큼 찍히게 된다

image 28

이런 문제점(?)으로 인해 선언형 배포툴로 ansible을 사용할 수 있다

4. GitHub Actions with Ansible

Github Actions에 ansible을 설치해서 진행한다

그럼 로컬에서 ansible을 사용하듯 Github Actions의 가상서버가 주체가 되어 ansible을 대상서버에게 사용할 수 있다!

name: Run Ansible
on:
  workflow_dispatch:
  push:
    branches:
      - main

jobs:
  run-playbooks:
    runs-on: ubuntu-latest
    steps:
      - name: Github Repository Checkout
        uses: actions/checkout@v4

      - name: Setup Python 3
        uses: actions/setup-python@v5
        with:
          python-version: "3.8"

      - name: Upgrade Pip & Install Ansible
        run: |
          python -m pip install --upgrade pip
          python -m pip install ansible

      - name: Implement the Private SSH Key
        run: |
          mkdir -p ~/.ssh/
          echo "$" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa

      - name: Ansible Inventory File for Remote host
        run: |
          mkdir -p ./devops/ansible/
          export INVENTORY_FILE=./devops/ansible/inventory.ini
          echo "[my_host_group]" > $INVENTORY_FILE
          echo "$" >> $INVENTORY_FILE

      - name: Ansible Default Configuration File
        run: |
          mkdir -p ./devops/ansible/
          cat <<EOF > ./devops/ansible/ansible.cfg
          [defaults]
          ansible_python_interpreter = '/usr/bin/python3'
          ansible_ssh_private_key_file = ~/.ssh/id_rsa
          remote_user = ubuntu
          inventory = ./inventory.ini
          host_key_checking = False
          EOF

      - name: Ping Ansible Hosts
        working-directory: ./devops/ansible/
        run: |
          ansible all -m ping

파이썬 버전은 그대로 사용해도 되지만 3.8으로 버전을 명시했다

간단하게 ansible 설정을 해 두고

인벤토리에 정의된 모든 호스트를 대상으로 ping 요청을 보내 대상 서버와 연결이 가능한지를 확인할 수 있다.

ansible.cfg 파일에 inventory를 지정했기 때문에 ansible이 동작할 때 -i 옵션으로 인벤토리를 지정하지 않아도 된다

image 29

이제 test.txt 파일이 선언형으로 지정되는지 확인해 보고자 아래 내용만 추가해 주었다.

      - name: Run Ansible Playbook
        working-directory: ./devops/ansible/
        run: |
          echo '---
          - name: Ensure text.txt contains only "test"
            hosts: all
            tasks:
              - name: Ensure the file contains only "test"
                lineinfile:
                  path: /home/ubuntu/text.txt
                  line: "test"
                  state: present
                  create: yes' > playbook.yml
          
          ansible-playbook playbook.yml

기본적으로 ansible은 localhost에서 실행되기 때문에 기본적으로 local 인벤토리를 참조하게 되어서 이 때, working-directory: ./devops/ansible/를 추가해 주어야 이 경로에서 인벤토리 파일을 불러올 수 있다

image 30

하나는 실행 전이고 하나는 실행 후이다. 동일하게 test가 찍힌다.

image 31

line: "test" 부분에 test가 아닌 test123으로 변경했을 때는 덮어쓰기가 아니라 라인 추가가 되었다.

image 32

Categories:

Updated: