<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>기록 저장소</title>
    <link>https://hwara-dev.tistory.com/</link>
    <description>배움의 기쁨</description>
    <language>ko</language>
    <pubDate>Mon, 8 Jun 2026 12:02:59 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>hwara_</managingEditor>
    <image>
      <title>기록 저장소</title>
      <url>https://tistory1.daumcdn.net/tistory/7862682/attach/c15520fe967c4a46af35f4110fb723db</url>
      <link>https://hwara-dev.tistory.com</link>
    </image>
    <item>
      <title>GitHub Actions에서 커밋 해시 기반 Docker 이미지 태그로 배포 자동화하기</title>
      <link>https://hwara-dev.tistory.com/24</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;GitHub Actions에서 커밋 해시 기반 Docker 이미지 태그로 배포 자동화하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 GitHub Actions를 이용해 Docker 이미지를 빌드하고, 커밋 해시 기반 태그로 이미지를 푸시한 뒤, Kubernetes 배포에 사용하는 Kustomize overlay 파일까지 자동으로 수정하는 과정을 정리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트에서는 로컬 Kubernetes 환경에 서비스를 배포하고 있으며, 이미지는 로컬 private registry에 푸시합니다. 이후 Argo CD가 Git 저장소의 변경 사항을 감지해 클러스터에 배포하는 GitOps 흐름을 사용하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;main&lt;/code&gt; 브랜치에 코드가 push됨&lt;/li&gt;
&lt;li&gt;GitHub Actions가 실행됨&lt;/li&gt;
&lt;li&gt;커밋 해시를 기반으로 Docker 이미지 태그 생성&lt;/li&gt;
&lt;li&gt;Docker Compose로 각 서비스 이미지 빌드 및 push&lt;/li&gt;
&lt;li&gt;Kustomize overlay의 이미지 태그 수정&lt;/li&gt;
&lt;li&gt;변경된 &lt;code&gt;kustomization.yaml&lt;/code&gt;을 다시 GitHub에 commit/push&lt;/li&gt;
&lt;li&gt;Argo CD가 변경 사항을 감지하고 배포&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현재 사용 중인 GitHub Actions 예시&lt;/h3&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;name: CD Local GitOps

on:
  push:
    branches:
      - main

  workflow_dispatch:

permissions: {}

concurrency:
  group: cd-local-gitops-main
  cancel-in-progress: false

env:
  REGISTRY: 172.25.46.10:32000

jobs:
  build-push-and-update-overlay:
    name: Build, push, and update local overlay
    if: github.actor != 'github-actions[bot]' &amp;amp;&amp;amp; github.ref_name == 'main'
    permissions:
      contents: write
    runs-on: [self-hosted, Linux, X64]
    steps:
      - name: Check out repository
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
        with:
          ref: ${{ github.ref_name }}
          persist-credentials: true

      - name: Check Docker access
        run: docker version

      - name: Set image tag
        run: |
          echo &quot;IMAGE_TAG=${GITHUB_SHA::12}&quot; &amp;gt;&amp;gt; &quot;$GITHUB_ENV&quot;
          echo &quot;TAG=${GITHUB_SHA::12}&quot; &amp;gt;&amp;gt; &quot;$GITHUB_ENV&quot;

      - name: Create Compose env placeholders
        shell: bash
        run: |
          set -euo pipefail

          services=(
            api-gateway
            user-service
            product-service
            order-service
            payment-service
            notification-service
          )

          for service in &quot;${services[@]}&quot;; do
            env_file=&quot;services/${service}/.env&quot;
            if [ ! -f &quot;${env_file}&quot; ]; then
              touch &quot;${env_file}&quot;
            fi
          done

      - name: Build and push service images
        shell: bash
        run: |
          set -euo pipefail

          docker compose -f docker/services.yaml build
          docker compose -f docker/services.yaml push

      - name: Set up Kustomize
        uses: imranismail/setup-kustomize@2ba527d4d055ab63514ba50a99456fc35684947f

      - name: Commit and push overlay update
        shell: bash
        run: |
          set -euo pipefail

          git fetch origin main
          git rebase origin/main

          services=(
            user-service
            product-service
            order-service
            payment-service
            notification-service
            api-gateway
          )

          pushd k8s/services/overlays/local
          for service in &quot;${services[@]}&quot;; do
            kustomize edit set image &quot;${service}=${REGISTRY}/${service}:${IMAGE_TAG}&quot;
          done
          popd

          if git diff --quiet -- k8s/services/overlays/local/kustomization.yaml; then
            echo &quot;No overlay image tag changes to commit.&quot;
            exit 0
          fi

          git config user.name &quot;github-actions[bot]&quot;
          git config user.email &quot;41898282+github-actions[bot]@users.noreply.github.com&quot;
          git add k8s/services/overlays/local/kustomization.yaml
          git commit -m &quot;chore: update local image tags ${IMAGE_TAG}&quot;
          git push&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Workflow 실행 조건 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 workflow의 실행 조건과 공통 설정은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;on:
  push:
    branches:
      - main

  workflow_dispatch:

permissions: {}

concurrency:
  group: cd-local-gitops-main
  cancel-in-progress: false

