13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[카테고리:] cloud

  • [Cloud] Ansible Playbook 디버깅: 흔한 오류 해결 및 실전 팁

    [Cloud] Ansible Playbook 디버깅: 흔한 오류 해결 및 실전 팁

    목차

    Ansible Playbook 디버깅, 처음엔 저도 막막했습니다

    자동화 스크립트를 짜놓고 실행했는데 갑자기 빨간 글씨가 쭉 올라올 때… 그 순간의 당혹감, 혹시 공감되시나요? 저도 처음 Ansible을 도입했을 때 Playbook이 중간에 뻗어버리면 어디서부터 봐야 할지 몰라서 진짜 한참 헤맸거든요. 에러 메시지는 길고, 어디가 문제인지는 모르겠고, 설상가상으로 운영 서버에서 터지기라도 하면… 식은땀이 흘렀죠.

    13년 동안 인프라 엔지니어로 일하면서 Ansible을 본격적으로 쓴 게 벌써 7년이 넘었는데, 그 사이에 정말 별의별 Ansible 오류를 다 겪어봤습니다. 오늘은 그 삽질의 결정체를 모아서, Ansible Playbook 디버깅을 어떻게 체계적으로 접근하면 되는지 실전 경험 기반으로 정리해드릴게요. 처음 보시는 분도, 어느 정도 써보셨는데 아직도 에러 앞에서 막히시는 분도 모두 도움이 됐으면 합니다.

    Ansible Playbook 디버깅 전체 흐름도 — 오류 발생부터 해결까지 단계별 프로세스

    ▲ Ansible Playbook 디버깅의 전체 흐름 — 오류 발생부터 해결까지 단계별 접근 방법


    Ansible Playbook 디버깅이란? 기본 개념부터 잡고 가기

    쉽게 말해, Ansible Playbook 디버깅은 자동화 스크립트(Playbook)가 의도한 대로 동작하지 않을 때 원인을 찾고 수정하는 과정이에요. 일반적인 코드 디버깅이랑 비슷하긴 한데, Ansible만의 특성이 있어서 접근 방식이 좀 달라요.

    Ansible은 기본적으로 YAML(야믈, 사람이 읽기 쉬운 데이터 직렬화 형식) 기반의 선언형 자동화 도구거든요. 그래서 오류가 크게 세 가지 층위에서 발생합니다.

    • YAML 문법 오류: 들여쓰기 하나 잘못되면 바로 터집니다
    • Ansible 모듈 오류: 모듈(Module, Ansible의 기능 단위) 파라미터가 잘못되거나 버전이 맞지 않을 때
    • 원격 호스트 오류: 대상 서버에서 실제로 실행했을 때 발생하는 문제

    이 세 가지를 구분할 수 있어야 Ansible Playbook 디버깅이 빨라져요. 저도 처음엔 이게 다 뒤섞여 보여서 엄청 헤맸는데, 익숙해지면 에러 메시지만 봐도 어느 층위 문제인지 바로 감이 오더라고요.


    디버깅 전에 먼저 해야 할 것들: 사전 점검

    1. –syntax-check로 문법 먼저 확인

    실행하기 전에 문법 검사를 먼저 돌리는 게 기본 중의 기본입니다. 이거 안 하고 바로 돌리다가 원격 서버에서 절반쯤 실행되다 터지면 더 골치 아파요.

    # Playbook 문법 검사
    ansible-playbook site.yml --syntax-check
    
    # 인벤토리 파일 지정하는 경우
    ansible-playbook -i inventory/production site.yml --syntax-check

    문법 오류가 있으면 이렇게 나와요:

    ERROR! Syntax Error while loading YAML.
      found character that cannot start any token
    
    The error appears to be in '/home/user/playbooks/site.yml': line 15, column 3
    
      13: tasks:
      14:   - name: Install nginx
      15:    apt:   # <--- 여기 들여쓰기 문제
            ^ here

    라인 번호까지 알려주니까 찾기는 어렵지 않아요. 근데 YAML 들여쓰기는 진짜... 탭(Tab)이랑 스페이스(Space)를 섞으면 절대 안 됩니다. 이거 때문에 저도 초반에 얼마나 삽질했는지 ㅎㅎ

    2. --check 모드로 드라이런(Dry-run) 실행

    실제로 변경을 가하지 않고 "만약 실행하면 어떻게 될까"를 미리 보는 방법이에요. Check Mode(체크 모드)라고도 하고, Dry-run(드라이런, 실제 적용 없이 테스트 실행)이라고도 해요.

    # 드라이런 실행
    ansible-playbook site.yml --check
    
    # 드라이런 + 변경 예정 내용 상세 확인
    ansible-playbook site.yml --check --diff

    💡 팁: --diff 옵션을 같이 쓰면 파일이 어떻게 변경될지 diff 형식으로 보여줍니다. 설정 파일 배포 전에 꼭 써보세요. 진짜 유용해요.


    Ansible Playbook 디버깅 핵심 도구: 상세 출력과 debug 모듈

    Verbosity(상세 출력) 레벨 활용

    이게 제가 가장 자주 쓰는 방법이에요. -v 옵션을 붙이면 더 자세한 출력이 나오는데, 최대 4개까지 붙일 수 있습니다.

    # -v: 기본 상세 출력 (task 결과)
    ansible-playbook site.yml -v
    
    # -vv: 더 자세히 (파일/디렉토리 작업 포함)
    ansible-playbook site.yml -vv
    
    # -vvv: 연결 정보까지 (SSH 연결 디버깅에 유용)
    ansible-playbook site.yml -vvv
    
    # -vvvv: 가장 상세 (네트워크 패킷 수준)
    ansible-playbook site.yml -vvvv

    저는 보통 -vv나 -vvv를 주로 써요. -vvvv는 너무 많이 나와서 오히려 찾기 어려울 때도 있거든요.

    debug 모듈로 변수 값 확인하기

    Ansible 문제 해결에서 제일 많이 쓰는 게 바로 debug 모듈이에요. 변수 값이 의도한 대로 들어가 있는지 확인할 때 필수입니다.

    ---
    - name: 변수 디버깅 예제
      hosts: webservers
      vars:
        app_version: "1.2.3"
        deploy_path: "/opt/myapp"
    
      tasks:
        - name: 변수 값 확인
          debug:
            msg: "앱 버전: {{ app_version }}, 경로: {{ deploy_path }}"
    
        - name: 전체 변수 목록 확인 (hostvars)
          debug:
            var: hostvars[inventory_hostname]
    
        - name: 특정 변수만 확인
          debug:
            var: app_version
            verbosity: 2  # -vv 이상일 때만 출력

    verbosity 파라미터가 있는 게 포인트예요. 평소엔 안 보이다가 디버깅할 때만 출력되게 설정할 수 있거든요. 프로덕션 Playbook에 debug 태스크를 남겨둘 때 이렇게 해두면 깔끔합니다.

    register로 태스크 결과 캡처하기

    태스크(Task, Ansible의 개별 작업 단위) 실행 결과를 변수에 저장해서 다음 단계에서 활용하거나 디버깅할 수 있어요.

      tasks:
        - name: 서비스 상태 확인
          command: systemctl status nginx
          register: nginx_status
          ignore_errors: yes  # 오류가 나도 계속 진행
    
        - name: 결과 출력
          debug:
            var: nginx_status
    
        - name: 특정 필드만 확인
          debug:
            msg: |
              Return Code: {{ nginx_status.rc }}
              stdout: {{ nginx_status.stdout }}
              stderr: {{ nginx_status.stderr }}
    Ansible debug 모듈과 register를 활용한 Playbook 디버깅 설정 코드 예시

    ▲ debug 모듈과 register를 조합한 Ansible Playbook 디버깅 — 태스크 결과를 변수에 저장하고 단계별로 확인하는 방법


    Ansible 오류 유형별 해결 방법: 실전 트러블슈팅

    ⚠️ 오류 1: UNREACHABLE — 호스트에 접근 불가

    이거 처음 보면 당황하는 분들이 많은데, 대부분 SSH(시큐어 쉘, 원격 접속 프로토콜) 문제예요.

    TASK [Gathering Facts] *****
    fatal: [192.168.1.100]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh", "unreachable": true}

    체크리스트:

    1. SSH 키 등록 여부 확인: ssh -i ~/.ssh/id_rsa [email protected]
    2. 인벤토리(Inventory, 관리 대상 호스트 목록) 파일의 IP/호스트명 확인
    3. ansible_user, ansible_port 변수 확인
    4. 방화벽 규칙 확인
    # SSH 연결 직접 테스트
    ansible all -m ping -i inventory/hosts -vvv
    
    # 특정 호스트만 테스트
    ansible webserver01 -m ping -i inventory/hosts

    ⚠️ 오류 2: 변수 undefined — 변수를 찾을 수 없음

    Jinja2(진자2, Ansible의 템플릿 엔진) 템플릿에서 변수를 참조했는데 정의가 안 되어 있을 때 발생해요. 저도 이거 때문에 한참 헤맸습니다.

    fatal: [server01]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'db_password' is undefined"}

    해결 방법:

      tasks:
        # 방법 1: default 필터로 기본값 지정
        - name: DB 설정
          template:
            src: db.conf.j2
            dest: /etc/myapp/db.conf
          vars:
            db_password: "{{ db_password | default('changeme') }}"
    
        # 방법 2: vars_prompt로 실행 시 입력 받기
      vars_prompt:
        - name: db_password
          prompt: "DB 패스워드를 입력하세요"
          private: yes  # 입력 내용 숨김
    
        # 방법 3: ansible-vault로 암호화된 변수 파일 사용
        # ansible-playbook site.yml --ask-vault-pass

    ⚠️ 오류 3: 권한 오류 — Permission Denied

    원격 서버에서 sudo(슈퍼유저 권한 실행) 권한이 필요한 작업을 할 때 자주 만나는 Ansible 오류예요.

    ---
    - name: 웹서버 설정
      hosts: webservers
      become: yes          # sudo 권한으로 실행
      become_user: root    # root로 전환
    
      tasks:
        - name: nginx 설치
          apt:
            name: nginx
            state: present
    
        # 특정 태스크만 권한 상승
        - name: 로그 파일 수정
          file:
            path: /var/log/myapp
            mode: '0755'
          become: yes
          become_user: www-data
    # sudo 비밀번호 입력이 필요한 경우
    ansible-playbook site.yml --ask-become-pass

    ⚠️ 오류 4: 조건부 실행 문제 — when 절 오작동

    When(조건절) 설정이 잘못되면 실행되어야 할 태스크가 건너뛰어지거나, 반대로 실행되면 안 되는 게 실행되는 상황이 생겨요. 이거 진짜 찾기 어렵습니다.

      tasks:
        - name: OS 정보 수집
          setup:
            gather_subset:
              - distribution
    
        - name: 변수 타입 확인 (디버깅용)
          debug:
            msg: |
              OS Family: {{ ansible_os_family }}
              Distribution: {{ ansible_distribution }}
              Version: {{ ansible_distribution_version }}
    
        # 잘못된 예 — 문자열 비교 실수
        - name: Ubuntu에서만 실행 (잘못된 방법)
          apt:
            name: nginx
          when: ansible_distribution == 'ubuntu'  # 대소문자 주의!
    
        # 올바른 예
        - name: Ubuntu에서만 실행 (올바른 방법)
          apt:
            name: nginx
            state: present
          when: ansible_distribution == 'Ubuntu'  # 대문자 U
    
        # 복잡한 조건 — 가독성 높게 작성
        - name: 특정 환경에서만 실행
          command: /opt/deploy.sh
          when:
            - ansible_distribution == 'Ubuntu'
            - ansible_distribution_version is version('20.04', '>=')
            - env == 'production'

    ⚠️ 오류 5: 모듈 파라미터 오류

    Ansible 버전이 올라가면서 deprecated(더 이상 사용 안 하는) 파라미터가 생기거나, 파라미터명이 바뀌는 경우가 있어요. 이런 Ansible 오류는 메시지가 꽤 친절하게 나옵니다.

    [DEPRECATION WARNING]: The 'include' module is deprecated, use 'import_tasks' or 'include_tasks' instead.
    
    # 또는
    [WARNING]: Module did not set no_log for password
      tasks:
        # 구식 방법 (deprecated)
        - include: tasks/setup.yml
    
        # 새로운 방법
        - import_tasks: tasks/setup.yml   # 정적 포함 (컴파일 타임)
        - include_tasks: tasks/setup.yml  # 동적 포함 (런타임)

    고급 디버깅 기법: 실무에서 정말 유용한 것들

    특정 태스크만 실행하기 — tags 활용

    Playbook 전체를 돌리지 않고 문제가 있는 태스크만 콕 집어서 실행할 수 있어요. 태그(Tag) 기능인데, Ansible Playbook 디버깅할 때 진짜 유용합니다.

      tasks:
        - name: 패키지 설치
          apt:
            name: "{{ item }}"
            state: present
          loop:
            - nginx
            - git
            - curl
          tags:
            - packages
            - install
    
        - name: 설정 파일 배포
          template:
            src: nginx.conf.j2
            dest: /etc/nginx/nginx.conf
          tags:
            - config
            - nginx
    # 특정 태그만 실행
    ansible-playbook site.yml --tags "config"
    
    # 특정 태그 제외하고 실행
    ansible-playbook site.yml --skip-tags "packages"
    
    # 여러 태그 지정
    ansible-playbook site.yml --tags "config,nginx"

    특정 태스크부터 시작하기 — --start-at-task

    중간에 실패했을 때 처음부터 다시 돌리기 싫을 때 쓰는 방법이에요. 저 이거 알고 나서 삽질 시간이 확 줄었어요.

    # 특정 태스크명부터 실행
    ansible-playbook site.yml --start-at-task "설정 파일 배포"
    
    # step 모드: 태스크마다 실행 여부 물어봄
    ansible-playbook site.yml --step

    ansible-lint로 Playbook 품질 검사

    ansible-lint(앤서블 린트)는 Playbook의 코드 품질을 자동으로 검사해주는 도구예요. 문법 오류뿐 아니라 Best Practice(베스트 프랙티스, 모범 사례) 위반도 잡아줍니다.

    # 설치
    pip install ansible-lint
    
    # 검사 실행
    ansible-lint site.yml
    
    # 특정 규칙 무시하고 실행
    ansible-lint site.yml --exclude-path .ansible-lint
    # .ansible-lint 설정 파일
    skip_list:
      - yaml[line-length]  # 줄 길이 규칙 무시
      - name[casing]       # 이름 대소문자 규칙 무시
    
    warn_list:
      - experimental       # 실험적 규칙은 경고만

    Callback Plugin으로 출력 가독성 높이기

    기본 출력이 너무 지저분하다 싶으면 Callback Plugin(콜백 플러그인, 출력 형식 변환 도구)을 바꿔보세요.

    # ansible.cfg 설정
    [defaults]
    stdout_callback = yaml     # YAML 형식으로 출력 (가독성 좋음)
    # stdout_callback = dense  # 간결하게 출력
    # stdout_callback = debug  # 디버깅용 상세 출력
    
    callback_whitelist = profile_tasks  # 각 태스크 실행 시간 표시

    저는 yaml 콜백이랑 profile_tasks 조합을 제일 좋아해요. 어느 태스크가 오래 걸리는지 한눈에 보이거든요.

    Ansible yaml callback plugin 적용 전후 출력 화면 비교 — 가독성 향상으로 문제 해결 효율 증가

    ▲ yaml callback plugin 적용 전후 비교 — 출력 가독성이 크게 향상되어 Ansible 문제 해결이 훨씬 수월해집니다


    Ansible 오류 유형별 빠른 참조 표

    자주 만나는 오류들을 정리해봤습니다. 북마크해두고 쓰세요 ✅

    오류 유형 주요 증상 원인 해결 방법
    UNREACHABLE 호스트 연결 실패 SSH 설정 문제, 네트워크 오류 SSH 키 확인, 인벤토리 점검, -vvv로 연결 추적
    FAILED (문법) Playbook 시작 전 종료 YAML 들여쓰기, 문법 오류 --syntax-check, ansible-lint
    FAILED (변수) undefined variable 에러 변수 미정의, 오타 debug 모듈로 변수 확인, default 필터
    FAILED (권한) Permission denied sudo 설정 미흡 become/become_user 설정, sudoers 확인
    SKIPPED (예상치 못한) 태스크가 건너뜀 when 조건 오류 debug로 변수 값 확인, 조건식 재검토
    CHANGED (의도치 않은) 멱등성(Idempotency) 깨짐 모듈 사용 오류, command 모듈 남용 전용 모듈 사용, changed_when 설정
    MODULE ERROR 모듈 실행 실패 파라미터 오류, 버전 불일치 공식 문서 확인, -vv로 상세 확인

    멱등성(Idempotency) 확인: 자동화 스크립트의 핵심

    멱등성(Idempotency, 동일한 작업을 여러 번 실행해도 결과가 같아야 하는 성질)은 Ansible의 핵심 철학이에요. 이게 깨지면 Playbook을 두 번 돌렸을 때 뭔가 이상해지는 상황이 생기더라고요.

      tasks:
        # 나쁜 예: command 모듈은 멱등성이 없음
        - name: 디렉토리 생성 (나쁜 방법)
          command: mkdir /opt/myapp
    
        # 좋은 예: file 모듈은 멱등성 보장
        - name: 디렉토리 생성 (좋은 방법)
          file:
            path: /opt/myapp
            state: directory
            mode: '0755'
            owner: www-data
    
        # command/shell을 써야 할 때는 changed_when으로 제어
        - name: 애플리케이션 초기화 (한 번만 실행)
          command: /opt/myapp/init.sh
          args:
            creates: /opt/myapp/.initialized  # 이 파일 있으면 건너뜀
    
        # 또는 register + when 조합
        - name: 초기화 여부 확인
          stat:
            path: /opt/myapp/.initialized
          register: init_check
    
        - name: 초기화 실행
          command: /opt/myapp/init.sh
          when: not init_check.stat.exists

    자주 묻는 질문 (FAQ)

    Q. Ansible Playbook 실행 중에 특정 호스트만 실패했을 때 어떻게 하나요?

    A. --limit 옵션으로 특정 호스트만 재실행할 수 있어요. 실패한 호스트 목록은 .retry 파일에 자동 저장되기도 합니다.

    # 특정 호스트만 실행
    ansible-playbook site.yml --limit webserver01
    
    # retry 파일 활용
    ansible-playbook site.yml --limit @site.retry

    Q. 변수 우선순위가 헷갈려요. 어떻게 확인하나요?

    A. Ansible 변수 우선순위(Variable Precedence)는 복잡한데, ansible-config dump나 debug 모듈로 실제 적용된 값을 확인하는 게 제일 빠릅니다. 공식 문서에 22단계 우선순위가 나와 있는데... 저도 다 외우진 못해요 ㅎㅎ

    Q. ansible-playbook 실행이 너무 느린데 디버깅 말고 속도 개선 방법은요?

    A. 이건 별도로 다룰 주제인데, Forks(병렬 실행 수) 조정, Fact Caching(팩트 캐싱), Pipelining(파이프라이닝) 활성화가 주요 방법이에요. 다음 글에서 Ansible 성능 최적화를 다룰 예정입니다.


    마무리: 디버깅도 실력이다

    Ansible Playbook 디버깅 핵심 방법 5가지 요약 인포그래픽 — syntax-check부터 ansible-lint까지

    ▲ Ansible Playbook 디버깅 핵심 방법 요약 — syntax-check부터 debug 모듈, 태그 활용까지 단계별 접근법

    오늘 다룬 내용을 간단히 정리하면요:

    • ✅ 실행 전: --syntax-check로 문법 검사, --check --diff로 드라이런
    • ✅ 실행 중: -v ~ -vvv 상세 출력, debug 모듈로 변수 확인
    • ✅ 범위 좁히기: --tags, --limit, --start-at-task 활용
    • ✅ 코드 품질: ansible-lint로 사전 검사, 멱등성 확인
    • ✅ 가독성: yaml callback plugin + profile_tasks로 출력 개선

    솔직히 말씀드리면, 처음에 Ansible 오류를 만났을 때 저도 막막했어요. 에러 메시지가 길고 복잡해 보이는데, 결국 패턴이 있더라고요. 오늘 소개한 접근 방법들을 익히면 웬만한 Ansible 문제 해결은 훨씬 수월해질 거예요.

    🎉 핵심은 체계적인 접근이에요. 무턱대고 코드 고치지 말고, 문법 → 연결 → 변수 → 모듈 순서로 하나씩 좁혀가는 거죠. 그리고 debug 모듈은 정말 친한 친구처럼 자주 써보세요. 저도 매일 씁니다.

    다음 글에서는 Ansible Vault(앤서블 볼트)를 활용한 시크릿(Secret, 민감한 정보) 관리 방법을 다뤄볼 예정이에요. 패스워드나 API 키 같은 민감한 정보를 Playbook에 안전하게 넣는 방법인데, 이것도 실무에서 꼭 알아야 할 내용이라 기대해주세요.

    궁금한 점이나 여러분이 겪었던 특이한 Ansible 오류 경험이 있으시면 댓글로 남겨주세요. 같이 해결해봐요! 😊

  • [Cloud] GitHub Actions 자체 호스팅 러너 보안 강화: 백도어 방지 및 2026년 최신 가이드

    [Cloud] GitHub Actions 자체 호스팅 러너 보안 강화: 백도어 방지 및 2026년 최신 가이드

    CI/CD 파이프라인의 약한 고리, 자체 호스팅 러너

    몇 달 전에 팀 내에서 꽤 아찔한 일이 있었습니다. 동료 엔지니어가 운영하던 GitHub Actions 자체 호스팅 러너(Self-hosted Runner)가 공급망 공격(Supply Chain Attack)의 타깃이 될 뻔했거든요. 다행히 조기에 발견했지만, 그때 이후로 저도 홈랩이랑 실무 환경 전체를 다시 들여다보게 됐습니다.

    솔직히 말씀드리면, 저도 처음에는 “GitHub에서 제공하는 거니까 그냥 쓰면 되겠지”라고 생각했었어요. 근데 자체 호스팅 러너는 얘기가 완전히 다릅니다. 여러분 서버 위에 올라가는 순간, 그 보안 책임은 100% 여러분 몫이거든요.

    이번 글에서는 GitHub Actions 자체 호스팅 러너 보안을 실질적으로 강화하는 방법을 단계별로 정리해 드릴게요. 2025~2026년 기준으로 GitHub에서 권고하는 최신 가이드라인과 제가 직접 적용해본 경험을 섞어서 써봤습니다.

    GitHub Actions 자체 호스팅 러너 보안 아키텍처 전체 개요 다이어그램

    ▲ GitHub Actions 자체 호스팅 러너의 전체 보안 아키텍처 개요 — 러너, 리포지토리, 네트워크, 시크릿 관리 레이어별 보안 포인트를 한눈에 보여줍니다.


    자체 호스팅 러너(Self-hosted Runner)란 무엇인가요?

    GitHub Actions에는 두 가지 러너 타입이 있습니다.

    • GitHub 호스팅 러너(GitHub-hosted Runner): GitHub이 관리하는 클라우드 VM. 워크플로우 실행 후 바로 폐기됩니다.
    • 자체 호스팅 러너(Self-hosted Runner): 여러분이 직접 서버를 준비하고, 그 위에 러너 에이전트를 설치해서 운영하는 방식입니다.

    쉽게 말해, 자체 호스팅 러너는 “내 서버를 GitHub CI/CD 파이프라인에 연결하는 것”입니다. 비용 절감, 내부망 접근, 특수 하드웨어 활용 등 장점이 많죠. 근데 바로 그 유연성 때문에 보안 리스크도 같이 따라옵니다.

    왜 위험한가요?

    핵심 문제는 워크플로우 파일(.yml)이 코드 저장소에 존재한다는 것입니다. 누군가 PR(Pull Request)을 통해 악성 워크플로우를 심으면, 그게 여러분 서버 위에서 실행될 수 있어요. GitHub 호스팅 러너라면 그 VM은 바로 폐기되지만, 자체 호스팅 러너는 여러분 서버가 그대로 남아있고, 내부 네트워크에도 접근 가능하죠.

    구분 GitHub 호스팅 러너 자체 호스팅 러너
    인프라 관리 GitHub 책임 사용자 책임
    보안 패치 자동 직접 관리
    실행 후 환경 VM 폐기 (격리) 서버 유지 (잔류 위험)
    내부망 접근 불가 가능 (위험 요소)
    비용 분당 과금 자체 서버 비용
    공급망 공격 위험 낮음 높음

    GitHub Actions 자체 호스팅 러너 보안 강화 — 단계별 실전 가이드

    1단계: 러너 접근 범위를 최소화하세요

    제가 처음 자체 호스팅 러너를 설정할 때 Organization 레벨로 등록했었어요. 편하긴 한데, 나중에 생각해보니 이게 꽤 위험한 설정이더라고요. 모든 리포지토리의 워크플로우가 그 러너에 접근할 수 있으니까요.

    권장 설정: 러너를 특정 리포지토리 레벨에만 등록하거나, Runner Group으로 접근을 제한하세요.

    GitHub Enterprise나 Organization을 쓰신다면 Runner Group(러너 그룹) 기능을 꼭 활용하세요.

    1. GitHub Organization → Settings → Actions → Runner groups
    2. 새 그룹 생성 후, 접근 허용할 리포지토리만 선택
    3. “Allow public repositories” 옵션은 반드시 비활성화
    # 러너 등록 시 특정 리포지토리 레벨로 등록하는 예시
    # Organization 레벨 대신 리포지토리 레벨 토큰 사용
    ./config.sh \
      --url https://github.com/your-org/specific-repo \
      --token YOUR_REPO_LEVEL_TOKEN \
      --name "secure-runner-01" \
      --labels "self-hosted,linux,secure"

    2단계: 에페머럴 러너(Ephemeral Runner) 도입 — 이게 진짜 핵심입니다

    이거 처음 알았을 때 “왜 진작 이걸 안 썼지?” 싶었어요. 에페머럴(Ephemeral, 일회성)이라는 단어처럼, 하나의 잡(Job)을 처리하고 나면 러너가 자동으로 폐기되는 방식입니다. GitHub 호스팅 러너처럼요.

    이렇게 하면 이전 잡에서 심어진 악성 코드나 환경 오염이 다음 잡으로 이어지지 않습니다. 공급망 공격 방어에 가장 효과적인 방법 중 하나거든요.

    # 에페머럴 모드로 러너 실행
    ./config.sh \
      --url https://github.com/your-org/your-repo \
      --token YOUR_TOKEN \
      --ephemeral  # 이 플래그 하나로 일회성 러너가 됩니다
    
    ./run.sh

    컨테이너 환경이라면 Actions Runner Controller(ARC)를 활용하면 Kubernetes 위에서 에페머럴 러너를 자동으로 스케일링할 수 있어요. 저도 홈랩 k3s 클러스터에 ARC 올려서 쓰고 있는데, 진짜 편하더라고요.

    # Helm으로 Actions Runner Controller 설치
    helm install arc \
      --namespace "arc-systems" \
      --create-namespace \
      oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller
    
    # 에페머럴 러너 스케일셋 설정 (values.yaml)
    githubConfigUrl: "https://github.com/your-org/your-repo"
    githubConfigSecret: "arc-github-secret"
    
    containerMode:
      type: "dind"  # Docker-in-Docker로 격리 강화
    
    template:
      spec:
        containers:
          - name: runner
            image: ghcr.io/actions/actions-runner:latest
            resources:
              limits:
                cpu: "2"
                memory: "4Gi"
    GitHub Actions 에페머럴 러너 라이프사이클 다이어그램 - 잡 실행 후 자동 폐기 과정

    ▲ 에페머럴 러너의 라이프사이클 — 잡 수신 → 실행 → 자동 폐기 과정을 통해 환경 오염을 원천 차단합니다.

    3단계: 워크플로우 권한(Permissions)을 최소화하세요

    이게 은근히 놓치기 쉬운 부분이에요. GitHub Actions 워크플로우는 기본적으로 꽤 넓은 권한을 가질 수 있거든요. 최소 권한 원칙(Principle of Least Privilege)을 워크플로우에도 적용해야 합니다.

    # .github/workflows/ci.yml
    name: CI Pipeline
    
    on:
      push:
        branches: [main]
      pull_request:
        branches: [main]
    
    # 워크플로우 전체 기본 권한을 최소화
    permissions:
      contents: read  # 코드 읽기만 허용
    
    jobs:
      build:
        runs-on: [self-hosted, linux, secure]
        
        # 잡별로 필요한 권한만 추가
        permissions:
          contents: read
          packages: write  # 패키지 빌드가 필요한 경우에만
        
        steps:
          - name: Checkout
            uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
            with:
              persist-credentials: false  # 크리덴셜 잔류 방지!
          
          - name: Build
            run: make build

    특히 persist-credentials: false 옵션, 저도 처음엔 그냥 넘겼다가 나중에 알고 깜짝 놀랐어요. 이게 없으면 체크아웃 후 GitHub 토큰이 git 설정에 남아있을 수 있거든요.

    4단계: 외부 액션(Third-party Actions) SHA 핀닝(Pinning)

    공급망 공격(Supply Chain Attack)에서 가장 많이 노출되는 지점이 바로 여기입니다. uses: some-action/checkout@main처럼 브랜치 태그를 쓰면, 그 액션 저장소가 해킹당했을 때 여러분 파이프라인도 같이 당하게 돼요.

    해결책: 액션을 커밋 SHA 해시로 고정(Pin)하세요.

    # ❌ 이렇게 쓰면 위험합니다
    - uses: actions/checkout@v4
    - uses: actions/setup-node@main
    
    # ✅ 커밋 SHA로 고정하는 것이 안전합니다
    - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
    - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af  # v4.1.0

    SHA 해시를 일일이 관리하기 귀찮으시다면 Dependabot을 활용하세요. .github/dependabot.yml에 설정하면 액션 버전 업데이트를 자동으로 PR로 올려줍니다.

    # .github/dependabot.yml
    version: 2
    updates:
      - package-ecosystem: "github-actions"
        directory: "/"
        schedule:
          interval: "weekly"
        # SHA 핀닝된 액션도 자동으로 업데이트 PR 생성
        groups:
          actions:
            patterns:
              - "*"

    5단계: 시크릿(Secrets) 관리와 환경 보호 규칙

    시크릿 관리도 허술하면 다 무너져요. 몇 가지 중요한 포인트를 짚어드릴게요.

    • 환경(Environment) 보호 규칙 설정: Production 환경에는 반드시 Reviewer 승인 단계를 추가하세요.
    • 시크릿 범위 최소화: Organization 시크릿보다는 리포지토리 시크릿, 그것보다는 환경 시크릿이 더 안전합니다.
    • OIDC(OpenID Connect) 활용: 장기 자격증명(Long-lived Credentials) 대신 단기 토큰을 사용하세요.
    # OIDC로 AWS 인증하는 예시 — 장기 Access Key 불필요!
    jobs:
      deploy:
        runs-on: [self-hosted, linux]
        environment: production  # 환경 보호 규칙 적용
        
        permissions:
          id-token: write  # OIDC 토큰 발급에 필요
          contents: read
        
        steps:
          - name: Configure AWS Credentials via OIDC
            uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502  # v4
            with:
              role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
              aws-region: ap-northeast-2
              # Access Key/Secret Key가 전혀 필요 없습니다!

    6단계: 러너 서버 자체 하드닝(Hardening)

    러너가 올라가는 서버 자체도 단단하게 만들어야 하거든요. 이건 일반적인 리눅스 서버 보안과 겹치는 부분도 있는데, 자체 호스팅 러너 특화 포인트만 짚어드릴게요.

    # 러너를 전용 비권한 사용자로 실행
    sudo useradd -m -s /bin/bash github-runner
    sudo usermod -aG docker github-runner  # Docker 사용이 필요한 경우만
    
    # 러너 디렉토리 권한 설정
    sudo chown -R github-runner:github-runner /opt/actions-runner
    
    # systemd 서비스로 등록 (루트 실행 방지)
    cd /opt/actions-runner
    sudo ./svc.sh install github-runner  # 전용 유저로 서비스 설치
    sudo ./svc.sh start
    # /etc/systemd/system/actions.runner.*.service 에서 확인
    # User=github-runner 로 설정되어 있어야 합니다
    
    # Docker 소켓 접근 제한 (필요한 경우 rootless Docker 고려)
    # /etc/docker/daemon.json
    {
      "userns-remap": "github-runner",  # 사용자 네임스페이스 리매핑
      "no-new-privileges": true,
      "log-driver": "json-file",
      "log-opts": {
        "max-size": "10m",
        "max-file": "3"
      }
    }

    그리고 네트워크 측면에서는, 러너 서버가 외부 인터넷에 직접 노출되지 않도록 해야 해요. GitHub Actions 자체 호스팅 러너는 아웃바운드 연결만 사용합니다. 인바운드 포트를 열 필요가 없거든요. 방화벽 규칙을 아웃바운드 443(HTTPS)만 허용하는 식으로 좁힐 수 있습니다.

    GitHub Actions 자체 호스팅 러너 보안 강화 설정 체크리스트 대시보드

    ▲ 자체 호스팅 러너 보안 강화 체크리스트 — 네트워크, OS, 워크플로우, 시크릿 관리 등 레이어별 적용 현황을 확인하세요.


    ⚠️ 실제로 삽질했던 문제들과 해결법

    문제 1: pull_request 이벤트와 포크 리포지토리

    이거 진짜 주의하셔야 해요. 퍼블릭 리포지토리에서 자체 호스팅 러너를 쓰면 포크(Fork)된 리포지토리의 PR도 트리거될 수 있거든요. 외부 기여자가 악성 워크플로우를 PR에 담아서 보내면… 생각만 해도 아찔하죠.

    해결책: 퍼블릭 리포지토리에는 자체 호스팅 러너를 절대 사용하지 마세요. GitHub도 공식적으로 이를 권고하지 않아요. 꼭 써야 한다면 pull_request_target 이벤트 대신 pull_request를 쓰고, 환경 보호 규칙으로 외부 기여자 PR은 수동 승인 후 실행되도록 설정하세요.

    # 외부 기여자 PR 실행 제한 예시
    on:
      pull_request:
        branches: [main]
    
    jobs:
      build:
        # 포크 PR의 경우 시크릿 접근 불가 (GitHub 기본 동작)
        # 자체 호스팅 러너는 내부 PR만 처리하도록 조건 추가
        if: github.event.pull_request.head.repo.full_name == github.repository
        runs-on: [self-hosted, linux]

    문제 2: 러너 업데이트 누락

    홈랩 운영하다 보면 러너 에이전트 업데이트를 깜빡하기 쉬워요. 저도 몇 달 방치했다가 나중에 보니 꽤 여러 버전이 밀려있더라고요. 자동 업데이트 설정을 켜두는 게 좋습니다.

    # 러너 자동 업데이트 설정 확인
    # .runner 파일에서 disableUpdate 옵션 확인
    cat /opt/actions-runner/.runner
    
    # 자동 업데이트가 비활성화되어 있다면
    # 주기적으로 업데이트 스크립트를 cron으로 실행
    # 에페머럴 러너라면 이미지 자체를 최신으로 유지하는 것이 더 좋습니다

    문제 3: 워크플로우 로그에 시크릿 노출

    워크플로우 실행 로그가 공개되는 경우, 시크릿이 실수로 echo 되거나 에러 메시지에 포함되는 경우가 있어요. GitHub은 등록된 시크릿 값을 자동으로 마스킹해주지만, 파생된 값(예: base64 인코딩된 시크릿)은 마스킹이 안 될 수 있거든요.

    # 파생 값도 마스킹하려면 add-mask 명령을 사용하세요
    - name: Mask derived secret
      run: |
        DERIVED_SECRET=$(echo "${{ secrets.MY_SECRET }}" | base64)
        echo "::add-mask::$DERIVED_SECRET"  # 이 값도 로그에서 마스킹됨
        echo "DERIVED_SECRET=$DERIVED_SECRET" >> $GITHUB_ENV

    보안 검증 — 설정이 제대로 됐는지 확인하기

    설정을 다 했다면 실제로 제대로 동작하는지 확인해봐야 하거든요. 제가 주기적으로 체크하는 항목들입니다.

    GitHub의 보안 권고 확인

    리포지토리 → Security → Code scanning alerts에서 워크플로우 파일의 보안 문제를 스캔할 수 있어요. CodeQL이나 actionlint를 CI에 넣어두면 자동으로 체크됩니다.

    # actionlint로 워크플로우 파일 정적 분석
    # 로컬에서 실행
    docker run --rm -v $(pwd):/repo rhysd/actionlint:latest -color /repo/.github/workflows/
    
    # CI에 통합
    - name: Lint GitHub Actions workflows
      uses: raven-actions/actionlint@v2
      with:
        files: .github/workflows/*.yml

    보안 강화 체크리스트 최종 점검

    • ✅ 러너가 특정 리포지토리/그룹에만 등록되어 있는가?
    • ✅ 에페머럴 모드로 실행되는가?
    • ✅ 워크플로우 기본 권한이 read-all 또는 그 이하인가?
    • ✅ 외부 액션이 SHA 해시로 핀닝되어 있는가?
    • ✅ Dependabot으로 액션 업데이트를 추적하는가?
    • ✅ 프로덕션 환경에 보호 규칙이 설정되어 있는가?
    • ✅ 장기 자격증명 대신 OIDC를 사용하는가?
    • ✅ 러너가 비권한 사용자로 실행되는가?
    • ✅ 러너 서버의 인바운드 포트가 닫혀 있는가?
    • ✅ 퍼블릭 리포지토리에 자체 호스팅 러너를 사용하지 않는가?
    GitHub Actions 자체 호스팅 러너 보안 강화 10가지 핵심 체크리스트 인포그래픽

    ▲ GitHub Actions 자체 호스팅 러너 보안 강화 요약 인포그래픽 — 10가지 핵심 체크리스트를 한눈에 확인하세요.


    마무리 — CI/CD 보안은 한 번이 아닙니다

    긴 글 읽어주셔서 감사합니다. 정리하자면, GitHub Actions 자체 호스팅 러너 보안의 핵심은 이렇습니다.

    1. 접근 최소화: 러너 범위를 필요한 리포지토리로만 제한
    2. 에페머럴 러너: 잡 실행 후 환경을 폐기해서 오염 방지
    3. 최소 권한: 워크플로우 권한을 필요한 것만 허용
    4. 공급망 공격 방어: 외부 액션을 SHA로 핀닝하고 Dependabot으로 관리
    5. 시크릿 보호: OIDC 활용, 환경 보호 규칙 설정
    6. 서버 하드닝: 비권한 사용자 실행, 네트워크 제한

    CI/CD 보안은 “한 번 설정하면 끝”이 아니거든요. 공급망 공격 기법은 계속 진화하고 있고, GitHub도 꾸준히 새로운 보안 기능을 추가하고 있어요. 저도 이 글 쓰면서 다시 한 번 제 홈랩 설정을 점검했는데, 고쳐야 할 부분이 몇 개 보이더라고요.

    💡 다음 글에서는 Kubernetes 기반의 Actions Runner Controller(ARC)를 활용한 에페머럴 러너 자동 스케일링 설정을 더 자세히 다룰 예정이에요. 홈랩에 k3s 올려서 직접 구성해본 경험을 공유해드릴 거니까 기대해주세요.

    궁금한 점이나 추가로 다뤄줬으면 하는 내용이 있으면 댓글로 남겨주세요. 제 경험이 여러분의 파이프라인을 조금 더 안전하게 만드는 데 도움이 됐으면 좋겠습니다. 🎉