AWS 기술 블로그

Amazon QuickSight 분석을 망분리 환경에 정방향 배포하기

금융권은 전자금융감독규정에 따라 개발망과 운영망을 분리(망분리)하고, 변경 사항을 하위 환경에서 상위 환경으로만(정방향) 전파하도록 통제합니다. 실제 배포 경로는 연구개발망(RND) → 개발계(Dev) → 검증계(STG) → 운영계(PRD) 한 방향이며, 역방향 배포(예: PRD→STG)나 상위 환경에서의 직접 수정은 허용되지 않습니다. 운영 데이터·설정이 하위로 흘러가는 것을 막고, 모든 변경이 검증 절차를 거치도록 강제하기 위한 통제입니다.

이 통제는 애플리케이션 코드에는 익숙하지만, Amazon QuickSight 자산에는 까다로운 문제를 만듭니다. QuickSight 분석·대시보드는 개발자가 접근 가능한 하위 환경(RND/Dev)에서 작성되는데, 대시보드·분석·데이터셋은 코드처럼 텍스트로 형상관리되지 않고 각 계정(환경) 내부의 상태로 존재합니다. 게다가 환경마다 계정 ID·데이터셋 ID·데이터소스가 다릅니다. 따라서 상위 환경에 반영하려면 단순 복사가 아니라, 참조를 치환하며 환경 간에 자산을 정방향으로 이관(promote)하는 체계가 필요합니다.

여기에 Amazon SageMaker Unified Studio(이하 SMUS)로 데이터 분석 환경을 통합하면 제약이 하나 더 붙습니다. SMUS 프로젝트가 만드는 제한 폴더(restricted folder)는 QuickSight의 일반적인 자산 이관 방식인 Assets-as-Bundle API import를 그대로 받지 않습니다. 폴더에 리소스 기반 거부(deny) 정책이 걸려 있어 기존 자산을 폴더에 추가(create-folder-membership)하는 요청이 막히기 때문입니다.

이 블로그는 금융권 망분리 환경에서 QuickSight 분석·대시보드를 RND→Dev→STG→PRD로 정방향 배포하는 방법을, aws quicksight CLI와 간단한 브라우저 변환 도구(index.html)로 SMUS 제한 폴더 제약까지 해결하는 방식으로 설명합니다. 일반적인 계정 간 이관은 QuickSight 자산 배포 자동화 글에 잘 정리되어 있으니 함께 참고하기 바랍니다.

솔루션 개요

환경 승격 모델 (정방향·단방향)

배포는 아래처럼 한 방향으로만 흐릅니다. 각 환경은 별도의 AWS 계정이며, 역방향 이동이나 상위 환경에서의 직접 편집은 없습니다.

  • QuickSight 분석·대시보드는 RND에서 작성하고, 이후 환경으로 정방향 승격만 합니다.
  • 환경마다 계정 ID·데이터셋 ID·데이터소스가 다르므로, 승격 시 이 참조들을 타겟 환경 값으로 치환해야 합니다.
  • 승격은 콘솔에서 사람이 반복하는 대신 CI/CD로 자동화해, 각 환경에 동일한 자산이 일관되게 반영되도록 합니다.

왜 정방향만 허용되나 (규제 관점)

금융권의 망분리·변경통제는 다음을 위해 역방향 배포와 상위 환경 직접 수정을 차단합니다.

  • 데이터·설정 유출 차단: 운영계(PRD)의 실데이터·구성이 하위(개발) 환경으로 흘러가지 못하게 합니다. 운영망은 인터넷·개발자의 직접 접근 자체가 차단됩니다.
  • 변경 통제·검증 강제: 모든 변경은 반드시 하위→상위로 올라가며 각 단계의 검증·승인을 거칩니다. 검증되지 않은 변경이 운영에 유입되는 것을 막습니다.
  • 감사 추적성: “누가·언제·무엇을” 승격했는지 파이프라인 이력으로 남아, 전자금융감독규정이 요구하는 변경관리·감사 요건을 충족합니다.
  • 역방향 부재의 함의: 그래서 운영계에서 대시보드를 급히 고쳐 하위로 내리는 경로가 없습니다. 수정은 항상 RND에서 시작해 다시 정방향으로 승격해야 하며, 이것이 QuickSight 이관 체계가 필요한 근본 이유입니다.

