비밀 관리

비밀 관리 (Secret Management)

이 페이지는 Haystack 컴포넌트에서 비밀(secret)을 관리하는 방법과, 구조화된 비밀 처리를 위한 Secret 타입을 소개해요. 코드에 비밀을 하드코딩하는 것의 단점을 설명하고, 대신 환경 변수를 사용할 것을 권장합니다.

출처: 공식문서

많은 Haystack 컴포넌트는 Azure, Google Vertex AI, OpenAI 같은 제3자 프레임워크와 서비스 프로바이더와 상호작용해요. 이들의 라이브러리는 보통 사용자가 인증을 해야 그 밑바탕의 제품에 접근할 수 있게 합니다. 인증 과정은 대개 제3자 백엔드에게 불투명한 식별자(identifier) 역할을 하는 비밀 값으로 동작하죠.

이 페이지에서는 비밀의 두 가지 주요 유형, 즉 토큰 기반환경 변수 기반 비밀을 설명하고, Haystack을 쓸 때 이들을 어떻게 다루는지 알아볼게요.

Secret 클래스에 대한 추가 세부사항은 API 참조에서 확인할 수 있습니다.

문제 진술 (Problem Statement)

쿼리를 임베딩하고, Retriever 컴포넌트로 쿼리와 관련된 문서를 찾고, 검색된 문서를 바탕으로 LLM이 답변을 생성하도록 하는 예제 RAG 파이프라인을 생각해 볼게요.

아래 파이프라인에 쓰인 OpenAIChatGenerator 컴포넌트는 OpenAI 서버에 인증하고 생성을 수행하려면 API 키를 기대합니다. 이 컴포넌트가 str 값을 받는다고 가정해 보죠.

generator = OpenAIChatGenerator(api_key="sk-xxxxxxxxxxxxxxxxxx")
pipeline.add_component("generator", generator)

이건 임시방편으로는 동작하지만 좋지 않은 관행이에요. 이런 비밀을 코드베이스에 하드코딩하면 안 되니까요. 대안은 키를 환경 변수에 외부적으로 저장해 두고, Python에서 읽어서 컴포넌트에 넘기는 것입니다.

import os

api_key = os.environ.get("OPENAI_API_KEY")
generator = OpenAIChatGenerator(api_key=api_key)
pipeline.add_component("generator", generator)

이 편이 더 낫습니다. 파이프라인은 의도대로 동작하고, 코드에 비밀을 하드코딩하지도 않으니까요.

한 가지 기억할 점은 파이프라인은 직렬화 가능하다는 거예요. API 키는 비밀이므로 디스크에 저장하는 것은 당연히 피해야 합니다. 컴포넌트의 to_dict 메서드를 수정해서 키를 제외해 볼게요.

def to_dict(self) -> Dict[str, Any]:
    # Do not pass the `api_key` init parameter.
    return default_to_dict(self, model=self.model)

그런데 파이프라인을 디스크에서 불러오면 어떻게 될까요? 가장 좋은 경우에는 컴포넌트의 백엔드가 하드코딩된 환경 변수에서 키를 자동으로 읽으려 시도하고, 그 키가 직렬화되기 전에 컴포넌트에 전달됐던 키와 같을 거예요. 하지만 최악의 경우, 백엔드가 하드코딩된 환경 변수에서 키를 찾지 못해서 pipeline.run() 호출 안에서 호출될 때 실패하게 됩니다.

Import

코드 안에서 Haystack 비밀을 사용하려면 먼저 다음과 같이 import 하세요.

from haystack.utils import Secret

토큰 기반 비밀 (Token-Based Secrets)

from_token 메서드를 사용해 토큰을 문자열로 직접 붙여넣을 수 있어요.

llm = OpenAIChatGenerator(api_key=Secret.from_token("sk-randomAPIkeyasdsa32ekasd32e"))

이런 유형의 코드는 직렬화될 수 없다는 점을 주의하세요. 즉 위의 컴포넌트를 딕셔너리로 변환하거나, 그 컴포넌트가 들어 있는 파이프라인을 YAML 파일로 저장할 수 없습니다. 민감한 데이터가 실수로 노출되는 것을 막기 위한 보안 기능이에요.

환경 변수 기반 비밀 (Environment Variable-Based Secrets)

환경 변수 기반 비밀은 더 유연합니다. 비밀이 들어 있을 수 있는 환경 변수를 하나 이상 지정할 수 있어요.

