본문 바로가기
개발환경, 인프라

ELK 스택 구성 및 로컬 네트워크 접속 설정 - 삽질 기록

by Pendine 2026. 4. 28.
728x90
반응형

개인 서버에 ELK 스택을 docker-compose로 구성하고 GitLab CI로 배포 자동화하는 과정에서 두 가지 문제가 발생했다.

  1. Kibana가 며칠째 "서버 준비 안 됨" 상태 → kibana_system 패스워드 미설정 + CI에서 curl 명령어 실행 불가
  2. 로컬 네트워크에서 Kibana 접속 불가 → docker-compose 포트 바인딩이 127.0.0.1로 고정되어 있었음

구성 환경

  • 개발/배포 서버: Windows + Docker (WSL2 기반)
  • ELK 버전: Elasticsearch 8.13.4 / Kibana 8.13.4 / APM Server 8.13.4
  • 배포 방식: GitLab Runner + CI/CD 파이프라인
  • 구성 파일: docker-compose.elastic-shared.yml

ELK 스택 구성 방식

docker-compose.elastic-shared.yml 한 파일로 Elasticsearch, Kibana, APM Server 3개 컨테이너를 정의했다.
별도 Dockerfile은 사용하지 않고 Elastic 공식 이미지를 그대로 사용한다.

개발 환경(devnet)과 운영 환경(prodnet) 두 네트워크에 동시에 join시켜, dev/prod 백엔드가 APM Server 하나를 공유하는 구조다.

  services:
    elasticsearch-shared:
      image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4
      environment:
        - discovery.type=single-node
        - ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
        - xpack.security.enabled=true
        - xpack.security.http.ssl.enabled=false
        - ES_JAVA_OPTS=-Xms1g -Xmx1g
        - bootstrap.memory_lock=false
      ports:
        - "127.0.0.1:9200:9200"   # ES는 외부 노출 안 함
      networks:
        - devnet
        - prodnet

    kibana-shared:
      image: docker.elastic.co/kibana/kibana:8.13.4
      depends_on:
        elasticsearch-shared: { condition: service_healthy }
      environment:
        - ELASTICSEARCH_USERNAME=kibana_system   # elastic 슈퍼유저 사용 불가
        - ELASTICSEARCH_PASSWORD=${KIBANA_SYSTEM_PASSWORD}
      ports:
        - "5601:5601"
      networks:
        - devnet
        - prodnet

    apm-server-shared:
      image: docker.elastic.co/apm/apm-server:8.13.4
      depends_on:
        elasticsearch-shared: { condition: service_healthy }
      ports:
        - "127.0.0.1:8200:8200"   # APM Server도 외부 노출 안 함
      networks:
        - devnet
        - prodnet

  networks:
    devnet:
      external: true
    prodnet:
      external: true

네트워크는 external: true로 선언하고 CI 스크립트에서 직접 생성한다.
docker-compose가 네트워크를 만들면 라벨이 붙어서 다른 compose와 충돌이 발생하기 때문이다.


GitLab CI 배포 구성

ELK 스택은 백엔드/프론트엔드와 별도로 deploy-elastic-shared 잡으로 분리했다.
dev나 main 브랜치에서 수동 트리거로만 실행된다.

비밀번호 등 민감한 값은 GitLab CI/CD Variables에 Protected + Masked로 등록하고,
잡 실행 시 .env 파일로 생성 후 사용 즉시 삭제한다.

  deploy-elastic-shared:
    stage: deploy-infra
    rules:
      - if: $CI_COMMIT_BRANCH == "dev" || $CI_COMMIT_BRANCH == "main"
        when: manual
    script:
      - docker network create devnet 2>/dev/null || true
      - docker network create account_book_product_network 2>/dev/null || true
      - cat > .env.elastic-shared <<EOF
        ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
        KIBANA_SYSTEM_PASSWORD=${KIBANA_SYSTEM_PASSWORD}
        ...
        EOF
      - chmod 600 .env.elastic-shared
      - docker-compose -p elastic-shared --env-file .env.elastic-shared \
          -f docker-compose.elastic-shared.yml up -d
      - rm -f .env.elastic-shared

문제 1. Kibana "서버 준비 안 됨" — kibana_system 패스워드 미설정

결론: Kibana 8.x는 kibana_system 전용 계정을 써야 하고, 이 계정의 패스워드를 ES에 직접 등록해야 한다.