SMUS 제한 폴더 제약

타겟 환경을 SMUS 프로젝트로 통합하면 제약이 하나 더 붙습니다.

  • SMUS 제한 폴더는 기존 자산 추가를 거부합니다(resource-based deny). 즉 번들을 import한 뒤 폴더에 넣는 방식이 불가합니다.
  • 제한 폴더 바깥의 분석은 폴더 안 데이터셋을 참조할 수 없습니다.
  • 따라서 자산은 폴더 안에 새로 생성(create-analysis --folder-arns)하는 방식으로만 넣을 수 있고, 참조 데이터셋은 passable 데이터셋(명시적 deny가 없는 데이터셋)이어야 합니다. SMUS가 잠근 프로젝트 데이터셋은 CLI로 참조할 수 없습니다.

이 제약 때문에, 자산 전체를 담는 번들(bundle)이 아니라 분석 정의(Definition)를 기반으로 이관합니다. 각 승격 단계의 흐름은 세 단계이며, 모두 aws quicksight CLI로 수행합니다.

  1. 소스 계정에서 분석 정의를 검증하고 export (describe-analysis-definition)
  2. 정의의 계정 ID·데이터셋 ID를 타겟 값으로 치환 (브라우저 도구 index.html)
  3. 타겟 계정의 제한 폴더에 create-analysis --folder-arns(신규) / update-analysis(갱신)

아키텍처 다이어그램

img-01.png

참고: 일반 대시보드(제한 폴더가 아닌 경우)는 기존 Assets-as-Bundle 번들 이관을 그대로 사용하고, 제한 폴더에 넣어야 할 때만 아래 정의 기반 방식을 사용합니다.

단계 요약

  • 단계 1 : 검증 & 정의 exportdescribe-analysis-definition으로 정의 오류를 확인하고, 분석 정의(+테마 ARN)를 파일로 내보냅니다.
  • 단계 2 : index.html로 계정·데이터셋 치환 — 브라우저 도구에 정의를 올려 소스 계정 ID·데이터셋 ID를 타겟 환경 값(passable 데이터셋)으로 치환하고, 실행할 create-analysis 명령을 얻습니다.
  • 단계 3 : 제한 폴더에 생성(정방향 배포)create-analysis --folder-arns로 제한 폴더에 생성(멱등: update-analysis), 필요 시 대시보드까지 게시하고 Dev→STG→PRD로 승격합니다.

사전 준비사항

이 솔루션을 수행하려면 아래가 필요합니다.

  • 환경별 AWS 계정(RND 작성용, Dev/STG/PRD 타겟 — 최소 소스 1 + 타겟 1) 및 각 계정에 Amazon QuickSight Enterprise 활성화
  • 타겟에 passable 데이터셋(동일 Amazon Athena 테이블을 가리키고 deny가 없는 데이터셋)
  • 대상 SMUS 제한 폴더 ID (aws quicksight list-folders로 확인)
  • 실행 주체(CI 역할 또는 사용자)에 quicksight:DescribeAnalysisDefinition, quicksight:CreateAnalysis, quicksight:CreateDashboard, 데이터셋 PassDataSet 권한
  • AWS CLI v2(단계 1·3 실행, CloudShell 권장) 와 최신 브라우저(단계 2의 index.html 실행용). CLI로 직접 치환하려면 jq도 사용.
  • 리전은 예시에서 ap-northeast-2로 고정합니다.

아래 예시의 계정 ID 111122223333(소스), 222233334444(타겟)는 placeholder입니다.

단계 1 : 소스 계정에서 검증 & 정의 export

