<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[개발자 경험 - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[개발자 경험 - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/kr/search-labs/blog/category/developer-experience</link>
    </image>
    <link>https://www.elastic.co/kr/search-labs/blog/category/developer-experience</link>
    <atom:link href="https://www.elastic.co/kr/search-labs/rss/category/developer-experience.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[kr]]></language>
    <lastBuildDate>Tue, 29 Sep 2026 09:33:31 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Kibana Dashboards API: 정식 출시 전에 50개 이상의 팀이 테스트한 모든 패널 유형을 위한 안정적인 계약]]></title>
    <description><![CDATA[Kibana 대시보드를 코드로 관리하세요. Git에 커밋하고, 환경 간에 승격하며, Kibana API와 Terraform을 사용해 배포를 자동화할 수 있습니다.]]></description>
    <content:encoded><![CDATA[<p><a href="https://dashboardsapispec.kibana.dev/dashboards#tag/Dashboards">Kibana Dashboards 및 Visualizations API</a>는 Elastic 9.5에서 프로덕션 환경에 사용할 수 있으며, 모든 구독 등급에서 제공되고 완전한 이전 버전과의 호환성을 지원합니다. 대시보드를 JSON으로 정의하고 Git에 커밋한 후, 지속적 통합 및 지속적 배포(CI/CD) 파이프라인, <a href="https://registry.terraform.io/providers/elastic/elasticstack/latest/docs/resources/kibana_dashboard">Terraform</a> 또는 이미 사용 중인 도구를 통해 여러 환경에 배포할 수 있습니다.  50개가 넘는 팀이 <a href="https://www.elastic.co/search-labs/blog/kibana-dashboards-as-code-terraform-api">9.4의 기술 미리보기</a> 기간에 API를 테스트했으며, 일부는 이미 프로덕션에서 사용하고 있습니다. 버전 9.5에서는 <a href="https://dashboardsapispec.kibana.dev/tags.html">태그</a>용 새 엔드포인트도 기술 미리 보기로 추가됩니다. <a href="https://dashboardsapispec.kibana.dev/markdowns.html">Markdown</a> 및 <a href="https://dashboardsapispec.kibana.dev/links.html#tag/Links">Links</a> 패널 엔드포인트는 현재 Elastic Cloud Serverless에서 제공되며 9.6에 추가될 예정입니다.</p><h2>Kibana Dashboards API에서 이전 버전과의 호환성이 의미하는 사항</h2><p>기술 미리 보기 기간에는 릴리스 간에 API 형태가 변경될 수 있었습니다.[1] 이제는 그렇지 않습니다. 정식 출시(GA)는 다음을 의미합니다.</p><ul><li><p><strong>완전한 이전 버전과의 호환성.</strong> 시간이 지남에 따라 새 필드와 패널 유형이 추가되지만, 기존 필드와 동작은 변경되지 않습니다. 향후 호환성이 깨지는 변경 사항은 매우 신중하게 검토되며, 새로운 주요 스택 버전에서만 도입됩니다.</p></li><li><p><strong>프로덕션 준비 및 완전한 지원.</strong> API에는 Elastic의 완전한 지원 보증이 적용됩니다. 자동화된 배포, 환경 승격 및 프로그래밍 방식의 대시보드 관리에 이 API를 프로덕션 환경에서 안심하고 사용할 수 있습니다.</p></li></ul><h2>태그, Markdown 및 Links 패널을 위한 새로운 Kibana API 엔드포인트</h2><p>Elastic 9.5에서는 대시보드를 분류하고 필터링할 수 있는 <a href="https://dashboardsapispec.kibana.dev/tags.html"><strong>태그</strong></a>용 새로운 독립형 엔드포인트도 도입합니다. 이제 전용 CRUD 엔드포인트를 통해 태그를 프로그래밍 방식으로 관리할 수 있어, 여러 환경에서 대규모로 대시보드를 더 쉽게 구성할 수 있습니다.	</p><p>새로운 <a href="https://dashboardsapispec.kibana.dev/markdowns.html"><strong>Markdown</strong></a> 및 <a href="https://dashboardsapispec.kibana.dev/links.html#tag/Links"><strong>Links</strong></a> 패널 엔드포인트는 현재 Serverless에서 제공되며, 다음 스택 릴리스인 9.6에 추가될 예정입니다.</p><h2>Kibana Dashboards API는 어떤 패널 유형을 지원하나요?</h2><p>Dashboards API는 9.5의 모든 <em>by-value</em> 패널을 지원합니다. by-value 패널은 재사용을 위해 저장된 라이브러리 패널과 달리 대시보드에 직접 정의된 패널입니다. 지원되는 모든 패널 유형에는 타입이 지정되고 검증된 스키마가 있습니다.</p><p><strong>패널 유형</strong></p><p><strong>상태</strong></p><p>XY 차트</p><p>지원됨</p><p>메트릭</p><p>지원됨</p><p>파이</p><p>지원됨</p><p>게이지</p><p>지원됨</p><p>히트맵</p><p>지원됨</p><p>데이터 테이블</p><p>지원됨</p><p>트리맵</p><p>지원됨</p><p>Discover 세션</p><p>지원됨</p><p>컨트롤</p><p>지원됨</p><p>Markdown</p><p>지원됨</p><p>Links</p><p>지원됨</p><p>ML 패널</p><p>지원됨</p><p>Observability 패널</p><p>지원됨</p><p>Maps</p><p>출시 예정</p><p>Vega</p><p>출시 예정</p><h2>Kibana 대시보드를 코드로 관리하는 방법</h2><p>Dashboards API를 사용하면 코드 기반의 대시보드를 위한 전체 워크플로우를 구현할 수 있습니다. 대시보드를 차이 비교가 가능한 깔끔한 JSON으로 내보내고, 이를 단일 정보 원천으로 Git에 커밋하며, 풀 리퀘스트에서 변경 사항을 검토하고, 개발·스테이징·프로덕션 전반에 동일한 정의를 배포할 수 있습니다. 대시보드를 코드로 관리하기 시작했다면 Git을 단일 정보 원천으로 취급해야 합니다. UI에서 직접 변경한 내용은 다음 배포 시 덮어써집니다.</p><p>대시보드를 스페이스, 클러스터 또는 단계 간에 이동할 때 가장 큰 과제는 대시보드가 데이터 뷰와 라이브러리 시각화 같은 객체를 ID로 참조한다는 점입니다. 이러한 ID는 자동 생성되며 환경마다 다르므로, 한 환경에서 내보낸 대시보드가 다른 환경에는 존재하지 않는 객체를 가리킬 수 있습니다. 이를 처리하는 방법은 자동화 수준이 높은 순서부터 낮은 순서까지 다음 세 가지입니다.</p><ul><li><p><strong>Terraform 사용.</strong> <a href="https://registry.terraform.io/providers/elastic/elasticstack/latest/docs/resources/kibana_dashboard">Elastic Stack Terraform provider</a>는 각 리소스를 추적하고 환경별 ID를 자동으로 매핑하므로, 대시보드를 개발 환경에서 프로덕션으로 승격할 때 참조가 일관되게 유지됩니다.</p></li><li><p><strong>by-value </strong><a href="https://www.elastic.co/docs/explore-analyze/visualize/esorql"><strong>Elasticsearch Query Language (ES|QL) 패널</strong></a><strong> 정의.</strong> 가장 이식성이 높은 패널 생성 방법은 대시보드에서 ES|QL을 직접 사용해 시각화를 정의하는 것입니다. <a href="https://www.elastic.co/docs/explore-analyze/query-filter/languages/esql-kibana">ES|QL</a> 쿼리는 쿼리 내에 지정한 인덱스에서 읽어 오므로, 패널에는 데이터 뷰나 라이브러리 객체에 대한 외부 참조가 없습니다. 그 결과 완전히 독립적이고 이식 가능한 대시보드가 만들어집니다..</p></li><li><p><strong>일치하는 ID 할당.</strong> 데이터 뷰나 라이브러리 시각화와 같은 저장된 객체를 참조하는 경우, ID를 자동 생성하는 POST 대신 PUT(업서트)을 사용하여 선택한 ID로 객체를 생성하세요. logs-prod와 같이 사람이 읽을 수 있는 ID를 사용하면 여러 환경에서 쉽게 재사용하고 식별할 수 있습니다.</p></li></ul><p>이러한 이식성 패턴과 코드 기반의 대시보드를 위한 전체 워크플로우에 관한 자세한 안내는 <a href="https://www.elastic.co/docs/explore-analyze/dashboards/manage-dashboards-as-code#dashboards-as-code-portability">코드 기반의 대시보드 관리</a> 설명서를 참조하세요.</p><h3>PUT을 사용하여 Dashboards API로 Kibana 대시보드 생성</h3><p>다음은 POST 대신 PUT을 사용해 대시보드 이름인 service-health-overview를 사용자 지정 ID로 할당하고, 메트릭 패널을 포함한 대시보드를 생성하는 간단한 예입니다. 동일한 로직은 라이브러리에 저장되는 독립형 시각화를 만들 때도 적용됩니다.</p>PUT kbn:/api/dashboards/service-health-overview
{
  "title": "Service health overview",
  "description": "Key service metrics — managed via API",
  "tags": [
    "production",
    "sre-team"
  ],
  "panels": [
    {
      "type": "vis",
      "grid": {
        "x": 0,
        "y": 0,
        "w": 12,
        "h": 8
      },
      "config": {
        "title": "Error rate (5xx)",
        "type": "metric",
        "data_source": {
          "type": "esql",
          "query": "FROM logs-* | WHERE http.response.status_code &gt;= 500 | STATS error_rate=count(*) BY host.name"
        },
        "metrics": [
          {
            "type": "primary",
            "column": "count"
          }
        ]
      }
    }
  ]
}<h2>Kibana Dashboards API 로드맵: Maps, Vega 및 독립형 엔드포인트</h2><p>Elastic은 API 기능 범위를 활발히 확장하고 있습니다. 다음으로는 Maps 및 Vega 패널 지원을 추가하고, 이들 패널의 타입이 지정된 스키마를 제공합니다. 또한 기존 대시보드 패널 지원 범위를 넘어 Discover 세션용 독립형 CRUD 엔드포인트와 Vega, Maps 및 Annotations용 독립형 CRUD 엔드포인트도 구축하고 있습니다. 이 엔드포인트는 대시보드 수명 주기와 분리됩니다.</p><p>전체 스키마 정의는 <a href="https://dashboardsapispec.kibana.dev/dashboards#tag/Dashboards">Dashboards API 설명서</a>에서 확인하세요. Terraform 사용자는 <a href="https://registry.terraform.io/providers/elastic/elasticstack/latest/docs/resources/kibana_dashboard">Elastic Stack Terraform provider</a>를 통해 정식 출시된 Dashboards API를 사용할 수 있습니다.</p><h2>참고</h2><ol><li><p>핵심 엔드포인트는 기술 미리보기와 비교해 변경되지 않았습니다. 9.4를 대상으로 통합을 구축했다면 9.5에서도 작동합니다. 호환성이 깨지는 변경 사항은 대시보드 목록 표시 및 기간 단위 형식에 영향을 주는 사소한 변경 두 가지뿐이며, <a href="https://www.elastic.co/docs/release-notes/kibana/breaking-changes">여기</a>에서 설명합니다.</p></li></ol>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/dashboards-as-code-kibana-api</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/dashboards-as-code-kibana-api</guid>
    <category><![CDATA[Kibana]]></category>
    <category><![CDATA[개발자 경험]]></category>
    <category><![CDATA[통합]]></category>
    <dc:creator><![CDATA[Teresa Alvarez Soler]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8ed7e33de291f255/6a730619c8b7ac02b251f9d3/image1.png" length="0" type="image/png"/>
    <pubDate>Wed, 05 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elastic Cloud Serverless 및 Elasticsearch용 통합 API 키 소개]]></title>
    <description><![CDATA[Elastic이 글로벌 분산형 IAM 아키텍처를 통해 서버리스 환경에서 컨트롤 플레인과 데이터 플레인 인증을 어떻게 통합하는지 알아보세요. 클라우드 및 Elasticsearch API에 하나의 API 키를 사용하세요.]]></description>
    <content:encoded><![CDATA[<p>성장하는 Elastic Cloud Serverless 프로젝트 플릿을 책임지는 사이트 신뢰성 엔지니어(SRE)라고 상상해 보세요. 여기에는 프로덕션 인프라를 위한 Elastic Observability, 보안 운영 센터(SOC) 팀을 위한 Elastic Security, 고객 대면 애플리케이션을 위한 Elasticsearch가 포함됩니다. 각 프로젝트에는 고유한 Elasticsearch API 키가 있습니다. 지속적 통합 및 지속적 배포(CI/CD) 파이프라인은 해당 프로젝트를 프로비저닝하고 관리하기 위해 별도의 클라우드 API 키를 필요로 합니다. 매 분기마다 순환일이 찾아오면 각 프로젝트를 살펴보고, 새로운 키를 발급하고, 테라폼 상태를 업데이트하고, 파이프라인을 재배치하고, 아무 문제 없이 진행되기를 바라야 합니다. 새벽 2시에 사고가 발생하여 액세스를 신속하게 취소해야 할 경우, 어떤 키가 어떤 프로젝트 및 서비스에 속하는지 파악하기 위해 자격 증명 스프레드시트를 교차 참조하게 됩니다.</p><p>오늘날에는 그 이야기가 훨씬 더 간단해졌습니다. <strong>Elastic Cloud API 키</strong>를 사용하여 <strong>Elastic Cloud Serverless</strong>에서 <strong>Elasticsearch</strong> 및 <strong>Kibana</strong> API에 대해 직접 인증할 수 있습니다. 이제 단일 자격 증명을 사용해 조직의 리소스를 관리하고, <em>이에 덧붙여</em> Elasticsearch 쿼리 언어(ES|QL) 쿼리, 데이터 수집 및 알림과 같은 데이터 작업을 실행할 수 있습니다.</p><p>이를 구축한 이유, 이를 가능하게 하기 위해 전 세계적으로 분산된 ID 계층을 설계한 방법, 프로젝트 간 검색의 토대를 마련한 방법을 살펴보세요.</p><h2>비밀 관리 부담</h2><p>데이터 플랫폼을 중심으로 안정적인 CI/CD 파이프라인, GitOps 워크플로우 또는 테라폼 자동화를 구축하는 데에는 숨겨진 비용, 즉 비밀스러운 확산이 따릅니다.</p><p>이전 모델에서 개발자들은 단절된 인증 방식에 직면했습니다.</p><ul><li><p><strong>컨트롤 플레인(Elastic Cloud API 키):</strong> <a href="https://www.elastic.co/docs/api/doc/cloud/">Elastic Cloud API</a>를 통해 프로젝트를 생성하고, 사용자를 초대하고, 청구를 관리하는 데 사용되는 조직 범위의 키입니다.</p></li><li><p><strong>데이터 플레인(Elasticsearch API 키):</strong> 특정 서버리스 프로젝트 <em>내부</em>에서 생성된 프로젝트 범위 키로 <a href="https://www.elastic.co/docs/api/doc/elasticsearch-serverless/">Elasticsearch</a> 및 <a href="https://www.elastic.co/docs/api/doc/serverless">Kibana</a> API와 상호 작용합니다.</p></li></ul><p>즉, 배포 스크립트는 Elastic Cloud에 인증하고 서버리스 프로젝트를 프로비저닝하며, 해당 프로젝트에서 새로 생성된 Elasticsearch API 키를 추출한 뒤 <em>그 두 번째 키</em>를 하위 애플리케이션이나 자동화 도구에 주입해야 했습니다. 이로 인해 복잡한 파이프라인, 조각화된 감사 로그, 자격 증명 유출 위험이 증가했습니다.</p><h2>Elastic Cloud Serverless의 통합 인증</h2><p>이번 릴리스에서는 서버리스 프로젝트의 분할이 사라졌습니다. 이제 <strong>클라우드, Elasticsearch 및 Kibana API</strong>에 대해 명시적으로 권한이 부여된 Elastic Cloud API 키를 생성할 수 있습니다.</p><ul><li><p><strong>이전:</strong> Elastic Cloud API 키는 컨트롤 플레인 토큰으로만 사용되었습니다. 프로젝트를 생성하고, 청구를 관리하며, 사용자를 초대할 수 있었지만 한계가 있었습니다. 해당 프로젝트 내에서 Elasticsearch나 Kibana API를 호출하는 데는 사용할 수 없었습니다. 데이터 작업을 위해서는 항상 프로젝트별로 구분된 두 번째 키가 필요했습니다.</p></li><li><p><strong>현재:</strong> Elastic Cloud API 키를 생성할 때 <strong>클라우드, Elasticsearch 및 Kibana API</strong> 액세스를 선택하면 Serverless에 대한 엄격한 경계가 제거됩니다. 해당 API 키는 진정한 통합 자격 증명이 됩니다. 해당 솔루션은 조직의 인프라를 관리하는 기능을 유지하면서, 동시에 모든 권한 있는 서버리스 프로젝트에서 데이터를 쿼리, 인제스트 및 분석할 수 있는 기본 액세스 권한을 확보합니다.</p></li></ul><p>Elastic Cloud API 키 하나로 이를 통합함으로써 범위를 지정하고, 감사하고, 순환하고, 하나의 단위로 취소할 수 있는 단일 ID를 얻게 됩니다. 모든 API 호출은 새 프로젝트를 프로비저닝하든 ES|QL 쿼리를 실행하든 감사 로그에서 동일한 자격 증명으로 표시되므로, 사고 조사 또는 규정 준수 검토 중에 단일 추적을 따를 수 있습니다. 자격 증명 순환이 더 이상 컨트롤 플레인 및 데이터 플레인 비밀 정보를 각각 따로 업데이트하는 방식이 아니라, 단일 단계 작업으로 처리됩니다. 또한 역할 할당은 프로젝트별로 이루어지기 때문에, 하나의 키로 여러 프로젝트에 걸쳐 통합 가시성 프로젝트에서 수집을 관리하고 보안 프로젝트에서 쿼리를 실행할 수 있으며, 각각에 대해 별도의 자격 증명을 처리하지 않아도 됩니다.</p><p>중요한 점은, <em>'통합'</em> 이 <em>'전능함'</em>을 의미하지는 않는다는 것입니다. <code>role_assignments</code> 페이로드를 사용하면 통합 키의 범위를 단일 프로젝트와 특정 역할(예: 읽기 전용)로 엄격하게 제한할 수 있어, 자격 증명이 노출되더라도 영향 범위가 완전히 제한되도록 보장합니다. 개발자가 퇴사하거나 애플리케이션이 폐기되는 경우, Elastic Cloud 콘솔에서 단일 키를 취소하여 컨트롤 플레인 및 연결된 모든 Elasticsearch 프로젝트 전반에 걸쳐 액세스를 즉시 종료할 수 있습니다.</p><p><em>(참고: Elastic Cloud Hosted/관리형 배포의 경우 클라우드 API 키는 여전히 컨트롤 플레인만 관리할 수 있습니다. 호스팅 스택 API에 대한 지원 확장은 향후 릴리스에서 제공될 예정입니다.)</em></p><h2>워크플로우 자동화</h2><p>시작 방법은 간단합니다. 이 모든 설정은 Elastic Cloud 콘솔을 통해 하거나 <a href="https://www.elastic.co/docs/deploy-manage/api-keys/elastic-cloud-api-keys">Elastic Cloud API</a>를 사용해 자동화할 수 있습니다.</p><p>UI 프로세스는 동일하게 유지되지만, 이제 프로젝트 역할 할당에서 <strong>클라우드, Elasticsearch 및 Kibana API</strong> 액세스 권한을 선택할 수 있습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltda0a18945295aa84/6a1707bd509168fab4e1ba19/c4f802f130655290cd474b283001a954d14c3088-2801x1681.png" alt="Elastic Cloud 화면에 API 키 페이지가 표시되며, 이름, 만료, 역할 할당 필드가 포함된 API 키 생성 모달이 열려 있습니다." /><p>다음은 Elastic Cloud API를 사용하여 프로그래밍 방식으로 통합 키를 생성하는 방법입니다. <code>application_roles</code> 배열에 주목하세요. 이 배열이 바로 키에 Elasticsearch 데이터 플레인에 대한 네이티브 액세스 권한을 부여하는 부분입니다.</p>curl -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: ApiKey $EC_API_KEY" \
  "https://api.elastic-cloud.com/api/v1/users/auth/keys" \
  -d '{
    "description": "unified-automation-key",
    "expiration": "90d",
    "role_assignments": {
      "project": {
        "elasticsearch": [
          {
            "role_id": "elasticsearch-admin",
            "organization_id": "YOUR_ORG_ID",
            "all": false,
            "project_ids": ["YOUR_PROJECT_ID"],
            "application_roles": ["admin"]
          }
        ]
      }
    }
  }'<p>생성된 후에는 이 키를 <code>Authorization: ApiKey</code> 헤더에 포함하여 <code>api.elastic-cloud.com</code>과 특정 서버리스 Elasticsearch 엔드포인트에 전달합니다.</p><h2>작동 원리: 분산 ID 계층 구축</h2><p>클라우드 API 키를 컨트롤 플레인과 데이터 플레인 모두에서 작동하도록 만드는 것은 단순히 토큰을 전달하는 것만큼 간단하지 않습니다. 이를 위해서는 근본적인 분산 시스템 문제를 해결해야 합니다.</p><p>과거에는 클라우드 API 키가 중앙 집중식 글로벌 보안 클러스터에 저장되었습니다. 컨트롤 플레인 작업에서는 더 높은 대기 시간이 허용되므로 잘 작동합니다. 하지만 Elasticsearch 데이터 요청은 지연 시간이 매우 짧아야 합니다. 모든 검색 쿼리나 인제스트 요청을 검증하기 위해 전 세계를 중앙 제어기로 왕복하는 비용을 감당할 수 없습니다.</p><p>이 문제를 해결하기 위해 당사는 전 세계적으로 분산된 데이터 저장소를 기반으로 하는 새로운 인증 아키텍처를 도입했습니다. 다음 시퀀스 다이어그램은 클라이언트가 Elastic Cloud API 키를 사용해 Elasticsearch 쿼리를 전송하는 것을 보여주며, 글로벌 컨트롤 플레인으로의 왕복 없이 전적으로 로컬 영역 내에서 인증이 어떻게 이루어지는지 설명합니다. Elasticsearch는 전 세계적으로 분산된 데이터베이스의 로컬 복제본에 대해 키를 검증하고 역할 할당을 확인하는 지역 IAM 서비스에 인증을 위임합니다. 인증이 완료되면 Elasticsearch는 쿼리를 실행하고 결과를 클라이언트에 반환합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf4fa84c3f33f88f7/6a1707baacf088989abe9a8e/3e38d7a862b9981523c5393c441b92eae13aeb90-2401x1351.webp" alt="클라우드 API 키를 포함한 클라이언트 요청이 Elasticsearch Serverless, 지역 IAM 서비스, 분산 데이터베이스 복제본을 거쳐 결과를 반환하기까지의 과정을 보여주는 시퀀스 다이어그램입니다." /><h3>전 세계적으로 분산된 지속성</h3><p>이제 Elastic Cloud API 키와 관련 역할 정의는 중앙 집중식 보안 클러스터에만 의존하는 대신, 전 세계적으로 분산되어 가용성이 높은 데이터베이스에 저장됩니다. 이 데이터베이스는 글로벌 컨트롤 플레인과 서버리스 프로젝트가 실제로 실행되는 지역 데이터 플레인 간에 아이덴티티 및 액세스 관리(IAM) 데이터를 동기화합니다.</p><h3>지역 IAM을 통한 로컬 유효성 검사</h3><p>클라이언트가 Elastic Cloud API 키를 사용하여 Elasticsearch에 요청을 보내면 해당 요청은 글로벌 제어 플레인으로 되돌아가지 않습니다. 대신 새로운 지역 IAM 서비스로 라우팅됩니다. 로컬 데이터베이스 복제본에 대해 키를 검증하여 지연 시간이 거의 없는 상태에서 인증이 이루어지고 글로벌 컨트롤 플레인 중단으로부터 완전히 격리되도록 합니다.</p><h3>동적 역할 매핑</h3><p>인증은 절반의 성공에 불과합니다. 시스템은 요청을 승인하는 절차도 수행해야 합니다. 지역 IAM 서비스는 클라우드 수준 역할 할당(예: <code>application_roles</code>)을 기본 Elasticsearch 권한으로 즉시 변환합니다. 그 후 Elasticsearch는 로컬 <code>.security</code> 인덱스 없이도 로컬에서 요청을 승인하고 실행할 수 있습니다.</p><h2>프로젝트 간 검색의 기반</h2><p>이 분산형 아이덴티티 아키텍처는 Elastic 플랫폼의 미래를 위한 기반이 되는 핵심 구성 요소입니다.</p><p>신원과 접근 권한이 통합되어 전 세계적으로 동기화되므로, 서로 다른 프로젝트 간에 안전하게 신원을 전달할 수 있는 프레임워크를 갖추게 되었습니다. 이를 통해 향후 Serverless 환경에서 구현될 <strong>프로젝트 간 검색(CPS)</strong> 기능을 사용할 수 있게 됩니다.</p><p>CPS를 사용하면 보안 및 통합 가시성 워크로드를 결합하는 것처럼 여러 원격 서버리스 프로젝트에 걸쳐 있는 데이터를 단일 데이터 세트처럼 쉽게 쿼리할 수 있습니다. 통합된 API 키를 사용함으로써 시스템은 복잡한 신뢰 관계, 인증서 또는 중복 자격 증명을 각 대상 프로젝트에 구성할 필요 없이 모든 프로젝트에 걸쳐 사용자의 권한을 자동으로 평가할 수 있습니다.</p><h2>자세히 알아보기</h2><p>스택을 간소화할 준비가 되셨나요?</p><ul><li><p><a href="https://www.elastic.co/docs/deploy-manage/api-keys/elastic-cloud-api-keys">Elastic Cloud API 키 문서</a>를 읽고 스택 접근 권한을 할당하는 방법을 알아보세요.</p></li><li><p>키 생성을 자동화하려면 <a href="https://www.elastic.co/docs/api/doc/cloud/operation/operation-create-api-key">API 키 생성(Elastic Cloud API)</a> 참고 자료를 확인하세요.</p></li><li><p>Elastic 플랫폼 전반에서 키 유형을 완전히 비교하려면 <a href="https://www.elastic.co/docs/deploy-manage/api-keys">Elastic API 키</a>를 확인하세요.</p></li></ul><p>지금 바로 <a href="https://cloud.elastic.co/registration">Elastic Cloud</a>에서 빌드를 시작하거나 계속하세요.</p><h2>안내 및 면책 조항</h2><p>이 게시물에서 설명된 모든 기능이나 성능의 출시와 일정은 Elastic의 단독 재량에 따라 결정됩니다. 현재 제공되지 않는 기능이나 성능은 예정된 시간에 출시되지 않을 수도 있으며 아예 제공되지 않을 수도 있습니다.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elastic-cloud-api-keys-unified-serverless</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elastic-cloud-api-keys-unified-serverless</guid>
    <category><![CDATA[Elastic Cloud Serverless]]></category>
    <category><![CDATA[개발자 경험]]></category>
    <dc:creator><![CDATA[ Alex Chalkias]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt16ca1a6af7e5bab8/6a1707b7a6c2b900abe7965b/864e229f00eb2018084f13dd7f0e390e18383ed4-1980x1188.png" length="0" type="image/png"/>
    <pubDate>Mon, 20 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elastic Workflows를 활용한 Kibana 대시보드 조회수 모니터링]]></title>
    <description><![CDATA[Elastic Workflows를 사용하여 30분마다 Kibana 대시보드 조회 지표를 수집하고 이를 Elasticsearch에 인덱싱하는 방법을 알아보세요. 수집된 데이터를 바탕으로 맞춤형 분석 및 시각화 기능을 직접 구축할 수 있습니다.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/kibana">Kibana</a>는 각 대시보드의 조회수를 추적하지만 해당 데이터는 기본으로 제공되는 대시보드 내에서 직접적으로 확인할 수는 없습니다. 이 글에서는 <strong>Elastic Workflows</strong>를 사용하여 30분마다 해당 데이터를 자동으로 수집하고 Elasticsearch에 인덱싱하고 이를 바탕으로 직접 분석 환경을 구축하는 방법을 알아보겠습니다.</p><p><a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic Workflows</a>는 Kibana에 내장된 자동화 엔진입니다. 간단한 YAML 설정을 통해 여러 단계로 구성된 프로세스를 정의할 수 있습니다. 각 워크플로우는 일정 또는 이벤트에 따라 트리거될 수 있으며 <a href="https://www.elastic.co/docs/explore-analyze/ai-features/elastic-agent-builder">Elastic Agent Builder</a>의 도구로 활용할 수 있습니다. 또한 각 단계에서 Kibana API 호출, Elasticsearch 쿼리 실행, 데이터 변환 작업을 수행할 수 있습니다.</p><p>여기서는 대시보드 조회수를 구체적인 예로 사용하지만 Kibana saved objects API를 통해 노출되는 모든 메트릭에 동일한 패턴을 적용할 수 있습니다.</p><h2>필수 구성 요소</h2><ul><li><p>9.3 버전이 실행 중인 <a href="https://www.elastic.co/cloud">Elastic Cloud</a> 또는 <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed">자체 관리형</a> 클러스터</p></li><li><p><a href="https://www.elastic.co/docs/explore-analyze/workflows/get-started#workflows-prerequisites">워크플로우 기능 활성화</a>(고급 설정)</p></li></ul><h2>1단계: <a href="https://www.elastic.co/docs/explore-analyze/query-filter/tools/console">Dev Tools</a>에서 원시 데이터를 탐색하기</h2><p>본격적인 구축에 앞서 우리가 활용할 데이터가 어떤 구성을 갖추고 있는지 먼저 이해해 봅시다. Kibana는 대부분의 설정 정보와 메타데이터를 전용 내부 인덱스에 <a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects">Saved Objects</a>라는 형태로 저장합니다. Kibana가 이러한 방식으로 추적하는 항목 중 하나는 대시보드 조회수입니다. 이때 'Usage Counters'라는 특수한 유형의 Saved Object를 사용하게 됩니다. 다음과 같이 Dev Tools에서 직접 쿼리할 수 있습니다.</p>GET kbn:/api/saved_objects/_find?type=usage-counter&amp;filter=usage-counter.attributes.domainId:"dashboard"%20and%20usage-counter.attributes.counterType:"viewed"&amp;per_page=10000<p>응답은 다음과 같습니다.</p>{
  "page": 1,
  "per_page": 10000,
  "total": 1,
  "saved_objects": [
    {
      "type": "usage-counter",
      "id": "dashboard:346f3c64-ebca-484d-9d57-ec600067d596:viewed:server:20260310",
      "attributes": {
        "domainId": "dashboard",
        "counterName": "346f3c64-ebca-484d-9d57-ec600067d596",
        "counterType": "viewed",
        "source": "server",
        "count": 1
      },
      ...
    }
  ]<p><code>counterName</code> 필드는 대시보드의 ID를 나타내며 <code>count</code>는 해당 특정 날짜의 대시보드 누적 조회수입니다. Kibana는 대시보드당 하루에 하나의 카운터 객체를 생성합니다. 객체 ID의 날짜 접미사(예: ...viewed:server:20260310)를 통해 이를 확인할 수 있습니다. 조회수는 사용자가 대시보드를 열 때마다 하루 동안 계속해서 증가합니다.</p><p>인덱스에 이러한 일일 문서 모델을 그대로 복제하는 대신 워크플로우 실행당 하나의 문서를 생성해 보겠습니다. 각 문서는 캡처 시점을 기준으로 해당 대시보드의 당일 누적 조회수를 기록합니다.</p><h2>2단계: 대상 인덱스 생성</h2><p>대시보드 조회 스냅샷을 저장할 인덱스가 필요합니다. 나중에 집계와 시각화가 가능하도록 명시적 매핑을 사용하여 다음과 같이 인덱스를 생성합니다. Dev Tools에서 다음 명령을 실행하세요.</p>PUT dashboard-views
{
  "mappings": {
    "properties": {
      "captured_at": {
        "type": "date"
      },
      "dashboard_id": {
        "type": "keyword"
      },
      "dashboard_name": {
        "type": "keyword"
      },
      "view_count": {
        "type": "integer"
      }
    }
  }
}<p>ID와 이름에 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/keyword"><code>keyword</code></a> 매핑을 사용하면 <a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">집계</a>를 수행할 수 있습니다. <code>view_count</code>에 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/number"><code>integer</code></a> 타입을 사용하는 것은 안전한 기본값입니다. Kibana는 카운터를 매일 초기화하므로 32비트 제한(하루 20억 회 이상의 조회수)에 도달하는 것은 현실적인 우려 사항이 아니기 때문입니다. 이 타입은 여전히 <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-max-aggregation"><code>max</code></a>, <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-avg-aggregation"><code>avg</code></a>, <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-min-aggregation"><code>min</code></a>과 같은 수치 연산을 지원합니다.</p><h2>3단계: 워크플로우 생성</h2><p><strong>Stack Management &gt; Workflows &gt; New Workflow</strong> 메뉴로 이동한 뒤 아래의 워크플로우 YAML 설정 내용을 복사하여 붙여넣으세요.</p>name: dashboard-views-ingestion
triggers:
  - type: scheduled
    with:
      every: 30m

steps:
  - name: fetch_dashboard_views
    type: kibana.request
    with:
      method: GET
      path: &gt;-
        /api/saved_objects/_find?type=usage-counter&amp;per_page=10000&amp;filter=usage-counter.attributes.domainId:"dashboard"%20and%20usage-counter.attributes.counterType:"viewed"

  - name: index_each_dashboard
    type: foreach
    foreach: "{{ steps.fetch_dashboard_views.output.saved_objects }}"
    steps:
      - name: fetch_dashboard_name
        type: kibana.request
        with:
          method: GET
          path: /api/saved_objects/dashboard/{{ foreach.item.attributes.counterName }}
        on-failure:
          continue: true

      - name: index_doc
        type: elasticsearch.request
        with:
          method: POST
          path: /dashboard-views/_doc
          body:
            dashboard_id: "{{ foreach.item.attributes.counterName }}"
            dashboard_name: "{{ steps.fetch_dashboard_name.output.attributes.title }}"
            view_count: "${{ foreach.item.attributes.count | plus: 0 }}"
            captured_at: "{{ execution.startedAt | date: '%Y-%m-%dT%H:%M:%SZ' }}"<p>다음 섹션에서는 워크플로를 단계별로 자세히 살펴봅니다.</p><h3>워크플로우 작동 방식</h3><h4>트리거</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7672aa533b4bc9ed/6a17dc5b420229d07c29f4d4/5670991d65c64ee833924225c2d375a1be868b13-325x162.png" alt=" 예약된 트리거" /><p>워크플로우는 30분 간격의 예약된 트리거에 따라 실행됩니다. 이를 통해 API에 무리를 주지 않으면서도 시계열 데이터를 확보할 수 있습니다.</p><h4>fetch_dashboard_views</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltab2a16f1f11304ea/6a17dc5d25daab26f608a117/66eaec147c3d01c524c67cf1c7f663ac56a3259d-812x215.png" alt=" 대시보드 가져오기" /><p><code>kibana.request</code>를 사용하여 Kibana Saved Objects API를 호출합니다. 별도의 인증 설정은 필요하지 않습니다. 워크플로우 엔진이 실행 컨텍스트에 따라 적절한 헤더를 자동으로 첨부하기 때문입니다.</p><h4>index_each_dashboard (foreach)</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte6b2611b0216555e/6a17dc5f445de95b584cffe5/aad45e8aed8dc81ded6260cd6199ff78dcffe3b4-1892x290.png" alt="각 대시보드 인덱싱하기" /><p>이전 단계에서 반환된 <a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects"><code>saved_objects</code></a> 배열을 순회합니다. 각 반복에서 현재 항목은 <code>foreach.item</code>을 통해 접근할 수 있습니다. 루프 내부에서는 각 대시보드에 대해 두 개의 하위 단계를 실행합니다.</p><p><strong>1. </strong><strong><code>fetch_dashboard_name</code></strong><strong>:</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb3733b3c24629abf/6a17dc60a292993fe3d02b75/db21ec5094b743018b9cd66c5052681f14c7d7e3-1999x431.png" alt=": 대시보드 이름 가져오기" /><p><code>GET /api/saved_objects/dashboard/{id}</code>를 호출하여 사용자가 읽을 수 있는 대시보드 제목을 확인합니다. <code>on-failure: continue: true</code>설정을 추가합니다. 이를 통해 특정 대시보드가 삭제되었더라도 조회수 카운터가 남아있는 경우 전체 실행이 실패 처리되지 않고 루프가 계속 지속됩니다.</p><p><strong>2. </strong><strong><code>index_doc</code></strong><strong>:</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt385c1f29d717c280/6a17dc62faa91353cb93c759/f49dd0c9f0817bb1e1e5d9f4a2b05d13ef331054-1999x626.png" alt=" Elasticsearch 요청" /><p><code>POST /dashboard-views/_doc</code>을 사용해 명시적 ID 없이 각 문서를 인덱싱하며 이 과정에서 Elasticsearch가 ID를 자동으로 생성합니다. 이는 실행할 때마다 새로운 문서를 생성합니다. 이전 스냅샷을 덮어쓰지 않고 시간에 따른 조회수 이력을 쌓을 수 있습니다.</p><p>주목할 만한 두 가지 사항이 있습니다:</p><ul><li><p><code>captured_at</code> 필드는 날짜 필터를 사용하여 타임스탬프를 <a href="https://www.iso.org/iso-8601-date-and-time-format.html">ISO 8601</a> 형식으로 변환합니다. 이 설정이 없으면 값이 <code>Tue Mar 10 2026 05:03:47 GMT+0000</code>과 같은 JavaScript 날짜 문자열로 출력됩니다. 이 경우 Elasticsearch가 이를 날짜 형식으로 매핑하지 못합니다.</p></li><li><p><code>view_count</code>는 숫자 유형을 유지하기 위해 <code>${{ }}</code> 구문과 <code>| plus: 0</code>을 사용합니다. 단순히 <code>{{ }}</code>를 사용하면 값이 문자열로 렌더링되어 대시보드에서 수치 연산을 수행할 수 없게 됩니다.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3b94bb6c22c0253e/6a17dc6425daab37a508a11b/6d48c8784d5df6192e8b5175e69dbab5098194bc-919x774.png" alt="" /><p><em>UI를 통해 워크플로의 각 단계를 편리하게 디버깅할 수 있습니다.</em></p><h2>4단계: 통계 대시보드 구축</h2><p>워크플로우가 몇 번 실행되어 데이터가 수집되면 dashboard-views 데이터 뷰를 사용하여 Kibana에서 새 대시보드를 만듭니다.</p><p>활용 가능한 패널 예시:</p><ul><li><p><strong>조회수별 주요 대시보드:</strong> X축에 <code>dashboard_name</code>을, Y축에 <code>last_value(view_count)</code>를 설정한 <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/bar-charts"><strong>막대 차트</strong></a>를 사용하세요. 이를 통해 각 대시보드의 현재 일일 조회수를 확인할 수 있습니다.</p></li><li><p><strong>시간에 따른 조회수 변화:</strong> X축에 <code>captured_at</code>을, Y축에 <code>last_value(view_count)</code>를 설정하고 <code>dashboard_name</code>으로 구분한 <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/line-charts"><strong>선형 차트</strong></a>를 사용하세요. 워크플로우를 실행할 때마다 새로운 문서가 추가되므로 중복 합산 대신 'Last value' 집계를 사용하여 시간 버킷당 최종 수치를 가져옵니다.</p></li><li><p><strong>현재 스냅샷:</strong> 가장 최근의 <code>captured_at</code> 데이터를 사용하는 <a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/tables"><strong>데이터 테이블</strong></a>을 만들어 모든 대시보드의 최신 조회수를 표시하세요.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt18d0390e0526b215/6a17dc65a292991da7d02b79/e245b95f67daf76a2aaf4cb9df2c75ef4cfef582-1462x747.png" alt="" /><p>각 워크플로우 실행 시마다 새 문서가 생성되므로 시간 범위를 필터링하여 특정 기간의 활동을 분석하거나 주간 단위로 비교할 수 있습니다. 또한 대시보드 조회수가 임계값 아래로 떨어질 때 알림을 구축할 수 있습니다.</p><h2><strong>결론</strong></h2><p>Elastic Workflows는 이러한 주기적인 데이터 수집에 매우 적합합니다. 데이터 소스(Kibana API)와 대상(Elasticsearch)이 모두 네이티브로 연결되어 있어 자격 증명을 별도로 관리할 필요가 없기 때문입니다. 워크플로우 엔진이 <code>kibana.request</code> 및 <code>elasticsearch.request</code>단계의 인증을 자동으로 처리하므로 사용자는 로직만 작성하면 됩니다.</p><h2><strong>리소스</strong></h2><ul><li><p><a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic Workflows</a></p></li><li><p><a href="https://www.elastic.co/docs/api/doc/kibana/">Kibana API</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/monitor-kibana-dashboard-views-elastic-workflows</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/monitor-kibana-dashboard-views-elastic-workflows</guid>
    <category><![CDATA[개발자 경험]]></category>
    <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltef604bbb6dee6be0/6a17dc67a29299db23d02b7d/0ed94ce00962287b5507f45c92ecb60fdcbf2718-1280x720.png" length="0" type="image/png"/>
    <pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Kubernetes에서의 종속성 관리]]></title>
    <description><![CDATA[Renovate CLI와 Argo Workflows를 사용하여 Kubernetes에서 종속성 관리를 간소화하는 방법]]></description>
    <content:encoded><![CDATA[<p>다음은 업데이트를 자동화하고 일반적인 취약성 및 노출(CVE)을 신속하게 해결하며 수천 개의 리포지토리에 새 패키지 버전을 효율적으로 전파하기 위해 Kubernetes, Argo Workflows, Argo Events 및 Renovate CLI로 셀프 호스팅된 종속성 관리 플랫폼을 구축한 방법입니다.</p><h2><strong>Elastic의 종속성 관리</strong></h2><p>Elastic에서는 비공개 및 공개를 포함하여 수백 개, 심지어 수천 개의 리포지토리를 관리해야 합니다. 중요한 CVE가 발견되면 여러 질문에 대한 즉각적인 답변과 조치가 필요합니다. '어떤 리포지토리가 취약한가?', '얼마나 빨리 패치를 적용할 수 있는가?' 등이 있습니다. 보안 문제 외에 '수작업에 너무 많은 시간을 들이지 않고 새 패키지 버전의 릴리스를 해당 패키지에 의존하는 모든 리포지토리에 신속하게 전파하는 방법은 무엇인가?'라는 생산성 관련 질문도 제기됩니다.</p><p>종속성 관리 방법을 모색하게 된 초기 계기는 <a href="https://www.elastic.co/blog/reducing-cves-in-elastic-container-images">CVE 감소</a>를 위해 자동화된 업데이트와 함께 안전한 기반을 구축해야 하는 필요성 때문이었습니다. 종속성 관리 솔루션을 신중하게 검토한 후, 우선 자체 호스팅된 인프라 구축에 착수했습니다. Mend Renovate Community 셀프 호스팅 에디션을 실행하기 위해 자체 Kubernetes 클러스터를 사용하고 있었습니다. 사용자들이 셀프 서비스 방식으로 접근할 수 있는 종속성 관리 플랫폼을 제공하는 것이 아이디어의 핵심이었습니다.</p><p>초기 실험이 성공적이었기 때문에 점점 더 많은 팀이 플랫폼에 온보딩하여 일상적인 리포지토리의 수명 주기에서 업데이트 및 CVE 패치에 이를 사용하기 시작했습니다. 이 과정이 너무 빨리 진행되어 곧 자체 호스팅 설치 용량의 한계에 도달했습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc99617fc3eed538d/6a170ea9964cea459d08bc67/e14d9f98d4eccaa08a335d5bd23d88e5debbb344-1600x1103.png" alt="Elastic의 종속성 관리" /><h3><strong>과제: 대규모 조직에서 수많은 리포지토리를 관리할 때, 종속성 관리 플랫폼을 어떻게 확장할 수 있을까요?</strong></h3><p>기존 종속성 관리 플랫폼은 한 번에 하나의 리포지토리만 처리할 수 있었고, 수많은 리포지토리로 인해 순차적 처리 모델로는 대처할 수 없었습니다. 우리는 <strong>종속성 관리 도구의 단일 인스턴스</strong>로 점점 커지는 리포지토리 목록을 처리할 수 있다는 생각 자체에 문제가 있다는 것을 이미 인식하고 있었습니다. 리포지토리는 대기열에서 기다렸고, 때로는 몇 시간이 걸렸습니다. 매일 50% 이상의 리포지토리가 처리되지 않았습니다. 즉, 리포지토리의 50% 이상이 되기까지 24시간 이상 대기해야 했습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0d205fd379e3c07a/6a170eab961e691e1fc4cfca/45ade5bda08f82bed0b3d0d3736cbd6f056e7a4e-1312x816.jpg" alt="의존성 관리 문제" /><p>대규모 리포지토리는 방대한 코드베이스와 여러 개의 열린 PR로 인해 더 큰 병목 현상을 일으켰습니다. GitHub 웹훅 이벤트는 시퀀스를 중단시켰습니다. Automerge는 스캔 타이밍을 예측할 수 없기 때문에 신뢰할 수 없었습니다. 사용자들에게 스캔 빈도에 대해 약속했지만, 그 약속을 지키지 못했습니다.</p><h3><strong>자체 구축 결정: Elastic 고유의 확장성 및 보안 요구 사항 충족</strong></h3><p><strong>Mend의 Renovate Self-Hosted Enterprise 셀프 호스팅 에디션</strong>을 포함한 상용 옵션을 고려하면서, Elastic 내부적으로는 몇 가지 주요 이니셔티브를 강화하고 있었습니다.</p><p>Elastic의 타협할 수 없는 특정한 요구 사항을 충족할 수 있는 심도 깊은 맞춤형 솔루션만이 유일한 해결책이라는 인식에 따라 자체 플랫폼을 구축하기로 결정했습니다.</p><ol><li><p><strong>내부 개발자 플랫폼 투자:</strong> 당시 내부 개발자 플랫폼에 대해 이미 대규모 투자를 시작한 상태였습니다. 각 서비스를 여기에 조화시키는 방식에 대해 논의하고 설계했습니다. 즉, 자체적으로 만든 종속성 관리 플랫폼의 규칙과 관행을 시험적으로 적용해 보고자 했습니다. 게다가 새로운 가이드라인이 시행될 예정이었기 때문에, 그 전에 플랫폼을 설계하고 싶었습니다.</p></li><li><p><strong>네이티브 통합 및 워크플로우 맞춤 설정:</strong> 내부 도구 및 내부 프로세스와의 간편한 통합이 필요했습니다. 예를 들어, 서비스 카탈로그(Backstage)를 사용하여 구성을 코드로 중앙 집중화하고자 했습니다. Backstage의 사용에 관한 특정 요구 사항이 있었으며 플랫폼이 이러한 요구 사항과 호환되기를 원했습니다. 따라서 Renovate 셀프 호스팅 API를 Backstage 자동화와 함께 사용하는 것이 가능하지만, 이것으로는 내부 프로세스를 완전히 지원할 수 없습니다.</p></li><li><p><strong>Elastic에 특화된 심층 방어 보안:</strong> 엄격한 보안 규정 준수를 위해서는 에코시스템에 맞춘 맞춤형 보안 메커니즘이 필요했습니다. 우리는 <a href="https://entro.security/blog/how-elastic-scaled-secrets-nhi-security-elastics-playbook-from-visibility-to-automation/">"비인간 ID"의 사용을 강화하기 위해 노력하고 있었습니다.</a> 이러한 액세스 강화 방식으로 인해 비표준적인 GitHub 인증 수단은 이 내부 구현을 지원하지 않는 기성 도구에서는 작동하지 않았습니다. 워크플로우에는 부모-자식 워크플로우 비밀 암호화 패턴 구현과 일시적인 일회성 GitHub 토큰 사용이 포함되었습니다. 자체 구축이 이러한 고유한 보안 계층을 내장하고 복잡한 멀티클라우드 환경 전반에 걸쳐 공격 표면을 최소화하는 유일한 현실적인 방법이었습니다.</p></li></ol><h2><strong>해결책: 종속성 관리를 위한 워크플로우 오케스트레이션</strong></h2><p>솔루션은 이미 사용 중인 종속성 관리 도구를 대체하거나 다른 솔루션을 찾지 않고 이를 기반으로 구축한다는 데서 출발했습니다. 이는 가능성의 징후를 보였으며, 조직 전반에 걸쳐 다양한 요구에 맞추는 유연성이 중요했습니다. 다양한 솔루션을 고려했으며, 우리가 충족해야 할 중대하고 때로는 특별한 요구 사항을 토대로 결정을 내릴 수 있었습니다. 각 리포지토리가 독립적으로 처리되어 병목 현상이 발생하지 않고 성장을 위한 기반을 제공할 수 있도록 신뢰할 수 있고 확장성 있는 종속성 관리 플랫폼을 구축하기로 결정했습니다.</p><p>세 가지 핵심 원칙에 따라 플랫폼을 설계했습니다.</p><h3><strong>1. 병렬 처리</strong></h3><p>모든 리포지토리는 자체 종속성 관리 처리 환경을 갖습니다. 더 이상 대기열이 없습니다. 동시성은 사용하는 리소스의 수에 의해서만 제한됩니다. 또한 GitHub 속도 제한을 피하기 위해 스마트 분산 스케줄링을 적용했습니다.</p><h3><strong>2. 셀프 서비스 가능</strong></h3><p>서비스 카탈로그(백스테이지)를 사용하여 새로운 리포지토리를 자동으로 온보딩하고 관리합니다. 자체 리소스 정의를 사용하여 최종 사용자에게 리포지토리 처리 빈도, 스케줄에 할당할 리소스 수, 그리고 어떤 이유로든 처리를 끌지 아니면 다시 켤지 선택할 수 있는 옵션을 제공합니다. 사용자 요구 사항이 발전하고 새로운 설치 환경에 익숙해짐에 따라 이러한 방식으로 더 많은 옵션을 추가할 계획입니다.</p><h3><strong>3. 비밀 범위 축소 및 네임스페이스 격리</strong></h3><p>보안 강화를 위해, 각 워크플로우 시작 시 생성되는 임시 GitHub 토큰을 종속성 관리 포드에 제공합니다. 그뿐만 아니라, 필요한 비밀 키만 제공받을 수 있도록 워크로드를 특정 네임스페이스로 격리합니다. Kubernetes RBAC를 사용하여 각 종속성 관리 워크플로우에서 액세스할 수 있는 비밀 정보를 제어합니다. 또한 GitHub 토큰을 부모 워크플로우에서 자식 워크플로우로 전파할 때 암호화를 사용합니다.</p><p>Kubernetes를 사용하여 플랫폼을 재구축했으며, Kubernetes의 강력한 기능을 활용해 Argo Workflows는 프로세스 로직을 강화하고, Renovate CLI는 한 번에 하나의 리포지토리를 스캔하고 처리하도록 설정되었습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3548539ab52fbb79/6a170eac0c48573da601ab26/5560ed20e2bd9ecdd574a9c835126d12b24c332f-1600x1157.png" alt="Kubernetes에서의 종속성 관리 워크플로우 개요" /><p><strong>장점:</strong> 검증된 오픈 소스 프로젝트를 독창적인 방식으로 활용하여 모든 프로젝트에 대해 새로운 작동 예제를 제공하는 동시에, 개발 속도를 높이고 팀의 CVE 감소를 강화합니다.</p><h2><strong>종속성 관리 아키텍처: 4개의 마이크로서비스</strong></h2><p>이 플랫폼은 맞춤 제작된 4가지 구성 요소로 이루어져 있습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6451ff19da4db511/6a170eaec1e8a562e0f88378/2b3d4046c05bb261e45d40c59f864eb51fb9eaa9-1217x1600.png" alt="Kubernetes에서 종속성 관리를 위한 구성 요소" /><h3><strong>워크플로우 오퍼레이터(Go/Kubebuilder)</strong></h3><p>3가지 맞춤형 리소스 정의(CRD)를 통해 워크플로우 수명 주기를 관리하는 Kubernetes Operator:</p><ul><li><p><strong>RepoConfig CRD:</strong> 리포지토리 구성을 위한 단일 정보 소스입니다.</p></li></ul><p>다음은 오퍼레이터에서 RepoConfig가 정의되는 방식입니다.</p>// RepoConfig is the Schema for the repoconfigs API
type RepoConfig struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitempty,omitzero"`

	// spec defines the desired state of RepoConfig
	// +required
	Spec RepoConfigSpec `json:"spec"`

	// status defines the observed state of RepoConfig
	// +optional
	Status RepoConfigStatus `json:"status,omitempty,omitzero"`
}<p>다음은 RepoConfig 인스턴스의 모습입니다.</p>apiVersion: workflows.elastic.co/v1
kind: RepoConfig
metadata:
  generation: 3
  name: elastic-test-repo
  namespace: dependency-management-operator
spec:
  owner: group:my-team
  renovate:
    config:
      resourceGroup: SMALL
      runFrequency: 4h
    enabled: true
  repository: elastic/test-repo<ul><li><p><strong>부모 CRD:</strong> 예약된 스캔을 위한 CronWorkflows를 관리합니다.</p></li></ul><p>부모 컨트롤러의 조정 루프 내에서 워크플로우 설정이 생성되고 최신 상태로 유지되거나, 필요한 경우 삭제되는지 확인합니다.</p><p>먼저 워크플로우에 대한 몇 가지 전역 설정을 가져옵니다.</p>func (r *ParentReconciler) reconcileSubResources(ctx context.Context, req ctrl.Request, parent *workflowsv1.Parent) error {
	logger := logf.FromContext(ctx)
	logger.Info("Reconcile SubResources for Parent", "name", req.NamespacedName)
	wfSet := workflowsettings.WorkflowSettings{
		RunFrequency:   parent.Spec.RunFrequency,
		ResourceGroups: "parent",
	}<p>유사한 워크플로우가 동시에 실행되는 것을 방지하기 위해 mutex configmap이 최신 상태인지 확인합니다.</p>	cfMngr := resources.NewConfigMapManager(r.Client, r.Scheme, r.OperatorConfig.ParentNamespace)
	err := cfMngr.CreateOrUpdateSyncMutexConfigmap(ctx, fmt.Sprintf("%s%s", r.OperatorConfig.ResourcesPrefix, r.OperatorConfig.SyncMutexCfgMapName), strings.TrimPrefix(parent.Spec.Repository, "elastic/"), r.OperatorConfig.SemaphoreConcurrencyLimit)<p>그런 다음 CronWorkflows와 워크플로우 템플릿을 생성하거나 업데이트하는 구조체인 Workflow Manager를 생성합니다.</p>	wfMngr := resources.NewArgoWorkflowManager(r.Client,
		r.Scheme,
		curateResourceName(
			strings.ReplaceAll(parent.Spec.Repository, "/", "-"),
		),
		parent.Namespace,
		"parent-workflow",
		false).
		WithOrganization(r.OperatorConfig.GitHubOrg).
		WithRepoName(parent.Spec.Repository).
		Init(true, true).
		WithPrefix(r.OperatorConfig.ResourcesPrefix).
		WithWfTemplateName(r.OperatorConfig.ParentWorkflowTemplate).
		WithResources(wfSet.GetResourceCategory()).
		WithSchedule(wfSet.GetCronSchedule()).
		WithImagePullSecrets([]corev1.LocalObjectReference{{
			Name: r.OperatorConfig.WorkflowImagePullSecrets,
		}}).
		AddArgument(true, true, "extra_cli_args").
		SetArgument(true, false, "extra_cli_args", "none").
		AddTemplate(resources.NewParentDAGTemplateInstance()).
		AddTemplate(resources.NewWorkflowsTemplateInstance("check-child-workflows", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddTemplate(resources.NewWorkflowsTemplateInstance("security", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddTemplate(resources.NewWorkflowsTemplateInstance("submit-child-workflow", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector))
	wfMngr.OverWriteCommand("submit-child-workflow", r.OperatorConfig.ChildNamespace)
	wfMngr.OverwriteWfTemplateName("parent-wftmpl")
	wfMngr.AddSynchronization(fmt.Sprintf("%s%s", r.OperatorConfig.ResourcesPrefix, r.OperatorConfig.SyncMutexCfgMapName), "{{workflow.parameters.repo_name}}")
	err = wfMngr.CreateOrUpdateCronWorkflow(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update cron workflow: %w", err)
	}
	err = wfMngr.CreateOrUpdateWorkflowTemplate(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update workflow template: %w", err)
	}
	return nil<ul><li><p><strong>자식 CRD:</strong> 리포지토리별 리소스로 워크플로우 템플릿을 관리합니다.</p></li></ul><p>자식 컨트롤러는 부모와 유사한 조정 의무를 가지고 있지만, 이번에는 부모 워크플로우에 의해 트리거될 자식 네임스페이스의 워크플로우 템플릿에 대한 책임이 있습니다.</p>func (r *ChildReconciler) reconcileSubResources(ctx context.Context, req ctrl.Request, child *workflowsv1.Child) error {
	logger := logf.FromContext(ctx)
	logger.Info("Reconcile SubResources for Child", "name", req.NamespacedName)
	wfSet := workflowsettings.WorkflowSettings{
		ResourceGroups: child.Spec.ResourceCategory,
	}
	wfMngr := resources.NewArgoWorkflowManager(r.Client,
		r.Scheme,
		curateResourceName(
			strings.ReplaceAll(child.Spec.Repository, "/", "-"),
		),
		child.Namespace,
		"runner",
		true).
		Init(false, true). // only manage workflow template
		WithPrefix(r.OperatorConfig.ResourcesPrefix).
		WithSuffix("-child-wftmpl").
		WithRepoName(child.Spec.Repository).
		WithOrganization(r.OperatorConfig.GitHubOrg).
		WithResources(wfSet.GetResourceCategory()). // will override resources of presets if set
		WithImagePullSecrets([]corev1.LocalObjectReference{{
			Name: r.OperatorConfig.WorkflowImagePullSecrets,
		}}).
		AddTemplate(resources.NewWorkflowsTemplateInstance("runner", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddArgument(false, true, "repo_full_name").
		AddArgument(false, true, "repo_name").
		AddArgument(false, true, "encrypted_token").
		AddArgument(false, true, "extra_cli_args")
	wfMngr.OverWriteCommand("runner", r.OperatorConfig.ChildNamespace)
	err := wfMngr.CreateOrUpdateWorkflowTemplate(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update workflow template: %w", err)
	}
	return nil
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta735156ba6e370ef/6a170eaf7d8d6706fd70e7e4/7ac70492a1266ba02cb8afbafc5a486cb38a0edc-1600x1290.png" alt="Kubernetes에서의 종속성 관리 워크플로우" /><p>멀티 컨트롤러 패턴은 명확한 분리를 제공합니다. RepoConfig 컨트롤러는 온보딩/오프보딩을 처리하고, 부모 컨트롤러는 스케줄링을 관리하며, 자식 컨트롤러는 실행 템플릿을 처리합니다.</p><h3><strong>GitHub Events Gateway(Go)</strong></h3><p>GitHub 웹훅을 수신하고 서명을 확인하고 조직/리포지토리별로 필터링한 후 Argo Events로 라우팅하는 안전한 웹훅 프록시입니다. 종속성 대시보드 상호 작용, PR 이벤트 및 패키지 업데이트에 반응하는 10개의 서로 다른 센서를 구축했습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7e748ceb93c13a5a/6a170eb1a6c2b908f8e797b4/4828625456cbd6efa8020a20f10d23f294f98a02-1306x1600.png" alt="Kubernetes에서의 종속성 대시보드 상호 작용" /><p>이 게이트웨이는 다음을 통해 GitHub 앱과 통합할 수 있도록 지원합니다.</p><ul><li><p>보안을 위해 수신되는 GitHub 웹훅 서명을 확인합니다.</p></li><li><p>유효한 이벤트를 모든 관련 헤더 및 인증 정보와 함께 Argo Events EventSource로 전달합니다.</p></li><li><p>또한 EventSource에 authSecret을 구성하고 전달된 요청의 Bearer 헤더로 이를 제공합니다.</p></li><li><p>로깅, 메트릭 및 재시도 로직을 제공합니다.</p></li></ul><p>각 GitHub 이벤트 요청에 대해 다양한 검증을 수행합니다.</p><p>일부 HTTP 속성이 있는지 확인합니다:</p>// ValidateRequestMethod checks if the request method is POST.
func ValidateRequestMethod(r *http.Request) error {
	if r.Method != http.MethodPost {
		return fmt.Errorf("method not allowed, only POST is accepted")
	}
	return nil
}

// ValidateRequiredHeaders checks for required GitHub headers.
func ValidateRequiredHeaders(r *http.Request) error {
	eventType := r.Header.Get("X-GitHub-Event")
	deliveryID := r.Header.Get("X-GitHub-Delivery")
	signature := r.Header.Get("X-Hub-Signature-256")
	if eventType == "" || deliveryID == "" || signature == "" {
		return fmt.Errorf("missing required GitHub headers")
	}
	return nil
}

// ValidateUserAgent checks that the User-Agent header starts with GitHub-Hookshot/
func ValidateUserAgent(r *http.Request) error {
	userAgent := r.Header.Get("User-Agent")
	if !strings.HasPrefix(userAgent, "GitHub-Hookshot/") {
		return fmt.Errorf("invalid User-Agent")
	}
	return nil
}<p>또한 각 요청의 서명과 그 구성도 검증합니다.</p>// ValidateSignature verifies the GitHub webhook signature.
func ValidateSignature(r *http.Request, secret string) ([]byte, error) {
	payload, err := GitHub.ValidatePayload(r, []byte(secret))
	if err != nil {
		return nil, fmt.Errorf("invalid GitHub signature: %w", err)
	}
	return payload, nil
}

// ValidateAllowedOwner checks if the organization login is in the allowed organizations list.
func ValidateAllowedOwner(payload []byte, allowedGitHubOrganizations []string) (string, error) {
	var orgLogin string
	var payloadMap map[string]any
	if err := json.Unmarshal(payload, &amp;payloadMap); err == nil {
		if orgObj, ok := payloadMap["organization"].(map[string]any); ok {
			if login, ok := orgObj["login"].(string); ok {
				orgLogin = login
			} else if name, ok := orgObj["name"].(string); ok {
				orgLogin = name
			}
		}
	}
	if !slices.Contains(allowedGitHubOrganizations, orgLogin) {
		return orgLogin, fmt.Errorf("organization login not allowed")
	}
	return orgLogin, nil
}<p>마지막으로 이벤트 유형에 따라 Argo Events로 라우팅됩니다.</p>	// Map eventType to Argo `EventSource` path
	var endpoint string
	switch eventType {
	case "push":
		endpoint = "/push"
	case "issues":
		endpoint = "/issues"
	case "pull_request":
		endpoint = "/pull-requests"
	default:
		slog.Info("Ignoring unhandled event type", "event_type", eventType, "delivery_id", deliveryID)
		w.WriteHeader(http.StatusOK)
		_,  = w.Write([]byte("ok"))
		return
	}
	forwardURL := h.config.ArgoEventSourceForwardURL + endpoint<p>Argo Events 측에서는 10개의 센서가 새로운 이벤트를 감지하기 위해 Argo Events EventBus를 모니터링합니다.</p>apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
  name: {{ .Values.sensors.packageUpdateOnDefaultBranch.name }}
  namespace: {{ .Release.Namespace }}
spec:
  eventBusName: {{ .Values.eventBus.name }}<p>그 후 스크립트는 각 센서의 논리를 적용합니다:</p>script: |
          local e = event
          if not e or not e.body or not e.body.repository then
            return false
          end

          -- e.g., "refs/heads/main"
          local ref = e.body.ref
          local default_branch = e.body.repository.default_branch
          if not ref or not default_branch then
            return false
          end

          local expected = "refs/heads/" .. default_branch
          if ref ~= expected then
            return false
          end

        {{- if .Values.sensors.packageUpdateOnDefaultBranch.packageFiles }}
          patterns = { {{- range $i, $f := .Values.sensors.packageUpdateOnDefaultBranch.packageFiles }}{{ if $i }}, {{ end }}"{{ $f }}"{{- end }} }
        {{- end }}

          local function anyMatch(path)
            if type(path) ~= "string" then return false end
            for _, pat in ipairs(patterns) do
              -- match filename at repo root, or anywhere under subdirs
              if path:match(pat) or path:match(".+/" .. pat) then
                return true
              end
            end
            return false
          end

          local function filesContainPackage(paths)
            if type(paths) ~= "table" then return false end
            for _, p in ipairs(paths) do
              if anyMatch(p) then return true end
            end
            return false
          end

          -- Inspect all commits (GitHub includes added/modified/removed lists)
          local commits = e.body.commits
          if type(commits) ~= "table" then
            -- Fallback: some payloads include only head_commit
            commits = {}
            if type(e.body.head_commit) == "table" then
              table.insert(commits, e.body.head_commit)
            end
          end

          for _, c in ipairs(commits) do
            if filesContainPackage(c.added) or filesContainPackage(c.modified) or filesContainPackage(c.removed) then
              return true
            end
          end

          return false<h3><strong>Backstage Syncer(Go)</strong></h3><p>이 작업은 서비스 카탈로그(Backstage)에서 리포지토리 실제 리소스 엔티티를 폴링하고, 이를 RepoConfig CRD로 변환하며, 플랫폼을 구성 변경 사항과 동기화된 상태로 유지합니다. 변경 사항은 3분 이내에 적용됩니다.</p>repoMap := make(map[string]map[string]interface{})
			for i := range entities {
				entity := &amp;entities[i]
				if entity.Spec.Type != "GitHub-repository" {
					continue
				}

				implRaw, err := json.Marshal(entity.Spec.Implementation)
				if err != nil {
					logger.Error("Failed to marshal implementation", "error", err)
					continue
				}

				var implMap map[string]interface{}
				err = json.Unmarshal(implRaw, &amp;implMap)
				if err != nil {
					logger.Error("Failed to unmarshal implementation map", "error", err)
					continue
				}
				var repoName string
				if specMap, ok := implMap["spec"].(map[string]interface{}); ok {
					if repo, ok := specMap["repository"].(string); ok {
						repoName = repo
					}
				}
				if repoName == "" {
					continue
				}

				var workflowsRaw []byte
				if v, ok := implMap["spec"].(map[string]interface{}); ok {
					if r, ok := v["renovate"]; ok {
						workflowsRaw,  = json.Marshal(r)
					} else {
						workflowsRaw = []byte(`{}`)
					}
				} else {
					workflowsRaw = []byte(`{}`)
				}

				var workflowsWithDefaults schema.WorkflowsMetadata
				err = json.Unmarshal(workflowsRaw, &amp;rworkflowsWithDefaults)
				if err != nil {
					logger.Error("Failed to unmarshal workflows config", "error", err)
					continue
				}

				workflowsMap := map[string]interface{}{
					"enabled":        workflowsWithDefaults.Enabled,
					"require_pr":     workflowsWithDefaults.RequirePr,
					"resource_group": string(workflowsWithDefaults.ResourceGroup),
					"run_frequency":  string(workflowsWithDefaults.RunFrequency),
				}
				repoMap[repoName] = map[string]interface{}{
					"renovate": workflowsMap,
					"owner":    entity.Spec.Owner,
				}
			}
			logger.Info("Fetched GitHub Repository data from Backstage", "repository_count", len(repoMap), "status_code", resp.StatusCode)<p>마지막으로 해당 데이터를 RepoConfig 인스턴스에 기록합니다.</p><h3><strong>워크플로우 베이스(혼합: JavaScript, Go, Helm)</strong></h3><p>기반 레이어에는 Helm 차트, JavaScript 구성, 암호화 지원 기능을 갖춘 Renovate CLI용 Go 래퍼, 그리고 Alpine 패키지용 맞춤형 APK 인덱서가 포함되어 있습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4c1b5b840854ddf5/6a170eb47d8d67694e70e7e8/908d19278face3ce1119dbee9146c1264b6e2f30-1600x873.png" alt=" Kubernetes에서 종속성 관리를 위한 기본 구성 요소" /><h2><strong>셀프 서비스 구성</strong></h2><p>팀은 Backstage를 통해 리포지토리를 선언적으로 구성합니다.</p>spec:
  renovate:
    enabled: true
    config:
      resourceGroup: LARGE      # SMALL | MEDIUM | LARGE  
      runFrequency: "0 */4 * * *"  # Every 4 hours<p>리소스 그룹은 리포지토리 크기에 따라 CPU와 메모리를 할당합니다.</p><ul><li><p><strong>SMALL:</strong> 500m CPU, 1Gi 메모리.</p></li><li><p><strong>MEDIUM:</strong> 1000m CPU, 2Gi 메모리.</p></li><li><p><strong>LARGE:</strong> 2000m CPU, 4Gi 메모리.</p></li></ul><p>구성은 버전이 제어되고 감사 가능하며 자동으로 적용됩니다.</p><h2><strong>부모-자식 패턴</strong></h2><p>실행 모델은 부모-자식 워크플로우 패턴을 사용합니다.</p><ul><li><p><strong>부모 워크플로우:</strong> 예약된 시간에 실행되는 경량 CronWorkflow입니다. 비밀 정보를 암호화하고, 검사 실행 여부를 결정하며, 구성 정보를 자식 워크플로우에 전달합니다.</p></li><li><p><strong>자식 워크플로우:</strong> Renovate CLI가 실행되는 임시 포드입니다. 동적으로 리소스를 할당하고, 격리된 상태에서 비밀 정보를 해독하며, 완료 후 종료합니다.</p></li></ul><p>이러한 분리는 보안(부모 레벨에서 암호화된 비밀), 리소스 최적화(부모 레벨에서 최소한의 리소스 사용), 그리고 확장성(자식 레벨에서 병렬 실행)을 제공합니다.</p><h2><strong>결과</strong></h2><h3><strong>성능 변환</strong></h3><ul><li><p><strong>이전:</strong> 한 번에 하나의 리포지토리만 처리하고, 일부 리포지토리는 하루 이상 처리되지 않기도 했으며, 하루에 1,000건 미만의 스캔이 처리되었습니다.</p></li><li><p><strong>이후:</strong> 100개 이상의 동시 스캔, 하루에 보통 8,000개 스캔, 최대 10,000개의 기록된 스캔이 이루어지며, 사용 가능한 리소스의 양과 GitHub 속도 제한을 처리하는 방법에 의해서만 제한됩니다.</p></li></ul><h3><strong>비용 효율성</strong></h3><p>이상하게 들릴지 모르지만, 하루에 8,000개의 포드를 실행하는 것이 동일한 결과를 달성하기 위해 하나의 장기 실행 포드를 사용하는 것보다 훨씬 저렴할 수 있습니다.</p><p>이전 설정에서는 단일 인스턴스를 운영했고, 양호한 날에는 500~600번의 스캔을 수행했습니다. 또한, 서로 다른 종류의 리포지토리가 같은 포드에서 실행되기 때문에 가장 큰 리포지토리에 맞게 포드 크기를 조절해야 했습니다. 이는 포드용 CPU 8개와 16G 메모리를 사용하는 현재보다 훨씬 규모가 커질 수 있습니다.</p><p>현재의 일일 출력을 충족하려면 단일 포드는 12일 동안 실행되어야 합니다. 12일 동안 실행되는 단일 포드의 비용을 매일 실행되는 8,000개의 “MEDIUM” 크기 포드와 비교하면, 동일한 스캔 출력에서 새로운 설계가 훨씬 더 효율적입니다.</p><p>메트릭</p><p>시나리오 A(워크플로우)</p><p>시나리오 B(장기 실행 단일 포드)</p><p>설정</p><p>포드 8,000개(1 vCPU/2GB)</p><p>포드 1개(8 vCPU/ 16GB)*</p><p>기간</p><p>각각 10분</p><p>12일 연속</p><p>총 작업 시간</p><p>1,333 컴퓨팅 시간</p><p>288 컴퓨팅 시간</p><p>총 비용</p><p>$65.83</p><p>$113.75</p><p>이제 워크로드 기본값이 "SMALL"로 설정되어 있으며, 대다수가 0.5 CPU와 1G RAM으로 성공적으로 실행되고 소수만 중간, 대형으로 변경해야 하는 경우를 생각해 보겠습니다. 작업 부하의 60%가 "SMALL", 30%가 "MEDIUM", 그리고 10%가 "LARGE"에서 실행된다면 어떻게 되는지 살펴봅시다. 이는 실제 상황에 더 가깝습니다.</p><p>메트릭</p><p>시나리오 A(혼합 군집)</p><p>시나리오 B(장기 실행)</p><p>전략</p><p>포드 8,000개(혼합 크기)</p><p>포드 1개(8 vCPU/ 16GB)*</p><p>기간</p><p>각각 10분</p><p>12일 연속</p><p>총 비용</p><p>$52.66</p><p>$113.75</p><p>비용 절감</p><p>$61.09 (54% 더 저렴합니다)</p><p>—</p><p>동일한 출력을 얻을 때 현재 설정이 훨씬 더 비용 효율적이라는 것을 알 수 있습니다.</p><h3><strong>보안 강화</strong></h3><ul><li><p>일시적인 GitHub 토큰(노출 시간이 일 단위가 아닌 분 단위)</p></li><li><p>역할 기반 액세스 제어(RBAC) 경계를 사용한 네임스페이스 격리</p></li><li><p>부모 워크플로우에서 저장 데이터 비밀 암호화</p></li><li><p>직접 볼트 액세스 제거</p></li></ul><h3><strong>예측 가능한 성능</strong></h3><p>스캔 빈도가 보장되어 마침내 서비스 수준 목표(SLO)를 설정할 수 있습니다. Automerge는 안정적으로 작동합니다. 팀은 플랫폼이 약속대로 실행될 것이라고 신뢰합니다.</p><h2><strong>주요 설계 결정 사항</strong></h2><p>다음은 플랫폼의 모습을 형성하는 데 중요한 역할을 한 몇 가지 주요 설계 결정 사항입니다.</p><ul><li><p><strong>부모-자식 워크플로우를 사용하는 이유는 무엇인가요?</strong></p></li></ul><p><strong>심층 방어</strong> 전략을 실행하기 위해 이 패턴을 채택했습니다. 중요한 자격 증명(예: GitHub 앱 비밀)을 잠긴 전용 네임스페이스로 제한함으로써 임시 실행 포드가 민감한 데이터에 임의로 액세스할 수 없도록 하기 위해 <strong>RBAC</strong>를 사용합니다. 최근의 공급망 취약성(예: <strong>"Shai Hulud"</strong> 지속적 통합/지속적 배포[CI/CD] 공격)은 자격 증명 저장소에서 동적 스크립트를 실행하는 런타임 환경을 격리하는 것이 얼마나 중요한지를 보여주었습니다.</p><p>동시에 이러한 분리를 통해 <strong>세분화된 리소스 최적화</strong>가 가능합니다. "부모" 워크플로우는 최소한의 풋프린트로 가벼운 오케스트레이터 역할을 하는 반면, "자식" 워크플로우는 컴퓨팅 집약적인 종속성 스캐닝을 처리합니다. 이러한 분리는 <strong>수명 주기 관리</strong>를 단순화하여 각 계층에 별개의 조정 로직을 적용할 수 있게 하므로, 사용자는 실행 매개 변수(자식)에 대한 제어권을 갖는 반면 관리자는 스케줄링 및 보안 인프라(부모)에 대한 관리 제어권을 유지할 수 있습니다.</p><ul><li><p><strong>셀프 서비스를 사용하는 이유는 무엇인가요?</strong></p></li></ul><p>팀이 리포지토리 구성의 병목 현상이 되지 않도록 하는 것은 중요한 요구 사항이었습니다. 다양한 사용 사례를 지원할 수 있는 확장 가능한 <strong>셀프 서비스 플랫폼</strong>을 구축하는 것이 목표였습니다. 우리는 엄청난 양의 리포지토리를 고려할 때, 모든 구성 변경에 대해 <strong>게이트키퍼</strong> 역할을 하는 것은 지속 불가능하다는 것을 인식했습니다. 대신, 사용자가 "열차"를 운전할 수 있도록(실행 및 맞춤화) "레일"(인프라 및 <strong>가드레일</strong>)을 제공한다는 지원 철학을 채택했습니다. 이와 같이 <strong>팀 자율성</strong>으로 전환하여 사용자가 시스템을 특정 운영 요구 사항에 맞게 조정할 수 있도록 하면 생산성이 크게 향상된다고 믿습니다.</p><ul><li><p><strong>Kubernetes Operator 패턴을 사용하는 이유는 무엇인가요?</strong></p></li></ul><p>위에서 언급했듯이, 기본적인 설계 원칙은 플랫폼이 완전히 <strong>셀프 서비스 가능</strong>하도록 하는 것이었습니다. 사용자 의도(스캔 전환, 일정 주기 조정, 런타임 리소스 제한 조정 등)를 파악하고 변경 사항을 기본 워크플로우에 즉시 전파하려면 자동화된 메커니즘이 필요했습니다. 또한 향후 요구 사항을 예상하여 시스템은 쉽게 <strong>확장 가능</strong>해야 했습니다.</p><p>이를 위해 맞춤형 <strong>종속성 관리 Kubernetes Operator</strong>를 개발했습니다. <strong>CRD</strong>를 구성 인터페이스로 사용함으로써, <strong>Kubernetes 네이티브 조정 루프</strong>를 구축했습니다. 이 오퍼레이터는 사용자가 정의한 원하는 상태를 지속적으로 모니터링하고, 워크플로우 인프라에 필요한 업데이트를 자동으로 오케스트레이션합니다. 이를 통해 플랫폼 로직이 모든 복잡한 작업을 백그라운드에서 처리하므로 <strong>이벤트 기반</strong>의 원활한 작동이 보장됩니다.</p><ul><li><p><strong>GitHub Events Gateway를 설계하는 이유는 무엇인가요?</strong></p></li></ul><p>플랫폼의 응답성을 위해 <strong>이벤트 기반 아키텍처 (EDA)</strong> 채택이 필수적이었습니다. CronWorkflows는 신뢰할 수 있는 기준 일정을 제공했지만, 사용자가 대시보드를 통해 수동으로 스캔을 트리거하는 등 <strong>임시 실행</strong>을 처리할 수 있는 민첩성이 필요했습니다. 이를 위해서는 페이로드 무결성을 검증하고 요청을 지능적으로 라우팅할 전용 <strong>수집 게이트웨이</strong>가 필요했습니다.</p><p>우리는 Argo용 기본 GitHub EventSource를 포함한 기존 솔루션을 평가했지만, <strong>운영 오버헤드</strong> 및 엄격한 <strong>GitHub API 할당량</strong>(예: 리포지토리당 웹훅 제한)과 관련하여 상당한 위험이 있음을 확인했습니다. 결과적으로 이러한 제한으로부터 인프라를 분리하기 위해 맞춤형 게이트웨이를 구축했습니다.</p><p>무엇보다도, 이 게이트웨이는 마이그레이션 중에 전략적인 <strong>트래픽 제어 지점</strong> 역할을 했습니다. 이는 기존 시스템에서 새로운 인프라로 <strong>점진적이고 세분화된 배포</strong>(트래픽 전환)를 수행할 수 있도록 스위치 역할을 했습니다. 이를 통해 수천 개의 리포지토리를 온보딩하는 과정이 "빅뱅" 전환 방식이 아닌 통제되고 위험 없는 프로세스로 수행되었습니다.</p><p></p><h2><strong>얻은 교훈</strong></h2><p>우리가 얻은 몇 가지 교훈은 <a href="https://www.elastic.co/about/our-source-code">Elastic 소스 코드</a>와 밀접한 관련이 있습니다.</p><ol><li><p><strong>고객 우선: </strong>플랫폼은 사용자를 위해 구축됩니다. 따라서 사용자의 요구를 최우선으로 고려하는 것이 중요합니다. 이를 통해 플랫폼을 효율적으로 설계된 인프라와 애플리케이션으로 형성하여 사용자와의 마찰을 줄이고, 플랫의 확장을 단순화하며 원활한 도입일 촉진할 수 있습니다.</p></li><li><p><strong>공간, 시간: </strong>저항이 가장 적은 경로는 때때로 <strong>불안정한 상황</strong>으로 이어지곤 합니다. 처음에는 기존의 순차적 처리 모델을 최적화하려고 했지만, 이것으로 문제를 해결할 수 없었습니다. 오히려 더 많은 복잡성과 미해결 문제를 초래했습니다. 병렬 처리로 플랫폼을 <strong>재설계</strong>하기로 한 과감한 결정은 상당한 사전 노력을 필요로 했습니다. 하지만 결과적으로 플랫폼의 지속 가능한 성장을 위한 길을 열었고, 지루한 일상적인 관리 업무를 사실상 제거했습니다.</p></li><li><p><strong>IT, 종속성: </strong>플랫폼은 단독으로 운영될 수 없습니다. 플랫폼의 성공은 더 넓은 에코시스템과 얼마나 잘 통합되는지에 달려 있습니다. 이 사례에서는 <strong>Backstage</strong>와의 통합이 매우 중요했습니다. 원활한 서비스 온보딩을 위한 정보 소스 역할을 하기 때문입니다. 마찬가지로 <strong>Artifactory</strong>에 연결하여 개인 패키지 업데이트를 효율적으로 관리할 수 있게 되었습니다. 그 외에도 필수 통합 목록은 계속 이어집니다.</p></li><li><p><strong>발전, 단순한 완벽함: </strong>구현 과정 전반에 걸쳐 초기 가정에 대해 지속적으로 엄격한 테스트를 수행하고 새로운 장벽이 나타나면 이에 맞게 조정했습니다. 완벽주의에 빠지기보다는 <strong>반복 접근 방식</strong>을 채택하여 과제를 하나씩 해결하고 실제 환경에 맞게 마이그레이션 전략을 조정했습니다.</p></li></ol><h2><strong>그럼 이제 무엇을 해야 할까요?</strong></h2><p>플랫폼 제공은 더 의미 있는 작업을 가능하게 하여 플랫폼의 UX와 효율성을 향상시키는 데 도움이 됩니다. 몇 가지 예는 다음과 같습니다.
</p><ul><li><p><strong>자동 병합 도입을 늘리고 가드레일 구축</strong></p></li></ul><p>자동 병합 기능은 지루한 수동 작업을 없애 팀의 작업 속도를 크게 향상시킵니다. 하지만 속도 향상으로 인해 보안이 훼손되지 않도록 엄격한 <strong>가드레일</strong>을 마련해야 합니다.
</p><ul><li><p><strong>최종 사용자 경험에 대한 통합 가시성 향상</strong></p></li></ul><p>로드맵의 중요한 우선순위는 플랫폼 수준뿐만 아니라 특히 <strong>최종 사용자의 관점</strong>에서 통합 가시성을 강화하는 것입니다. 인프라 메트릭을 파악하는 것은 간단하지만, 실제 사용자 경험을 이해하려면 더 심층적인 인사이트가 필요합니다. 우리는 사용자 불만으로 이어지기 <strong>전에</strong> 원격 측정 데이터를 통해 문제점과 성능 문제를 탐지할 수 있도록 사용자 중심의 핵심 성과 지표(KPI)를 정의하고 있습니다.</p><ul><li><p><strong>장벽을 제거하여 도입 촉진</strong></p></li></ul><p>앞으로 최우선 과제는 플랫폼 도입을 방해하는 모든 장벽을 파악하고 제거하는 것입니다. 새로운 통합 기능을 개발하든 특정 기능 세트를 배포하든, 데이터 기반 계획 수립에 전념합니다. 확장성을 고려하여 설계된 플랫폼을 성공적으로 구축했으며, 이제 <strong>그 잠재력을 극대화</strong>하는 데 집중하고 있습니다.
</p><h2><strong>더 큰 그림</strong></h2><p>종속성 관리 워크플로우 프로젝트는 더 광범위한 원칙을 제시합니다. <strong>오픈 소스 도구를 기본 배포 모델 이상으로 확장해야 할 때 Kubernetes 네이티브 패턴이 해결책을 제시한다는 것입니다</strong>.</p><p>수용함으로써:</p><ul><li><p>구성용 CRD</p></li><li><p>수명 주기 관리를 위한 오퍼레이터</p></li><li><p>응답성을 위한 이벤트 중심 아키텍처</p></li><li><p>배포용 GitOps</p></li></ul><p>관리하는 리포지토리 수와 관계없이 확장 가능한 오케스트레이션을 구축했습니다. 하나의 리포지토리를 스캔하는 성능은 100개를 관리하든 1,000개를 관리하든 동일합니다.</p><p>중요한 CVE가 발표되면 이제 몇 시간이 아니라 몇 분 만에 답변을 받을 수 있습니다. 이것이 병목 현상과 경쟁 우위의 차이입니다.</p><h2><strong>감사의 말씀</strong></h2><p>이 플랫폼은 뛰어난 오픈 소스 도구를 기반으로 구축되었습니다.</p><ul><li><p><strong>Kubebuilder:</strong> 워크플로우를 부트스트랩하고 오케스트레이션하는 Kubernetes Operator를 시작하는 데 사용한 오픈 소스 프레임워크입니다. [<a href="https://github.com/kubernetes-sigs/kubebuilder">1</a>][<a href="https://book.kubebuilder.io/">2</a>]</p></li><li><p><strong>Backstage:</strong> 서비스 카탈로그를 구축하는 데 신뢰할 수 있는 정보 소스로 사용한 오픈 소스 프레임워크입니다. [<a href="https://github.com/backstage/backstage">1</a>][<a href="https://backstage.io/">2</a>]</p></li><li><p><strong>Argo Workflows 및 Argo Events:</strong> 복잡한 프로세스를 오케스트레이션하고 이벤트에 기반한 동적 처리를 추가하는 데 사용한 오픈 소스 제품군입니다. [1][<a href="https://argo-workflows.readthedocs.io/en/stable/">2</a>][<a href="https://argoproj.github.io/argo-events/">3</a>][<a href="https://github.com/argoproj/argo-events">4</a>]</p></li><li><p><strong>Renovate CLI:</strong> 리포지토리를 처리하는 오픈 소스 종속성 관리 도구입니다. [<a href="https://github.com/renovatebot/renovate">1</a>][<a href="https://docs.renovatebot.com/getting-started/running/">2</a>]</p></li></ul><p>* AWS Fargate 가격 모델이 단일 포드의 비용을 참조하는 데 사용되었지만, 워크로드가 반드시 AWS에서 실행되는 것은 아니며 완전한 Kubernetes 클러스터에서 실행되고 있습니다.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/dependency-management-kubernetes</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/dependency-management-kubernetes</guid>
    <category><![CDATA[개발자 경험]]></category>
    <dc:creator><![CDATA[Nikos Fotiou]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6033995d6660d149/6a170eb5839dfa63f1dcff9b/00519840e6eec7101c1fb096afcae976ee0c454e-1280x720.png" length="0" type="image/png"/>
    <pubDate>Thu, 19 Feb 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Kibana의 Elasticsearch 쿼리 규칙 UI 소개]]></title>
    <description><![CDATA[Elasticsearch 쿼리 규칙 UI를 사용해 자연 순위에 영향을 주지 않고 Kibana에서 사용자 정의 가능한 규칙 세트를 사용해 검색 쿼리에서 문서를 추가하거나 제외하는 방법을 알아보세요.]]></description>
    <content:encoded><![CDATA[<p>검색 엔진의 역할은 관련성 높은 결과를 반환하는 것입니다. 그러나 세일 강조, 시즌 상품 우선순위 지정, 스폰서 상품 소개 등 그 이상의 비즈니스 요구사항이 있으며, 개발자가 검색 쿼리에서 이를 항상 수행할 수는 없습니다.</p><p>또한 이러한 사용 사례는 일반적으로 시간에 민감하며 일반적인 개발 단계(코드 브랜치를 생성한 다음 새 릴리스를 기다리는 과정)를 거치는 것은 시간이 많이 소요되는 프로세스입니다.</p><p>그렇다면 이 모든 프로세스를 API 호출만으로, 더 나아가 Kibana에서 클릭 몇 번으로 수행할 수 있다면 어떨까요?</p><h2>쿼리 규칙 UI</h2><p>Elasticsearch 8.10에는 <a href="https://www.elastic.co/blog/introducing-query-rules-elasticsearch-8-10"><strong>쿼리 규칙과</strong></a> <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers/rule-retriever"><strong>규칙 검색기가</strong></a> 도입되었습니다. 규칙에 따라 자연 <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-pinned-query"><em>검색 결과의 순위에</em></a> 영향을 주지 않고 고정된 결과를 쿼리에 삽입하기 위해 고안된 도구입니다. 선언적이고 단순한 방식으로 결과 위에 비즈니스 로직을 추가할 뿐입니다.</p><p>쿼리 규칙의 몇 가지 일반적인 사용 사례는 다음과 같습니다:</p><ul><li><p><strong>프로모션 목록 또는 판매 강조 표시</strong>: 세일 중이거나 스폰서된 품목을 상단에 표시합니다.</p></li><li><p><strong>문맥 또는 지리적 위치에 따라 제외</strong>: 현지 규정으로 인해 특정 항목을 표시할 수 없는 경우 숨기기.</p></li><li><p><strong>주요 결과의 우선순위 지정</strong>: 자연 검색 순위와 관계없이 인기 검색어 또는 고정 검색어가 항상 상단에 표시되도록 합니다.</p></li></ul><p>인터페이스에 액세스하고 이러한 도구와 상호 작용하려면 Kibana 사이드 메뉴를 클릭하고  <strong>관련성</strong>아래의 쿼리 규칙으로 이동해야 합니다:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltac12541cddd58e36/6a170853a29299941cd00fc2/242e33e89d1a07ffa0e76009c46b3a9236722741-458x1010.png" alt="정확도 조건에서 Elasticsearch의 쿼리 규칙에 액세스하기" /><p>쿼리 규칙 메뉴가 나타나면 <strong>첫 번째 규칙 집합 만들기를 클릭</strong>합니다:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcc28329c0f3c3aa9/6a17085547d49c67e22d893b/30b3a91bbbf243d314cf38298e01ca5cff784430-1600x945.png" alt="Elasticsearch에서 첫 번째 쿼리 규칙 규칙 세트 만들기" /><p>다음으로 규칙 집합의 이름을 지정해야 합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb37d271297a4f148/6a170856a29299782cd00fc6/26c5462f88678867776f933b5655ca0df0d72a16-708x446.png" alt="Elasticsearch에서 쿼리 규칙 규칙 집합 이름 지정하기" /><p>각 규칙을 정의하는 양식에는 세 가지 주요 구성 요소가 있습니다:</p><ul><li><p><strong>기준</strong>: 조건: 규칙을 적용하기 위해 충족해야 하는 조건입니다. 예를 들어, "쿼리 문자열 필드에 <em>크리스마스</em>값이 포함된 경우" 또는 "국가 필드가 <em>CO인</em>경우"와 같은 예입니다.</p></li><li><p><strong>액션</strong>: 조건이 충족될 때 수행하려는 작업입니다. 고정(문서를 최상위 결과에 고정)하거나 제외(문서를 숨김)할 수 있습니다.</p></li><li><p><strong>메타데이터</strong>: 쿼리가 실행될 때 쿼리와 함께 제공되는 필드입니다. 여기에는 위치나 언어와 같은 사용자 정보뿐만 아니라 검색 데이터(쿼리 문자열)도 포함될 수 있습니다. 규칙 적용 여부를 결정하기 위해 기준에서 사용하는 값입니다.</p></li></ul><h2>예: 인기 품목</h2><p>다양한 품목이 있는 이커머스 사이트가 있다고 가정해 보겠습니다. 지표를 살펴보면 콘솔 카테고리에서 가장 많이 판매된 상품 중 하나는 '듀얼쇼크 4 무선 컨트롤러'로, 특히 사용자가 'PS4' 또는 'PlayStation 4' 키워드를 검색할 때 가장 많이 판매되는 상품임을 확인할 수 있습니다. 따라서 사용자가 해당 키워드를 검색할 때마다 이 제품을 결과 상단에 표시하기로 결정했습니다.</p><p>먼저 대량 API 요청을 사용하여 각 항목에 대한 문서를 색인해 보겠습니다:</p>POST _bulk
{ "index": { "_index": "products", "_id": "1" } }
{ "id": "1", "name": "PlayStation 4 Slim 1TB", "category": "console", "brand": "Sony", "price": 1200 }
{ "index": { "_index": "products", "_id": "2" } }
{ "id": "2", "name": "DualShock 4 Wireless Controller", "category": "accessory", "brand": "Sony", "price": 250 }
{ "index": { "_index": "products", "_id": "3" } }
{ "id": "3", "name": "PlayStation 4 Camera", "category": "accessory", "brand": "Sony", "price": 200 }
{ "index": { "_index": "products", "_id": "4" } }
{ "id": "4", "name": "PlayStation 4 VR Headset", "category": "accessory", "brand": "Sony", "price": 900 }
{ "index": { "_index": "products", "_id": "5" } }
{ "id": "5", "name": "Charging Station for DualShock 4", "category": "accessory", "brand": "Sony", "price": 80 }<p>쿼리에 개입하지 않으면 해당 항목은 일반적으로 네 번째 위치에 표시됩니다. 쿼리는 다음과 같습니다:</p>GET products/_search
{
 "query": {
   "match": {
     "name": "PlayStation 4"
   }
 }
}<p>결과는 다음과 같습니다.</p>{
 "took": 1,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 5,
     "relation": "eq"
   },
   "max_score": 0.6973252,
   "hits": [
     {
       "_index": "products",
       "_id": "3",
       "_score": 0.6973252,
       "_source": {
         "id": "3",
         "name": "PlayStation 4 Camera",
         "category": "accessory",
         "brand": "Sony",
         "price": 200
       }
     },
     {
       "_index": "products",
       "_id": "1",
       "_score": 0.6260078,
       "_source": {
         "id": "1",
         "name": "PlayStation 4 Slim 1TB",
         "category": "console",
         "brand": "Sony",
         "price": 1200
       }
     },
     {
       "_index": "products",
       "_id": "4",
       "_score": 0.6260078,
       "_source": {
         "id": "4",
         "name": "PlayStation 4 VR Headset",
         "category": "accessory",
         "brand": "Sony",
         "price": 900
       }
     },
     {
       "_index": "products",
       "_id": "2",
       "_score": 0.08701137,
       "_source": {
         "id": "2",
         "name": "DualShock 4 Wireless Controller",
         "category": "accessory",
         "brand": "Sony",
         "price": 250
       }
     },
     {
       "_index": "products",
       "_id": "5",
       "_score": 0.07893815,
       "_source": {
         "id": "5",
         "name": "Charging Station for DualShock 4",
         "category": "accessory",
         "brand": "Sony",
         "price": 80
       }
     }
   ]
 }
}<p>이를 변경하는 쿼리 규칙을 만들어 보겠습니다. 먼저 다음과 같이 규칙 집합에 추가해 보겠습니다:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1576d4f4a2e60548/6a170858cdacbfccb07d298d/fdc42646fb3e76a09bca7d19047a76efe343f7a2-1600x650.png" alt="Elasticsearch에서 쿼리 규칙 규칙 집합을 편집하는 방법" /><p>또는 이에 상응하는 <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-query-rules-put-ruleset">API 요청</a>:</p>PUT _query_rules/my-rules
{
  "rules": [
    {
      "rule_id": "rule-1232",
      "type": "pinned",
      "criteria": [
        {
          "type": "exact",
          "metadata": "query_string",
          "values": [
            "PS4",
            "PlayStation 4"
          ]
        }
      ],
      "actions": {
        "docs": [
          {
            "_index": "products",
            "_id": "2"
          }
        ]
      }
    }
  ]
}<p>쿼리에서 <strong>규칙 집합을 </strong>사용하려면 쿼리 규칙 유형을 사용해야 합니다. 이러한 종류의 쿼리는 크게 두 부분으로 구성됩니다:</p>GET /products/_search
{
 "retriever": {
   "rule": {
     "retriever": {
       "standard": {
         "query": {
           "match": { "name": "PlayStation 4" }
         }
       }
     },
     "match_criteria": {
       "query_string": "PlayStation 4"
     },
     "ruleset_ids": ["my-rules"]
   }
 }
}<ul><li><p><strong>match_criteria</strong>: 사용자의 쿼리와 비교하는 데 사용되는 메타데이터입니다. 이 예에서는 쿼리 문자열 필드에 "PlayStation 4" 값이 있을 때 규칙 집합이 활성화됩니다.</p></li><li><p><strong>쿼리</strong>: 검색 및 자연 검색 결과를 얻는 데 사용되는 실제 쿼리입니다.</p></li></ul><p>이렇게 하면 먼저 오가닉 쿼리를 실행한 다음 Elasticsearch가 규칙 세트의 규칙을 적용합니다:</p>{
 "took": 17,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 5,
     "relation": "eq"
   },
   "max_score": 1.7014122e+38,
   "hits": [
     {
       "_index": "products",
       "_id": "2",
       "_score": 1.7014122e+38,
       "_source": {
         "id": "2",
         "name": "DualShock 4 Wireless Controller",
         "category": "accessory",
         "brand": "Sony",
         "price": 250
       }
     },
     {
       "_index": "products",
       "_id": "3",
       "_score": 0.6973252,
       "_source": {
         "id": "3",
         "name": "PlayStation 4 Camera",
         "category": "accessory",
         "brand": "Sony",
         "price": 200
       }
     },
     {
       "_index": "products",
       "_id": "1",
       "_score": 0.6260078,
       "_source": {
         "id": "1",
         "name": "PlayStation 4 Slim 1TB",
         "category": "console",
         "brand": "Sony",
         "price": 1200
       }
     },
     {
       "_index": "products",
       "_id": "4",
       "_score": 0.6260078,
       "_source": {
         "id": "4",
         "name": "PlayStation 4 VR Headset",
         "category": "accessory",
         "brand": "Sony",
         "price": 900
       }
     },
     {
       "_index": "products",
       "_id": "5",
       "_score": 0.07893815,
       "_source": {
         "id": "5",
         "name": "Charging Station for DualShock 4",
         "category": "accessory",
         "brand": "Sony",
         "price": 80
       }
     }
   ]
 }
}<h2>예: 사용자 기반 메타데이터</h2><p>쿼리 규칙의 또 다른 흥미로운 적용 사례는 메타데이터를 사용하여 사용자 또는 웹페이지의 문맥 정보를 기반으로 특정 문서를 표시하는 것입니다.</p><p>예를 들어, 사용자의 충성도 수준에 따라 숫자 값으로 표시되는 아이템이나 맞춤형 판매를 강조하고 싶다고 가정해 보겠습니다.</p><p>이 메타데이터를 쿼리에 직접 수집하여 해당 값이 특정 기준을 충족할 때 규칙이 활성화되도록 할 수 있습니다.</p><p>먼저, 충성도 수준이 높은 사용자만 볼 수 있는 문서를 색인화합니다:</p>POST _bulk
{ "index": { "_index": "products", "_id": "6" } }
{ "id": "6", "name": "PlayStation Plus Deluxe Card - 12 months", "category": "membership", "brand": "Sony", "price": 300 }<p>이제 동일한 규칙 집합 내에 새 규칙을 만들어 loyalty_level이 80 이상일 때 해당 항목이 결과 상단에 표시되도록 해 보겠습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt158578005df8c76d/6a17085aab7f086dc0db9de3/58de12dff93305440608f51465462fcc68653a08-1421x496.png" alt="Elasticsearch에서 쿼리 규칙 규칙 집합을 편집하는 방법" /><p>규칙과 규칙 집합을 저장합니다.</p><p>다음은 이에 상응하는 REST 요청입니다:</p>PUT _query_rules/my-rules
{
  "rules": [
    {
      "rule_id": "pin-premiun-user",
      "type": "pinned",
      "criteria": [
        {
          "type": "gte",
          "metadata": "loyalty_level",
          "values": [
            80
          ]
        }
      ],
      "actions": {
        "docs": [
          {
            "_index": "products",
            "_id": "6"
          }
        ]
      }
    }
  ]
}<p>이제 쿼리를 실행할 때 메타데이터에 새 매개변수 <strong>loyalty_level을 </strong>포함시켜야 합니다. 규칙의 조건이 충족되면 새 문서가 결과 위에 표시됩니다.</p><p>예를 들어 충성도_레벨이 80인 쿼리를 전송하는 경우입니다:</p>POST /products/_search
{
  "retriever": {
    "rule": {
      "retriever": {
        "standard": {
          "query": {
            "match": {
              "name": "PlayStation"
            }
          }
        }
      },
      "match_criteria": {
        "query_string": "PlayStation",
        "loyalty_level": 80
      },
      "ruleset_ids": ["my-rules"]
    }
  }
}<p>결과 위에 로열티 문서가 표시됩니다:</p>{
  "took": 31,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 4,
      "relation": "eq"
    },
    "max_score": 1.7014122e+38,
    "hits": [
      {
        "_index": "products",
        "_id": "6",
        "_score": 1.7014122e+38,
        "_source": {
          "id": "6",
          "name": "PlayStation Plus Deluxe Card - 12 months",
          "category": "membership",
          "brand": "Sony",
          "price": 300
        }
      },
      {
        "_index": "products",
        "_id": "3",
        "_score": 0.5054567,
        "_source": {
          "id": "3",
          "name": "PlayStation 4 Camera",
          "category": "accessory",
          "brand": "Sony",
          "price": 200
        }
      },
      {
        "_index": "products",
        "_id": "1",
        "_score": 0.45618832,
        "_source": {
          "id": "1",
          "name": "PlayStation 4 Slim 1TB",
          "category": "console",
          "brand": "Sony",
          "price": 1200
        }
      },
      {
        "_index": "products",
        "_id": "4",
        "_score": 0.45618832,
        "_source": {
          "id": "4",
          "name": "PlayStation 4 VR Headset",
          "category": "accessory",
          "brand": "Sony",
          "price": 900
        }
      }
    ]
  }
}<p>아래의 경우 충성도 레벨이 70이므로 규칙이 충족되지 않으므로 해당 아이템이 상단에 표시되지 않아야 합니다:</p>POST /products/_search
{
  "retriever": {
    "rule": {
      "retriever": {
        "standard": {
          "query": {
            "match": {
              "name": "PlayStation"
            }
          }
        }
      },
      "match_criteria": {
        "query_string": "PlayStation",
        "loyalty_level": 70
      },
      "ruleset_ids": ["my-rules"]
    }
  }
}<p>결과는 다음과 같습니다:</p>{
  "took": 7,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 4,
      "relation": "eq"
    },
    "max_score": 0.5054567,
    "hits": [
      {
        "_index": "products",
        "_id": "3",
        "_score": 0.5054567,
        "_source": {
          "id": "3",
          "name": "PlayStation 4 Camera",
          "category": "accessory",
          "brand": "Sony",
          "price": 200
        }
      },
      {
        "_index": "products",
        "_id": "1",
        "_score": 0.45618832,
        "_source": {
          "id": "1",
          "name": "PlayStation 4 Slim 1TB",
          "category": "console",
          "brand": "Sony",
          "price": 1200
        }
      },
      {
        "_index": "products",
        "_id": "4",
        "_score": 0.45618832,
        "_source": {
          "id": "4",
          "name": "PlayStation 4 VR Headset",
          "category": "accessory",
          "brand": "Sony",
          "price": 900
        }
      },
      {
        "_index": "products",
        "_id": "6",
        "_score": 0.3817649,
        "_source": {
          "id": "6",
          "name": "PlayStation Plus Deluxe Card - 12 months",
          "category": "membership",
          "brand": "Sony",
          "price": 300
        }
      }
    ]
  }
}<h2>예: 즉시 제외</h2><p><strong>듀얼쇼크 4 무선 컨트롤러(ID 2)</strong> 를 일시적으로 사용할 수 없어 판매할 수 없다고 가정해 보겠습니다. 따라서 비즈니스 팀은 문서를 수동으로 삭제하거나 일부 데이터 프로세스가 시작될 때까지 기다리는 대신 그 동안 검색 결과에서 해당 문서를 제거하기로 결정합니다.</p><p>방금 인기 있는 항목에 적용한 것과 비슷한 프로세스를 사용하되 이번에는 <em>고정됨을</em> 선택하는 대신 <em>제외를</em> 선택합니다. 이 규칙은 일종의 블랙리스트 역할을 합니다. 쿼리가 실행될 때마다 제외가 작동하도록 기준을 <strong>항상으로</strong> 변경합니다.</p><p>규칙은 다음과 같이 표시되어야 합니다:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt38564c0b7f4a6ee2/6a17085c1949f78692e7a989/f10971e4f1bc9520105111adfa3a476581a27130-1600x623.png" alt="Elasticsearch의 즉각적인 제외 규칙 세트 예시" /><p>규칙과 규칙 집합을 저장하여 변경 사항을 적용합니다. 다음은 이에 상응하는 REST 요청입니다:</p>PUT _query_rules/my-rules
{
  "rules": [
    {
      "rule_id": "rule-6358",
      "type": "pinned",
      "criteria": [
        {
          "type": "always"
        }
      ],
      "actions": {
        "docs": [
          {
            "_index": "products",
            "_id": "2"
          }
        ]
      }
    }
  ]
}<p>이제 쿼리를 다시 실행하면 이전 규칙이 항목을 고정하는 것이었음에도 불구하고 해당 항목이 더 이상 결과에 없는 것을 볼 수 있습니다. 이는 <strong>제외가 고정 결과보다 우선하기</strong> 때문입니다.</p>{
 "took": 6,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 4,
     "relation": "eq"
   },
   "max_score": 2.205655,
   "hits": [
     {
       "_index": "products",
       "_id": "3",
       "_score": 2.205655,
       "_source": {
         "id": "3",
         "name": "PlayStation 4 Camera",
         "category": "accessory",
         "brand": "Sony",
         "price": 200
       }
     },
     {
       "_index": "products",
       "_id": "1",
       "_score": 1.9738505,
       "_source": {
         "id": "1",
         "name": "PlayStation 4 Slim 1TB",
         "category": "console",
         "brand": "Sony",
         "price": 1200
       }
     },
     {
       "_index": "products",
       "_id": "4",
       "_score": 1.9738505,
       "_source": {
         "id": "4",
         "name": "PlayStation 4 VR Headset",
         "category": "accessory",
         "brand": "Sony",
         "price": 900
       }
     },
     {
       "_index": "products",
       "_id": "5",
       "_score": 0.69247496,
       "_source": {
         "id": "5",
         "name": "Charging Station for DualShock 4",
         "category": "accessory",
         "brand": "Sony",
         "price": 80
       }
     }
   ]
 }
}<h2>결론</h2><p><strong>쿼리 규칙을</strong> 사용하면 코드를 변경하지 않고도 관련성을 매우 쉽게 조정할 수 있습니다. 새로운 <strong>Kibana</strong> <strong>UI를 </strong>사용하면몇 초 만에 이러한 변경을 수행할 수 있으므로, 여러분과 여러분의 비즈니스 팀이 검색 결과를 더 잘 제어할 수 있습니다.</p><p>쿼리 규칙은 전자상거래 외에도 지원 포털에서 문제 해결 가이드를 강조 표시하고, 지식창고에 주요 내부 문서를 표시하고, 뉴스 사이트에서 속보를 홍보하고, 만료된 작업 또는 콘텐츠 목록을 필터링하는 등 다양한 시나리오에 활용할 수 있습니다. 사용자 역할이나 지역에 따라 제한된 자료를 숨기는 등의 규정 준수 규칙을 적용할 수도 있습니다.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-query-rules-ui-introduction</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-query-rules-ui-introduction</guid>
    <category><![CDATA[기본]]></category>
    <category><![CDATA[개발자 경험]]></category>
    <dc:creator><![CDATA[Jhon Guzmán]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt565ed0eb407e098d/6a17085d8b73cb363d189fb1/1fb10bd31c509cc9b9bb4f71f49970f140e6c36f-1600x945.png" length="0" type="image/png"/>
    <pubDate>Fri, 07 Nov 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Mastra와 Elasticsearch를 사용하여 시맨틱 리콜 기능을 갖춘 지식 에이전트 구축하기]]></title>
    <description><![CDATA[메모리 및 정보 검색을 위한 벡터 저장소로 Mastra와 Elasticsearch를 사용해 시맨틱 리콜 기능을 갖춘 지식 에이전트를 구축하는 방법을 알아보세요.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/search-labs/blog/context-engineering-overview">컨텍스트 엔지니어링은</a> 안정적인 AI 에이전트 및 아키텍처를 구축하는 데 있어 점점 더 중요해지고 있습니다. 모델이 점점 더 개선됨에 따라 그 효과와 신뢰성은 학습된 데이터보다는 올바른 맥락에 얼마나 잘 근거를 두고 있는지에 따라 달라집니다. 가장 관련성 높은 정보를 적시에 검색하고 적용할 수 있는 에이전트가 정확하고 신뢰할 수 있는 결과물을 만들어낼 가능성이 훨씬 높습니다.</p><p>이 블로그에서는 <a href="https://mastra.ai/">Mastra를</a> 사용해 사용자가 말한 내용을 기억하고 나중에 관련 정보를 불러올 수 있는 지식 에이전트를 구축하는 데 Elasticsearch를 메모리 및 검색 백엔드로 사용하겠습니다. 동일한 개념을 실제 사용 사례로 쉽게 확장하여 지원 상담원이 과거의 대화와 해결 방법을 기억할 수 있어 특정 사용자에게 맞춤형 응답을 제공하거나 이전 컨텍스트를 기반으로 더 빠르게 해결책을 제시할 수 있다고 생각하면 됩니다.</p><p>여기를 따라 단계별로 구축하는 방법을 알아보세요. 길을 잃었거나 완성된 예제를 실행하고 싶다면 <a href="https://github.com/jdarmada/getting-started-mastra-elastic/tree/main">여기에서</a> 리포지토리를 확인하세요.</p><h2>마스트라란 무엇인가요?</h2><p>Mastra는 추론, 메모리 및 도구에 대한 교체 가능한 부품으로 AI 에이전트를 구축하기 위한 오픈 소스 TypeScript 프레임워크입니다. <a href="https://mastra.ai/docs/memory/semantic-recall">시맨틱 리콜</a> 기능을 통해 상담원은 메시지를 벡터 데이터베이스에 임베딩으로 저장하여 과거 상호작용을 기억하고 검색할 수 있습니다. 이를 통해 상담원은 장기적인 대화 맥락과 연속성을 유지할 수 있습니다. Elasticsearch는 효율적인 고밀도 벡터 검색을 지원하기 때문에 이 기능을 활성화하는 데 탁월한 벡터 저장소입니다. 시맨틱 리콜이 트리거되면 에이전트는 관련 과거 메시지를 모델의 컨텍스트 창으로 가져와서 모델이 검색된 컨텍스트를 추론 및 응답의 기초로 사용할 수 있도록 합니다.</p><h2>시작하기 위해 필요한 사항</h2><ul><li><p>노드 v18+</p></li><li><p>Elasticsearch(버전 8.15 이상)</p></li><li><p>Elasticsearch API 키</p></li><li><p><a href="https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key">OpenAI API 키</a></p></li></ul><p>참고: 데모에서는 OpenAI 공급자를 사용하므로 이 공급자가 필요하지만, Mastra는 다른 AI SDK 및 커뮤니티 모델 공급자를 지원하므로 설정에 따라 쉽게 교체할 수 있습니다.</p><h2>Mastra 프로젝트 구축</h2><p>Mastra에 내장된 CLI를 사용하여 프로젝트의 스캐폴딩을 제공하겠습니다. 명령을 실행합니다:</p>npm create mastra@latest<p>다음과 같은 일련의 프롬프트가 표시됩니다:</p><p>1. 프로젝트 이름을 지정합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt87f941f654d03827/6a16f7af67045b214d45bfa1/2b9fe559e0276140dd539e24f916a73c60870405-620x84.png" alt="Mastra 앱에서 프롬프트 이름 지정하기" /><p>2. 이 기본값을 그대로 사용해도 되므로 비워두셔도 됩니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7dbb3d4f27435cac/6a16f7b0cdacbf29497d27de/e04729eb03bce8499e973e18c28642402340d0e5-852x68.png" alt="프롬프트 파일을 어디에 보관할지 마스트라에게 알려주기" /><p>3. 이 프로젝트에서는 OpenAI에서 제공하는 모델을 사용합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1f654f6cb9397e94/6a16f7b2964cea899a08b942/a86596a469a71bdf8bd99cbaf528d0f0cf7272c0-436x222.png" alt="Mastra에서 OpenAI가 제공하는 모델 선택하기" /><p>4. 모든 환경 변수를 이후 단계에서 구성할 '.env' 파일에 저장하므로 '지금은 건너뛰기' 옵션을 선택합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltff106117521a3519/6a16f7b3c1e8a5031af880d8/02b19ccc34af0bdacf52fd94b519d036540ca2e6-426x114.png" alt="OpenAI 키에 대해 지금은 건너뛰기를 선택합니다." /><p>5. 이 옵션을 건너뛸 수도 있습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcda7d9c51c3878d7/6a16f7b450916809dbe1b892/b3fe63d19d270bc2e0de1dd92033bf8b26750819-990x208.png" alt="" /><p>초기화가 완료되면 다음 단계로 넘어갈 수 있습니다.</p><h3>종속성 설치</h3><p>다음으로 몇 가지 종속성을 설치해야 합니다:</p>npm install ai @ai-sdk/openai @elastic/elasticsearch dotenv<ul><li><p><code>ai</code> - 자바스크립트/타입스크립트에서 AI 모델, 프롬프트 및 워크플로를 관리할 수 있는 도구를 제공하는 핵심 AI SDK 패키지입니다. Mastra는 Vercel의 <a href="https://ai-sdk.dev/">AI SDK를</a> 기반으로 구축되었으므로 에이전트와의 모델 상호 작용을 활성화하려면 이 종속성이 필요합니다.</p></li><li><p><code>@ai-sdk/openai</code> - AI SDK를 OpenAI 모델(예: GPT-4, GPT-4o 등)에 연결하여 OpenAI API 키를 사용하여 API 호출을 가능하게 하는 플러그인입니다.</p></li><li><p><code>@elastic/elasticsearch</code> - <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript">Node.js용 공식 Elasticsearch 클라이언트</a>, 인덱싱, 검색, 벡터 작업을 위해 Elastic Cloud 또는 로컬 클러스터에 연결하는 데 사용됩니다.</p></li><li><p><code>dotenv</code> - .env에서 환경 변수를 로드합니다. 파일을 process.env에 추가합니다, API 키와 Elasticsearch 엔드포인트와 같은 자격 증명을 안전하게 삽입할 수 있습니다.</p></li></ul><h3>환경 변수 구성</h3><p>프로젝트 루트 디렉터리에 <code>.env</code> 파일이 없는 경우 이 파일을 만듭니다. 또는 <a href="https://github.com/jdarmada/getting-started-mastra-elastic/blob/main/.env.example">리포지토리에</a> 제공한 예제 <code>.env</code> 를 복사하여 이름을 바꿀 수 있습니다. 이 파일에서 다음 변수를 추가할 수 있습니다:</p>ELASTICSEARCH_ENDPOINT="your-endpoint-here"
ELASTICSEARCH_API_KEY="your-key-here"
OPENAI_API_KEY="your-key-here"<p>이것으로 기본 설정이 끝났습니다. 여기에서 이미 상담원 구축 및 오케스트레이션을 시작할 수 있습니다. 여기서 한 걸음 더 나아가 저장 및 벡터 검색 레이어로 Elasticsearch를 추가하겠습니다.</p><h2>벡터 저장소로 Elasticsearch 추가하기</h2><p><code>stores</code> 이라는 새 폴더를 만들고 그 안에 이 <a href="https://github.com/jdarmada/getting-started-mastra-elastic/blob/main/src/mastra/stores/elastic-store.ts">파일을</a> 추가합니다. Mastra와 Elastic이 공식 Elasticsearch 벡터 저장소 통합을 출시하기 전에 <a href="https://github.com/abhiaiyer91">Abhi Aiyer</a>(Mastra CTO)가 초기 프로토타입 클래스( <code>ElasticVector</code>)를 공유했습니다. 간단히 말해, Mastra의 메모리 추상화와 Elasticsearch의 고밀도 벡터 기능을 연결하여 개발자가 에이전트를 위한 벡터 데이터베이스로 Elasticsearch를 도입할 수 있습니다.</p><p>통합의 중요한 부분을 자세히 살펴보겠습니다:</p><h3>Elasticsearch 클라이언트 수집</h3><p>이 섹션에서는 <code>ElasticVector</code> 클래스를 정의하고 표준 배포와 서버리스 배포를 모두 지원하는 Elasticsearch 클라이언트 연결을 설정합니다.</p>export interface ElasticVectorConfig extends ClientOptions {
    /**
     * Explicitly specify if connecting to Elasticsearch Serverless.
     * If not provided, will be auto-detected on first use.
     */
    isServerless?: boolean;
    
    /**
     * Maximum documents to count accurately when describing indices.
     * Higher values provide accurate counts but may impact performance on large indices.
     * 
     * @default 10000
     */
    maxCountAccuracy?: number;
}

export class ElasticVector extends MastraVector {
    private client: Client;
    private isServerless: boolean | undefined;
    private deploymentChecked: boolean = false;
    private readonly maxCountAccuracy: number;

    constructor(config: ElasticVectorConfig) {
        super();
        this.client = new Client(config);
        this.isServerless = config.isServerless;
        this.maxCountAccuracy = config.maxCountAccuracy ?? 10000;
    }
}<ul><li><p><code>ElasticVectorConfig extends ClientOptions</code>: 이렇게 하면 모든 Elasticsearch 클라이언트 옵션(예: <code>node</code>, <code>auth</code>, <code>requestTimeout</code>)을 상속하고 사용자 정의 속성을 추가하는 새로운 구성 인터페이스가 생성됩니다. 즉, 사용자는 서버리스 전용 옵션과 함께 유효한 모든 Elasticsearch 구성을 전달할 수 있습니다.</p></li><li><p><code>extends MastraVector</code>: 이를 통해 <code>ElasticVector</code> 은 모든 벡터 스토어 통합이 준수하는 공통 인터페이스인 Mastra의 기본 <code>MastraVector</code> 클래스에서 상속할 수 있습니다. 이렇게 하면 Elasticsearch가 에이전트의 관점에서 다른 모든 Mastra 벡터 백엔드처럼 작동합니다.</p></li><li><p><code>private client: Client</code>: Elasticsearch JavaScript 클라이언트의 인스턴스를 보관하는 개인 속성입니다. 이렇게 하면 클래스가 클러스터와 직접 대화할 수 있습니다.</p></li><li><p><code>isServerless</code> 및 <code>deploymentChecked</code>: 이러한 속성은 함께 작동하여 서버리스 또는 표준 Elasticsearch 배포에 연결되어 있는지 여부를 감지하고 캐시합니다. 이 감지는 처음 사용할 때 자동으로 수행되거나 명시적으로 구성할 수 있습니다.</p></li><li><p><code>constructor(config: ClientOptions)</code>: 이 생성자는 구성 객체(Elasticsearch 자격 증명 및 선택적 서버리스 설정이 포함된)를 가져와서 <code>this.client = new Client(config)</code> 줄에서 클라이언트를 초기화하는 데 사용합니다.</p></li><li><p><code>super()</code>: 이것은 Mastra의 기본 생성자를 호출하므로 로깅, 유효성 검사 헬퍼 및 기타 내부 훅을 상속합니다.</p></li></ul><p>이 시점에서 Mastra는 다음과 같은 새로운 벡터 스토어가 있다는 것을 알고 있습니다. <code>ElasticVector</code></p><h3>배포 유형 감지</h3><p>인덱스를 생성하기 전에 어댑터는 표준 Elasticsearch를 사용 중인지 아니면 Elasticsearch 서버리스를 사용 중인지 자동으로 감지합니다. 서버리스 배포에서는 수동 샤드 구성을 허용하지 않기 때문에 이 점이 중요합니다.</p>private async detectServerless(): Promise&lt;boolean&gt; {
    // Return cached result if already detected
    if (this.deploymentChecked) {
        return this.isServerless ?? false;
    }

    // Use explicit configuration if provided
    if (this.isServerless !== undefined) {
        this.deploymentChecked = true;
        this.logger?.info(
            `Using explicit deployment type: ${this.isServerless ? 'Serverless' : 'Standard'}`
        );
        return this.isServerless;
    }

    try {
        const info = await this.client.info();
        
        // Primary detection: build flavor (most reliable)
        const isBuildFlavorServerless = info.version?.build_flavor === 'serverless';
        
        // Secondary detection: tagline (fallback)
        const isTaglineServerless = info.tagline?.toLowerCase().includes('serverless') ?? false;
        
        this.isServerless = isBuildFlavorServerless || isTaglineServerless;
        this.deploymentChecked = true;
        
        this.logger?.info(
            `Auto-detected ${this.isServerless ? 'Serverless' : 'Standard'} Elasticsearch deployment`,
            { 
                buildFlavor: info.version?.build_flavor, 
                version: info.version?.number,
                detectionMethod: isBuildFlavorServerless ? 'build_flavor' : 'tagline'
            }
        );
        
        return this.isServerless;
    } catch (error) {
        this.logger?.warn(
            'Could not auto-detect deployment type, assuming Standard Elasticsearch. ' +
            'Set isServerless: true explicitly in config if using Serverless.',
            { error: error instanceof Error ? error.message : String(error) }
        );
        this.isServerless = false;
        this.deploymentChecked = true;
        return false;
    }
}<p>무슨 일이 일어나고 있나요?</p><ul><li><p>먼저 구성에서 <code>isServerless</code> 을 명시적으로 설정했는지 확인합니다(자동 감지 건너뛰기).</p></li><li><p>Elasticsearch의 <code>info()</code> API를 호출하여 클러스터 정보를 가져옵니다.</p></li><li><p><code>build_flavor field</code> (서버리스 배포는 <code>serverless</code>)를 확인합니다.</p></li><li><p>빌드 플레이버를 사용할 수 없는 경우 태그 라인 확인으로 돌아가기</p></li><li><p>반복되는 API 호출을 방지하기 위해 결과 캐시</p></li><li><p>탐지에 실패하면 표준 배포로 기본 설정됩니다.</p></li></ul><p> 사용 예시:</p>// Option 1: Auto-detect (recommended)
const vector = new ElasticVector({
    node: 'https://your-cluster.es.cloud',
    auth: { apiKey: 'your-api-key' }
});
// Detection happens automatically on first index operation

// Option 2: Explicit configuration (faster startup)
const vector = new ElasticVector({
    node: 'https://your-serverless.es.cloud',
    auth: { apiKey: 'your-api-key' },
    isServerless: true  // Skips auto-detection
});<h3>Elasticsearch에서 "메모리" 저장소 만들기</h3><p>아래 함수는 임베딩을 저장하기 위한 Elasticsearch 인덱스를 설정합니다. 인덱스가 이미 존재하는지 확인합니다. 그렇지 않은 경우 임베딩 및 사용자 정의 유사성 메트릭을 저장할 <code>dense_vector</code> 필드가 포함된 아래 매핑을 사용하여 매핑을 생성합니다.</p><p>몇 가지 주의해야 할 사항:</p><ul><li><p><code>dimension</code> 매개변수는 각 임베딩 벡터의 길이로, 사용 중인 임베딩 모델에 따라 달라집니다. 이 경우, <code>1536</code> 크기의 벡터를 출력하는 OpenAI의 <code>text-embedding-3-small</code> 모델을 사용하여 임베딩을 생성하겠습니다. 이 값을 기본값으로 사용하겠습니다.</p></li><li><p>아래 매핑에 사용된 <code>similarity</code> 변수는 <code>metric</code> 매개변수의 값을 받아 선택한 거리 메트릭에 대해 Elasticsearch 호환 키워드로 변환하는 도우미 함수 c<code>onst similarity = this.mapMetricToSimilarity(metric)</code> 에서 정의됩니다.</p><ul><li><p>예를 들어: 예를 들어, Mastra는 <code>cosine</code>, <code>euclidean</code>, <code>dotproduct</code> 와 같은 벡터 유사성에 대한 일반적인 용어를 사용합니다. <code>euclidean</code> 메트릭을 Elasticsearch 매핑에 직접 전달하면, Elasticsearch는 <code>l2_norm</code> 키워드가 유클리드 거리를 나타낼 것으로 예상하기 때문에 오류가 발생합니다.</p></li></ul></li><li><p>서버리스 호환성: 서버리스 배포를 위한 샤드 및 복제본 설정은 Elasticsearch 서버리스에서 자동으로 관리되므로 코드에서 자동으로 생략됩니다.</p></li></ul>async createIndex(params: CreateIndexParams): Promise&lt;void&gt; {
    const { indexName, dimension = 1536, metric = 'cosine' } = params;

    try {
        const exists = await this.client.indices.exists({ index: indexName });

        if (exists) {
            try {
                await this.validateExistingIndex(indexName, dimension, metric);
                this.logger?.info(`Index "${indexName}" already exists and is valid`);
                return;
            } catch (validationError) {
                throw new Error(
                    `Index "${indexName}" exists but does not match the required configuration: ${
                        validationError instanceof Error ? validationError.message : String(validationError)
                    }`
                );
            }
        }

        const isServerless = await this.detectServerless();
        const similarity = this.mapMetricToSimilarity(metric);

        const indexConfig: any = {
            index: indexName,
            mappings: {
                properties: {
                    vector: {
                        type: 'dense_vector',
                        dims: dimension,
                        index: true,
                        similarity: similarity,
                    },
                    metadata: {
                        type: 'object',
                        enabled: true,
                        dynamic: true, // Allows flexible metadata structures
                    },
                },
            },
        };

        // Only configure shards/replicas for non-serverless deployments
        // Serverless manages infrastructure automatically
        if (!isServerless) {
            indexConfig.settings = {
                number_of_shards: 1,
                number_of_replicas: 0, // Increase for production HA deployments
            };
        }

        await this.client.indices.create(indexConfig);

        this.logger?.info(
            `Created ${isServerless ? 'Serverless' : 'Standard'} Elasticsearch index "${indexName}"`,
            { dimension, metric, similarity }
        );
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to create index "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to create index "${indexName}": ${errorMessage}`);
    }
}<h3>상호 작용 후 새 메모리 또는 노트 저장하기</h3><p>이 함수는 각 상호 작용 후에 생성된 새 임베딩을 메타데이터와 함께 가져온 다음 Elastic의 <code>bulk</code> API를 사용하여 인덱스에 삽입하거나 업데이트합니다. <code>bulk</code> API는 여러 개의 쓰기 작업을 단일 요청으로 그룹화하여 인덱싱 성능을 개선함으로써 에이전트의 메모리가 계속 증가함에 따라 업데이트가 효율적으로 유지되도록 합니다.</p>async upsert(params: UpsertVectorParams): Promise&lt;string[]&gt; {
    const { indexName, vectors, metadata = [], ids } = params;

    try {
        // Generate unique IDs if not provided
        const vectorIds = ids || vectors.map((_, i) =&gt; 
            `vec_${Date.now()}_${i}_${Math.random().toString(36).substr(2, 9)}`
        );

        const operations = vectors.flatMap((vec, index) =&gt; [
            { index: { _index: indexName, _id: vectorIds[index] } },
            {
                vector: vec,
                metadata: metadata[index] || {},
            },
        ]);

        const response = await this.client.bulk({
            refresh: true,
            operations,
        });

        if (response.errors) {
            const erroredItems = response.items.filter((item: any) =&gt; item.index?.error);
            const erroredIds = erroredItems.map((item: any) =&gt; item.index?._id);
            const errorDetails = erroredItems.slice(0, 3).map((item: any) =&gt; ({
                id: item.index?._id,
                error: item.index?.error?.reason || item.index?.error,
                type: item.index?.error?.type
            }));
            
            const errorMessage = `Failed to upsert ${erroredIds.length}/${vectors.length} vectors`;
            console.error(`${errorMessage}. Sample errors:`, JSON.stringify(errorDetails, null, 2));
            this.logger?.error(errorMessage, { 
                failedCount: erroredIds.length, 
                totalCount: vectors.length,
                sampleErrors: errorDetails 
            });
            
            // Still return successfully inserted IDs
            const successfulIds = vectorIds.filter((id, idx) =&gt; 
                !erroredIds.includes(id)
            );
            
            if (successfulIds.length === 0) {
                throw new Error(`${errorMessage}. All operations failed. See logs for details.`);
            }
            
            return successfulIds;
        }

        this.logger?.info(`Successfully upserted ${vectors.length} vectors to "${indexName}"`);
        return vectorIds;
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to upsert vectors to "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to upsert vectors to "${indexName}": ${errorMessage}`);
    }
}<h3>시맨틱 리콜을 위해 유사한 벡터 쿼리하기</h3><p>이 기능은 시맨틱 리콜 기능의 핵심입니다. 에이전트는 벡터 검색을 사용하여 인덱스 내에서 유사한 저장된 임베딩을 찾습니다.</p>async query(params: QueryVectorParams&lt;any&gt;): Promise&lt;QueryResult[]&gt; {
    const { indexName, queryVector, topK = 10, filter, includeVector = false } = params;

    try {
        const knnQuery: any = {
            field: 'vector',
            query_vector: queryVector,
            k: topK,
            num_candidates: Math.max(topK * 10, 100), // Search more candidates for better recall
        };

        // Apply metadata filters if provided
        if (filter) {
            knnQuery.filter = this.buildElasticFilter(filter);
        }

        const sourceFields = ['metadata'];
        if (includeVector) {
            sourceFields.push('vector');
        }

        const response = await this.client.search({
            index: indexName,
            knn: knnQuery,
            size: topK,
            _source: sourceFields,
        });

        const results = response.hits.hits.map((hit: any) =&gt; ({
            id: hit._id,
            score: hit._score || 0,
            metadata: hit._source?.metadata || {},
            vector: includeVector ? hit._source?.vector : undefined,
        }));

        this.logger?.debug(`Query returned ${results.length} results from "${indexName}"`);
        return results;
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to query vectors from "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to query vectors from "${indexName}": ${errorMessage}`);
    }
}<p>내부를 들여다보세요:</p><ul><li><p>Elasticsearch에서 <code>knn</code> API를 사용하여 <a href="https://www.elastic.co/docs/solutions/search/vector/knn">kNN</a> (k-nearest neighbors) 쿼리를 실행합니다.</p></li><li><p>입력 쿼리 벡터와 유사한 상위 K개의 벡터를 검색합니다.</p></li><li><p>선택적으로 메타데이터 필터를 적용하여 결과 범위를 좁힐 수 있습니다(예: 특정 카테고리 또는 시간 범위 내에서만 검색).</p></li><li><p>문서 ID, 유사도 점수, 저장된 메타데이터를 포함한 구조화된 결과를 반환합니다.</p></li></ul><h2>지식창고 만들기</h2><p>이제 <code>ElasticVector</code> 통합을 통해 Mastra와 Elasticsearch 간의 연결을 확인했으니, 지식 에이전트 자체를 생성해 보겠습니다.</p><p><code>agents</code> 폴더 안에 <code>knowledge-agent.ts</code> 이라는 파일을 만듭니다. 환경 변수를 연결하고 Elasticsearch 클라이언트를 초기화하는 것으로 시작할 수 있습니다.</p>import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { openai } from '@ai-sdk/openai';
import { Client } from '@elastic/elasticsearch';
import { ElasticVector } from '../stores/elastic-store';
import dotenv from "dotenv";

dotenv.config();

const ELASTICSEARCH_ENDPOINT = process.env.ELASTICSEARCH_ENDPOINT;
const ELASTICSEARCH_API_KEY = process.env.ELASTICSEARCH_API_KEY;

//Error check for undefined credentials
if (!ELASTICSEARCH_ENDPOINT || !ELASTICSEARCH_API_KEY) {
  throw new Error('Missing Elasticsearch credentials');
}

//Check to see if a connection can be established
const testClient = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: { 
    apiKey: ELASTICSEARCH_API_KEY 
  },
});