env:
  REGISTRY: 172.25.46.10:32000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;on.push.branches&lt;/code&gt;는 &lt;code&gt;main&lt;/code&gt; 브랜치에 commit이 push되었을 때 workflow를 실행한다는 의미입니다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;workflow_dispatch:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;workflow_dispatch&lt;/code&gt;는 GitHub Actions 페이지에서 사용자가 직접 workflow를 실행할 수 있도록 하는 설정입니다. 자동 실행뿐만 아니라 필요할 때 수동으로 배포를 실행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;permissions: {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역 &lt;code&gt;permissions&lt;/code&gt;는 비워두고, 실제 권한이 필요한 job에서만 필요한 권한을 부여하도록 구성했습니다. 이렇게 하면 workflow 전체에 불필요한 권한이 부여되는 것을 줄일 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;concurrency:
  group: cd-local-gitops-main
  cancel-in-progress: false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;concurrency&lt;/code&gt;는 동시에 실행되는 workflow를 제어하기 위한 설정입니다. 동일한 concurrency group을 사용하는 workflow가 중복 실행되지 않도록 제한할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정이 필요한 이유는 이미지 빌드와 배포가 동시에 여러 번 실행될 경우, 어떤 이미지 태그가 최신 상태인지 혼란스러워질 수 있기 때문입니다. 특히 GitOps 방식에서는 Git 저장소의 최종 상태가 실제 배포 상태의 기준이 되므로, 배포 관련 workflow의 동시 실행은 주의해서 제어하는 것이 좋습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;env:
  REGISTRY: 172.25.46.10:32000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;env&lt;/code&gt;에는 workflow에서 공통으로 사용할 환경변수를 선언했습니다. 여기서는 Docker 이미지를 push할 private registry 주소를 &lt;code&gt;REGISTRY&lt;/code&gt;로 정의했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Job 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 실제 배포 작업을 수행하는 job 설정입니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;jobs:
  build-push-and-update-overlay:
    name: Build, push, and update local overlay
    if: github.actor != 'github-actions[bot]' &amp;amp;&amp;amp; github.ref_name == 'main'
    permissions:
      contents: write
    runs-on: [self-hosted, Linux, X64]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;build-push-and-update-overlay&lt;/code&gt;는 job의 식별자입니다. GitHub Actions 화면에서는 &lt;code&gt;name&lt;/code&gt;에 지정한 &lt;code&gt;Build, push, and update local overlay&lt;/code&gt;라는 이름으로 표시됩니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;if: github.actor != 'github-actions[bot]' &amp;amp;&amp;amp; github.ref_name == 'main'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 조건은 workflow가 실행되더라도 실제 job이 실행될지 한 번 더 검사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 부분은 다음 조건입니다.&lt;/p&gt;
&lt;pre class=&quot;erlang-repl&quot;&gt;&lt;code&gt;github.actor != 'github-actions[bot]'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 workflow는 마지막 단계에서 &lt;code&gt;kustomization.yaml&lt;/code&gt;을 수정한 뒤 &lt;code&gt;github-actions[bot]&lt;/code&gt; 계정으로 다시 commit/push합니다. 만약 이 조건이 없다면, bot이 push한 commit으로 인해 workflow가 다시 실행될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이 조건은 GitHub Actions가 만든 commit으로 인해 workflow가 반복 실행되는 상황을 막기 위한 장치입니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;permissions:
  contents: write&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 job은 저장소의 파일을 수정하고 다시 push해야 하므로 &lt;code&gt;contents: write&lt;/code&gt; 권한이 필요합니다.&lt;/p&gt;
&lt;pre class=&quot;apache&quot;&gt;&lt;code&gt;runs-on: [self-hosted, Linux, X64]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;runs-on&lt;/code&gt;은 job을 실행할 runner를 지정합니다. 현재는 로컬 환경의 Docker와 private registry를 사용해야 하므로 GitHub-hosted runner가 아니라 self-hosted runner를 사용하도록 설정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 runner에는 Docker 명령어를 실행할 수 있는 환경이 미리 준비되어 있어야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Repository checkout&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;- name: Check out repository
  uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
  with:
    ref: ${{ github.ref_name }}
    persist-credentials: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 단계에서는 &lt;code&gt;actions/checkout&lt;/code&gt;을 사용해 현재 브랜치의 코드를 runner로 가져옵니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ref: ${{ github.ref_name }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 workflow가 실행된 브랜치를 checkout합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;persist-credentials: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 단계에서 GitHub에 commit/push를 해야 하므로 인증 정보를 유지합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker 사용 가능 여부 확인&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;- name: Check Docker access
  run: docker version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;self-hosted runner에서 Docker 명령어를 정상적으로 사용할 수 있는지 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계에서 실패한다면 runner 환경에서 Docker가 설치되어 있지 않거나, 현재 runner 사용자가 Docker에 접근할 권한이 없는 상태일 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커밋 해시 기반 이미지 태그 생성&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;- name: Set image tag
  run: |
    echo &quot;IMAGE_TAG=${GITHUB_SHA::12}&quot; &amp;gt;&amp;gt; &quot;$GITHUB_ENV&quot;
    echo &quot;TAG=${GITHUB_SHA::12}&quot; &amp;gt;&amp;gt; &quot;$GITHUB_ENV&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계에서는 현재 commit hash의 앞 12자리를 이미지 태그로 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 commit hash가 다음과 같다면,&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;a1b2c3d4e5f67890...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 태그는 다음처럼 생성됩니다.&lt;/p&gt;
&lt;pre class=&quot;dns&quot;&gt;&lt;code&gt;a1b2c3d4e5f6&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;latest&lt;/code&gt; 태그를 사용하면 실제 어떤 commit으로 빌드된 이미지인지 추적하기 어렵습니다. 반면 commit hash 기반 태그를 사용하면 특정 이미지가 어떤 코드 상태에서 만들어졌는지 추적하기 쉬워집니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;echo &quot;IMAGE_TAG=${GITHUB_SHA::12}&quot; &amp;gt;&amp;gt; &quot;$GITHUB_ENV&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;$GITHUB_ENV&lt;/code&gt;에 값을 기록하면 이후 step에서도 해당 환경변수를 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 &lt;code&gt;IMAGE_TAG&lt;/code&gt;와 &lt;code&gt;TAG&lt;/code&gt;를 모두 같은 값으로 설정했습니다. &lt;code&gt;docker compose&lt;/code&gt; 파일에서 &lt;code&gt;TAG&lt;/code&gt; 환경변수를 참조하고 있다면, 이 값이 이미지 태그로 사용됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker Compose용 env 파일 생성&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;- name: Create Compose env placeholders
  shell: bash
  run: |
    set -euo pipefail

    services=(
      api-gateway
      user-service
      product-service
      order-service
      payment-service
      notification-service
    )

    for service in &quot;${services[@]}&quot;; do
      env_file=&quot;services/${service}/.env&quot;
      if [ ! -f &quot;${env_file}&quot; ]; then
        touch &quot;${env_file}&quot;
      fi
    done&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 &lt;code&gt;docker compose&lt;/code&gt; 설정에서는 각 서비스의 &lt;code&gt;.env&lt;/code&gt; 파일을 참조하고 있습니다. 하지만 GitHub에 &lt;code&gt;.env&lt;/code&gt; 파일을 올리지 않는 경우, runner 환경에서는 해당 파일이 존재하지 않아 compose 실행 중 오류가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 방지하기 위해 각 서비스 디렉터리에 &lt;code&gt;.env&lt;/code&gt; 파일이 없으면 빈 파일을 생성하도록 했습니다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;set -euo pipefail&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 bash script를 더 안전하게 실행하기 위해 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;-e&lt;/code&gt;: 명령어 실패 시 즉시 종료&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-u&lt;/code&gt;: 선언되지 않은 변수를 사용하면 오류 처리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-o pipefail&lt;/code&gt;: pipe 중간에서 실패한 명령도 실패로 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker 이미지 빌드 및 Push&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;- name: Build and push service images
  shell: bash
  run: |
    set -euo pipefail

    docker compose -f docker/services.yaml build
    docker compose -f docker/services.yaml push&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계가 실제 Docker 이미지를 빌드하고 registry에 push하는 부분입니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;docker compose -f docker/services.yaml build&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker/services.yaml&lt;/code&gt;에 정의된 서비스 이미지를 빌드합니다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;docker compose -f docker/services.yaml push&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드된 이미지를 registry로 push합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 workflow에서는 직접 Docker 명령어를 실행하고 있지만, 상황에 따라 &lt;code&gt;docker/build-push-action&lt;/code&gt; 같은 미리 만들어진 GitHub Action을 사용할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 현재처럼 self-hosted runner에서 로컬 private registry를 사용하고, 여러 서비스를 compose 기반으로 한 번에 빌드하는 경우에는 직접 &lt;code&gt;docker compose&lt;/code&gt; 명령어를 실행하는 방식도 단순하고 직관적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kustomize 설치&lt;/h3&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;- name: Set up Kustomize
  uses: imranismail/setup-kustomize@2ba527d4d055ab63514ba50a99456fc35684947f&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지를 빌드하고 push한 뒤에는 Kubernetes manifest에서 사용하는 이미지 태그를 새 태그로 변경해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트에서는 Kustomize를 사용하고 있으므로, &lt;code&gt;kustomize edit set image&lt;/code&gt; 명령어를 실행하기 위해 Kustomize를 설치합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kustomize overlay 이미지 태그 수정 및 commit&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;- name: Commit and push overlay update
  shell: bash
  run: |
    set -euo pipefail

    git fetch origin main
    git rebase origin/main

    services=(
      user-service
      product-service
      order-service
      payment-service
      notification-service
      api-gateway
    )

    pushd k8s/services/overlays/local
    for service in &quot;${services[@]}&quot;; do
      kustomize edit set image &quot;${service}=${REGISTRY}/${service}:${IMAGE_TAG}&quot;
    done
    popd

    if git diff --quiet -- k8s/services/overlays/local/kustomization.yaml; then
      echo &quot;No overlay image tag changes to commit.&quot;
      exit 0
    fi

    git config user.name &quot;github-actions[bot]&quot;
    git config user.email &quot;41898282+github-actions[bot]@users.noreply.github.com&quot;
    git add k8s/services/overlays/local/kustomization.yaml
    git commit -m &quot;chore: update local image tags ${IMAGE_TAG}&quot;
    git push&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계에서는 Kustomize overlay에 정의된 서비스 이미지 태그를 새로 빌드한 이미지 태그로 변경합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 원격 저장소의 최신 상태를 가져온 뒤 rebase합니다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;git fetch origin main
git rebase origin/main&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 workflow가 실행되는 동안 다른 commit이 main에 추가되었을 수 있으므로, push 전에 최신 상태를 반영하기 위한 처리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 이미지 태그를 변경할 서비스 목록을 정의합니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;services=(
  user-service
  product-service
  order-service
  payment-service
  notification-service
  api-gateway
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Kustomize overlay 디렉터리로 이동합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;pushd k8s/services/overlays/local&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서비스에 대해 &lt;code&gt;kustomize edit set image&lt;/code&gt;를 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;for service in &quot;${services[@]}&quot;; do
  kustomize edit set image &quot;${service}=${REGISTRY}/${service}:${IMAGE_TAG}&quot;
done&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어를 실행하면 &lt;code&gt;kustomization.yaml&lt;/code&gt;의 이미지 설정이 새 태그로 변경됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업이 끝나면 다시 기존 working directory로 돌아옵니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;popd&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변경 사항이 없을 때는 commit하지 않기&lt;/h3&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;if git diff --quiet -- k8s/services/overlays/local/kustomization.yaml; then
  echo &quot;No overlay image tag changes to commit.&quot;
  exit 0
fi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 &lt;code&gt;kustomization.yaml&lt;/code&gt;에 실제 변경 사항이 있는지 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 사항이 없다면 굳이 commit을 만들 필요가 없으므로 workflow를 정상 종료합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GitHub Actions Bot 계정으로 commit/push&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;git config user.name &quot;github-actions[bot]&quot;
git config user.email &quot;41898282+github-actions[bot]@users.noreply.github.com&quot;
git add k8s/services/overlays/local/kustomization.yaml
git commit -m &quot;chore: update local image tags ${IMAGE_TAG}&quot;
git push&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 사항이 있다면 GitHub Actions bot 계정으로 commit을 생성하고 push합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 commit이 Git 저장소에 반영되면, Argo CD는 변경된 &lt;code&gt;kustomization.yaml&lt;/code&gt;을 기준으로 새로운 이미지 태그를 감지하고 Kubernetes 클러스터에 배포할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 설정을 통해 &lt;code&gt;main&lt;/code&gt; 브랜치에 push하거나 GitHub Actions 화면에서 workflow를 수동 실행했을 때 다음 작업을 자동화할 수 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;repository checkout&lt;/li&gt;
&lt;li&gt;commit hash 기반 이미지 태그 생성&lt;/li&gt;
&lt;li&gt;Docker Compose 기반 이미지 빌드&lt;/li&gt;
&lt;li&gt;private registry로 이미지 push&lt;/li&gt;
&lt;li&gt;Kustomize overlay의 이미지 태그 수정&lt;/li&gt;
&lt;li&gt;변경된 manifest 파일 commit/push&lt;/li&gt;
&lt;li&gt;Argo CD를 통한 GitOps 배포 연계&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 이미지 태그를 commit hash 기반으로 관리하면, 어떤 코드가 어떤 이미지로 빌드되었는지 추적하기 쉬워집니다. 또한 Kustomize overlay 파일까지 자동으로 수정하므로, 이미지 빌드 이후 Kubernetes 배포 manifest를 직접 수정하는 과정을 줄일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 응용하면 로컬 Kubernetes 환경뿐만 아니라 개발, 스테이징, 운영 환경별 overlay를 분리해 더 체계적인 GitOps 배포 흐름으로 확장할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/ko/actions/get-started/quickstart&quot;&gt;https://docs.github.com/ko/actions/get-started/quickstart&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Devops/GitHub Actions</category>
      <author>hwara_</author>
      <guid isPermaLink="true">https://hwara-dev.tistory.com/24</guid>
      <comments>https://hwara-dev.tistory.com/24#entry24comment</comments>
      <pubDate>Fri, 29 May 2026 12:32:21 +0900</pubDate>
    </item>
    <item>
      <title>Kustomize secretGenerator와 Argo CD 동기화 오류 해결하기</title>
      <link>https://hwara-dev.tistory.com/23</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Kustomize의 &lt;code&gt;secretGenerator&lt;/code&gt;를 사용해 Secret을 생성하던 중, Argo CD에서 동기화 오류가 발생한 문제를 정리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서는 &lt;code&gt;.env&lt;/code&gt; 파일을 기준으로 Secret을 생성해 사용할 수 있었지만, 해당 &lt;code&gt;.env&lt;/code&gt; 파일은 민감 정보이기 때문에 GitHub에 업로드하지 않았습니다. 그 결과 Argo CD가 Git 저장소를 기준으로 매니페스트를 생성하는 과정에서 &lt;code&gt;.env&lt;/code&gt; 파일을 찾지 못해 에러가 발생했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kustomize에서 다음과 같은 방식으로 Secret을 생성하고 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;secretGenerator:
  - name: user-database-secret
    envs:
      - secrets/user-database-secret.env&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Deployment에서는 해당 Secret을 참조해 환경 변수를 주입받는 구조였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 환경에서는 &lt;code&gt;secrets/user-database-secret.env&lt;/code&gt; 파일이 존재했기 때문에 정상적으로 동작했습니다. 하지만 이 파일은 데이터베이스 비밀번호 같은 민감 정보를 포함하고 있어 GitHub에는 업로드하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Argo CD에서 Application을 동기화하려고 하자 다음과 같은 에러가 발생했습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ComparisonError
Failed to load target state: failed to generate manifest for source 1 of 1: rpc error: code = Unknown desc = `kustomize build &amp;lt;path to cached source&amp;gt;/k8s/services/overlays/local` failed exit status 1: Error: loading KV pairs: env source files: [secrets/user-database-secret.env]: evalsymlink failure on '&amp;lt;path to cached source&amp;gt;/k8s/services/overlays/local/secrets/user-database-secret.env' : lstat &amp;lt;path to cached source&amp;gt;/k8s/services/overlays/local/secrets/user-database-secret.env: no such file or directory&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러의 핵심은 다음 부분입니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;secrets/user-database-secret.env: no such file or directory&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Argo CD는 Git 저장소에 있는 파일을 기준으로 매니페스트를 생성합니다. 즉, Argo CD가 &lt;code&gt;kustomization.yaml&lt;/code&gt;을 읽고 &lt;code&gt;kustomize build&lt;/code&gt;를 실행할 때, &lt;code&gt;secretGenerator&lt;/code&gt;에 명시된 &lt;code&gt;.env&lt;/code&gt; 파일도 함께 존재해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 구조는 다음과 같았습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Kustomize의 &lt;code&gt;secretGenerator&lt;/code&gt;는 &lt;code&gt;.env&lt;/code&gt; 파일을 읽어 Kubernetes Secret을 생성한다.&lt;/li&gt;
&lt;li&gt;Deployment는 생성된 Secret을 참조해 환경 변수를 사용한다.&lt;/li&gt;
&lt;li&gt;민감 정보가 포함된 &lt;code&gt;.env&lt;/code&gt; 파일은 GitHub에 업로드하지 않았다.&lt;/li&gt;
&lt;li&gt;Argo CD는 Git 저장소를 기준으로 &lt;code&gt;kustomize build&lt;/code&gt;를 실행한다.&lt;/li&gt;
&lt;li&gt;Git 저장소에는 &lt;code&gt;.env&lt;/code&gt; 파일이 없으므로 Secret 생성에 실패한다.&lt;/li&gt;
&lt;li&gt;결과적으로 Argo CD의 비교 및 동기화 과정에서 &lt;code&gt;ComparisonError&lt;/code&gt;가 발생한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면, &lt;b&gt;Kustomize가 필요로 하는 입력 파일은 Argo CD가 접근 가능한 Git 저장소 안에 있어야 하는데, Secret의 기반이 되는 &lt;code&gt;.env&lt;/code&gt; 파일이 Git에 없어서 발생한 문제&lt;/b&gt;였습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방법은 크게 몇 가지가 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Sealed Secrets 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sealed Secrets를 사용하면 Secret 값을 암호화된 형태로 Git에 저장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git에는 일반 Secret이 아니라 암호화된 &lt;code&gt;SealedSecret&lt;/code&gt; 리소스를 저장하고, 클러스터 내부의 컨트롤러가 이를 복호화해 실제 Kubernetes Secret을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 GitOps 흐름과 잘 맞지만, 별도의 Sealed Secrets 컨트롤러 설치와 키 관리가 필요합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. External Secrets Operator 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서는 External Secrets Operator, 즉 ESO를 사용하는 방법도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ESO는 AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager 같은 외부 Secret 저장소에서 값을 가져와 Kubernetes Secret으로 동기화할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 민감 정보를 Git에 직접 저장하지 않으면서도 Argo CD 기반 배포 흐름을 유지할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경이라면 단순히 Secret을 수동 생성하는 방식보다는 ESO 같은 Secret 관리 도구를 도입하는 것이 더 적절해 보입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Secret을 클러스터에 직접 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 로컬 테스트 환경이었기 때문에 가장 단순한 방식으로 해결했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;kustomization.yaml&lt;/code&gt;에서 &lt;code&gt;secretGenerator&lt;/code&gt; 설정을 제거하고, Secret을 미리 생성하는 스크립트를 별도로 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Secret 생성 스크립트를 실행한다.&lt;/li&gt;
&lt;li&gt;Kubernetes 클러스터에 Secret을 먼저 생성한다.&lt;/li&gt;
&lt;li&gt;Argo CD에서 Application을 Sync한다.&lt;/li&gt;
&lt;li&gt;Deployment는 이미 생성된 Secret을 참조한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Argo CD가 Kustomize를 실행할 때 더 이상 Git에 없는 &lt;code&gt;.env&lt;/code&gt; 파일을 참조하지 않도록 변경했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 방식으로 Secret을 미리 생성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl create secret generic user-database-secret \
  --from-env-file=secrets/user-database-secret.env \
  -n &amp;lt;namespace&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 동일한 이름의 Secret이 존재할 수 있다면 다음처럼 &lt;code&gt;dry-run&lt;/code&gt;과 &lt;code&gt;apply&lt;/code&gt;를 조합해 사용할 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl create secret generic user-database-secret \
  --from-env-file=secrets/user-database-secret.env \
  -n &amp;lt;namespace&amp;gt; \
  --dry-run=client -o yaml | kubectl apply -f -&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어에서 &lt;code&gt;&amp;lt;namespace&amp;gt;&lt;/code&gt;는 실제 Secret을 사용할 네임스페이스로 변경해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;적용 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;kustomization.yaml&lt;/code&gt;에서 &lt;code&gt;secretGenerator&lt;/code&gt;를 제거한 뒤, Secret을 먼저 생성하고 Argo CD Sync를 다시 실행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 Argo CD가 더 이상 Git에 없는 &lt;code&gt;.env&lt;/code&gt; 파일을 찾지 않았고, Application이 정상적으로 배포되는 것을 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 방식은 로컬 테스트 환경에서는 간단하게 적용할 수 있는 해결책이었습니다. 다만 운영 환경에서는 Secret을 수동으로 생성하는 방식이 관리 포인트를 늘릴 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 운영 환경에서는 다음과 같은 방식을 고려하는 것이 좋습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Sealed Secrets를 사용해 암호화된 Secret을 Git에 저장&lt;/li&gt;
&lt;li&gt;External Secrets Operator를 사용해 외부 Secret 저장소와 연동&lt;/li&gt;
&lt;li&gt;Secret 생성 및 갱신 방식을 배포 프로세스 안에서 명확하게 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제는 Kustomize의 &lt;code&gt;secretGenerator&lt;/code&gt; 자체 문제라기보다는, Argo CD가 Git 저장소를 기준으로 매니페스트를 생성한다는 점을 고려하지 않아 발생한 문제였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에는 &lt;code&gt;.env&lt;/code&gt; 파일이 존재했지만, GitHub에는 해당 파일이 없었고 Argo CD는 GitHub에 있는 파일만 기준으로 &lt;code&gt;kustomize build&lt;/code&gt;를 실행했습니다. 따라서 &lt;code&gt;secretGenerator&lt;/code&gt;가 참조하는 &lt;code&gt;.env&lt;/code&gt; 파일을 찾지 못해 배포가 실패했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 테스트 환경에서는 Secret을 미리 생성하는 방식으로 해결할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 운영 환경에서는 Secret을 GitOps 흐름 안에서 안전하게 관리할 수 있도록 Sealed Secrets나 External Secrets Operator 같은 도구를 사용하는 것이 더 적절합니다.&lt;/p&gt;</description>
      <category>Devops/Kubernetes</category>
      <author>hwara_</author>
      <guid isPermaLink="true">https://hwara-dev.tistory.com/23</guid>
      <comments>https://hwara-dev.tistory.com/23#entry23comment</comments>
      <pubDate>Tue, 26 May 2026 11:32:05 +0900</pubDate>
    </item>
    <item>
      <title>Helm으로 Argo CD 설치 후 Gateway API와 TLS로 접속 구성하기</title>
      <link>https://hwara-dev.tistory.com/22</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Kubernetes 클러스터에 Argo CD를 설치하고, Gateway API를 이용해 외부에서 접속할 수 있도록 구성하는 과정을 정리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치는 Helm Chart를 사용했으며, 외부 노출은 Gateway API를 통해 처리했습니다. 로컬 테스트 환경에서는 Argo CD가 생성한 인증서를 별도의 TLS Secret으로 복사해 Gateway에서 사용하도록 구성했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구성 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 구성 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Helm으로 Argo CD 설치&lt;/li&gt;
&lt;li&gt;Gateway에서 사용할 TLS Secret 준비&lt;/li&gt;
&lt;li&gt;Gateway와 HTTPRoute를 적용해 Argo CD Server 외부 노출&lt;/li&gt;
&lt;li&gt;초기 admin 계정으로 로그인&lt;/li&gt;
&lt;li&gt;Argo CD Application 리소스 생성&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 Gateway가 HTTPS를 종료하고, Argo CD Server에는 HTTP로 트래픽을 전달하는 방식을 사용합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Argo CD 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Argo CD 설치에 사용할 &lt;code&gt;argocd-values.yaml&lt;/code&gt; 파일을 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;configs:
  params:
    server.insecure: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 설정은 다음 값입니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;server.insecure: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 Argo CD Server가 자체적으로 HTTPS를 처리하지 않고 HTTP로 동작하도록 만드는 설정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 구성에서는 Gateway가 HTTPS TLS Termination을 담당하고, Argo CD Server에는 HTTP 포트인 &lt;code&gt;80&lt;/code&gt;으로 요청을 전달합니다. 따라서 Argo CD Server 입장에서는 HTTPS가 아닌 HTTP 요청을 받도록 설정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 역할을 나누면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Gateway: 외부 HTTPS 요청 수신 및 TLS 종료&lt;/li&gt;
&lt;li&gt;Argo CD Server: Gateway로부터 HTTP 요청 수신&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Helm으로 Argo CD를 설치합니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;helm repo add argo https://argoproj.github.io/argo-helm
helm repo update

helm upgrade --install my-argo-cd argo/argo-cd \
  --version 9.5.15 \
  -n argocd \
  --create-namespace \
  -f argocd-values.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 끝나면 환경에 맞게 Argo CD Server를 외부에 노출해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 테스트만 필요하다면 &lt;code&gt;kubectl port-forward&lt;/code&gt;를 사용할 수도 있습니다. 이번 구성에서는 Gateway API를 사용해 외부에서 접근할 수 있도록 구성했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Gateway TLS Secret 준비&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gateway API에서 HTTPS Listener를 사용하려면 TLS 인증서 Secret이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Argo CD Helm Chart는 기본적으로 &lt;code&gt;argocd-secret&lt;/code&gt;을 생성하며, 이 Secret 안에는 &lt;code&gt;tls.crt&lt;/code&gt;, &lt;code&gt;tls.key&lt;/code&gt; 데이터가 포함될 수 있습니다. 하지만 &lt;code&gt;argocd-secret&lt;/code&gt;의 타입은 일반적으로 &lt;code&gt;Opaque&lt;/code&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gateway API의 &lt;code&gt;certificateRefs&lt;/code&gt;에서 사용하는 인증서 Secret은 &lt;code&gt;kubernetes.io/tls&lt;/code&gt; 타입이어야 하므로, &lt;code&gt;argocd-secret&lt;/code&gt;을 그대로 Gateway의 인증서 Secret으로 사용할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 로컬 테스트 환경에서는 &lt;code&gt;argocd-secret&lt;/code&gt;에 들어 있는 인증서 데이터를 별도의 TLS Secret으로 복사했습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl get secret argocd-secret -n argocd \
  -o jsonpath='{.data.tls\.crt}' | base64 -d &amp;gt; /tmp/argocd-tls.crt

kubectl get secret argocd-secret -n argocd \
  -o jsonpath='{.data.tls\.key}' | base64 -d &amp;gt; /tmp/argocd-tls.key

kubectl create secret tls argocd-gateway-tls \
  -n argocd \
  --cert=/tmp/argocd-tls.crt \
  --key=/tmp/argocd-tls.key \
  --dry-run=client -o yaml | kubectl apply -f -&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어는 다음 순서로 동작합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;argocd-secret&lt;/code&gt;에서 &lt;code&gt;tls.crt&lt;/code&gt; 값을 꺼내 인증서 파일로 저장&lt;/li&gt;
&lt;li&gt;&lt;code&gt;argocd-secret&lt;/code&gt;에서 &lt;code&gt;tls.key&lt;/code&gt; 값을 꺼내 개인키 파일로 저장&lt;/li&gt;
&lt;li&gt;두 파일을 사용해 &lt;code&gt;kubernetes.io/tls&lt;/code&gt; 타입의 Secret 생성&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성되는 Secret 이름은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;argocd-gateway-tls&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Secret은 이후 Gateway의 &lt;code&gt;certificateRefs&lt;/code&gt;에서 참조됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서는 이처럼 Argo CD 내부 Secret을 복사하기보다는, 일반적으로 &lt;code&gt;cert-manager&lt;/code&gt;나 외부 인증서 관리 방식을 사용하는 것이 좋습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Gateway와 HTTPRoute 적용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Gateway API 리소스를 작성해 Argo CD Server를 외부에 노출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Gateway YAML을 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: argocd-gateway
  namespace: argocd
spec:
  gatewayClassName: eg
  listeners:
    - protocol: HTTPS
      port: 443
      name: argocd-gateway
      allowedRoutes:
        namespaces:
          from: Same
      tls:
        mode: Terminate
        certificateRefs:
          - name: argocd-gateway-tls
            kind: Secret
            group: &quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Gateway는 &lt;code&gt;443&lt;/code&gt; 포트에서 HTTPS 요청을 받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 설정은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;gatewayClassName: eg&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용할 GatewayClass를 지정합니다. 이 값은 클러스터에 설치된 Gateway Controller에 따라 달라집니다. 예제에서는 &lt;code&gt;eg&lt;/code&gt;를 사용했습니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;tls:
  mode: Terminate&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gateway에서 TLS를 종료한다는 의미입니다. 즉, 외부 클라이언트는 HTTPS로 Gateway에 접근하지만, Gateway 뒤쪽의 Argo CD Server로는 HTTP 요청이 전달됩니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;certificateRefs:
  - name: argocd-gateway-tls&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 생성한 TLS Secret을 참조합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 HTTPRoute YAML을 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: argocd-http-route
  namespace: argocd
spec:
  parentRefs:
    - name: argocd-gateway
  rules:
    - backendRefs:
        - name: my-argo-cd-argocd-server
          port: 80&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 HTTPRoute는 &lt;code&gt;argocd-gateway&lt;/code&gt;로 들어온 요청을 Argo CD Server Service로 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;backendRefs.name&lt;/code&gt;에는 Argo CD Server Service 이름을 지정합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;backendRefs:
  - name: my-argo-cd-argocd-server
    port: 80&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Helm 설치 시 release 이름을 &lt;code&gt;my-argo-cd&lt;/code&gt;로 지정했기 때문에, Argo CD Server Service 이름은 다음과 같은 형태가 됩니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;my-argo-cd-argocd-server&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 Service 이름은 Helm release 이름이나 Chart 버전에 따라 달라질 수 있으므로, &lt;br /&gt;적용 전에 &lt;code&gt;kubectl get svc -n argocd&lt;/code&gt;로 확인하는 것이 좋습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성한 리소스를 적용합니다.&lt;/p&gt;
&lt;pre class=&quot;tcl&quot;&gt;&lt;code&gt;kubectl apply -f k8s/argocd/argocd-gateway.yaml
kubectl apply -f k8s/argocd/argocd-http-route.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용 후 Gateway와 HTTPRoute 상태를 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;kubectl describe gateway argocd-gateway -n argocd
kubectl describe httproute argocd-http-route -n argocd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gateway와 HTTPRoute가 정상적으로 연결되었다면, Gateway Controller가 할당한 주소를 통해 Argo CD UI에 접근할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 초기 로그인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Argo CD의 초기 admin 비밀번호는 &lt;code&gt;argocd-initial-admin-secret&lt;/code&gt; Secret에 저장되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 명령어로 초기 비밀번호를 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl get secret argocd-initial-admin-secret -n argocd \
  -o jsonpath='{.data.password}' | base64 -d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 계정 정보는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;Username: admin
Password: 위 명령어로 확인한 값&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 비밀번호로 로그인한 뒤에는 보안을 위해 비밀번호를 변경합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Argo CD UI에서 다음 메뉴로 이동합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;User Info -&amp;gt; UPDATE PASSWORD&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호를 변경한 뒤에는 기존 초기 비밀번호 Secret이 더 이상 필요하지 않은지 확인하고, 운영 환경에서는 적절히 정리하는 것이 좋습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Application 적용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Argo CD는 &lt;code&gt;Application&lt;/code&gt;이라는 Kubernetes CRD를 통해 Git Repository와 배포 대상을 정의합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application은 UI에서 생성할 수도 있고, YAML로 작성해 Kubernetes에 직접 적용할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 구성에서는 Application YAML을 작성해 직접 적용했습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: micro-mart-local
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/Hwara/micro-mart.git
    targetRevision: main
    path: k8s/services/overlays/local
  destination:
    server: https://kubernetes.default.svc
    namespace: micro-mart-local
  syncPolicy: {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 설정의 의미는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;project: default&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application이 속할 Argo CD Project를 지정합니다. 별도로 Project를 생성하지 않았다면 기본값인 &lt;code&gt;default&lt;/code&gt;를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;source:
  repoURL: https://github.com/Hwara/micro-mart.git
  targetRevision: main
  path: k8s/services/overlays/local&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;source&lt;/code&gt;는 Argo CD가 추적할 Git 저장소와 배포 대상 매니페스트 위치를 정의합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;repoURL&lt;/code&gt;: Argo CD가 추적할 Git Repository URL&lt;/li&gt;
&lt;li&gt;&lt;code&gt;targetRevision&lt;/code&gt;: 추적할 브랜치, 태그 또는 커밋&lt;/li&gt;
&lt;li&gt;&lt;code&gt;path&lt;/code&gt;: Repository 내부에서 Kubernetes YAML 또는 Kustomize 설정이 위치한 경로&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제에서는 &lt;code&gt;main&lt;/code&gt; 브랜치의 &lt;code&gt;k8s/services/overlays/local&lt;/code&gt; 경로를 기준으로 배포 상태를 추적합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;destination:
  server: https://kubernetes.default.svc
  namespace: micro-mart-local&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;destination&lt;/code&gt;은 배포할 Kubernetes 클러스터와 네임스페이스를 지정합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;server&lt;/code&gt;: 배포 대상 클러스터 주소&lt;/li&gt;
&lt;li&gt;&lt;code&gt;namespace&lt;/code&gt;: 리소스를 배포할 네임스페이스&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;https://kubernetes.default.svc&lt;/code&gt;는 Argo CD가 설치된 현재 클러스터를 의미합니다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;syncPolicy: {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제에서는 자동 동기화를 설정하지 않았습니다. 따라서 Git Repository의 변경 사항이 감지되더라도 자동으로 배포되지는 않고, 사용자가 Argo CD UI나 CLI에서 수동으로 Sync를 실행해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 동기화를 사용하고 싶다면 &lt;code&gt;syncPolicy.automated&lt;/code&gt; 설정을 추가할 수 있습니다. 다만 운영 환경에서는 자동 동기화 적용 여부를 신중히 결정하는 것이 좋습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Argo CD UI에서 Application 상태 확인 및 수동 Sync&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application YAML을 Kubernetes에 적용했다면, 외부 노출한 Argo CD 주소로 접속합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 후 왼쪽 메뉴 또는 메인 화면의 &lt;b&gt;Applications&lt;/b&gt; 탭으로 이동하면, 앞에서 생성한 Application을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1421&quot; data-origin-height=&quot;1271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqTvpQ/dJMcaaMawNg/GRWMdyTSt7vQxathkNSHoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqTvpQ/dJMcaaMawNg/GRWMdyTSt7vQxathkNSHoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqTvpQ/dJMcaaMawNg/GRWMdyTSt7vQxathkNSHoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqTvpQ%2FdJMcaaMawNg%2FGRWMdyTSt7vQxathkNSHoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1421&quot; height=&quot;1271&quot; data-origin-width=&quot;1421&quot; data-origin-height=&quot;1271&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application 카드에서는 현재 배포 상태를 간단히 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 확인할 값은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;APP HEALTH&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션이 Kubernetes 클러스터에서 정상 동작 중인지 나타냅니다.&lt;/li&gt;
&lt;li&gt;예를 들어 리소스가 정상적으로 실행 중이면 &lt;code&gt;Healthy&lt;/code&gt;로 표시됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SYNC STATUS&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Git Repository에 정의된 상태와 실제 Kubernetes 클러스터 상태가 일치하는지 나타냅니다.&lt;/li&gt;
&lt;li&gt;Git의 매니페스트와 클러스터 상태가 일치하면 &lt;code&gt;Synced&lt;/code&gt;, 차이가 있으면 &lt;code&gt;OutOfSync&lt;/code&gt;로 표시됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 Application을 클릭하면 더 자세한 리소스 상태를 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2511&quot; data-origin-height=&quot;1294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GkANz/dJMcada0rG9/px1bB7TdvqmrABKeflH4sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GkANz/dJMcada0rG9/px1bB7TdvqmrABKeflH4sk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GkANz/dJMcada0rG9/px1bB7TdvqmrABKeflH4sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGkANz%2FdJMcada0rG9%2Fpx1bB7TdvqmrABKeflH4sk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2511&quot; height=&quot;1294&quot; data-origin-width=&quot;2511&quot; data-origin-height=&quot;1294&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세 화면에서는 Application에 포함된 Kubernetes 리소스들이 어떤 상태인지 시각적으로 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이를 통해 특정 리소스가 정상적으로 생성되었는지, Pod가 정상 실행 중인지, Service나 Deployment에 문제가 없는지 확인할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;수동 Sync 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제에서는 Application YAML에 자동 동기화 설정을 추가하지 않았습니다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;syncPolicy: {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Git Repository에 있는 매니페스트를 실제 클러스터에 반영하려면 Argo CD UI에서 수동으로 Sync를 실행해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수동 Sync는 다음 순서로 진행합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Argo CD UI에서 Application을 클릭합니다.&lt;/li&gt;
&lt;li&gt;상단의 &lt;b&gt;SYNC&lt;/b&gt; 버튼을 클릭합니다.&lt;/li&gt;
&lt;li&gt;Sync할 Revision과 옵션을 확인합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SYNCHRONIZE&lt;/b&gt; 버튼을 클릭합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;568&quot; data-origin-height=&quot;1283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bs96Be/dJMcahYPOLG/47NOS5y1jydu57XzfE5BrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bs96Be/dJMcahYPOLG/47NOS5y1jydu57XzfE5BrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bs96Be/dJMcahYPOLG/47NOS5y1jydu57XzfE5BrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbs96Be%2FdJMcahYPOLG%2F47NOS5y1jydu57XzfE5BrK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;568&quot; height=&quot;1283&quot; data-origin-width=&quot;568&quot; data-origin-height=&quot;1283&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SYNCHRONIZE&lt;/code&gt;를 실행하면 Argo CD가 Application에 등록된 GitHub Repository의 파일을 기준으로 Kubernetes 클러스터에 리소스를 배포합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 앞에서 작성한 Application의 다음 설정을 기준으로 동작합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;source:
  repoURL: https://github.com/Hwara/micro-mart.git
  targetRevision: main
  path: k8s/services/overlays/local
destination:
  server: https://kubernetes.default.svc
  namespace: micro-mart-local&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Argo CD는 &lt;code&gt;repoURL&lt;/code&gt;의 &lt;code&gt;targetRevision&lt;/code&gt; 브랜치에서 &lt;code&gt;path&lt;/code&gt;에 있는 Kubernetes 매니페스트를 읽고, &lt;code&gt;destination&lt;/code&gt;에 지정된 클러스터와 네임스페이스에 적용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sync가 완료되면 Application의 상태가 변경됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 반영되었다면 다음과 같은 상태를 기대할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;APP HEALTH: Healthy
SYNC STATUS: Synced&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Sync 상태 확인과 롤백&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application 상세 화면에서는 단순히 현재 상태만 확인하는 것이 아니라, Sync 이력과 롤백 기능도 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Argo CD UI에서는 다음 메뉴를 통해 배포 상태를 확인할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SYNC STATUS&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Git Repository와 실제 클러스터 상태가 일치하는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HISTORY AND ROLLBACK&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이전 Sync 이력 확인&lt;/li&gt;
&lt;li&gt;특정 Revision으로 롤백 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 GitOps 방식에서는 Git의 변경 이력이 곧 배포 이력이 되기 때문에, 어떤 Commit 또는 Revision이 클러스터에 반영되었는지 확인하는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 발생했을 때는 &lt;code&gt;HISTORY AND ROLLBACK&lt;/code&gt; 메뉴에서 이전에 정상 동작하던 Revision을 확인하고, 필요하다면 해당 Revision으로 롤백할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;명령어로 Application 상태 확인하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI뿐만 아니라 &lt;code&gt;kubectl&lt;/code&gt; 명령어로도 Application 상태를 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;kubectl get application -n argocd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 Application의 상세 상태를 확인하려면 다음 명령어를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;kubectl describe application micro-mart-local -n argocd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 대상 네임스페이스에 실제 리소스가 생성되었는지도 함께 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;kubectl get all -n micro-mart-local&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod 상태를 조금 더 자세히 확인하려면 다음 명령어를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;kubectl get pods -n micro-mart-local&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 Pod에 문제가 있다면 &lt;code&gt;describe&lt;/code&gt; 또는 &lt;code&gt;logs&lt;/code&gt; 명령어로 원인을 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;kubectl describe pod &amp;lt;pod-name&amp;gt; -n micro-mart-local
kubectl logs &amp;lt;pod-name&amp;gt; -n micro-mart-local&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 Argo CD UI에서 보이는 상태와 실제 Kubernetes 리소스 상태를 함께 비교할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Helm으로 Argo CD를 설치하고, Gateway API를 통해 외부에서 접근할 수 있도록 구성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 Argo CD Server를 &lt;code&gt;server.insecure: true&lt;/code&gt;로 설정하고, HTTPS 처리는 Gateway에서 담당하도록 역할을 나누는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Gateway API의 &lt;code&gt;certificateRefs&lt;/code&gt;는 &lt;code&gt;kubernetes.io/tls&lt;/code&gt; 타입 Secret을 요구하므로, 로컬 테스트 환경에서는 Argo CD가 생성한 인증서 데이터를 별도의 TLS Secret으로 복사해 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Argo CD Application 리소스를 생성해 Git Repository의 특정 경로를 Kubernetes 배포 대상으로 등록했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구성을 통해 Git에 정의된 Kubernetes 매니페스트를 기준으로 실제 클러스터 상태를 추적하고, 필요할 때 수동으로 Sync할 수 있는 GitOps 기반 배포 환경을 만들 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application을 생성한 뒤에는 Argo CD UI에서 APP HEALTH와 SYNC STATUS를 통해 배포 상태를 확인할 수 있습니다. 자동 동기화를 설정하지 않은 경우에는 SYNC 버튼을 눌러 수동으로 Git Repository의 매니페스트를 클러스터에 반영해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 HISTORY AND ROLLBACK 메뉴를 통해 이전 Sync 이력을 확인할 수 있으며, 문제가 발생했을 때 특정 Revision으로 롤백할 수도 있습니다. 이를 통해 Git Repository를 기준으로 Kubernetes 리소스의 배포 상태를 추적하고 관리할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://argo-cd.readthedocs.io/en/stable/operator-manual/ingress/&quot;&gt;https://argo-cd.readthedocs.io/en/stable/operator-manual/ingress/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Devops/Kubernetes</category>
      <author>hwara_</author>
      <guid isPermaLink="true">https://hwara-dev.tistory.com/22</guid>
      <comments>https://hwara-dev.tistory.com/22#entry22comment</comments>
      <pubDate>Tue, 26 May 2026 10:30:19 +0900</pubDate>
    </item>
    <item>
      <title>GitHub Actions Self-hosted Runner를 WSL 환경에 설치하고 서비스로 실행하기</title>
      <link>https://hwara-dev.tistory.com/21</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions는 기본적으로 GitHub에서 제공하는 hosted runner를 사용할 수 있습니다. 하지만 로컬 환경에서만 접근 가능한 리소스를 사용해야 하거나, 사설 레지스트리&amp;middot;로컬 Kubernetes 클러스터&amp;middot;내부 네트워크 환경에서 빌드와 배포를 수행해야 하는 경우에는 &lt;b&gt;self-hosted runner&lt;/b&gt;를 구성하는 것이 유용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;Windows 사용자 기준으로, Ubuntu WSL 개발 환경에 GitHub Actions self-hosted runner를 설치하고 서비스로 실행하는 과정&lt;/b&gt;을 정리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 개발 환경은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Windows에서 개발 진행&lt;/li&gt;
&lt;li&gt;Ubuntu WSL을 주 개발 환경으로 사용&lt;/li&gt;
&lt;li&gt;Docker는 Windows에 설치된 Docker Desktop 사용&lt;/li&gt;
&lt;li&gt;GitHub Actions job은 Ubuntu WSL에 설치한 self-hosted runner에서 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서는 Docker가 Windows Docker Desktop에서 실행되지만, 실제 빌드 명령은 Ubuntu WSL 환경에서 실행됩니다. 따라서 WSL 내부에서 &lt;code&gt;docker&lt;/code&gt; 명령어를 사용할 수 있도록 &lt;b&gt;Docker Desktop과 Ubuntu WSL을 연결하는 과정&lt;/b&gt;이 필요합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Self-hosted runner 비용 관련 주의사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Self-hosted runner를 사용할 때는 비용 정책도 함께 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub 공식 문서 기준으로 &lt;b&gt;현재 GitHub Actions의 self-hosted runner 사용은 무료&lt;/b&gt;로 안내되어 있습니다. 또한 public repository에서 standard GitHub-hosted runner를 사용하는 경우도 무료로 제공됩니다. (&lt;a title=&quot;GitHub Actions billing&quot; href=&quot;https://docs.github.com/billing/managing-billing-for-github-actions/about-billing-for-github-actions?utm_source=chatgpt.com&quot;&gt;GitHub Docs&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 GitHub는 한때 &lt;b&gt;2026년 3월 1일부터 self-hosted runner 사용에 대해 분당 &lt;code&gt;$0.002&lt;/code&gt;의 GitHub Actions cloud platform charge를 부과하겠다고 발표&lt;/b&gt;한 적이 있습니다. 이 발표에 따르면 public repository의 runner 사용은 계속 무료로 유지되지만, private repository에서 self-hosted runner를 사용하는 경우 과금 대상이 될 수 있었습니다. (&lt;a title=&quot;Update to GitHub Actions pricing - GitHub Changelog&quot; href=&quot;https://github.blog/changelog/2025-12-16-coming-soon-simpler-pricing-and-a-better-experience-for-github-actions/?utm_source=chatgpt.com&quot;&gt;The GitHub Blog&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 GitHub는 커뮤니티 피드백을 반영해 &lt;b&gt;self-hosted GitHub Actions에 대한 과금 변경을 보류&lt;/b&gt;하고, 접근 방식을 재검토하겠다고 공지했습니다. 따라서 글을 작성하는 시점에는 &amp;ldquo;self-hosted runner는 무료&amp;rdquo;로 이해할 수 있지만, &lt;b&gt;GitHub Actions 요금 정책은 변경될 수 있으므로 실제 운영 환경에서 사용하기 전에는 공식 billing 문서를 다시 확인&lt;/b&gt;하는 것이 좋습니다. (&lt;a title=&quot;Pricing changes for GitHub Actions&quot; href=&quot;https://github.com/resources/insights/2026-pricing-changes-for-github-actions?utm_source=chatgpt.com&quot;&gt;GitHub&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 변동사항이 생긴다면 댓글로 알려주시면 감사하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 기준 self-hosted runner 사용은 무료로 안내되어 있음&lt;/li&gt;
&lt;li&gt;2026년 3월 1일부터 분당 &lt;code&gt;$0.002&lt;/code&gt; 과금을 적용하겠다는 발표가 있었음&lt;/li&gt;
&lt;li&gt;이후 GitHub가 해당 변경을 보류하고 재검토하겠다고 공지함&lt;/li&gt;
&lt;li&gt;public repository와 private repository의 과금 정책은 다를 수 있으므로 운영 전 공식 문서 확인 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WSL에서 Docker 사용할 수 있도록 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows에서 Docker Desktop을 사용하고 Ubuntu WSL에서 개발하는 경우, WSL 내부에서 Docker 명령어를 실행하려면 Docker Desktop의 WSL Integration을 활성화해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Desktop에서 다음 경로로 이동합니다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;Settings -&amp;gt; Resources -&amp;gt; WSL Integration&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 설치해 둔 Ubuntu 배포판을 활성화한 뒤 &lt;code&gt;Apply&lt;/code&gt;를 클릭합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정을 적용하면 Ubuntu WSL 터미널에서 Windows Docker Desktop의 Docker Engine을 사용할 수 있습니다. 즉, WSL 내부에서 다음과 같은 Docker 명령어를 실행할 수 있게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;docker version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 연결되었다면 WSL 터미널에서도 &lt;code&gt;docker build&lt;/code&gt;, &lt;code&gt;docker push&lt;/code&gt; 같은 명령어를 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정이 필요한 이유는 self-hosted runner가 Ubuntu WSL에서 실행되기 때문입니다. GitHub Actions workflow 안에서 Docker build를 수행하려면, runner가 실행되는 WSL 환경에서도 Docker CLI를 사용할 수 있어야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Self-hosted runner 다운로드 및 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub 저장소에서 self-hosted runner를 등록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub 저장소의 다음 경로로 이동합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Repository -&amp;gt; Settings -&amp;gt; Actions -&amp;gt; Runners -&amp;gt; New self-hosted runner&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 화면에 표시되는 안내에 따라 runner를 다운로드하고 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Runner 패키지 다운로드&lt;/li&gt;
&lt;li&gt;압축 해제&lt;/li&gt;
&lt;li&gt;&lt;code&gt;config.sh&lt;/code&gt; 실행&lt;/li&gt;
&lt;li&gt;GitHub에서 제공하는 URL과 token을 사용해 runner 등록&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;mkdir actions-runner
cd actions-runner

# GitHub 안내에 표시되는 명령어를 사용해 runner 다운로드
# 예: curl -o actions-runner-linux-x64-...tar.gz -L ...

tar xzf ./actions-runner-linux-x64-*.tar.gz

# GitHub 안내에 표시되는 URL과 token을 사용해 설정
./config.sh --url &amp;lt;repository-url&amp;gt; --token &amp;lt;runner-token&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 다운로드 URL과 token은 GitHub에서 runner를 생성할 때마다 달라질 수 있으므로, GitHub 화면에 표시되는 명령어를 그대로 사용하는 것이 좋습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Self-hosted runner를 서비스로 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Self-hosted runner는 기본적으로 다음 명령어로 실행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;./run.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 방식은 터미널을 열어둔 상태에서 직접 실행해야 하므로 번거롭습니다. 따라서 runner를 서비스로 등록해두면 백그라운드에서 실행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 runner를 다운로드하고 설정한 디렉터리로 이동합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;cd actions-runner&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 설치합니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;sudo ./svc.sh install&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 시작합니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;sudo ./svc.sh start&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 상태를 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;sudo ./svc.sh status&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 실행 중이라면 runner가 GitHub Actions 작업을 받을 준비가 된 상태입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Self-hosted runner 서비스 중지 및 제거&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 이상 runner를 사용하지 않거나 설정을 다시 구성해야 한다면 서비스를 중지하고 제거할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 중지합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;sudo ./svc.sh stop&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 제거합니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;sudo ./svc.sh uninstall&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요하다면 GitHub 저장소의 runner 목록에서도 해당 runner를 제거할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Repository -&amp;gt; Settings -&amp;gt; Actions -&amp;gt; Runners&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Runner 등록 상태 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub 저장소의 다음 경로로 이동합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Repository -&amp;gt; Settings -&amp;gt; Actions -&amp;gt; Runners&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 등록한 runner가 목록에 표시되는지 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Runner의 상태가 &lt;code&gt;Idle&lt;/code&gt; 또는 &lt;code&gt;Active&lt;/code&gt;로 표시된다면 정상적으로 등록된 것입니다. 반대로 &lt;code&gt;Offline&lt;/code&gt; 상태라면 runner 서비스가 실행 중인지 확인해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;sudo ./svc.sh status&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GitHub Actions workflow에서 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Self-hosted runner를 사용하려면 workflow의 job에서 &lt;code&gt;runs-on&lt;/code&gt; 값을 self-hosted runner 라벨에 맞게 지정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같이 작성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;apache&quot;&gt;&lt;code&gt;runs-on: [self-hosted, Linux, X64]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 다음 조건을 만족하는 runner에서 해당 job을 실행하겠다는 의미입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;self-hosted&lt;/code&gt; 라벨을 가진 runner&lt;/li&gt;
&lt;li&gt;Linux 환경의 runner&lt;/li&gt;
&lt;li&gt;X64 아키텍처의 runner&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;jobs:
  build-push-and-update-overlay:
    name: Build, push, and update local overlay
    permissions:
      contents: write
    runs-on: [self-hosted, Linux, X64]
    steps:
      - name: Check out repository
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
        with:
          ref: ${{ github.ref_name }}
          persist-credentials: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 설정하면 GitHub Actions가 해당 job을 실행할 때 GitHub-hosted runner가 아니라, 직접 등록한 self-hosted runner를 사용합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Windows 사용자가 Ubuntu WSL 개발 환경에서 GitHub Actions self-hosted runner를 설치하고 서비스로 실행하는 과정을 정리했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Windows Docker Desktop에서 WSL Integration 활성화&lt;/li&gt;
&lt;li&gt;Ubuntu WSL에서 Docker 명령어가 동작하는지 확인&lt;/li&gt;
&lt;li&gt;GitHub 저장소에서 self-hosted runner 생성&lt;/li&gt;
&lt;li&gt;Ubuntu WSL에 runner 다운로드 및 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;svc.sh&lt;/code&gt;를 이용해 runner를 서비스로 등록&lt;/li&gt;
&lt;li&gt;GitHub Actions workflow의 &lt;code&gt;runs-on&lt;/code&gt;에 self-hosted runner 라벨 지정&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Self-hosted runner를 사용하면 로컬 네트워크, 사설 Docker Registry, 로컬 Kubernetes 클러스터처럼 GitHub-hosted runner에서 접근하기 어려운 리소스를 활용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 self-hosted runner는 사용자의 로컬 머신 또는 서버에서 직접 명령을 실행하므로 보안에 주의해야 합니다. 특히 public repository에서 외부 PR이 self-hosted runner에서 실행되지 않도록 workflow trigger와 권한 설정을 신중하게 관리하는 것이 좋습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GitHub Docs - Adding self-hosted runners&lt;/li&gt;
&lt;li&gt;GitHub Docs - Configuring the self-hosted runner application as a service&lt;/li&gt;
&lt;li&gt;GitHub Docs - GitHub Actions billing&lt;/li&gt;
&lt;li&gt;GitHub Blog - GitHub Actions pricing changes&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Devops/GitHub Actions</category>
      <author>hwara_</author>
      <guid isPermaLink="true">https://hwara-dev.tistory.com/21</guid>
      <comments>https://hwara-dev.tistory.com/21#entry21comment</comments>
      <pubDate>Mon, 25 May 2026 14:33:08 +0900</pubDate>
    </item>
    <item>
      <title>Kubernetes kubelet의 컨테이너 로그 로테이션 설정 정리</title>
      <link>https://hwara-dev.tistory.com/20</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes에서 컨테이너 로그는 노드에 계속 쌓일 수 있기 때문에, 별도의 관리가 없다면 디스크 사용량 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 kubelet은 기본적으로 컨테이너 로그에 대해 로테이션을 수행합니다. 즉, 로그 파일이 일정 크기를 넘으면 새 파일로 교체하고, 오래된 로그 파일은 일정 개수까지만 유지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 kubelet의 컨테이너 로그 로테이션 기본 설정과, 로그 보관 시 주의할 점을 정리합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;kubelet의 기본 컨테이너 로그 로테이션 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes 공식 문서에 따르면 kubelet은 컨테이너 로그 로테이션을 위해 다음 설정을 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;containerLogMaxSize: 10Mi
containerLogMaxFiles: 5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 설정의 의미는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;containerLogMaxSize&lt;/code&gt;: 컨테이너 로그 파일 하나의 최대 크기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;containerLogMaxFiles&lt;/code&gt;: 컨테이너 하나당 유지할 로그 파일 개수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본값 기준으로 보면, 컨테이너 로그 파일 하나가 &lt;code&gt;10Mi&lt;/code&gt;를 넘으면 로그 로테이션이 발생합니다. 그리고 컨테이너 하나당 최대 &lt;code&gt;5&lt;/code&gt;개의 로그 파일만 유지됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 단순 계산으로는 컨테이너 하나당 최대 약 &lt;code&gt;50Mi&lt;/code&gt; 정도의 로그 파일이 노드에 저장될 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;10Mi &amp;times; 5 files = 50Mi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이는 컨테이너 하나를 기준으로 한 값입니다. 실제 노드 전체 로그 사용량은 실행 중인 Pod 수, 컨테이너 수, 로그 발생량에 따라 훨씬 커질 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;kubelet 로그 로테이션의 목적&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubelet의 로그 로테이션은 일반적인 &lt;code&gt;logrotate&lt;/code&gt;처럼 로그를 압축해서 장기 보관하기 위한 기능은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목적은 다음에 더 가깝습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 로그가 노드에 무한히 쌓여 디스크를 계속 사용하는 상황을 방지하는 것&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, kubelet의 로그 로테이션은 노드 디스크 보호를 위한 최소한의 안전장치입니다. 로그를 장기간 보관하고 검색해야 한다면, Loki, Fluentd 같은 중앙 로그 저장소를 별도로 구성하는 것이 일반적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes 자체는 클러스터 수준의 로그 저장소를 기본 제공하지 않으며, 로그를 저장하고 분석하기 위한 별도의 백엔드가 필요합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;kubectl logs&lt;/code&gt; 조회 시 주의할 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 용량을 줄이기 위해 &lt;code&gt;containerLogMaxSize&lt;/code&gt; 값을 작게 설정할 수도 있습니다. 하지만 이 경우 주의해야 할 점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에 따르면 &lt;code&gt;kubectl logs&lt;/code&gt;는 최신 로그 파일의 내용만 제공합니다. 즉, 로그 로테이션으로 이전 파일로 넘어간 로그는 &lt;code&gt;kubectl logs&lt;/code&gt;에서 직접 확인할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 기본 설정인 &lt;code&gt;10Mi&lt;/code&gt;를 사용 중이라면, 특정 컨테이너가 많은 로그를 출력해 로테이션이 발생했을 때 &lt;code&gt;kubectl logs&lt;/code&gt;로 확인할 수 있는 범위는 최신 로그 파일에 남아 있는 내용으로 제한됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;containerLogMaxSize: 10Mi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 &lt;code&gt;kubectl logs&lt;/code&gt;로 볼 수 있는 로그는 최대 최신 &lt;code&gt;10Mi&lt;/code&gt; 파일의 내용입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;code&gt;containerLogMaxSize&lt;/code&gt;를 너무 작게 설정하면 문제가 발생했을 때 필요한 로그가 이미 로테이션되어 사라졌을 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그 용량을 줄이고 싶을 때 고려할 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 로그 용량을 줄이고 싶다면 kubelet 설정을 조정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 값을 줄이면 컨테이너별 로그 저장량을 줄일 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;containerLogMaxSize: 5Mi
containerLogMaxFiles: 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 컨테이너 하나당 대략 다음 정도의 로그만 유지됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;5Mi &amp;times; 3 files = 15Mi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 단순히 값을 줄이기보다는 다음을 함께 고려하는 것이 좋습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장애 분석에 필요한 최소 로그량은 어느 정도인지&lt;/li&gt;
&lt;li&gt;애플리케이션이 과도하게 로그를 출력하고 있지는 않은지&lt;/li&gt;
&lt;li&gt;로그 레벨이 운영 환경에 적절하게 설정되어 있는지&lt;/li&gt;
&lt;li&gt;Loki 같은 중앙 로그 저장소에서 정상적으로 로그를 수집하고 있는지&lt;/li&gt;
&lt;li&gt;노드 디스크 용량과 Pod 수를 고려했을 때 적절한 보관량인지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 중앙 로그 저장소를 사용하고 있다면, 노드에는 짧은 기간의 로그만 남기고 장기 보관과 검색은 중앙 로그 시스템에 맡기는 구조가 더 적절할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubelet은 기본적으로 컨테이너 로그 로테이션을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 설정은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;containerLogMaxSize: 10Mi
containerLogMaxFiles: 5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 컨테이너 로그가 노드에 무한히 쌓이는 것을 방지하기 위한 장치입니다. 다만 장기 보관이나 검색 목적의 기능은 아니므로, 운영 환경에서 로그를 지속적으로 보관해야 한다면 Loki 같은 중앙 로그 저장소를 함께 구성해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;code&gt;containerLogMaxSize&lt;/code&gt;를 너무 작게 설정하면 &lt;code&gt;kubectl logs&lt;/code&gt;로 확인할 수 있는 최신 로그 범위가 줄어들 수 있습니다. 따라서 로그 용량 절감과 장애 분석 가능성 사이에서 적절한 균형을 잡는 것이 중요합니다.&lt;/p&gt;</description>
      <category>Devops/Kubernetes</category>
      <author>hwara_</author>
      <guid isPermaLink="true">https://hwara-dev.tistory.com/20</guid>
      <comments>https://hwara-dev.tistory.com/20#entry20comment</comments>
      <pubDate>Mon, 25 May 2026 13:52:06 +0900</pubDate>
    </item>
    <item>
      <title>Kubernetes 노드와 Docker 환경에서 디스크 용량 확인 및 정리하기</title>
      <link>https://hwara-dev.tistory.com/19</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개발 환경이나 로컬 Kubernetes 클러스터를 오래 사용하다 보면 어느 순간 디스크 용량이 부족해지는 경우가 있습니다. 특히 컨테이너 이미지, 빌드 캐시, Pod 로그, 컨테이너 writable layer 등이 계속 쌓이면서 &lt;code&gt;/var&lt;/code&gt; 디렉터리의 용량이 커질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Linux 환경에서 디스크 용량을 확인하는 방법과, Kubernetes에서 사용하는 containerd 및 Docker의 용량을 정리하는 방법을 정리합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 디스크 용량 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 루트 디렉터리 기준으로 어떤 디렉터리가 용량을 많이 사용하는지 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;gml&quot;&gt;&lt;code&gt;sudo du -xh / --max-depth=1 | sort -h&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션의 의미는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;du&lt;/code&gt;: 디렉터리별 디스크 사용량 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-x&lt;/code&gt;: 다른 파일 시스템은 제외&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-h&lt;/code&gt;: 사람이 읽기 쉬운 단위로 출력&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--max-depth=1&lt;/code&gt;: 바로 아래 단계의 디렉터리까지만 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sort -h&lt;/code&gt;: 용량 기준으로 정렬&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 서버나 Kubernetes 노드에서는 &lt;code&gt;/var&lt;/code&gt; 디렉터리의 용량이 커지는 경우가 많습니다. &lt;code&gt;/var&lt;/code&gt;이 크다면 다음과 같이 한 단계 더 좁혀서 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;gml&quot;&gt;&lt;code&gt;sudo du -xh /var --max-depth=1 | sort -h&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식으로 &lt;code&gt;/var/log&lt;/code&gt;, &lt;code&gt;/var/lib&lt;/code&gt;, &lt;code&gt;/var/lib/containerd&lt;/code&gt;처럼 용량이 큰 경로를 단계적으로 추적할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Pod 및 컨테이너 로그 용량 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes 환경에서는 Pod 로그와 컨테이너 로그가 디스크를 많이 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 명령어로 로그 디렉터리의 용량을 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;sudo du -sh /var/log/pods
sudo du -sh /var/log/containers&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/var/log/pods&lt;/code&gt;에는 Pod 단위의 로그가 저장되고, &lt;code&gt;/var/log/containers&lt;/code&gt;에는 컨테이너 로그에 접근하기 위한 링크 형태의 파일들이 위치합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 수집 도구로 Loki, Fluent Bit, Promtail 등을 사용하더라도 노드에 기록되는 컨테이너 로그 자체가 바로 사라지는 것은 아닙니다. 기본적으로 컨테이너 런타임과 kubelet은 로그 파일을 로컬 노드에 기록하고, 수집기는 해당 로그를 읽어서 외부 저장소로 전달하는 방식으로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 로그 수집 시스템을 사용하더라도 노드의 로그 로테이션 설정은 별도로 확인하는 것이 좋습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;containerd 용량 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes 클러스터에서 containerd를 컨테이너 런타임으로 사용한다면, 이미지와 컨테이너 관련 데이터는 보통 다음 경로에 저장됩니다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;sudo du -sh /var/lib/containerd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경로에는 컨테이너 이미지 레이어, 스냅샷, 컨테이너 writable layer 등이 포함될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;containerd에서 현재 저장된 이미지 목록은 &lt;code&gt;crictl&lt;/code&gt;로 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo crictl images&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하지 않는 이미지를 정리하려면 다음 명령어를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;sudo crictl rmi --prune&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어는 현재 사용 중이지 않은 이미지를 정리합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kubernetes의 이미지 GC&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes 노드에서는 kubelet이 이미지 가비지 컬렉션을 자동으로 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 kubelet에는 다음과 같은 이미지 GC 기준이 설정되어 있습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;imageGCHighThresholdPercent: 85
imageGCLowThresholdPercent: 80&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 시스템 사용량이 &lt;code&gt;85%&lt;/code&gt;를 넘으면 이미지 GC 시작&lt;/li&gt;
&lt;li&gt;사용하지 않는 이미지를 정리&lt;/li&gt;
&lt;li&gt;사용량이 &lt;code&gt;80%&lt;/code&gt; 아래로 내려갈 때까지 정리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Kubernetes 노드에서는 사용하지 않는 이미지가 무한히 쌓이지 않도록 kubelet이 일정 기준에 따라 정리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 설정은 kubelet 설정에 따라 달라질 수 있으므로, 실제 운영 환경에서는 노드의 kubelet 설정을 직접 확인하는 것이 좋습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Docker 용량 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker를 사용하는 환경에서는 다음 명령어로 전체 디스크 사용량을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker system df&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          6         0         5.308GB   2.472GB (46%)
Containers      0         0         0B        0B
Local Volumes   6         0         143.6MB   143.6MB (100%)
Build Cache     72        0         3.585GB   2.679GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주요 항목은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Images&lt;/code&gt;: Docker 이미지가 사용하는 용량&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Containers&lt;/code&gt;: 컨테이너 writable layer가 사용하는 용량&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Local Volumes&lt;/code&gt;: Docker volume이 사용하는 용량&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Build Cache&lt;/code&gt;: Docker 빌드 과정에서 생성된 캐시 용량&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RECLAIMABLE&lt;/code&gt;: 정리 가능한 용량&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 이미지를 자주 빌드하는 개발 환경에서는 &lt;code&gt;Build Cache&lt;/code&gt;가 생각보다 큰 용량을 차지할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용하지 않는 Docker 이미지 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하지 않는 이미지를 정리하려면 다음 명령어를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;docker image prune -a&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-a&lt;/code&gt; 옵션을 사용하면 사용 중이지 않은 모든 이미지를 정리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할 점은, 같은 이미지 ID를 가진 이미지가 여러 태그를 가지고 있더라도 실제 레이어는 중복 저장되지 않는다는 것입니다. 즉, 태그가 여러 개라고 해서 이미지 용량이 태그 개수만큼 늘어나는 것은 아닙니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이미지를 삭제해도 용량이 줄지 않는 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 이미지를 모두 삭제했는데도 &lt;code&gt;docker system df&lt;/code&gt;에서 &lt;code&gt;Images&lt;/code&gt;의 &lt;code&gt;SIZE&lt;/code&gt;가 크게 보이는 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 상황입니다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          0         0         9.033GB   0B (0%)
Containers      0         0         0B        0B
Local Volumes   6         0         143.6MB   143.6MB (100%)
Build Cache     189       0         10.94GB   10.94GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우에는 이미지 자체는 삭제되었지만, 빌드 캐시에 이미지 레이어가 남아 있을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker는 빌드 속도를 높이기 위해 이전 빌드에서 사용한 레이어를 캐시로 보관합니다. 그래서 이미지를 삭제하더라도 빌드 캐시가 남아 있으면 실제 디스크 사용량이 크게 줄지 않을 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Docker Build Cache 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 캐시를 정리하려면 다음 명령어를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;docker builder prune&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오래된 캐시만 삭제하고 싶다면 &lt;code&gt;until&lt;/code&gt; 필터를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker builder prune --filter until=24h&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일정 용량만 남기고 정리하고 싶다면 &lt;code&gt;--keep-storage&lt;/code&gt; 옵션을 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;docker builder prune --keep-storage 10GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 빌드 캐시를 삭제하면 다음 빌드 시 기존 캐시를 사용할 수 없습니다. 따라서 다음 빌드 시간이 길어질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 환경에서 디스크 용량이 부족할 때는 유용하지만, 자주 빌드하는 프로젝트라면 필요한 캐시까지 모두 삭제하지 않도록 주의하는 것이 좋습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Docker Desktop의 Build Cache GC&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Desktop에서 확인해보면 기본적으로 일정 용량 이상이 되었을 때 Build Cache를 정리하는 GC 설정이 존재하는 것으로 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 환경에 따라 &lt;code&gt;20GB&lt;/code&gt;를 기준으로 빌더 캐시를 정리하도록 설정되어 있을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 값은 Docker Desktop 버전이나 설정에 따라 달라질 수 있으므로, 실제 값은 Docker Desktop 설정 또는 현재 사용 중인 Builder 설정을 확인하는 것이 좋습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스크 용량이 부족할 때는 무작정 파일을 삭제하기보다, 먼저 어떤 경로가 용량을 많이 사용하는지 확인하는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes 노드에서는 주로 다음 경로를 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;sudo du -xh / --max-depth=1 | sort -h
sudo du -xh /var --max-depth=1 | sort -h
sudo du -sh /var/log/pods
sudo du -sh /var/log/containers
sudo du -sh /var/lib/containerd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;containerd 환경에서는 다음 명령어로 이미지를 확인하고 정리할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;sudo crictl images
sudo crictl rmi --prune&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 환경에서는 다음 명령어를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker system df
docker image prune -a
docker builder prune&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면, Kubernetes 노드에서는 kubelet이 기본적으로 이미지 GC를 수행하지만, 로그나 빌드 캐시, Docker Desktop의 가상 디스크 사용량 등은 별도로 확인이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Docker 이미지를 삭제했는데도 용량이 줄지 않는다면, 이미지가 아니라 Build Cache가 남아 있는 상황일 수 있으므로 &lt;code&gt;docker builder prune&lt;/code&gt;을 함께 확인하는 것이 좋습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/ko/docs/concepts/architecture/garbage-collection/&quot;&gt;https://kubernetes.io/ko/docs/concepts/architecture/garbage-collection/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>환경구성</category>
      <author>hwara_</author>
      <guid isPermaLink="true">https://hwara-dev.tistory.com/19</guid>
      <comments>https://hwara-dev.tistory.com/19#entry19comment</comments>
      <pubDate>Mon, 25 May 2026 13:42:30 +0900</pubDate>
    </item>
    <item>
      <title>머지된 Git 브랜치를 안전하게 정리하는 방법</title>
      <link>https://hwara-dev.tistory.com/18</link>
      <description>&lt;h2 data-heading=&quot;Git 브랜치 삭제 명령어 정리&quot; data-ke-size=&quot;size26&quot;&gt;Git 브랜치 삭제 명령어 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git을 사용하다 보면 기능 개발이나 버그 수정을 위해 여러 브랜치를 만들게 됩니다.&lt;br /&gt;작업이 끝난 브랜치를 계속 남겨두면 로컬과 원격 저장소의 브랜치 목록이 복잡해질 수 있으므로, 필요 없는 브랜치는 주기적으로 정리하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 로컬 브랜치, 원격 브랜치, 로컬에 남아 있는 원격 브랜치 정보를 삭제하는 방법을 정리합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-heading=&quot;로컬 브랜치 삭제&quot; data-ke-size=&quot;size23&quot;&gt;로컬 브랜치 삭제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에 있는 브랜치는 다음 명령어로 삭제할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;git branch -D &amp;lt;branch_name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 feature/login 브랜치를 삭제하려면 다음과 같이 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;git branch -D feature/login
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-D 옵션은 강제 삭제 옵션입니다.&lt;br /&gt;해당 브랜치가 아직 merge되지 않았더라도 삭제할 수 있으므로, 정말 삭제해도 되는 브랜치인지 확인한 뒤 사용하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다 안전하게 삭제하려면 -d 옵션을 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;git branch -d &amp;lt;branch_name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-d 옵션은 merge되지 않은 브랜치를 삭제하려고 할 때 경고를 표시합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-heading=&quot;원격 브랜치 삭제&quot; data-ke-size=&quot;size23&quot;&gt;원격 브랜치 삭제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원격 저장소에 올라가 있는 브랜치는 다음 명령어로 삭제할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;git push origin --delete &amp;lt;branch_name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 원격 저장소의 feature/login 브랜치를 삭제하려면 다음과 같이 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;git push origin --delete feature/login
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어는 origin 원격 저장소에 있는 브랜치를 삭제합니다.&lt;br /&gt;협업 중인 브랜치라면 다른 사람이 사용하고 있지 않은지 확인한 뒤 삭제하는 것이 좋습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-heading=&quot;로컬에 남아 있는 원격 브랜치 정보 정리&quot; data-ke-size=&quot;size23&quot;&gt;로컬에 남아 있는 원격 브랜치 정보 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원격 브랜치를 삭제해도 로컬에는 origin/브랜치명 형태의 원격 추적 브랜치 정보가 남아 있을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때는 다음 명령어로 정리할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;git fetch --prune
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어는 원격 저장소에서 이미 삭제된 브랜치 정보를 로컬에서도 정리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 원격에서 feature/login 브랜치가 삭제되었는데 로컬에서 여전히 origin/feature/login이 보인다면, git fetch --prune을 실행해 정리할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-heading=&quot;머지된 로컬 브랜치 삭제&quot; data-ke-size=&quot;size23&quot;&gt;머지된 로컬 브랜치 삭제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git은 브랜치가 merge되었다고 해서 로컬 브랜치를 자동으로 삭제하지 않습니다.&lt;br /&gt;따라서 작업이 끝난 브랜치는 직접 정리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 main 브랜치에 merge된 로컬 브랜치는 다음 명령어로 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;git branch --merged main | grep -v main
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어는 main 브랜치에 이미 merge된 브랜치 목록을 출력합니다.&lt;br /&gt;삭제하기 전에 먼저 이 명령어로 어떤 브랜치가 대상인지 확인하는 것이 안전합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 후 실제로 삭제하려면 다음 명령어를 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;git branch --merged main | grep -v main | xargs -n 1 git branch -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어는 main에 merge된 브랜치 중 main 브랜치를 제외한 나머지 브랜치를 하나씩 삭제합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;명령어별 정리&quot; data-ke-size=&quot;size26&quot;&gt;명령어별 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목적 명령어&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;로컬 브랜치 강제 삭제&lt;/td&gt;
&lt;td&gt;git branch -D &amp;lt;branch_name&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;로컬 브랜치 안전 삭제&lt;/td&gt;
&lt;td&gt;git branch -d &amp;lt;branch_name&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;원격 브랜치 삭제&lt;/td&gt;
&lt;td&gt;git push origin --delete &amp;lt;branch_name&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;삭제된 원격 브랜치 정보 정리&lt;/td&gt;
&lt;td&gt;git fetch --prune&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;merge된 브랜치 확인&lt;/td&gt;
&lt;td&gt;git branch --merged main \| grep -v main&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;merge된 로컬 브랜치 삭제&lt;/td&gt;
&lt;td&gt;git branch --merged main \| grep -v main \| xargs -n 1 git branch -d&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;정리&quot; data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git 브랜치는 작업 단위를 나누는 데 유용하지만, 사용하지 않는 브랜치를 계속 남겨두면 관리가 어려워질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브랜치를 정리할 때는 다음 순서로 진행하는 것이 좋습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;삭제할 브랜치가 merge되었는지 확인한다.&lt;/li&gt;
&lt;li&gt;로컬 브랜치를 삭제한다.&lt;/li&gt;
&lt;li&gt;필요하다면 원격 브랜치도 삭제한다.&lt;/li&gt;
&lt;li&gt;git fetch --prune으로 로컬에 남은 원격 브랜치 정보를 정리한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 여러 사람이 함께 사용하는 저장소에서는 원격 브랜치를 삭제하기 전에 해당 브랜치를 다른 사람이 사용하고 있지 않은지 확인하는 것이 좋습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;Reference&quot; data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://dev-sihyun.tistory.com/2&quot;&gt;https://dev-sihyun.tistory.com/2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Git</category>
      <author>hwara_</author>
      <guid isPermaLink="true">https://hwara-dev.tistory.com/18</guid>
      <comments>https://hwara-dev.tistory.com/18#entry18comment</comments>
      <pubDate>Sun, 24 May 2026 10:39:16 +0900</pubDate>
    </item>
    <item>
      <title>Slack Incoming Webhook으로 채널에 메시지 보내기</title>
      <link>https://hwara-dev.tistory.com/17</link>
      <description>&lt;h2 data-heading=&quot;Slack Incoming Webhook으로 채널에 메시지 보내기&quot; data-ke-size=&quot;size26&quot;&gt;Slack Incoming Webhook으로 채널에 메시지 보내기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Slack의 &lt;b&gt;Incoming Webhook&lt;/b&gt;을 사용해 특정 채널로 메시지를 보내는 방법을 정리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Incoming Webhook은 외부 애플리케이션이나 스크립트에서 Slack 채널로 메시지를 전송할 수 있게 해주는 기능입니다. Slack 공식 문서에서도 Webhook URL에 JSON payload를 POST 요청으로 보내면 앱이 Slack에 메시지를 게시할 수 있다고 설명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 배포 완료 알림, 에러 알림, 모니터링 알림 등을 Slack 채널로 보내고 싶을 때 사용할 수 있습니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;1. Slack 채널 생성&quot; data-ke-size=&quot;size23&quot;&gt;1. Slack 채널 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Webhook 메시지를 받을 Slack 채널을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slack에서 다음 순서로 진행합니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;Slack &amp;rarr; 채널 &amp;rarr; 채널 생성
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트용 Webhook이라면 운영 채널보다는 별도의 테스트 채널을 만들어두는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 채널을 만들 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;#webhook-test
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;2. Slack 앱 생성&quot; data-ke-size=&quot;size23&quot;&gt;2. Slack 앱 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slack Incoming Webhook은 Slack App을 통해 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Slack API 페이지로 이동합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;https://api.slack.com/apps
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 다음 순서로 앱을 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;Create New App &amp;rarr; From scratch
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 생성 화면에서 다음 정보를 입력합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;App Name: 사용할 Slack 앱 이름&lt;/li&gt;
&lt;li&gt;Pick a workspace: 앱을 설치할 Slack 워크스페이스&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 후 앱을 생성하면 Slack 앱 설정 화면으로 이동합니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;3. Incoming Webhooks 활성화&quot; data-ke-size=&quot;size23&quot;&gt;3. Incoming Webhooks 활성화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slack 앱 설정 화면의 좌측 메뉴에서 다음 항목으로 이동합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Features &amp;rarr; Incoming Webhooks
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 페이지에서 &lt;b&gt;Activate Incoming Webhooks&lt;/b&gt; 옵션을 활성화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slack 공식 문서에서도 Incoming Webhooks 메뉴에서 기능을 활성화한 뒤, Workspace에 Webhook을 추가하는 흐름으로 안내하고 있습니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;4. Webhook URL 생성&quot; data-ke-size=&quot;size23&quot;&gt;4. Webhook URL 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Incoming Webhooks를 활성화하면 아래쪽에 Webhook을 추가할 수 있는 버튼이 표시됩니다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;Add New Webhook to Workspace
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼을 클릭하면 Webhook 메시지를 보낼 채널을 선택하는 화면이 나타납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 만든 Slack 채널을 선택한 뒤 권한을 허용합니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;채널 선택 &amp;rarr; 허용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 완료되면 Slack 앱 설정 화면에 새로운 Webhook URL이 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 URL을 복사해두면 외부에서 Slack 채널로 메시지를 보낼 때 사용할 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Webhook URL은 외부에서 Slack 채널로 메시지를 보낼 수 있는 주소이므로, 코드에 직접 하드코딩하기보다는 환경 변수나 Secret으로 관리하는 것이 좋습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-heading=&quot;5. curl로 Webhook 테스트&quot; data-ke-size=&quot;size23&quot;&gt;5. curl로 Webhook 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Webhook URL이 정상적으로 동작하는지 curl 명령어로 테스트할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;curl -X POST \
  -H 'Content-type: application/json' \
  --data '{&quot;text&quot;:&quot;Hello, World!&quot;}' \
  &amp;lt;Webhook URL&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 옵션의 의미는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;diff&quot;&gt;&lt;code&gt;-X POST
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slack Webhook URL로 POST 요청을 보낸다는 의미입니다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;-H 'Content-type: application/json'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 본문이 JSON 형식임을 알립니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;--data '{&quot;text&quot;:&quot;Hello, World!&quot;}'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slack 채널에 보낼 메시지 내용을 JSON 형식으로 전달합니다.&lt;/p&gt;
&lt;pre class=&quot;apache&quot;&gt;&lt;code&gt;&amp;lt;Webhook URL&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slack 앱 설정에서 복사한 Webhook URL을 넣습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 요청이 처리되면 선택한 Slack 채널에 다음 메시지가 표시됩니다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;Hello, World!
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;정리&quot; data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slack Incoming Webhook을 사용하면 외부 시스템에서 Slack 채널로 간단하게 메시지를 보낼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Slack 채널 생성
&amp;rarr; Slack App 생성
&amp;rarr; Incoming Webhooks 활성화
&amp;rarr; Webhook URL 생성
&amp;rarr; curl로 메시지 전송 테스트
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 실제 프로젝트에서는 배포 스크립트, GitHub Actions, 모니터링 시스템, 백엔드 서버 등에서 이 Webhook URL을 호출해 Slack 알림을 보낼 수 있습니다.&lt;/p&gt;
&lt;h2 data-heading=&quot;Reference&quot; data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.naver.com/segasas/223311008126&quot;&gt;https://blog.naver.com/segasas/223311008126&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Slack 공식 문서: Incoming Webhooks - &lt;a href=&quot;https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/?utm_source=chatgpt.com&quot;&gt;https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/?utm_source=chatgpt.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>환경구성</category>
      <author>hwara_</author>
      <guid isPermaLink="true">https://hwara-dev.tistory.com/17</guid>
      <comments>https://hwara-dev.tistory.com/17#entry17comment</comments>
      <pubDate>Fri, 22 May 2026 11:15:01 +0900</pubDate>
    </item>
    <item>
      <title>Python 개발 환경 세팅하기: venv, Ruff, Black, Mypy, pre-commit 설정 정리</title>
      <link>https://hwara-dev.tistory.com/16</link>
      <description>&lt;h2 data-heading=&quot;개요&quot; data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python 프로젝트를 시작할 때는 기능 구현에 바로 들어가기 전에 기본 개발 환경을 먼저 정리해두는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 여러 명이 함께 개발하거나, 여러 서비스로 나뉜 프로젝트를 관리하는 경우에는 코드 스타일과 검사 기준이 제각각이면 유지보수가 어려워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Python 프로젝트를 시작할 때 설정하면 좋은 기본 개발 환경을 정리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다루는 내용은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Python 가상환경 생성&lt;/li&gt;
&lt;li&gt;공통 개발 도구 설치&lt;/li&gt;
&lt;li&gt;pyproject.toml을 이용한 lint, formatter, type checker 설정&lt;/li&gt;
&lt;li&gt;pre-commit을 이용한 커밋 전 자동 검사 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Python 프로젝트에서 가상환경을 어떻게 구성해야 하는지, 그리고 ruff, black, mypy, pre-commit이 각각 어떤 역할을 하는지 궁금한 독자를 대상으로 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;전체 구성&quot; data-ke-size=&quot;size26&quot;&gt;전체 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 구성할 개발 도구는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구 역할&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;venv&lt;/td&gt;
&lt;td&gt;프로젝트별 Python 가상환경 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ruff&lt;/td&gt;
&lt;td&gt;코드 품질 검사 및 import 정리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;black&lt;/td&gt;
&lt;td&gt;코드 포맷팅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mypy&lt;/td&gt;
&lt;td&gt;타입 검사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pre-commit&lt;/td&gt;
&lt;td&gt;커밋 전에 자동으로 검사 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pyproject.toml&lt;/td&gt;
&lt;td&gt;Python 개발 도구 설정을 모아두는 설정 파일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.pre-commit-config.yaml&lt;/td&gt;
&lt;td&gt;pre-commit 훅 설정 파일&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;Python 가상환경이 필요한 이유&quot; data-ke-size=&quot;size26&quot;&gt;Python 가상환경이 필요한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python 프로젝트에서는 보통 프로젝트마다 필요한 패키지 버전이 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 A 프로젝트에서는 FastAPI 0.110 버전을 사용하고, B 프로젝트에서는 다른 버전의 FastAPI를 사용할 수 있습니다. 이때 전역 Python 환경에 모든 패키지를 설치하면 프로젝트 간 의존성이 섞이면서 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 방지하기 위해 Python에서는 프로젝트별로 독립된 실행 환경을 만드는 &lt;b&gt;가상환경&lt;/b&gt;을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상환경을 사용하면 다음과 같은 장점이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트별 패키지 버전을 분리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;전역 Python 환경이 지저분해지는 것을 막을 수 있습니다.&lt;/li&gt;
&lt;li&gt;다른 개발자와 동일한 개발 환경을 맞추기 쉬워집니다.&lt;/li&gt;
&lt;li&gt;프로젝트를 삭제할 때 가상환경도 함께 제거할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;Python 가상환경 생성&quot; data-ke-size=&quot;size26&quot;&gt;Python 가상환경 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트에서 다음 명령어로 가상환경을 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;python3.12 -m venv .venv
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 .venv는 가상환경 디렉터리 이름입니다. 꼭 .venv라는 이름을 써야 하는 것은 아니지만, Python 프로젝트에서는 관례적으로 많이 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성한 가상환경을 활성화합니다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;source .venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows 환경에서는 다음 명령어를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;.venv\Scripts\activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상환경이 활성화되면 터미널 앞쪽에 (.venv)와 같은 표시가 붙습니다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;(.venv) $
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서 설치하는 Python 패키지는 전역 환경이 아니라 현재 프로젝트의 .venv 안에 설치됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;공통 개발 도구 설치&quot; data-ke-size=&quot;size26&quot;&gt;공통 개발 도구 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상환경을 활성화한 뒤, 개발 도구를 설치합니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;pip install pre-commit ruff black mypy
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서 설치하는 도구들의 역할은 다음과 같습니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;Ruff&quot; data-ke-size=&quot;size23&quot;&gt;Ruff&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ruff는 Python 코드의 문제를 빠르게 검사해주는 lint 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 문제를 찾아줍니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용하지 않는 import&lt;/li&gt;
&lt;li&gt;사용하지 않는 변수&lt;/li&gt;
&lt;li&gt;잠재적인 버그 패턴&lt;/li&gt;
&lt;li&gt;import 순서 문제&lt;/li&gt;
&lt;li&gt;오래된 Python 문법 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 일부 문제는 --fix 옵션을 통해 자동으로 수정할 수 있습니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;Black&quot; data-ke-size=&quot;size23&quot;&gt;Black&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;black은 Python 코드 formatter입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;formatter는 코드의 의미를 바꾸지 않고, 들여쓰기나 줄바꿈 같은 스타일을 자동으로 통일해주는 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 팀원마다 코드 스타일이 달라도 black을 적용하면 일정한 형식으로 정리됩니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;Mypy&quot; data-ke-size=&quot;size23&quot;&gt;Mypy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mypy는 Python 타입 힌트를 기반으로 타입 오류를 검사하는 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python은 동적 타입 언어이기 때문에 실행 전에는 타입 오류를 발견하기 어려운 경우가 있습니다. mypy를 사용하면 타입 힌트를 기준으로 일부 오류를 미리 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;pre-commit&quot; data-ke-size=&quot;size23&quot;&gt;pre-commit&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pre-commit은 Git 커밋 전에 특정 검사를 자동으로 실행해주는 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 커밋하기 전에 ruff, black, mypy를 자동으로 실행하도록 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 문제가 있는 코드가 저장소에 들어가기 전에 한 번 더 검사할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;&amp;#96;pyproject.toml&amp;#96;이란?&quot; data-ke-size=&quot;size26&quot;&gt;pyproject.toml이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pyproject.toml은 Python 프로젝트의 설정을 한곳에서 관리하기 위한 설정 파일입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에는 도구마다 설정 파일을 따로 두는 경우가 많았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 파일들이 각각 존재할 수 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.flake8
mypy.ini
setup.cfg
black 설정 파일
isort 설정 파일
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 최근 Python 프로젝트에서는 여러 개발 도구의 설정을 pyproject.toml에 모아서 관리하는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트에서는 pyproject.toml이 다음 도구들의 설정 파일 역할을 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ruff&lt;/li&gt;
&lt;li&gt;black&lt;/li&gt;
&lt;li&gt;mypy&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 개발자가 직접 ruff, black, mypy를 실행하거나, pre-commit이 커밋 전에 이 도구들을 실행할 때 모두 pyproject.toml에 작성된 설정을 기준으로 동작합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;기본 &amp;#96;pyproject.toml&amp;#96; 작성&quot; data-ke-size=&quot;size26&quot;&gt;기본 pyproject.toml 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트에 pyproject.toml 파일을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 아래와 같이 설정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;[tool.ruff]
target-version = &quot;py312&quot;
line-length = 100

[tool.ruff.lint]
select = [
    &quot;E&quot;,   # pycodestyle errors
    &quot;W&quot;,   # pycodestyle warnings
    &quot;F&quot;,   # pyflakes (미사용 import 등)
    &quot;I&quot;,   # isort (import 순서 정렬)
    &quot;B&quot;,   # flake8-bugbear (잠재적 버그 패턴)
    &quot;UP&quot;,  # pyupgrade (Python 최신 문법 권장)
]

[tool.black]
line-length = 100
target-version = [&quot;py312&quot;]

[tool.mypy]
python_version = &quot;3.12&quot;
strict = false
ignore_missing_imports = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 설정의 의미를 간단히 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;Ruff 설정&quot; data-ke-size=&quot;size23&quot;&gt;Ruff 설정&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;[tool.ruff]
target-version = &quot;py312&quot;
line-length = 100
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;target-version은 프로젝트에서 사용하는 Python 버전을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 Python 3.12를 기준으로 검사하도록 설정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;line-length는 한 줄의 최대 길이입니다. 이 값은 뒤에서 설정할 black과 동일하게 맞추는 것이 좋습니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;[tool.ruff.lint]
select = [
    &quot;E&quot;,
    &quot;W&quot;,
    &quot;F&quot;,
    &quot;I&quot;,
    &quot;B&quot;,
    &quot;UP&quot;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;select는 어떤 종류의 규칙을 검사할지 지정합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;E, W: Python 스타일 관련 오류 및 경고&lt;/li&gt;
&lt;li&gt;F: 미사용 import, 정의되지 않은 변수 등&lt;/li&gt;
&lt;li&gt;I: import 순서 정리&lt;/li&gt;
&lt;li&gt;B: 버그 가능성이 있는 코드 패턴&lt;/li&gt;
&lt;li&gt;UP: 최신 Python 문법 사용 권장&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;Black 설정&quot; data-ke-size=&quot;size23&quot;&gt;Black 설정&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;[tool.black]
line-length = 100
target-version = [&quot;py312&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;black은 코드 포맷팅을 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;line-length를 ruff와 동일하게 100으로 맞춰두면 두 도구가 서로 다른 기준으로 동작하는 것을 줄일 수 있습니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;Mypy 설정&quot; data-ke-size=&quot;size23&quot;&gt;Mypy 설정&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;[tool.mypy]
python_version = &quot;3.12&quot;
strict = false
ignore_missing_imports = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mypy는 타입 검사를 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;strict = false는 처음부터 너무 엄격한 타입 검사를 적용하지 않겠다는 의미입니다. 기존 코드가 많거나 타입 힌트가 아직 충분하지 않은 프로젝트라면, 처음에는 느슨하게 시작하고 점진적으로 강화하는 편이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ignore_missing_imports = true는 타입 정보를 제공하지 않는 외부 라이브러리로 인해 발생하는 오류를 무시하도록 설정합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;프로젝트를 진행하며 설정 변경하기&quot; data-ke-size=&quot;size26&quot;&gt;프로젝트를 진행하며 설정 변경하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 도구 설정은 처음부터 완벽하게 고정하기 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하다 보면 프레임워크 특성상 예외가 필요한 경우도 있고, 팀 규칙에 따라 특정 lint 규칙을 끄거나 추가해야 할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 제가 진행한 프로젝트에서는 다음과 같이 설정이 변경되었습니다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;[tool.ruff]
target-version = &quot;py312&quot;
line-length = 100

[tool.ruff.lint]
select = [
    &quot;E&quot;,   # pycodestyle errors
    &quot;W&quot;,   # pycodestyle warnings
    &quot;F&quot;,   # pyflakes (미사용 import 등)
    &quot;I&quot;,   # isort (import 순서 정렬)
    &quot;B&quot;,   # flake8-bugbear (잠재적 버그 패턴)
    &quot;UP&quot;,  # pyupgrade (Python 최신 문법 권장)
]
ignore = [
    &quot;UP042&quot;,  # dev_convention.md 기준: 상태값 Enum은 str + enum.Enum 유지
]

[tool.ruff.lint.per-file-ignores]
# sys.path 조작 후 지연 임포트가 필요한 파일들 (E402)
&quot;**/main.py&quot; = [&quot;E402&quot;]           # 각 서비스 main.py
&quot;**/tests/**/*.py&quot; = [&quot;E402&quot;]     # 모든 서비스의 tests 디렉터리
&quot;shared/**/*.py&quot; = [&quot;E402&quot;]       # shared/telemetry 포함 shared 전체

[tool.ruff.lint.flake8-bugbear]
# B008 예외: FastAPI Depends, Query, Body 등은 기본 인자로 사용하는 것이 공식 패턴
extend-immutable-calls = [
    &quot;fastapi.Depends&quot;,
    &quot;fastapi.Query&quot;,
    &quot;fastapi.Body&quot;,
    &quot;fastapi.Header&quot;,
    &quot;fastapi.Path&quot;,
    &quot;fastapi.Cookie&quot;,
    &quot;fastapi.File&quot;,
    &quot;fastapi.Form&quot;,
    &quot;fastapi.Security&quot;,
]

[tool.black]
line-length = 100
target-version = [&quot;py312&quot;]

[tool.mypy]
python_version = &quot;3.12&quot;
strict = false
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = &quot;redis.*&quot;
ignore_missing_imports = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 lint 도구의 경고를 무조건 코드 수정으로만 해결하지 않는다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 FastAPI에서는 Depends, Query, Body 등을 함수 기본 인자로 사용하는 패턴이 일반적입니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;from fastapi import Depends

def get_user(user=Depends(get_current_user)):
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 Python 코드에서는 함수 기본 인자에서 함수를 호출하는 패턴이 문제가 될 수 있습니다. 하지만 FastAPI에서는 의도된 사용 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이런 경우에는 코드를 억지로 바꾸기보다, 프로젝트에서 사용하는 프레임워크의 특성을 반영해 lint 예외를 설정하는 것이 더 적절합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;pre-commit이란?&quot; data-ke-size=&quot;size26&quot;&gt;pre-commit이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pre-commit은 Git 커밋 전에 자동으로 검사를 실행해주는 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 개발자는 코드를 수정한 뒤 다음과 같은 흐름으로 커밋합니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;git add .
git commit -m &quot;add user api&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 pre-commit을 설정해두면 실제 커밋이 생성되기 전에 자동으로 여러 검사가 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 작업을 커밋 전에 자동으로 실행할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ruff로 코드 검사&lt;/li&gt;
&lt;li&gt;black으로 코드 포맷팅&lt;/li&gt;
&lt;li&gt;mypy로 타입 검사&lt;/li&gt;
&lt;li&gt;줄 끝 공백 제거&lt;/li&gt;
&lt;li&gt;파일 끝 개행 확인&lt;/li&gt;
&lt;li&gt;YAML 문법 검사&lt;/li&gt;
&lt;li&gt;대용량 파일 커밋 방지&lt;/li&gt;
&lt;li&gt;개인키 커밋 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, pre-commit은 문제가 있는 코드가 Git 저장소에 들어가기 전에 막아주는 안전장치입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;&amp;#96;.pre-commit-config.yaml&amp;#96;이란?&quot; data-ke-size=&quot;size26&quot;&gt;.pre-commit-config.yaml이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.pre-commit-config.yaml은 pre-commit이 어떤 검사를 실행할지 정의하는 설정 파일입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일도 보통 프로젝트 루트에 위치합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pyproject.toml이 ruff, black, mypy의 세부 규칙을 정의한다면, .pre-commit-config.yaml은 &lt;b&gt;커밋 전에 어떤 도구를 실행할지&lt;/b&gt;를 정의합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 파일의 역할을 비교하면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 역할&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pyproject.toml&lt;/td&gt;
&lt;td&gt;각 개발 도구의 세부 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.pre-commit-config.yaml&lt;/td&gt;
&lt;td&gt;커밋 전에 실행할 훅 목록 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 pyproject.toml에는 ruff가 어떤 규칙을 검사할지 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;[tool.ruff.lint]
select = [&quot;E&quot;, &quot;W&quot;, &quot;F&quot;, &quot;I&quot;, &quot;B&quot;, &quot;UP&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 .pre-commit-config.yaml에는 커밋 전에 ruff를 실행하겠다고 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.4
    hooks:
      - id: ruff
        args: [--fix]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 관계를 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.pre-commit-config.yaml
  └─ 커밋 전에 ruff 실행

pyproject.toml
  └─ ruff가 실행될 때 어떤 규칙으로 검사할지 정의
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;&amp;#96;.pre-commit-config.yaml&amp;#96; 작성&quot; data-ke-size=&quot;size26&quot;&gt;.pre-commit-config.yaml 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트에 .pre-commit-config.yaml 파일을 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.4
    hooks:
      - id: ruff
        args: [--fix]   # 자동 수정 가능한 것은 자동으로 수정

  - repo: https://github.com/psf/black
    rev: 24.4.2
    hooks:
      - id: black

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.10.0
    hooks:
      - id: mypy
        additional_dependencies: [pydantic, fastapi]

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace        # 줄 끝 공백 제거
      - id: end-of-file-fixer          # 파일 끝 개행 확인
      - id: check-yaml                 # YAML 문법 검사
      - id: check-added-large-files    # 대용량 파일 실수 커밋 방지
      - id: detect-private-key         # 개인키 커밋 방지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 항목의 의미는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;repo: 훅을 제공하는 Git 저장소 주소
rev: 사용할 훅의 버전
hooks: 실행할 훅 목록
id: 실행할 훅의 이름
args: 훅 실행 시 전달할 옵션
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 아래 설정은 ruff 훅을 사용하겠다는 의미입니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;- repo: https://github.com/astral-sh/ruff-pre-commit
  rev: v0.4.4
  hooks:
    - id: ruff
      args: [--fix]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;args: [--fix]는 ruff가 자동 수정 가능한 문제를 발견하면 가능한 범위에서 자동으로 수정하라는 의미입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;pre-commit 훅 설치&quot; data-ke-size=&quot;size26&quot;&gt;pre-commit 훅 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 파일을 작성했다고 해서 바로 커밋 시점에 실행되는 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 한 번은 다음 명령어로 Git 훅을 설치해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;pre-commit install
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령을 실행하면 현재 Git 저장소의 .git/hooks/pre-commit 위치에 pre-commit 실행 스크립트가 설치됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후부터는 git commit을 실행할 때마다 .pre-commit-config.yaml에 정의된 훅들이 자동으로 실행됩니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;개발자
  └─ git commit 실행
      └─ Git pre-commit hook 실행
          └─ .pre-commit-config.yaml 읽기
              ├─ ruff 실행
              ├─ black 실행
              ├─ mypy 실행
              └─ 기타 검사 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;전체 파일을 대상으로 pre-commit 실행하기&quot; data-ke-size=&quot;size26&quot;&gt;전체 파일을 대상으로 pre-commit 실행하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 pre-commit을 설정했다면 전체 파일을 대상으로 한 번 실행해보는 것이 좋습니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;pre-commit run --all-files
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어는 현재 변경된 파일뿐 아니라 프로젝트의 전체 파일을 대상으로 훅을 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 프로젝트에 pre-commit을 뒤늦게 도입하는 경우에는 많은 파일이 한 번에 수정될 수 있습니다. 이때는 변경 내용을 확인한 뒤 커밋하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;git status
git diff
git add .
git commit -m &quot;chore: apply pre-commit formatting&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;커밋 중 pre-commit이 실패했을 때&quot; data-ke-size=&quot;size26&quot;&gt;커밋 중 pre-commit이 실패했을 때&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pre-commit 훅이 실패하면 커밋은 중단됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 black이나 ruff가 파일을 자동 수정했다면, 수정된 파일을 다시 스테이징해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;git status
git add .
git commit -m &quot;add user api&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 자동 수정이 불가능한 문제가 있다면, 출력된 에러 메시지를 보고 직접 코드를 수정한 뒤 다시 커밋하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름이 처음에는 번거롭게 느껴질 수 있지만, 결과적으로는 코드 리뷰 전에 기본적인 문제를 자동으로 걸러주기 때문에 유지보수에 도움이 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;&amp;#96;detect-private-key&amp;#96; 훅이 중요한 이유&quot; data-ke-size=&quot;size26&quot;&gt;detect-private-key 훅이 중요한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;detect-private-key 훅은 개인키가 Git 저장소에 커밋되는 것을 방지하기 위한 훅입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 JWT RS256 방식을 사용하면 .pem 형식의 개인키를 사용할 수 있습니다. 이런 파일이 실수로 Git에 올라가면 인증 토큰 서명에 사용되는 민감한 값이 외부에 노출될 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;- id: detect-private-key
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 훅을 추가해두면 개인키로 의심되는 내용이 커밋될 때 커밋을 중단시켜줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 훅만으로 모든 민감 정보 유출을 막을 수 있는 것은 아닙니다. .env, 인증서, 토큰, 비밀번호 파일 등은 .gitignore에도 함께 등록하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 파일은 일반적으로 Git에 올리지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;.env
*.pem
*.key
.venv/
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;최종 디렉터리 구조 예시&quot; data-ke-size=&quot;size26&quot;&gt;최종 디렉터리 구조 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 마치면 프로젝트 루트는 대략 다음과 같은 형태가 됩니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;project-root/
├── .venv/
├── .gitignore
├── .pre-commit-config.yaml
├── pyproject.toml
├── services/
│   ├── user-service/
│   ├── product-service/
│   └── ...
└── shared/
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 .venv는 로컬 개발 환경용 디렉터리이므로 Git에 올리지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 pyproject.toml과 .pre-commit-config.yaml은 팀원들과 공유해야 하는 설정 파일이므로 Git에 포함합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;정리&quot; data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Python 프로젝트를 시작할 때 설정하면 좋은 기본 개발 환경을 정리했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Python 프로젝트에서는 가상환경을 사용해 프로젝트별 의존성을 분리하는 것이 좋습니다.&lt;/li&gt;
&lt;li&gt;ruff는 코드 품질 검사와 import 정리를 담당합니다.&lt;/li&gt;
&lt;li&gt;black은 코드 포맷팅을 담당합니다.&lt;/li&gt;
&lt;li&gt;mypy는 타입 검사를 담당합니다.&lt;/li&gt;
&lt;li&gt;pyproject.toml은 Python 개발 도구의 설정을 모아두는 파일입니다.&lt;/li&gt;
&lt;li&gt;.pre-commit-config.yaml은 커밋 전에 실행할 검사 목록을 정의하는 파일입니다.&lt;/li&gt;
&lt;li&gt;pre-commit install을 실행하면 이후 git commit마다 자동 검사가 실행됩니다.&lt;/li&gt;
&lt;li&gt;민감 정보 유출을 막기 위해 detect-private-key 같은 훅과 .gitignore 설정을 함께 사용하는 것이 좋습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 모든 규칙을 엄격하게 적용하기보다는, 프로젝트 상황에 맞게 점진적으로 규칙을 추가하고 예외를 정리하는 방식이 현실적입니다.&lt;/p&gt;</description>
      <category>환경구성</category>
      <author>hwara_</author>
      <guid isPermaLink="true">https://hwara-dev.tistory.com/16</guid>
      <comments>https://hwara-dev.tistory.com/16#entry16comment</comments>
      <pubDate>Wed, 20 May 2026 12:08:24 +0900</pubDate>
    </item>
    <item>
      <title>Ruff E712와 SQLAlchemy Boolean 조건식 처리하기</title>
      <link>https://hwara-dev.tistory.com/15</link>
      <description>&lt;h2 data-heading=&quot;문제 상황&quot; data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQLAlchemy를 사용해 상품 목록을 조회하는 조건식을 작성하던 중, Ruff에서 다음과 같은 경고가 발생했습니다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;services\product-service\app\routes\products.py:94:27: E712 Avoid equality comparisons to `True`; use `if Product.is_active:` for truth checks
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 된 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;conditions.append(Product.is_active == True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ruff의 E712 규칙은 Python 코드에서 True, False와 직접 비교하는 패턴을 피하라는 경고입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 Python 코드에서는 다음과 같은 비교를 권장하지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;if value == True:
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 아래처럼 truthy/falsy 평가를 사용하는 것이 더 자연스럽습니다.&lt;/p&gt;
&lt;pre class=&quot;ceylon&quot;&gt;&lt;code&gt;if value:
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-heading=&quot;일반 Python 코드에서 문제가 되는 이유&quot; data-ke-size=&quot;size26&quot;&gt;일반 Python 코드에서 문제가 되는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python에서는 Boolean 값을 직접 == True, == False로 비교하지 않아도 조건문에서 참/거짓을 평가할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 코드는 불필요한 비교로 볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;if is_active == True:
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통은 다음과 같이 작성하는 것이 더 Pythonic합니다.&lt;/p&gt;
&lt;pre class=&quot;ceylon&quot;&gt;&lt;code&gt;if is_active:
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Ruff는 == True 같은 표현을 발견하면 E712 경고를 발생시킵니다.&lt;/p&gt;
&lt;h2 data-heading=&quot;SQLAlchemy에서는 상황이 다르다&quot; data-ke-size=&quot;size26&quot;&gt;SQLAlchemy에서는 상황이 다르다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 SQLAlchemy의 where 조건식에서는 일반적인 Python Boolean 비교와 의미가 다릅니다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;conditions.append(Product.is_active == True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 Python의 Boolean 값을 단순 비교하는 코드가 아니라, SQLAlchemy가 SQL 조건식을 만들기 위한 표현식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 의도는 다음과 같은 SQL 조건을 만드는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;WHERE product.is_active = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 일반 Python 코드 기준으로는 E712 경고 대상이지만, SQLAlchemy 표현식에서는 의도된 사용일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우 Ruff 입장에서는 SQLAlchemy의 표현식 생성 의도를 정확히 구분하기 어렵기 때문에 오탐처럼 보일 수 있습니다.&lt;/p&gt;
&lt;h2 data-heading=&quot;해결 방법&quot; data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQLAlchemy에서는 Boolean 또는 NULL 비교를 위해 is_() 메서드를 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 전 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;conditions.append(Product.is_active == True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 아래와 같이 변경할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;conditions.append(Product.is_active.is_(True))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 작성하면 Ruff의 E712 경고를 피하면서도, SQLAlchemy 표현식이라는 의도를 더 명확하게 드러낼 수 있습니다.&lt;/p&gt;
&lt;h2 data-heading=&quot;변경 전후 비교&quot; data-ke-size=&quot;size26&quot;&gt;변경 전후 비교&lt;/h2&gt;
&lt;h3 data-heading=&quot;변경 전&quot; data-ke-size=&quot;size23&quot;&gt;변경 전&lt;/h3&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;conditions.append(Product.is_active == True)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;변경 후&quot; data-ke-size=&quot;size23&quot;&gt;변경 후&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;conditions.append(Product.is_active.is_(True))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-heading=&quot;참고: &amp;#96;None&amp;#96; 비교도 같은 방식으로 처리 가능&quot; data-ke-size=&quot;size26&quot;&gt;참고: None 비교도 같은 방식으로 처리 가능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQLAlchemy 공식 문서에서도 None 비교에 대해 비슷한 예시를 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;statement.where(users.c.name == None)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PEP8 또는 linter 경고가 신경 쓰이는 경우, 다음과 같이 is_()를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;statement.where(users.c.name.is_(None))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Boolean 조건에서도 같은 방식으로 is_(True) 또는 is_(False)를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Product.is_active.is_(True)
Product.is_active.is_(False)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-heading=&quot;정리&quot; data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ruff의 E712는 일반 Python 코드에서 == True, == False 비교를 피하도록 안내하는 규칙입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 SQLAlchemy에서는 Product.is_active == True 같은 표현이 SQL 조건식을 만들기 위한 코드로 사용될 수 있습니다. 이 경우 일반적인 Python truth check와는 의미가 다르기 때문에 linter 경고가 오탐처럼 느껴질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 SQLAlchemy의 is_() 메서드를 사용해 해결할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;conditions.append(Product.is_active.is_(True))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 linter 경고를 제거하면서도, SQLAlchemy 조건식의 의도를 더 명확하게 표현할 수 있었습니다.&lt;/p&gt;
&lt;h2 data-heading=&quot;Reference&quot; data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQLAlchemy 공식 문서: &lt;a href=&quot;https://docs.sqlalchemy.org/en/14/core/tutorial.html&quot;&gt;https://docs.sqlalchemy.org/en/14/core/tutorial.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Language/Python</category>
      <author>hwara_</author>
      <guid isPermaLink="true">https://hwara-dev.tistory.com/15</guid>
      <comments>https://hwara-dev.tistory.com/15#entry15comment</comments>
      <pubDate>Wed, 20 May 2026 11:26:29 +0900</pubDate>
    </item>
  </channel>
</rss>