describe-analysis-definitioncreate-analysis와 호환되는 정의(Definition)와 함께 테마 ARN, 오류(Errors)를 반환합니다. 오류가 있으면(끊긴 필드·데이터셋 참조 등) 타겟에서 생성이 실패하므로 먼저 확인합니다.

SRC=111122223333; REGION=ap-northeast-2

# 1) 이관 대상 분석 ID 확인
aws quicksight list-analyses --aws-account-id $SRC --region $REGION \
  --query "AnalysisSummaryList[].{Name:Name,Id:AnalysisId}" --output table

# 2) 정의 오류 검증 (0 이면 이관 가능)
aws quicksight describe-analysis-definition --aws-account-id $SRC --region $REGION \
  --analysis-id <분석ID> --query "length(Errors)"

# 3) 정의를 파일로 export (+ 테마 ARN 확인)
aws quicksight describe-analysis-definition --aws-account-id $SRC --region $REGION \
  --analysis-id <분석ID> --query "Definition" > def.json
aws quicksight describe-analysis-definition --aws-account-id $SRC --region $REGION \
  --analysis-id <분석ID> --query "ThemeArn" --output text

테마 ARN은 다음 단계에서 --theme-arn으로 넘겨야 폰트·글자 크기가 정확히 재현됩니다(누락 시 기본 테마로 렌더).

단계 2 : index.html로 계정·데이터셋 치환

단계 1에서 내려받은 def.json은 소스 계정 ARN과 소스 데이터셋 ID를 참조합니다. 이를 타겟 값으로 치환해야 하는데, 데이터셋이 여러 개거나 손으로 고치면 실수하기 쉬워 브라우저 오프라인 도구(index.html)로 처리합니다. 외부 라이브러리·서버 없이 브라우저 안에서만 동작하므로 자격 증명·데이터가 외부로 나가지 않습니다. 데이터셋은 반드시 타겟의 passable 데이터셋(explicit deny 없음)으로 매핑해야 제한 폴더에서 생성이 성공합니다.

이 단계에서는 아래 절차를 따릅니다.

  1. index.html을 브라우저로 엽니다.
  2. 단계 1에서 내려받은 def.json을 업로드합니다 → 소스 계정과 참조 데이터셋이 자동 인식됩니다.
  3. 다음을 입력합니다.
    • 타겟 계정 ID (예: 222233334444)
    • 데이터셋 ID 매핑: 소스 데이터셋 ID → 타겟 passable 데이터셋 ID
    • SMUS 제한 폴더 ID, 새 분석 ID, 분석 이름
    • 테마 ARN: 단계 1에서 확인한 ThemeArn 값(비우면 기본 테마)
  4. 변환 및 다운로드를 누르면 (1) 치환된 정의 <새분석ID>.definition.json이 다운로드되고, (2) 화면에 단계 3에서 실행할 aws quicksight create-analysis 명령이 생성됩니다.

도구의 핵심 로직은 세 가지입니다 — ① 업로드한 정의의 첫 ARN에서 소스 계정을 감지하고 DataSetIdentifierDeclarations로 데이터셋 매핑을 자동 채움, ② 정의 텍스트에서 계정 ID·데이터셋 ID를 타겟 값으로 치환(관리형 테마는 계정이 없어 그대로 두고 커스텀 테마만 치환), ③ 제한 폴더에 생성할 create-analysis --folder-arns 명령 조립. 전체 소스는 아래와 같습니다(단일 HTML 파일, MIT-0).