Kibana 8.x부터 elastic 슈퍼유저로 ES에 접속하는 걸 거부한다.
kibana_system이라는 전용 계정을 사용해야 하는데, 이 계정의 패스워드는 컨테이너가 자동으로 설정하지 않는다.
ES가 정상 기동된 후 반드시 1회 수동으로 아래 curl 명령어를 실행해야 한다.

# powershell or cmd

  curl -u elastic:${ELASTIC_PASSWORD} \
    -X POST \
    -H 'Content-Type: application/json' \
    http://127.0.0.1:9200/_security/user/kibana_system/_password \
    -d '{"password":"${KIBANA_SYSTEM_PASSWORD}"}'

응답이 {} 이면 성공이다. 이후 docker restart kibana-shared 하면 Kibana가 정상 기동된다.

이 명령은 멱등하다. 이미 설정되어 있어도 같은 값으로 덮어쓸 뿐이므로 여러 번 실행해도 안전하다. 그래서 CI 파이프라인에 포함시켜 자동화했다.


문제 2. CI에서 curl 명령어 실행 불가

결론: GitLab dind 환경의 docker:latest 이미지에는 curl이 없다. ES 컨테이너 내부에서 실행해야 한다.

CI 파이프라인에 curl 명령어를 추가했더니 아래 오류가 발생했다.

  /bin/sh: eval: line 215: curl: not found

GitLab CI에서 Docker-in-Docker 방식으로 실행할 때
사용하는 docker:latest 이미지는 Alpine 기반 최소 이미지라 curl이 포함되어 있지 않다.

해결 방법은 docker exec로 Elasticsearch 컨테이너 내부에서 curl을 실행하는 것이다.
ES 컨테이너에는 curl이 기본 포함되어 있다.

잘못된 방법 — dind 환경에서 curl 없음

  • curl -u elastic:${ELASTIC_PASSWORD} http://127.0.0.1:9200/...

    올바른 방법 — ES 컨테이너 내부에서 실행

  • docker exec elasticsearch-shared curl -u elastic:${ELASTIC_PASSWORD} http://localhost:9200/...

    .gitlab-ci 에서 ES healthy 대기 루프도 동일하게 처리했다.

  - |
    for i in $(seq 1 30); do
      if docker exec elasticsearch-shared curl -sf \
          -u elastic:${ELASTIC_PASSWORD} \
          http://localhost:9200/_cluster/health > /dev/null 2>&1; then
        echo "Elasticsearch 준비 완료"
        break
      fi
      sleep 5
    done
  - |
    docker exec elasticsearch-shared curl -sf \
      -u elastic:${ELASTIC_PASSWORD} \
      -X POST -H 'Content-Type: application/json' \
      http://localhost:9200/_security/user/kibana_system/_password \
      -d '{"password":"'"${KIBANA_SYSTEM_PASSWORD}"'"}'
  - docker restart kibana-shared

문제 3. 로컬 네트워크에서 Kibana 접속 불가

결론: docker-compose의 포트 바인딩이 127.0.0.1로 고정되어 있으면 외부에서 접속할 수 없다.
5601:5601로 변경하면 된다.
처음에는 SSH 터널로만 Kibana에 접속하도록 설계했다.

  ports:
    - "127.0.0.1:5601:5601"   # 루프백 전용 바인딩

127.0.0.1은 루프백 주소로, 서버 자신에서만 접근 가능하다.
같은 네트워크의 다른 PC에서 172.30.1.31:5601로 접속해도 연결이 거부된다.
Windows 방화벽 인바운드 규칙을 열어도 소용없다.
방화벽이 아니라 Docker 바인딩 설정의 문제이기 때문이다.

수정은 간단하다.

  ports:
    - "5601:5601"   # 모든 인터페이스에서 수신

이렇게 변경하면 0.0.0.0:5601로 바인딩되어 서버의 모든 네트워크 인터페이스에서 수신한다.
여기에 Windows 방화벽 인바운드 TCP 5601 규칙만 추가하면 로컬 네트워크 어디서든 서버IP:5601로 접속할 수 있다.

변경 후 컨테이너를 재생성해야 적용된다. docker-compose up -d는 설정이 바뀐 컨테이너만 재생성하므로 데이터 볼륨은 유지된다.

보안 참고: APM Server(8200)와 Elasticsearch(9200)는 127.0.0.1로 유지해 외부에 노출하지 않는다.
Kibana만 로컬 네트워크에 열었으며, Kibana 자체 계정 인증(ID/PW)으로 접근을 제한한다.

728x90
반응형

'개발환경, 인프라' 카테고리의 다른 글

혼자 머리박치기로 쿠버네티스는 아니다  (0) 2024.09.22
소스코드 관리  (0) 2024.09.18

댓글