API 키가 필요한 기존 Haystack 컴포넌트(OpenAIChatGenerator처럼)는 Secret.from_env_var의 기본값(이 경우 OPENAI_API_KEY)을 가져요. 즉 OpenAIChatGenerator는 환경 변수 OPENAI_API_KEY의 값이 있으면 그것을 찾아 인증에 사용합니다. 그리고 파이프라인이 YAML로 직렬화될 때는 환경 변수의 이름만 YAML 파일에 저장됩니다. 이렇게 해서 보안 누출이 없도록 보장되므로 이 방법을 강력히 권장합니다.

## First, export an environment variable name `OPENAI_API_KEY` with its value
export OPENAI_API_KEY=sk-randomAPIkeyasdsa32ekasd32e

## or alternatively, using Python
## import os
## os.environ["OPENAI_API_KEY"]=sk-randomAPIkeyasdsa32ekasd32e
llm_generator = (
    OpenAIChatGenerator()
)  # Uses the default value from the env var for the component

또는 Secret이 기대되는 컴포넌트에서, API 키를 읽을 환경 변수의 이름을 커스터마이즈할 수 있어요.

# Export an environment variable with custom name and its value
llm_generator = OpenAIChatGenerator(api_key=Secret.from_env_var("YOUR_ENV_VAR"))

OpenAIChatGenerator가 파이프라인 안에서 직렬화될 때, 커스텀 변수 이름을 사용하면 YAML 코드는 이렇게 보입니다.

components:
  llm:
    init_parameters:
      api_base_url: null
      api_key:
        env_vars:
        - YOUR_ENV_VAR
        strict: true
        type: env_var
      generation_kwargs: {}
      http_client_kwargs: null
      max_retries: null
      model: gpt-5-mini
      organization: null
      streaming_callback: null
      timeout: null
      tools: null
      tools_strict: false
    type: haystack.components.generators.chat.openai.OpenAIChatGenerator
    ...

직렬화 (Serialization)

토큰 기반 비밀은 직렬화할 수 없는 반면, 환경 변수 기반 비밀은 딕셔너리로 변환하거나 딕셔너리에서 다시 만들 수 있어요.

# Convert to dictionary
env_secret_dict = env_secret.to_dict()

# Create from dictionary
new_env_secret = Secret.from_dict(env_secret_dict)

비밀 해석하기 (Resolving Secrets)

두 유형의 비밀 모두 resolve_value 메서드를 사용해 실제 값으로 해석할 수 있습니다. 이 메서드는 토큰 또는 환경 변수의 값을 반환해요.

# Resolve the token-based secret
token_value = api_key_secret.resolve_value()

# Resolve the environment variable-based secret
env_value = env_secret.resolve_value()

커스텀 컴포넌트 예제 (Custom Component Example)

다음은 Haystack에서 Secret 클래스를 사용하는 컴포넌트를 만드는 방법을 보여주는 완전한 예제예요. 토큰 기반과 환경 변수 기반 인증의 차이를 강조하고, 토큰 기반 비밀은 직렬화할 수 없음을 보여줍니다.

from haystack.utils import Secret, deserialize_secrets_inplace

@component
class MyComponent:
    def __init__(self, api_key: Optional[Secret] = None, **kwargs):
        self.api_key = api_key
        self.backend = None

    def warm_up(self):
        # Call resolve_value to yield a single result. The semantics of the result is policy-dependent.
        # Currently, all supported policies will return a single string token.
        self.backend = SomeBackend(
            api_key=self.api_key.resolve_value() if self.api_key else None,  # ...
        )

    def to_dict(self):
        # Serialize the policy like any other (custom) data. If the policy is token-based, it will
        # raise an error.
        return default_to_dict(
            self,
            api_key=self.api_key.to_dict() if self.api_key else None,  # ...
        )

    @classmethod
    def from_dict(cls, data):
        # Deserialize the policy data before passing it to the generic from_dict function.
        api_key_data = data["init_parameters"]["api_key"]
        api_key = Secret.from_dict(api_key_data) if api_key_data is not None else None
        data["init_parameters"]["api_key"] = api_key
        # Alternatively, use the helper function.
        # deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"])
        return default_from_dict(cls, data)

# No authentication.
component = MyComponent(api_key=None)

# Token based authentication
component = MyComponent(api_key=Secret.from_token("sk-randomAPIkeyasdsa32ekasd32e"))
component.to_dict()  # Error! Can't serialize authentication tokens

# Environment variable based authentication
component = MyComponent(api_key=Secret.from_env_var("OPENAI_API_KEY"))
component.to_dict()  # This is fine

더 알아보기 (Learn more)