<!--
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
-->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QuickSight → SMUS 제한 폴더 이관 도구</title>
<style>
  :root {
    --bg: #0f1b2d; --panel: #fff; --line: #e3e8ef; --ink: #1a2433;
    --muted: #5b6b7f; --accent: #ec7211; --accent2: #007faa;
  }
  * { box-sizing: border-box; }
  body { margin: 0; font-family: "Noto Sans KR", -apple-system, Segoe UI, Roboto, sans-serif;
         color: var(--ink); background: #f4f6f9; }
  header { background: var(--bg); color: #fff; padding: 18px 24px; }
  header h1 { margin: 0; font-size: 18px; }
  header p { margin: 4px 0 0; color: #9fb3cc; font-size: 13px; }
  main { max-width: 820px; margin: 0 auto; padding: 20px; }
  .card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
          padding: 18px 20px; margin-bottom: 16px; }
  .card h2 { margin: 0 0 4px; font-size: 15px; }
  .card .desc { color: var(--muted); font-size: 13px; margin: 0 0 14px; }
  label { display: block; font-size: 13px; font-weight: 600; margin: 12px 0 4px; }
  input[type=text] { width: 100%; padding: 8px 10px; border: 1px solid var(--line); border-radius: 6px;
                     font-size: 13px; font-family: ui-monospace, Menlo, monospace; }
  input[type=file] { font-size: 13px; }
  code { font-family: ui-monospace, Menlo, monospace; background: #eef3f8; padding: 1px 5px; border-radius: 4px; }
  .row { display: flex; gap: 10px; }
  .row > div { flex: 1; }
  .maprow { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; }
  .maprow input { flex: 1; }
  .maprow .arrow { color: var(--muted); font-weight: 700; }
  button { background: var(--accent); color: #fff; border: 0; border-radius: 6px; padding: 9px 16px;
           font-size: 13px; font-weight: 600; cursor: pointer; }
  button.secondary { background: #fff; color: var(--accent2); border: 1px solid var(--accent2); }
  button.small { padding: 5px 10px; font-size: 12px; }
  .log { background: #0b1220; color: #b6f0c4; font-family: ui-monospace, monospace; font-size: 12px;
         padding: 12px; border-radius: 8px; white-space: pre-wrap; max-height: 320px; overflow: auto; }
  .muted { color: var(--muted); font-size: 12px; }
  .hidden { display: none; }
</style>
</head>
<body>
<header>
  <h1>QuickSight → SMUS 제한 폴더 이관 도구</h1>
  <p><code>describe-analysis-definition</code>으로 내려받은 분석 정의를 타겟 계정용으로 치환해, SMUS 제한 폴더에 생성할 <code>aws quicksight create-analysis</code> 명령과 정의 파일을 만듭니다. 모든 처리는 브라우저 안에서만 수행됩니다. (리전: ap-northeast-2)</p>
</header>
<main>

  <div class="card" id="smusCard">
    <h2>SMUS 제한 폴더 분석 이관</h2>
    <p class="desc">
      분석 정의 JSON(<code>describe-analysis-definition --query Definition</code> 결과, 또는 <code>*.smusdef.json</code>)을 올려
      타겟 계정용으로 계정·데이터셋 ID를 치환하고, SMUS <b>제한 폴더에 분석을 생성</b>하는 CLI 명령과 정의 파일을 만듭니다.
    </p>

    <input type="file" id="smusFile" accept=".json" />
    <p class="muted hidden" id="smusInfo"></p>

    <label>소스 계정 ID (자동 감지, 필요 시 수정)</label>
    <input type="text" id="smusSourceAccount" placeholder="111122223333" />

    <label>타겟 계정 ID *</label>
    <input type="text" id="smusTargetAccount" placeholder="222233334444" />

    <div style="margin-top:18px;">
      <label>데이터셋 ID 매핑 <span class="muted">(소스 dataset ID → 타겟 passable dataset ID)</span></label>
      <div id="smusDatasetMap"></div>
      <button class="secondary small" id="smusAddDataset" type="button">+ 데이터셋 매핑 추가</button>
    </div>

    <div class="row" style="margin-top:18px;">
      <div>
        <label>SMUS 제한 폴더 ID *</label>
        <input type="text" id="smusFolderId" placeholder="sagemaker-dzd-...-..." />
      </div>
      <div>
        <label>새 분석 ID *</label>
        <input type="text" id="smusAnalysisId" placeholder="mig-analysis-001" />
      </div>
    </div>

    <label>분석/대시보드 이름</label>
    <input type="text" id="smusName" placeholder="분석 이름" />

    <label>테마 ARN <span class="muted">(비우면 기본 테마. describe-analysis-definition 의 ThemeArn 값을 넣으세요)</span></label>
    <input type="text" id="smusThemeArn" placeholder="arn:aws:quicksight:...:theme/... (없으면 비움)" />
    <p class="muted" style="margin:4px 0 0;">
      ⚠️ 커스텀 테마면 <b>타겟 계정에도 같은 테마가 있어야</b> 적용됩니다. 관리형 테마(MIDNIGHT 등)는 계정 무관하게 적용됩니다.
    </p>

    <button id="smusTransformBtn" type="button" style="margin-top:18px;">변환 및 다운로드 (정의 + CLI 명령)</button>

    <div id="smusResult" style="display:none; margin-top:16px;">
      <div class="log" id="smusLog"></div>
    </div>
  </div>

</main>

<script>
"use strict";

// ===========================================================================
// QuickSight → SMUS 제한 폴더 이관 (오프라인 변환)
//   describe-analysis-definition 결과(순수 Definition) 또는 *.smusdef.json 업로드
//   → 계정 ID + 데이터셋 ID 치환 → create-analysis 용 Definition 다운로드
//   → aws quicksight create-analysis --folder-arns ... 명령 생성 (제한 폴더에 생성)
// 모든 처리는 브라우저 안에서만 수행되며, 자격 증명/데이터는 외부로 전송되지 않는다.
// ===========================================================================

const $ = (id) => document.getElementById(id);
let smusWrapper = null; // { name?, sourceAccount?, region?, themeArn?, definition }

function download(blob, name) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url; a.download = name;
  document.body.appendChild(a); a.click(); a.remove();
  URL.revokeObjectURL(url);
}

// 데이터셋 ID 매핑 행 추가 (소스 → 타겟)
function addMapRow(container) {
  const div = document.createElement("div");
  div.className = "maprow";
  const from = document.createElement("input");
  from.type = "text"; from.placeholder = "소스 dataset ID";
  const arrow = document.createElement("span");
  arrow.className = "arrow"; arrow.textContent = "→";
  const to = document.createElement("input");
  to.type = "text"; to.placeholder = "타겟 dataset ID";
  const del = document.createElement("button");
  del.className = "secondary small"; del.type = "button"; del.textContent = "✕";
  del.onclick = () => div.remove();
  div.append(from, arrow, to, del);
  container.appendChild(div);
  return { div, from, to };
}

// 매핑 행 → { 소스ID: 타겟ID }
function readMap(container) {
  const map = {};
  container.querySelectorAll(".maprow").forEach(row => {
    const [from, to] = row.querySelectorAll("input");
    if (from.value.trim() && to.value.trim()) map[from.value.trim()] = to.value.trim();
  });
  return map;
}

// 파일 업로드: 순수 Definition 또는 *.smusdef.json(래퍼) 파싱 → 소스계정/데이터셋 자동 감지
$("smusFile").addEventListener("change", async (ev) => {
  const file = ev.target.files[0];
  if (!file) return;
  try {
    const obj = JSON.parse(await file.text());
    if (obj && obj.definition && typeof obj.definition === "object") {
      smusWrapper = obj; // 래퍼 형식
    } else if (obj && (obj.DataSetIdentifierDeclarations || obj.Sheets)) {
      smusWrapper = { name: "", sourceAccount: "", region: "ap-northeast-2", themeArn: "", definition: obj };
    } else {
      alert("분석 정의 JSON이 아닙니다. (describe-analysis-definition 의 Definition 또는 definition 래퍼 필요)");
      return;
    }
    const defText = JSON.stringify(smusWrapper.definition);
    let src = smusWrapper.sourceAccount || "";
    if (!src) { const m = defText.match(/arn:aws:quicksight:[a-z0-9-]+:(\d{12}):/); if (m) src = m[1]; }
    $("smusSourceAccount").value = src;
    $("smusName").value = smusWrapper.name || "";
    $("smusThemeArn").value = smusWrapper.themeArn || "";

    // 참조 데이터셋 ARN → 매핑 행 프리필 (소스 dataset ID)
    const decls = smusWrapper.definition.DataSetIdentifierDeclarations || [];
    const dsArns = decls.map(d => d.DataSetArn).filter(Boolean);
    const container = $("smusDatasetMap");
    container.innerHTML = "";
    dsArns.forEach(arn => {
      const sid = (arn.split("dataset/")[1] || "").trim();
      const { from } = addMapRow(container);
      from.value = sid;
    });
    if (!dsArns.length) {
      container.innerHTML = '<p class="muted">정의에 데이터셋 선언이 없습니다.</p>';
    }
    $("smusInfo").textContent = `분석: ${smusWrapper.name || "(이름없음)"} / 참조 데이터셋: ${dsArns.join(", ") || "(없음)"}`;
    $("smusInfo").classList.remove("hidden");
  } catch (e) {
    alert("JSON 읽기 실패: " + e.message);
  }
});

$("smusAddDataset").onclick = () => addMapRow($("smusDatasetMap"));

// 변환: 계정/데이터셋 치환 → definition.json 다운로드 + create-analysis 명령 생성
$("smusTransformBtn").addEventListener("click", () => {
  if (!smusWrapper) { alert("먼저 분석 정의 JSON을 업로드하세요."); return; }
  const targetAccount = $("smusTargetAccount").value.trim();
  const folderId = $("smusFolderId").value.trim();
  const analysisId = $("smusAnalysisId").value.trim();
  const srcAccount = $("smusSourceAccount").value.trim();
  const name = ($("smusName").value.trim() || analysisId);
  if (!targetAccount) { alert("타겟 계정 ID를 입력하세요."); return; }
  if (!folderId) { alert("SMUS 제한 폴더 ID를 입력하세요."); return; }
  if (!analysisId) { alert("새 분석 ID를 입력하세요."); return; }

  const dsMap = readMap($("smusDatasetMap"));

  // 정의(JSON 텍스트)에 데이터셋 ID + 계정 ID 치환
  let text = JSON.stringify(smusWrapper.definition);
  for (const [s, t] of Object.entries(dsMap)) {
    text = text.split(`dataset/${s}`).join(`dataset/${t}`);
  }
  if (srcAccount && targetAccount && srcAccount !== targetAccount) {
    text = text.split(srcAccount).join(targetAccount);
  }

  let outDef;
  try { outDef = JSON.parse(text); } catch (e) { alert("치환 후 JSON 파싱 실패: " + e.message); return; }

  const defName = `${analysisId}.definition.json`;
  download(new Blob([JSON.stringify(outDef, null, 2)], { type: "application/json" }), defName);

  // 테마 ARN: 커스텀 테마면 계정 ID 치환 (관리형 테마 arn:aws:quicksight::aws:theme/... 는 계정 없음)
  let themeArn = $("smusThemeArn").value.trim();
  if (themeArn && srcAccount && targetAccount && srcAccount !== targetAccount) {
    themeArn = themeArn.split(srcAccount).join(targetAccount);
  }

  const region = smusWrapper.region || "ap-northeast-2";
  const themeLine = themeArn ? ` \\\n  --theme-arn ${themeArn}` : "";
  const cmd =
    `aws quicksight create-analysis --aws-account-id ${targetAccount} --region ${region} \\\n` +
    `  --analysis-id ${analysisId} --name "${name}" \\\n` +
    `  --definition file://${defName}` + themeLine + ` \\\n` +
    `  --folder-arns arn:aws:quicksight:${region}:${targetAccount}:folder/${folderId}`;

  $("smusResult").style.display = "block";
  $("smusLog").textContent =
    `다운로드: ${defName}\n\n` +
    `타겟 계정 : ${targetAccount}\n제한 폴더 : ${folderId}\n분석 ID   : ${analysisId}\n이름      : ${name}\n` +
    `테마 ARN  : ${themeArn || "(없음 — 기본 테마)"}\n` +
    `데이터셋 매핑: ${Object.keys(dsMap).length ? JSON.stringify(dsMap) : "(없음 — 소스 ID 유지, 계정만 치환)"}\n\n` +
    `─ 실행 명령 (제한 폴더에 생성) ─\n${cmd}\n\n` +
    `※ 이미 있으면 create 대신 update-analysis 로 갱신하세요(멱등).\n` +
    `※ 실행 주체는 quicksight:CreateAnalysis 권한이 필요하고, 참조 데이터셋은 passable(explicit deny 없음)이어야 합니다.`;
});
</script>
</body>
</html>

CLI로 직접 치환하려면 jq로도 가능합니다: jq '(tostring | gsub("<소스계정>";"<타겟계정>") | gsub("dataset/<소스ID>";"dataset/<타겟ID>")) | fromjson' def.json > <새분석ID>.definition.json

단계 3 : 타겟 제한 폴더에 생성 (정방향 배포)

핵심은 제한 폴더에 “새로 생성”하는 것입니다. 기존 자산을 폴더에 추가(create-folder-membership)하는 방식은 거부되지만, --folder-arns로 폴더 안에 직접 생성하는 것은 허용됩니다.

TGT=222233334444; REGION=ap-northeast-2
FOLDER_ARN="arn:aws:quicksight:$REGION:$TGT:folder/<제한폴더ID>"

# 신규: 제한 폴더 안에 분석 생성
aws quicksight create-analysis --aws-account-id $TGT --region $REGION \
  --analysis-id <새분석ID> --name "<이름>" \
  --definition file://<새분석ID>.definition.json \
  --theme-arn <테마ARN> \
  --folder-arns "$FOLDER_ARN"

# 재실행(멱등): 이미 있으면 update-analysis (제자리 갱신, 폴더 멤버십 유지)
aws quicksight update-analysis --aws-account-id $TGT --region $REGION \
  --analysis-id <새분석ID> --name "<이름>" \
  --definition file://<새분석ID>.definition.json --theme-arn <테마ARN>

# (선택) 대시보드까지 게시 → SMUS 프로젝트 자산으로 자동 등록
aws quicksight create-dashboard --aws-account-id $TGT --region $REGION \
  --dashboard-id <대시보드ID> --name "<이름>" \
  --definition file://<새분석ID>.definition.json --theme-arn <테마ARN> \
  --folder-arns "$FOLDER_ARN"

배포를 검증합니다.

aws quicksight list-folder-members --aws-account-id $TGT \
  --folder-id <제한폴더ID> --region $REGION

콘솔에서 분석/대시보드를 열어 시각화·테마·열 너비가 정상 렌더링되는지 확인합니다.

자주 겪는 이슈

  • PassDataSet ... explicit deny: SMUS 잠금 데이터셋을 참조한 경우입니다. passable 데이터셋 ID로 치환하고 실행 역할에 PassDataSet 권한이 있는지 확인하세요.
  • CreateAnalysis ... no identity-based policy: analyst 역할에는 권한이 없습니다. data engineer/admin 역할을 사용하세요.
  • 폰트·글자 크기·테마가 다르게 보임: 테마 ARN 누락이 대부분입니다. --theme-arn을 넘겼는지, 커스텀 테마라면 타겟 계정에도 존재하는지 확인하세요.

환경별 정방향 승격 (DEV → STG → PRD)

위 단계 1~3을 환경마다 반복합니다(계정 ID·데이터셋 ID만 타겟 환경 값으로 바뀜). CI/CD로 자동화하면 상위 환경일수록 트리거·승인을 강화합니다(아래 값은 예시이며 조직 정책에 맞게 조정).

환경 트리거 승인 실행
Dev (개발계) main 머지 자동 create-analysis(신규) / update-analysis(기존) 멱등 실행
STG (검증계) stg-v* 태그 push 수동 승인 동일
PRD (운영계) prd-v* 태그 push 수동 승인 동일
  • 각 환경에서 분석이 없으면 create-analysis --folder-arns, 있으면 update-analysis로 멱등 실행하므로 재배포 시 충돌이 없습니다.
  • 역방향(예: PRD→STG) 경로와 상위 환경 직접 수정 경로는 파이프라인에 존재하지 않습니다. 수정이 필요하면 RND에서 다시 시작해 정방향으로 올립니다.

리소스 정리하기

실습에 사용한 리소스는 불필요한 과금을 막기 위해 삭제하세요.

# 분석 삭제
aws quicksight delete-analysis --aws-account-id 222233334444 \
  --analysis-id <분석ID> --region ap-northeast-2
# 대시보드 삭제
aws quicksight delete-dashboard --aws-account-id 222233334444 \
  --dashboard-id <대시보드ID> --region ap-northeast-2

테스트로 생성한 passable 데이터셋·데이터소스가 있다면 함께 정리합니다. SMUS 프로젝트 자체는 유지하려면 삭제하지 마세요.

결론

금융권 망분리 환경에서는 QuickSight 자산도 RND→DEV→STG→PRD 정방향으로만 승격해야 하며, 역방향 배포나 상위 환경 직접 수정이 불가능합니다. 이 블로그에서는 이 제약 아래에서 aws quicksight CLI와 브라우저 오프라인 변환 도구(index.html)로 (1) 정의를 검증·export하고, (2) 계정·데이터셋·테마 참조를 타겟 환경 값으로 치환한 뒤, (3) SMUS 제한 폴더에 번들 대신 정의 기반 create/update-analysis --folder-arns로 생성하는 방법을 살펴봤습니다. 각 환경에서 create/update 멱등 실행으로 반복 가능하게 승격되므로, 코드처럼 형상관리되지 않는 QuickSight 자산을 규제 산업의 다환경 구조에서 일관되게 배포할 수 있습니다.

더 알아보기:

저자소개

Daeyeol Seo

Daeyeol Seo

서대열 님은 AWS의 ProServe Senior Delivery Consultant로서, IoT 서비스 적용, 데이터 레이크 및 빅데이터 처리, 데이터 파이프라인 엔지니어링, 데이터 거버넌스 체계 수립 등의 컨설팅을 통해, 엔터프라이즈 고객의 IoT·데이터 아키텍처 설계와 데이터 기반 비즈니스 혁신을 지원하고 있습니다.

Cheol-Min Yu

Cheol-Min Yu

유철민 님은 AWS의 Senior Delivery Consultant로서, 기업 역량 성숙도 평가, 분석 유스케이스 발굴, 데이터 레이크 및 빅데이터 처리, 데이터 거버넌스 체계 수립, AI-Ready Data 구축 등의 컨설팅을 통해 고객의 데이터 분석 및 AI 전환 가속화를 지원하고 있습니다.

Woongsik Kim

Woongsik Kim

김웅식 ProServe Delivery Consultant는 Cloud Architecture, DevOps, Security, GenAI 등 다양한 기술 영역에서 고객의 클라우드 여정을 지원하며, Enterprise 레벨의 클라우드 솔루션 설계부터 구현, 마이그레이션까지 전 과정을 리드하고 AWS 전문 지식과 컨설팅 역량을 결합하여 고객의 비즈니스 목표 달성을 돕고 있습니다.

sewoong oh

sewoong oh

AWS의 Delivery Consultant로서 엔터프라이즈 데이터 웨어하우스(EDW) 구축, BI·데이터 시각화, 데이터 파이프라인 엔지니어링, 데이터 애널리틱스 등 데이터 플랫폼 전반과 문서 파싱·청킹, 임베딩 및 벡터 검색 최적화를 통한 RAG 성능 개선 등 생성형 AI 영역에 걸쳐, 통신·금융·유통·물류 등 다양한 산업 고객의 데이터 활용과 AI 전환을 지원하고 있습니다.