try {
  await testClient.ping();
  console.log('Connected to Elasticsearch successfully');
} catch (error: unknown) {
  if (error instanceof Error) {
    console.error('Failed to connect to Elasticsearch:', error.message);
  } else {
    console.error('Failed to connect to Elasticsearch:', error);
  }
  process.exit(1);
}
//Initialize the Elasticsearch vector store
const vectorStore = new ElasticVector({
  node: ELASTICSEARCH_ENDPOINT,
  auth: {
    apiKey: ELASTICSEARCH_API_KEY,
  },
//Optional: Explicitly set to true if using Elasticsearch Serverless to skip auto-detection and improve startup time
//isServerless: true,
});<p>여기, 우리:</p><ul><li><p><code>dotenv</code> 을 사용하여 <code>.env</code> 파일에서 변수를 로드합니다.</p></li><li><p>Elasticsearch 자격 증명이 올바르게 주입되고 있는지 확인하면 클라이언트에 성공적으로 연결할 수 있습니다.</p></li><li><p><code>ElasticVector</code> 생성자에 Elasticsearch 엔드포인트와 API 키를 전달하여 앞서 정의한 벡터 저장소의 인스턴스를 생성합니다.</p></li><li><p>선택적으로 Elasticsearch 서버리스를 사용하는 경우 <code>isServerless: true</code> 을 지정합니다. 이렇게 하면 자동 감지 단계를 건너뛰고 시작 시간이 단축됩니다. 이 옵션을 생략하면 처음 사용할 때 어댑터가 자동으로 배포 유형을 감지합니다.</p></li></ul><p>다음으로 Mastra의 <code>Agent</code> 클래스를 사용하여 에이전트를 정의할 수 있습니다.</p>export const knowledgeAgent = new Agent({
    name: 'KnowledgeAgent',
    instructions: 'You are a helpful knowledge assistant.',
    model: openai('gpt-4o'),
    memory: new Memory({

        vector: vectorStore,

        //embedder used to create embeddings for each message
        embedder: 'openai/text-embedding-3-small',

        //set semantic recall options
        options: {
            semanticRecall: {
                topK: 3, // retrieve 3 similar messages
                messageRange: 2, // include 2 messages before/after each match
                scope: 'resource',
            },
        },
    }),
});<p>정의할 수 있는 필드는 다음과 같습니다:</p><ul><li><p><code>name</code> 및 <code>instructions</code>: 아이덴티티와 기본 기능을 부여합니다.</p></li><li><p><code>model</code>: <code>@ai-sdk/openai</code> 패키지를 통해 OpenAI의 <code>gpt-4o</code> 를 사용하고 있습니다.</p></li><li><p><code>memory</code>:</p><ul><li><p><code>vector</code>: Elasticsearch 저장소를 가리키므로 임베딩이 저장되고 거기에서 검색됩니다.</p></li><li><p><code>embedder</code>: 임베딩 생성에 사용할 모델</p></li><li><p><code>semanticRecall</code> 옵션은 리콜 작동 방식을 결정합니다:</p><ul><li><p><code>topK</code>: 검색할 의미적으로 유사한 메시지 수입니다.</p></li><li><p><code>messageRange</code>: 각 경기에 포함할 대화 분량입니다.</p></li><li><p><code>scope</code>: 메모리 경계를 정의합니다.</p></li></ul></li></ul></li></ul><p>거의 다 끝났습니다. 새로 생성된 에이전트를 Mastra 구성에 추가하기만 하면 됩니다. <a href="http://index.ts/"><code>index.ts</code></a> 라는 파일에서 지식 에이전트를 가져와서 <code>agents</code> 필드에 삽입합니다.</p>export const mastra = new Mastra({
  agents: { knowledgeAgent },
  storage: new LibSQLStore({
    // stores observability, scores, ... into memory storage, if it needs to persist, change to file:../mastra.db
    url: ":memory:",
  }),
  logger: new PinoLogger({
    name: 'Mastra',
    level: 'info',
  }),
  telemetry: {
    // Telemetry is deprecated and will be removed in the Nov 4th release
    enabled: false, 
  },
  observability: {
    // Enables DefaultExporter and CloudExporter for AI tracing
    default: { enabled: true }, 
  },
});<p>다른 필드에는 다음이 포함됩니다:</p><ul><li><p><code>storage</code>: 실행 기록, 통합 가시성 메트릭, 점수 및 캐시를 위한 Mastra의 내부 데이터 저장소입니다. Mastra 스토리지에 대한 자세한 내용은 <a href="https://mastra.ai/docs/server-db/storage">여기를</a> 참조하세요.</p></li><li><p><code>logger</code>: Mastra는 경량 구조화된 JSON 로거인 <a href="https://github.com/pinojs/pino">Pino를</a> 사용합니다. 상담원 시작 및 중지, 도구 호출 및 결과, 오류, LLM 응답 시간 등의 이벤트를 캡처합니다.</p></li><li><p><code>observability</code>: 상담원의 AI 추적 및 실행 가시성을 제어합니다. 추적합니다:</p><ul><li><p>각 추론 단계의 시작/종료</p></li><li><p>어떤 모델 또는 도구를 사용했는지.</p></li><li><p>입력 및 출력.</p></li><li><p>점수 및 평가</p></li></ul></li></ul><h3>Mastra Studio로 에이전트 테스트</h3><p>축하합니다! 여기까지 왔다면 이 에이전트를 실행하여 시맨틱 리콜 기능을 테스트할 준비가 된 것입니다. 다행히도 Mastra는 기본 제공 채팅 UI를 제공하므로 자체적으로 구축할 필요가 없습니다.</p><p>Mastra 개발 서버를 시작하려면 터미널을 열고 다음 명령을 실행합니다:</p>npm run dev<p>서버를 처음 번들링하고 시작하면 플레이그라운드의 주소가 제공됩니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte5f857fddc74ffc9/6a16f7b6a6c2b995d5e794c0/8b045f70008d26aec4d2e6b59d61085555b9c5b2-686x116.png" alt="Playground의 서버 주소" /><p>이 주소를 브라우저에 붙여넣으면 마스트라 스튜디오로 이동합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc7fdda6ce46ce068/6a16f7b7b0367d4f7672bacf/69bc80fe8486edd9e0cf91d87b39f465aeb23111-1600x438.png" alt="플레이그라운드 주소를 붙여넣어 마스트라 스튜디오에 액세스하기" /><p><code>knowledgeAgent</code> 옵션을 선택하고 채팅을 시작합니다.</p><p>모든 것이 올바르게 연결되었는지 간단히 테스트하려면 "팀은 10월의 판매 실적이 12% 증가했다고 발표했습니다%, 주로 기업 리뉴얼에 힘입은 것입니다."와 같은 정보를 입력합니다. 다음 단계는 미드 마켓 고객으로 범위를 넓히는 것입니다." 다음으로 새 채팅을 시작하고 "다음에 어떤 고객 세그먼트에 집중해야 한다고 했나요?"와 같은 질문을 하세요. 지식 상담원은 첫 번째 채팅에서 제공한 정보를 기억할 수 있어야 합니다. 다음과 같은 응답이 표시되어야 합니다:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfec3266e81a7213b/6a16f7b92b835f6f70f4afe2/da8ebddad89874023ed440a8f1ad2cb04ed043f4-1070x288.png" alt="Mastra Studio에서 지식 상담원과 채팅하기 - 상담원이 정보를 불러올 수 있습니다." /><p>이와 같은 응답을 보면 에이전트가 이전 메시지를 Elasticsearch에 임베딩으로 성공적으로 저장하고 나중에 벡터 검색을 사용하여 검색했다는 뜻입니다.</p><h3>상담원의 장기 기억 저장소 검사하기</h3><p>Mastra Studio의 상담원 구성에서 <code>memory</code> 탭으로 이동합니다. 이를 통해 상담원이 시간이 지남에 따라 학습한 내용을 확인할 수 있습니다. Elasticsearch에 포함되고 저장되는 모든 메시지, 응답, 상호 작용은 이 장기 기억의 일부가 됩니다. 과거 상호작용을 의미론적으로 검색하여 상담원이 이전에 학습한 정보나 컨텍스트를 빠르게 찾을 수 있습니다. 이는 기본적으로 에이전트가 시맨틱 리콜 중에 사용하는 것과 동일한 메커니즘이지만, 여기서 직접 검사할 수 있습니다. 아래 예시에서는 '판매'라는 용어를 검색하여 판매에 관한 내용이 포함된 모든 상호작용을 반환하고 있습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte428134d7bf2a43a/6a16f7bbb0367d185872bad3/3decaa0c332d288c5ae0b11c25f592c7d50c2f0f-1104x1320.png" alt="지식 에이전트 장기 기억 저장소를 검사하는 방법" /><h2>결론</h2><p>Mastra와 Elasticsearch를 연결하면 컨텍스트 엔지니어링의 핵심 계층인 메모리를 에이전트에게 제공할 수 있습니다. 시맨틱 리콜을 통해 상담원은 시간이 지남에 따라 컨텍스트를 구축하여 학습한 내용을 기반으로 응답할 수 있습니다. 이는 보다 정확하고 안정적이며 자연스러운 상호작용을 의미합니다.</p><p>이 초기 통합은 시작에 불과합니다. 여기서 동일한 패턴으로 과거 티켓을 기억하는 지원 상담원, 관련 문서를 검색하는 내부 봇, 대화 중에 고객 세부 정보를 기억할 수 있는 AI 어시스턴트 등을 만들 수 있습니다. 또한, 가까운 시일 내에 이 페어링이 더욱 원활하게 이루어질 수 있도록 공식적인 Mastra 통합을 위해 노력하고 있습니다.</p><p>여러분이 다음에 무엇을 만들지 기대가 됩니다. 한 번 사용해보시고 <a href="https://mastra.ai/">Mastra와</a> 그 메모리 기능을 살펴보고 발견한 내용을 커뮤니티와 자유롭게 공유하세요.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/knowledge-agent-semantic-recall-mastra-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/knowledge-agent-semantic-recall-mastra-elasticsearch</guid>
    <category><![CDATA[에이전틱 AI]]></category>
    <category><![CDATA[개발자 경험]]></category>
    <category><![CDATA[통합]]></category>
    <dc:creator><![CDATA[JD Armada]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt09afdbff05603865/6a16f7bd839dfabbf2dcfcb5/b8d51c2726d5573385c9246a7821d12ade4f1b0e-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 06 Nov 2025 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>