<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>miiml</title>
    <link>https://miiml.tistory.com/</link>
    <description>miiml 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Wed, 3 Jun 2026 01:28:01 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>miiml</managingEditor>
    <item>
      <title>opencsp 서비스 다운타임 없이(?) 도메인 변경하기</title>
      <link>https://miiml.tistory.com/21</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcfdYX/dJMcaf00R5m/7ikehajgM0qtEcYnN0lpY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcfdYX/dJMcaf00R5m/7ikehajgM0qtEcYnN0lpY0/img.png&quot; data-origin-width=&quot;818&quot; data-origin-height=&quot;320&quot; data-is-animation=&quot;false&quot; width=&quot;300&quot; height=&quot;117&quot; style=&quot;width: 57.7012%; margin-right: 10px;&quot; data-widthpercent=&quot;58.38&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcfdYX/dJMcaf00R5m/7ikehajgM0qtEcYnN0lpY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcfdYX%2FdJMcaf00R5m%2F7ikehajgM0qtEcYnN0lpY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;818&quot; height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bE6tPD/dJMcaaMbtq3/gg6X8TSdFxdbPG7qrovlC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bE6tPD/dJMcaaMbtq3/gg6X8TSdFxdbPG7qrovlC1/img.png&quot; data-origin-width=&quot;1498&quot; data-origin-height=&quot;822&quot; data-is-animation=&quot;false&quot; width=&quot;300&quot; style=&quot;width: 41.136%;&quot; data-widthpercent=&quot;41.62&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bE6tPD/dJMcaaMbtq3/gg6X8TSdFxdbPG7qrovlC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbE6tPD%2FdJMcaaMbtq3%2Fgg6X8TSdFxdbPG7qrovlC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1498&quot; height=&quot;822&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;왼쪽이 기존, 새로 구매한게 오른쪽&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 사용하던 도메인이 곧 만료될 예정이라 새로 구매했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cloudflare Domains로 등록했는데 기존에 사용하던 것보다 저렴하고, whois 프라이버시도 더 잘 챙겨준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구매후엔 기존에 로컬에서 관리하던 cloudflare tunnel의 config.yml를 대시보드로 마이그레이션 했고(cloudflare 기능)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 새로운 도메인의 ingress를 하나씩 다 추가해주면 사용하던 터널 그대로 라우팅만 추가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UGaJ4/dJMcaa6zbZM/mG7FZiEZyMpY2LacLqv00K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UGaJ4/dJMcaa6zbZM/mG7FZiEZyMpY2LacLqv00K/img.png&quot; width=&quot;600&quot; height=&quot;361&quot; data-origin-width=&quot;2328&quot; data-origin-height=&quot;1400&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;47.43&quot; data-filename=&quot;blob&quot; style=&quot;width: 46.8812%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UGaJ4/dJMcaa6zbZM/mG7FZiEZyMpY2LacLqv00K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUGaJ4%2FdJMcaa6zbZM%2FmG7FZiEZyMpY2LacLqv00K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2328&quot; height=&quot;1400&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xywlY/dJMcageCf2c/8Ua9BxImUiKpZG94YpcTL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xywlY/dJMcageCf2c/8Ua9BxImUiKpZG94YpcTL1/img.png&quot; data-origin-width=&quot;2322&quot; data-origin-height=&quot;1260&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;52.57&quot; data-filename=&quot;blob&quot; style=&quot;width: 51.956%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xywlY/dJMcageCf2c/8Ua9BxImUiKpZG94YpcTL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxywlY%2FdJMcageCf2c%2F8Ua9BxImUiKpZG94YpcTL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2322&quot; height=&quot;1260&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;왼쪽이 기존, 오른쪽이 추가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 Core로 가는 트래픽은 내부에서 k8s ingress 설정과 환경변수, Secret 등도 같이 수정해줘야 하는데 그냥 수정하면 기존에 연결된 세션들과 데이터들이 손상될 수 있을 거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상용 서비스는 아니니까 그냥 바로 바꿔도 상관없긴하지만, 생각해보니까 마이그레이션 연습해볼 좋은 기회인거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;변경 계획&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 제약 조건을 셀프로 만들어봄&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;마이그레이션 중 기존 서비스의 접근 경로와 모든 기능은 동작해야 한다.&lt;/li&gt;
&lt;li&gt;새로운 도메인으로 접근 시 기존 기능 동작과 데이터가 동일해야한다.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2가지 조건을 만족하려면 블루그린이 확실하지만 도메인 교체를 위해 모든 인프라와 서비스를 1개씩 더 띄우는 건 너무 과한거 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 부분을 나눠서 변경 계획을 세워야 하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 scope 에선 하위 서비스들의 마이그레이션 -&amp;gt; 메인 서비스의 마이그레이션 순서로 가야하고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하위 서비스들도 데이터의 의존성에 따라 순서를 정해줘야 할 듯 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대충 생각해보면 Zitadel이 모든 사용자/테넌트 정보를 들고 있으니까 제일 중요할 것 같지만 (실제로는 맞지만)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenCSP에선 모든 Integrations를 BE가 담당하고 있다. (텔레포트 등의 OSS가 커뮤니티 버전에선 OIDC 연동, 마이그레이션 등을 지원하지 않기 때문에 이렇게 설계)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 순서 정리하면&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Teleport : 새로운 서비스 띄우고 인테그레이션을 변경해서 기능 테스트&lt;/li&gt;
&lt;li&gt;Semaphore, Cilium(hubble) : dual ingress로 띄워서 integration 테스트 할 수 있음&lt;/li&gt;
&lt;li&gt;Zitadel, OpenCSP : 동일 네임스페이스 내부 리소스 단위 블루그린으로 테스트&lt;/li&gt;
&lt;li&gt;Lago : ingress는 여러개 할 수 있지만 helm chart values에 apiUrl 같은 옵션들 때문에 의미 없어서 그냥 in-place로 업데이트 해주려고 함&lt;/li&gt;
&lt;li&gt;마이그레이션 완료 후 기존 리소스 정리&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 하면 될거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;hubble &lt;/b&gt;(&lt;a href=&quot;https://github.com/h001-lab/OpenCSP-core/commit/7c06b5c29d56db47c0621658ad2dba0f5ecb4f05&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/h001-lab/OpenCSP-core/commit/7c06b5c29d56db47c0621658ad2dba0f5ecb4f05&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 일단 작업 전에 만만한(?) hubble 먼저 수정해봤다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2260&quot; data-origin-height=&quot;1164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CBzUw/dJMcadPDyXG/XewtKv43X8Z11CGlbV6w20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CBzUw/dJMcadPDyXG/XewtKv43X8Z11CGlbV6w20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CBzUw/dJMcadPDyXG/XewtKv43X8Z11CGlbV6w20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCBzUw%2FdJMcadPDyXG%2FXewtKv43X8Z11CGlbV6w20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;412&quot; data-origin-width=&quot;2260&quot; data-origin-height=&quot;1164&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWqVp1/dJMcahEwDn0/sNXtemOjxXdmozmIYKLGx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWqVp1/dJMcahEwDn0/sNXtemOjxXdmozmIYKLGx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWqVp1/dJMcahEwDn0/sNXtemOjxXdmozmIYKLGx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWqVp1%2FdJMcahEwDn0%2FsNXtemOjxXdmozmIYKLGx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;104&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;104&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tls, rules(secret(인증서 SAN)이랑 ingress)에 하나씩 추가해줬고, 2개 도메인 다 접근 되는걸 확인함&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8CYqQ/dJMcahxOpDK/8NK3Y0j2SKILTzzzUqa5Fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8CYqQ/dJMcahxOpDK/8NK3Y0j2SKILTzzzUqa5Fk/img.png&quot; data-origin-width=&quot;2216&quot; data-origin-height=&quot;1034&quot; data-is-animation=&quot;false&quot; width=&quot;600&quot; height=&quot;280&quot; style=&quot;width: 49.2783%; margin-right: 10px;&quot; data-widthpercent=&quot;49.86&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8CYqQ/dJMcahxOpDK/8NK3Y0j2SKILTzzzUqa5Fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8CYqQ%2FdJMcahxOpDK%2F8NK3Y0j2SKILTzzzUqa5Fk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2216&quot; height=&quot;1034&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bevVQ3/dJMb99NfUUf/NwISJMT4Lu8iXmNzRWwt8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bevVQ3/dJMb99NfUUf/NwISJMT4Lu8iXmNzRWwt8K/img.png&quot; data-origin-width=&quot;2220&quot; data-origin-height=&quot;1030&quot; data-is-animation=&quot;false&quot; width=&quot;600&quot; height=&quot;278&quot; style=&quot;width: 49.5589%;&quot; data-widthpercent=&quot;50.14&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bevVQ3/dJMb99NfUUf/NwISJMT4Lu8iXmNzRWwt8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbevVQ3%2FdJMb99NfUUf%2FNwISJMT4Lu8iXmNzRWwt8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2220&quot; height=&quot;1030&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘되는거 확인했으니까 나머지 하나씩 진행해보겠음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Teleport, Semaphore &lt;/b&gt;(&lt;a href=&quot;https://github.com/h001-lab/OpenCSP-core/commit/31952fab6446be21d3eb0884ef69ab9d4507780a&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/h001-lab/OpenCSP-core/commit/31952fab6446be21d3eb0884ef69ab9d4507780a&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEaTLJ/dJMcaarVvfd/lXehekOIHZXMiVaZ0BMEek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEaTLJ/dJMcaarVvfd/lXehekOIHZXMiVaZ0BMEek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEaTLJ/dJMcaarVvfd/lXehekOIHZXMiVaZ0BMEek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEaTLJ%2FdJMcaarVvfd%2FlXehekOIHZXMiVaZ0BMEek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;95&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Teleport는 오랜만에 접속해봤더니 security patch가 있어서 업그레이드가 필요한 상황이었는데,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어차피 얘는 도메인 교체하려면 서비스 하나 새로 띄우는게 편해서 버전 업그레이드도 같이할 겸 블루그린으로 해보면 좋을 거 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Teleport 같은 경우는 처음 구성 시 사용한 clusterName 자체가 Identity 라서 모든 하위 리소스가 기존 도메인(clusterName)에 종속된다. (도메인 변경 시 새로운 클러스터 구성 필요)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 core에 teleport-miiml이라는 네임스페이스를 하나 더 만들어주고 클러스터를 하나 더 띄워줬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bruczp/dJMb990TUFJ/WKk1nzdD1T7YiubkUSi8YK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bruczp/dJMb990TUFJ/WKk1nzdD1T7YiubkUSi8YK/img.png&quot; data-origin-width=&quot;2918&quot; data-origin-height=&quot;1618&quot; data-is-animation=&quot;false&quot; style=&quot;width: 48.9001%; margin-right: 10px;&quot; data-widthpercent=&quot;49.48&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bruczp/dJMb990TUFJ/WKk1nzdD1T7YiubkUSi8YK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbruczp%2FdJMb990TUFJ%2FWKk1nzdD1T7YiubkUSi8YK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2918&quot; height=&quot;1618&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mpKxM/dJMcacccS5B/B22kbAYOH0CqVxnHQaxLE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mpKxM/dJMcacccS5B/B22kbAYOH0CqVxnHQaxLE0/img.png&quot; width=&quot;800&quot; height=&quot;434&quot; data-origin-width=&quot;2932&quot; data-origin-height=&quot;1592&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.9371%;&quot; data-widthpercent=&quot;50.52&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mpKxM/dJMcacccS5B/B22kbAYOH0CqVxnHQaxLE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmpKxM%2FdJMcacccS5B%2FB22kbAYOH0CqVxnHQaxLE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2932&quot; height=&quot;1592&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;왼쪽이 기존, 오른쪽이 새거&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 계정이나 수동으로 생성한 역할, audit 같은 데이터들을 옮겨야하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 계정들은 tbot과 opencsp가 연동되면 알아서 관리해줄거고, UI 관리를 위해 수동 생성한 admin 계정 1개만 만들어주면된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 이것도 사용자 계정 관리를 위해 teleport operator 를 활성화 시켜뒀기 때문에 동일하게 CR로 관리해주면 될 거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; &lt;a href=&quot;https://github.com/h001-lab/OpenCSP-core/commit/549b1eb7944071bbc3b34e4db1abc7fb2d8aafbc&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/h001-lab/OpenCSP-core/commit/549b1eb7944071bbc3b34e4db1abc7fb2d8aafbc&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;core에 TeleportUser 타입으로 파일 만들어주면 된다. (근데 이렇게 하면 초기에 비밀번호는 어쩔 수 없이 수동으로 설정해주긴 해야됨)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Audits, Session recordings 같은 건 지금은 로컬 볼륨이지만 나중에 외부 오브젝트 스토리지 만들고 values에서 연결해주면 될 거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Semaphore는 그냥 ingress 하나 추가로 바로 됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Zitadel, OpenCSP &lt;/b&gt;(&lt;a href=&quot;https://github.com/h001-lab/OpenCSP-core/commit/8213f7cc829b49e52f217a9dd59050cbe4c2e69f&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/h001-lab/OpenCSP-core/commit/8213f7cc829b49e52f217a9dd59050cbe4c2e69f&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얘네 둘은 postgresql에 있는 기존 데이터는 공유하도록 같은 네임스페이스에 Release 관련 리소스만 하나씩 더 만들어줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 중에 teleport-bot CR을 만들어주는 과정에서 teleport-operator가 2개라 충돌나는 부분이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 먼저 기존 teleport chart에서 operator.installCRDs: never로 변경해 CRD 설치하지 않게 수정해줬고 (근데 이건 영향이 없었던듯? )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/h001-lab/OpenCSP-core/commit/a506143d6df43e9785582accdd532151955dc466&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/h001-lab/OpenCSP-core/commit/a506143d6df43e9785582accdd532151955dc466&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에도 계속 cluster-apps 진행 과정에서 리소스 중복 에러가 나서 그냥 기존 teleport-bot을 제거해줬더니 잘 진행됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/h001-lab/OpenCSP-core/commit/80155495d7c28b0d47265481b96f1f7a3591713e&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/h001-lab/OpenCSP-core/commit/80155495d7c28b0d47265481b96f1f7a3591713e&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 마지막으로 리소스들 이름만 맞춰줬음&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2218&quot; data-origin-height=&quot;1740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TNjUw/dJMcageCAUk/TI7EaSkAKrHZkOm3ktsHgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TNjUw/dJMcageCAUk/TI7EaSkAKrHZkOm3ktsHgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TNjUw/dJMcageCAUk/TI7EaSkAKrHZkOm3ktsHgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTNjUw%2FdJMcageCAUk%2FTI7EaSkAKrHZkOm3ktsHgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;628&quot; data-origin-width=&quot;2218&quot; data-origin-height=&quot;1740&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;opencsp, zitadel 두 서비스 다 서로 다른 파드인데 의도한대로 데이터가 잘 공유된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 서비스들도 잘 살아있는 상태인데, 위에 teleport tbot 관련 CR 문제로 아마 기존 서비스에선 PAM 기능이 동작하지 않을 것 같다. (그래서 완전한 blue green은 아닌 듯 하고 기존 서비스에 실제 사용자 세션이 남아있었다면 인증서 만료되면서 연결이 끊기거나, 바로 세션이 끊겼을 거 같은 기분)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpnQsO/dJMcagZWNI2/C3k1nfQGUINWm4IxUp2kdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpnQsO/dJMcagZWNI2/C3k1nfQGUINWm4IxUp2kdK/img.png&quot; data-origin-width=&quot;2924&quot; data-origin-height=&quot;1608&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4263%; margin-right: 10px;&quot; data-widthpercent=&quot;50.01&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpnQsO/dJMcagZWNI2/C3k1nfQGUINWm4IxUp2kdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpnQsO%2FdJMcagZWNI2%2FC3k1nfQGUINWm4IxUp2kdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2924&quot; height=&quot;1608&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BrgMm/dJMcajoMLVe/UkEZwmr6aVs6kvm2iymo71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BrgMm/dJMcajoMLVe/UkEZwmr6aVs6kvm2iymo71/img.png&quot; data-origin-width=&quot;2934&quot; data-origin-height=&quot;1614&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4109%;&quot; data-widthpercent=&quot;49.99&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BrgMm/dJMcajoMLVe/UkEZwmr6aVs6kvm2iymo71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBrgMm%2FdJMcajoMLVe%2FUkEZwmr6aVs6kvm2iymo71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2934&quot; height=&quot;1614&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;왼쪽이 옛날거, 오른쪽이 새거&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LkRxn/dJMcacpMcap/zI0RsC13poYk3yaoWkAN7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LkRxn/dJMcacpMcap/zI0RsC13poYk3yaoWkAN7K/img.png&quot; data-origin-width=&quot;2928&quot; data-origin-height=&quot;1436&quot; data-is-animation=&quot;false&quot; style=&quot;width: 47.8%; margin-right: 10px;&quot; data-widthpercent=&quot;48.36&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LkRxn/dJMcacpMcap/zI0RsC13poYk3yaoWkAN7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLkRxn%2FdJMcacpMcap%2FzI0RsC13poYk3yaoWkAN7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2928&quot; height=&quot;1436&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBiBRq/dJMcaf020LO/oiVIzFA3Kv1dKRePBzr5k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBiBRq/dJMcaf020LO/oiVIzFA3Kv1dKRePBzr5k0/img.png&quot; data-origin-width=&quot;2926&quot; data-origin-height=&quot;1344&quot; data-is-animation=&quot;false&quot; style=&quot;width: 51.0372%;&quot; data-widthpercent=&quot;51.64&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBiBRq/dJMcaf020LO/oiVIzFA3Kv1dKRePBzr5k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBiBRq%2FdJMcaf020LO%2FoiVIzFA3Kv1dKRePBzr5k0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2926&quot; height=&quot;1344&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;zitadel도 잘 된다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Lago &lt;/b&gt;(&lt;a href=&quot;https://github.com/h001-lab/OpenCSP-core/commit/c1635bf23070d1938d46a5927d728eeac855392e&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/h001-lab/OpenCSP-core/commit/c1635bf23070d1938d46a5927d728eeac855392e&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lago의 helmRelease에서 apiUrl, frontUrl 등을 string으로 요구해서 dual-ingress 하더라도 연동이 제대로 안될 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 그냥 in-place로 업데이트해주기로함. (근데 실제 운영중인 서비스면 이렇게하면 안될 듯 -&amp;gt; billing 서비스 장애 + 이벤트 드리븐이라 누락된 빌링 이벤트 발생될 가능성?)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얘도 postgres, redis 냅두고 위에 zitadel 처럼하면 될거 같은데, 귀찮아서 그냥 하기로 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2802&quot; data-origin-height=&quot;1236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oYwgU/dJMb997CTtM/PIDkcKdQBja8iahLxWMaVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oYwgU/dJMb997CTtM/PIDkcKdQBja8iahLxWMaVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oYwgU/dJMb997CTtM/PIDkcKdQBja8iahLxWMaVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoYwgU%2FdJMb997CTtM%2FPIDkcKdQBja8iahLxWMaVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;1236&quot; data-origin-width=&quot;2802&quot; data-origin-height=&quot;1236&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OQfG0/dJMb990TWgI/sCuXkxOujqszqS8DPABIzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OQfG0/dJMb990TWgI/sCuXkxOujqszqS8DPABIzK/img.png&quot; data-origin-width=&quot;2912&quot; data-origin-height=&quot;1604&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.1398%; margin-right: 10px;&quot; data-widthpercent=&quot;49.72&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OQfG0/dJMb990TWgI/sCuXkxOujqszqS8DPABIzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOQfG0%2FdJMb990TWgI%2FsCuXkxOujqszqS8DPABIzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2912&quot; height=&quot;1604&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/APHvH/dJMcab5rEBb/cAJOOZc31ebvmWnipnJQV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/APHvH/dJMcab5rEBb/cAJOOZc31ebvmWnipnJQV1/img.png&quot; data-origin-width=&quot;2912&quot; data-origin-height=&quot;1586&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.6975%;&quot; data-widthpercent=&quot;50.28&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/APHvH/dJMcab5rEBb/cAJOOZc31ebvmWnipnJQV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAPHvH%2FdJMcab5rEBb%2FcAJOOZc31ebvmWnipnJQV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2912&quot; height=&quot;1586&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;그냥 바꿔서 기존 도메인은 사망&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;in-place로도 별 문제없이 데이터 그대로 도메인 잘바뀐거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업하면서 전역 CRD를 공유하는 operator 기반 구조(텔레포트 같은) 에선 완전한 blue-green이 구조적으로 어렵다는 걸 알게 됐다.&amp;nbsp;그래서 사실 이건 '무중단 blue-green'이 아니라 '데이터 보존되는 cutover'에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 이제 integrations랑 기능들 확인해보고 기존 서비스들만 제거해주면 도메인 변경 작업은 다 끝인 듯&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;874&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OCr4L/dJMcadvlBXP/1iQGKkYBdINTWRNRZaE1DK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OCr4L/dJMcadvlBXP/1iQGKkYBdINTWRNRZaE1DK/img.png&quot; data-alt=&quot;아슬아슬하게 완료&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OCr4L/dJMcadvlBXP/1iQGKkYBdINTWRNRZaE1DK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOCr4L%2FdJMcadvlBXP%2F1iQGKkYBdINTWRNRZaE1DK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;184&quot; data-origin-width=&quot;874&quot; data-origin-height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아슬아슬하게 완료&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에거 다 올라온 상태로 실제 메모리 사용량을 봤을 때 9.33GB -&amp;gt; 10.2G로 1G정도밖에 안늘어났다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다 2개씩 올리면 부족할거라고 생각했는데 생각보다 많이 아껴진듯 함 (다른 서비스들 조금 더 꾸겨넣어도 될듯)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1424&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bod5FC/dJMcaiXKhhJ/TT4vVnuI7tsm1wCTKKGRT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bod5FC/dJMcaiXKhhJ/TT4vVnuI7tsm1wCTKKGRT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bod5FC/dJMcaiXKhhJ/TT4vVnuI7tsm1wCTKKGRT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbod5FC%2FdJMcaiXKhhJ%2FTT4vVnuI7tsm1wCTKKGRT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;268&quot; data-origin-width=&quot;1424&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>OpenCSP/운영 관련</category>
      <author>miiml</author>
      <guid isPermaLink="true">https://miiml.tistory.com/21</guid>
      <comments>https://miiml.tistory.com/21#entry21comment</comments>
      <pubDate>Wed, 27 May 2026 12:46:53 +0900</pubDate>
    </item>
    <item>
      <title>OpenCSP MVP 개발 후기 (Core + Console 프로토타입)</title>
      <link>https://miiml.tistory.com/20</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;들어가며&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 작년(25년) 11월 정도에 시작했던 것 같은데 벌써 5개월이 흘렀다.&lt;br&gt;&amp;nbsp;&lt;br&gt;처음엔 단순한 VPS 서비스를 만들어서 밥 값이라도 벌어보려고 했었는데&lt;br&gt;만들다 보니까 이것저것 추가로 필요해지기도 했고&lt;br&gt;기획도 살을 붙이다보니까 생각보다 규모가 많이 커진거 같다.&lt;br&gt;&amp;nbsp;&lt;br&gt;'VPS 만들어서 VM 제공하고 돈받자' 에서&lt;br&gt;'VM 이미지만 잘 준비해두면 AWS처럼 해줄 수 있을 것 같은데?' 가 됐고&lt;br&gt;'추상화만 잘하면 다른 사람도 사용할 수 있겠다. (프레임워크)'는 생각이 들었다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그래서 여러 CSP들의 철학과 구조를 찾아봤고&lt;br&gt;각자 구성한 방법도 철학도 다르지만 공통된 조건들은 있다는 걸 알게됐다.(NIST의 클라우드 컴퓨팅 정의같은)&lt;br&gt;&lt;br&gt;그걸 기준으로 내가 아는 방식대로 설계하고 만들어봤음&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;구성&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;초기엔 거의 대부분을 혼자 만들고 운영해야 할테니&lt;br&gt;기존에 있는 성능 좋은 OSS, SDK, 라이브러리 등을 최대한 사용하자고 생각했다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;인프라&lt;/b&gt; 레벨에선 Proxmox, KVM, OpenStack 그리고 여러 하이퍼바이저들이 문제를 해결해줬고,&lt;br&gt;&lt;b&gt;네트워크&lt;/b&gt;는 VPN(Wireguard), OVN/OVS 등 SDN과 CloudFlare가&lt;br&gt;&lt;b&gt;스토리지&lt;/b&gt;는 인프라 노드 자체 볼륨 활용(하이퍼바이저, proxmox 등 인프라레벨에서 관리하되 스냅샷 백업만 외부)과 SeaweedFS(Object Storage)로&amp;nbsp;&lt;br&gt;&lt;b&gt;멀티 테넌시&lt;/b&gt;(테넌트, 유저, 롤, 인증/인가, 로그인 등)는 Zitadel, &lt;b&gt;리소스 접근과 감사&lt;/b&gt;는 Teleport가&lt;br&gt;&lt;b&gt;프로비저닝&lt;/b&gt;의 멱등성과 프로바이더는 Terraform과 Ansible이 해결해줬다.&lt;br&gt;(IaC, GitOps, 모니터링과 관측가능성, 보안, 빌링 등도 있지만 생략)&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2044&quot; data-origin-height=&quot;1480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUc9Vz/dJMcafT46v5/2r30yssT6eIE66qmuS2It1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUc9Vz/dJMcafT46v5/2r30yssT6eIE66qmuS2It1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUc9Vz/dJMcafT46v5/2r30yssT6eIE66qmuS2It1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUc9Vz%2FdJMcafT46v5%2F2r30yssT6eIE66qmuS2It1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2044&quot; height=&quot;1480&quot; data-origin-width=&quot;2044&quot; data-origin-height=&quot;1480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;그리고 대부분의 1인이 가진 여유 인프라는 많아봐야 적당한 성능의 데스크탑 1~2대 정도일거라고 생각했다. (내 기준)&lt;br&gt;&amp;nbsp;&lt;br&gt;그래서 위에 모든 서비스들은 최대한 적은 리소스만 사용해서 동작해야 했고,&lt;br&gt;동시에 큰 규모의 인프라를 가진 사용자들이나 고가용성을 원하는 사용자를 위해서 확장도 가능해야했기 때문에 쿠버네티스에 모든 서비스를 배포했다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;여기까지는 OpenCSP의 Core (&lt;a href=&quot;https://github.com/h001-lab/OpenCSP-core&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://github.com/h001-lab/OpenCSP-core&lt;/span&gt;&lt;/a&gt;) 에 대한 고민이었고&lt;br&gt;&amp;nbsp;&lt;br&gt;위에 모든 서비스들을 엮어서 하나로 동작하게 만들기 위해 Console이 필요해졌는데,&lt;br&gt;문제는 저 중에 하나라도 서비스가 deprecated 되거나 라이선스, 정책 등이 변경된다면&lt;br&gt;OpenCSP가 제대로 동작하기 어려워질테니 대부분의 연동 코드를 facade 구조로 설계해야 했다. (MVP에선 위에 구성된 도구들에 대해서만 구현되어서 다른 도구와의 연동은 검증되지 않음)&lt;br&gt;&amp;nbsp;&lt;br&gt;어차피 오픈소스로 만들면서 코드를 다 공개하고 있으니까&lt;br&gt;OpenCSP를 프레임워크처럼 사용해서 자체 클라우드 서비스(Public/ Private 또는 IDP)를 만들고 싶은 사용자들을 위해서,&lt;br&gt;그리고 각 레이어의 Scale-out을 위해서 FE(Front-end), BE(Back-end)를 분리해뒀고 (BE API만 사용하고 FE는 자체 개발이나 수정? -&amp;gt; Zitadel login 같은 패턴)&lt;br&gt;그래서 BE를 stateless하게 만들려고 최대한 고민해보긴 했다. (이 부분도 아직 검증되진 않음)&lt;br&gt;&amp;nbsp;&lt;br&gt;UI/ UX에도 고민이 좀 있었는데, AWS의 Dogfooding과 GCP 콘솔의 디자인 같은게 섞인걸 원했지만&lt;br&gt;1인이나 소규모 인원이 운영한다고 생각하면 다른 상용 솔루션 제품들처럼 별도의 Admin 페이지가 있는게 편할거 같았다.&lt;br&gt;그리고 i18n과 DB 기반 configuration 등을 지원해줘야 운영 편의성과 사용자 경험이 좋아질거 같았음&lt;br&gt;(i18n은 json 구조에서 키를 읽어서 UI에 보여주는 방식이므로 파일만 별도로 배포해두고 수정하면 console을 재시작하지 않고 수정이 가능)&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2912&quot; data-origin-height=&quot;1524&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbJqpC/dJMcadWdgIF/5IK3JsLPH9UUqXkxfGKLF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbJqpC/dJMcadWdgIF/5IK3JsLPH9UUqXkxfGKLF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbJqpC/dJMcadWdgIF/5IK3JsLPH9UUqXkxfGKLF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbJqpC%2FdJMcadWdgIF%2F5IK3JsLPH9UUqXkxfGKLF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2912&quot; height=&quot;1524&quot; data-origin-width=&quot;2912&quot; data-origin-height=&quot;1524&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2564&quot; data-origin-height=&quot;1228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rFINj/dJMcahj5jy3/aiIvgubuQ3xu0FVD0BPj70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rFINj/dJMcahj5jy3/aiIvgubuQ3xu0FVD0BPj70/img.png&quot; data-alt=&quot;언어별 json 파일만 추가해주면 다국어 지원됨&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rFINj/dJMcahj5jy3/aiIvgubuQ3xu0FVD0BPj70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrFINj%2FdJMcahj5jy3%2FaiIvgubuQ3xu0FVD0BPj70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2564&quot; height=&quot;1228&quot; data-origin-width=&quot;2564&quot; data-origin-height=&quot;1228&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;언어별 json 파일만 추가해주면 다국어 지원됨&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;그리고 Console은 아래 그림처럼 Core 내부에 배포해주는게 보안/ 네트워크/ CICD 등에서 유리하다고 생각했다.&lt;br&gt;근데 개발은 서로 분리된 상태에서 했기때문에 Core와 Console을 별도로 구성 가능하다. (이 경우 각 서비스에 접근 가능한 외부 엔드포인트가 필요)&lt;/p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cb1lnj/dJMcagrTHsZ/q4RKM8o3O4hfd0mk263O8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cb1lnj/dJMcagrTHsZ/q4RKM8o3O4hfd0mk263O8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cb1lnj/dJMcagrTHsZ/q4RKM8o3O4hfd0mk263O8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcb1lnj%2FdJMcagrTHsZ%2Fq4RKM8o3O4hfd0mk263O8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;1160&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;리소스의 프로비저닝과 연결은 아래처럼 가능한 상태고 (자세한 건 &lt;a href=&quot;https://miiml.tistory.com/18&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://miiml.tistory.com/18&lt;/span&gt;&lt;/a&gt; 참고)&lt;/p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2914&quot; data-origin-height=&quot;978&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cg05nL/dJMcahj5lbj/n4YisiZ7d8hNKh8Npduqt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cg05nL/dJMcahj5lbj/n4YisiZ7d8hNKh8Npduqt0/img.png&quot; data-alt=&quot;리소스 생성 및 프로비저닝(텔레포트 연결) 완료 상태&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cg05nL/dJMcahj5lbj/n4YisiZ7d8hNKh8Npduqt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcg05nL%2FdJMcahj5lbj%2Fn4YisiZ7d8hNKh8Npduqt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2914&quot; height=&quot;978&quot; data-origin-width=&quot;2914&quot; data-origin-height=&quot;978&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;리소스 생성 및 프로비저닝(텔레포트 연결) 완료 상태&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2916&quot; data-origin-height=&quot;1392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Kc39q/dJMcahqPJwX/F12Tu4X8tkLjkwFImI9ZwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Kc39q/dJMcahqPJwX/F12Tu4X8tkLjkwFImI9ZwK/img.png&quot; data-alt=&quot;상세 페이지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Kc39q/dJMcahqPJwX/F12Tu4X8tkLjkwFImI9ZwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKc39q%2FdJMcahqPJwX%2FF12Tu4X8tkLjkwFImI9ZwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2916&quot; height=&quot;1392&quot; data-origin-width=&quot;2916&quot; data-origin-height=&quot;1392&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;상세 페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2934&quot; data-origin-height=&quot;1424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpkc5q/dJMcahxA0p0/ZRNxAlIzIexlRJj14t1qJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpkc5q/dJMcahxA0p0/ZRNxAlIzIexlRJj14t1qJ0/img.png&quot; data-alt=&quot;연결 클릭 시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpkc5q/dJMcahxA0p0/ZRNxAlIzIexlRJj14t1qJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcpkc5q%2FdJMcahxA0p0%2FZRNxAlIzIexlRJj14t1qJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2934&quot; height=&quot;1424&quot; data-origin-width=&quot;2934&quot; data-origin-height=&quot;1424&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;연결 클릭 시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2940&quot; data-origin-height=&quot;652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brhWR7/dJMcagenMAV/NDKEnvW9e2ARRTpjW4Fj50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brhWR7/dJMcagenMAV/NDKEnvW9e2ARRTpjW4Fj50/img.png&quot; data-alt=&quot;웹 콘솔&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brhWR7/dJMcagenMAV/NDKEnvW9e2ARRTpjW4Fj50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrhWR7%2FdJMcagenMAV%2FNDKEnvW9e2ARRTpjW4Fj50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2940&quot; height=&quot;652&quot; data-origin-width=&quot;2940&quot; data-origin-height=&quot;652&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;웹 콘솔&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2416&quot; data-origin-height=&quot;1324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mmhMU/dJMcagMeFc6/Z1xe59Sav65KaNMNntAcn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mmhMU/dJMcagMeFc6/Z1xe59Sav65KaNMNntAcn1/img.png&quot; data-alt=&quot;Terminal&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mmhMU/dJMcagMeFc6/Z1xe59Sav65KaNMNntAcn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmmhMU%2FdJMcagMeFc6%2FZ1xe59Sav65KaNMNntAcn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2416&quot; height=&quot;1324&quot; data-origin-width=&quot;2416&quot; data-origin-height=&quot;1324&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Terminal&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;리소스에 대한 연결은 텔레포트를 거쳐서 웹 콘솔과 console에서 제공하는 install 스크립트를 통해 로컬에서도 ssh 연결이 가능하다. (아직 인증 직접해야하는 문제는 있음)&lt;br&gt;&amp;nbsp;&lt;br&gt;웹 콘솔의 경우는 tbot 기반 자동 인증서 갱신 로직이 있기때문에 언제 연결해도 작동하긴 하고,&lt;br&gt;terminal 컴포넌트에 자체 명령어 버퍼와 터미널 모드 감지 기능이 있기때문에 (vi 등 상호작용? 모드 같은거) 사용자 입장에선 지연이 적다고 느낄만 한거 같다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2672&quot; data-origin-height=&quot;886&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R6Tar/dJMcadaOosp/CKr3u2CDT5cbnfFf6lijL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R6Tar/dJMcadaOosp/CKr3u2CDT5cbnfFf6lijL1/img.png&quot; data-alt=&quot;위에 모든 사용자 행위는 이미지처럼 Teleport의 Audit 과 Session Recordings에 남는다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R6Tar/dJMcadaOosp/CKr3u2CDT5cbnfFf6lijL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR6Tar%2FdJMcadaOosp%2FCKr3u2CDT5cbnfFf6lijL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;265&quot; data-origin-width=&quot;2672&quot; data-origin-height=&quot;886&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;위에 모든 사용자 행위는 이미지처럼 Teleport의 Audit 과 Session Recordings에 남는다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;Billing은 아직 별 기능이 없지만 몇가지 메트릭으로 간단한 기능들만 테스트했다. (나중엔 이벤트 단위 리스트 필터링, 검색 등과 테넌트별 메트릭 기반 quota limits 같은게 들어가야됨)&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2914&quot; data-origin-height=&quot;1278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVnb4y/dJMcaiQK4OM/47mBokGQfHxHAE8AHY1jZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVnb4y/dJMcaiQK4OM/47mBokGQfHxHAE8AHY1jZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVnb4y/dJMcaiQK4OM/47mBokGQfHxHAE8AHY1jZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVnb4y%2FdJMcaiQK4OM%2F47mBokGQfHxHAE8AHY1jZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2914&quot; height=&quot;1278&quot; data-origin-width=&quot;2914&quot; data-origin-height=&quot;1278&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;구현된 기능과 아쉬운 점&amp;nbsp;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;현재까지 구현된 기능은 &lt;a href=&quot;https://miiml.tistory.com/19&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://miiml.tistory.com/19&lt;/span&gt;&lt;/a&gt; 에서 적었던 Basic 1단계의 아래 부분들은 다 어느정도 기본 틀은 잡힌거 같다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;Core 연동 부분&amp;nbsp; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;Provisioning Flow (FE -&amp;gt; BE -&amp;gt; Terraform -&amp;gt; Ansible)&lt;/li&gt; 
   &lt;li&gt;PAM (Teleport) 인증 및 SSH 릴레이&lt;/li&gt; 
   &lt;li&gt;IAM (Zitadel) 서비스 기반 중앙 인증 및 사용자 데이터 연동&lt;/li&gt; 
   &lt;li&gt;Billing 파이프라인(Lago) 연동 및 메트릭 가시화 페이지 개발&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;Console UI/UX 및 백엔드 구조 부분 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;싱글 테넌트 단위 UI 및 기능 개발 및 사용자 단위 리소스 격리 (2단계에서 고도화)&lt;/li&gt; 
   &lt;li&gt;직관적인 UI와 UX 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;적은 레이턴시/화면 리프레시, 프로비저닝 진행 상황 모니터링, Dynamic Configuration(Integrations) 등&lt;/li&gt; 
    &lt;/ul&gt; &lt;/li&gt; 
   &lt;li&gt;각 영역의 다른 오픈소스 서비스 연동이 고려된 백엔드 구조 (파사드 패턴)&lt;/li&gt; 
   &lt;li&gt;MSA 구조에서 데이터 안정성 및 백업/복구 정책 및 방법 구상&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;CI/CD 파이프라인 구성 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;github OCI로 helm chart와 버전별 컨테이너를 Core 인프라 내부에 배포&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;도메인 구매 후 서비스 오픈&amp;nbsp;&lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;아쉬운 부분들도 많은데 (다음 단계에서 해야되는 것들을 제외하고)&lt;br&gt;&amp;nbsp;&lt;br&gt;우선 운영자 입장에서 보면 컨테이너/ 패키지 버전 관리와 여러 서비스들의 의존성에 대한 관리 부담이 여전히 너무 많고 (1인 기준) 그것들에 대한 공급망 보안이 고려되지 않았다는 점인 거 같다.&lt;br&gt;&amp;nbsp;&lt;br&gt;(Harbor/Nexus 같은 내부 레지스트리와 Trivy 같은 걸 추가하면 어느정도 가능 하겠지만 OpenCSP에서 다룰 영역이 아니라고 생각했다 -&amp;gt; Core에서 사용자 환경에 맞게 커스텀)&lt;br&gt;&amp;nbsp;&lt;br&gt;그리고 들어간 기술이나 패턴, 툴은 많은데 아직 뽑아낸 플로우들이 적고 대시보드 같은 UI 퀄리티와 컴포넌트가 부족하다는 점과&lt;br&gt;&amp;nbsp;&lt;br&gt;전체적인 구조가 EDA가 아닌 API 연동 기반이고 별도의 API GW가 없어서 트래픽 처리의 한계값이 있다는 점이다. (물론 현재 규모에선 Scale-out으로 충분할거 같긴하고, 다음 단계에서 Temporal-NATS 를 도입해서 해결되면 좋을 듯)&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;다음 단계에서 해야되는 건&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;우선 테넌트 단위 격리, 네트워크 가상화, 클러스터/노드 오케스트레이션 같은 클라우드에서 핵심적인 기능들이 우선순위가 높고,&lt;br&gt;다음은 Temporal-NATS 로 사용자 요청을 이벤트 기반으로 처리 (이 경우 BE가 NATS 에 쌓인 이벤트를 병렬로 소비하고 그중 프로비저닝 같은 Transactional 이벤트들은 Temporal에 정의된 액션으로 처리 후 NATS를 통해 각 액션의 상태를 구독 받는 플로우)&lt;br&gt;&amp;nbsp;&lt;br&gt;그리고 여러 클라우드 이미지를 packer로 미리 만들어서 seaweed에 올려두고 위의 프로비저닝 로직을 활용해서&lt;br&gt;Managed services 배포하거나 인스턴스 타입들을 추가해주면 어느정도 사용 가능한 콘솔이 만들어 질 거 같다.&lt;br&gt;(이 때 teleport agent 같은게 들어 있는 이미지도 만들어주면 프로비저닝도 빨라짐)&lt;br&gt;&amp;nbsp;&lt;br&gt;추가로 아래에 있는 기능들도 제공해주면 좋을 거 같다.&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;사용자 인프라 조합을 미리 정의해서 사용하기 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;이건 개발자를 위한 기능인데 Docker Compose 또는 Terraform Module 같은걸 opencsp-modules에 올려두고 불러와 사용하는 형태로 (한번에 외부 노출할 수 있는 3티어 인프라 구성 등) IDP에 가까워 질 수 있는 기능들이다&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;admin &amp;gt; integraitions에 PG연동 UI 및 기능 추가 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;티어, 플랜, 메트릭 같은 미터링과 빌링 기능은 lago가 해주고, 실제 청구는 사용자가 직접 연동하는 구조&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;Core에 otel-collector gw 파드 추가 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;나중에 obserbavility node 와 연동하기 편해지고, 별도의 모니터링 스택이 있으면 바로 연동이 가능함&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위에 만든 것들을 우선 기존에 사용하던 도메인으로 배포해뒀다.&amp;nbsp;&lt;br&gt;(임시 도메인이므로 작성은 따로 하지 않고 아래 Console 레포지토리 우측 상단 링크에 남겨뒀음)&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/h001-lab/OpenCSP-console&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://github.com/h001-lab/OpenCSP-console&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>OpenCSP/MVP 프로젝트</category>
      <author>miiml</author>
      <guid isPermaLink="true">https://miiml.tistory.com/20</guid>
      <comments>https://miiml.tistory.com/20#entry20comment</comments>
      <pubDate>Fri, 8 May 2026 15:21:35 +0900</pubDate>
    </item>
    <item>
      <title>Teleport 거쳐서 사용자 리소스(VM) 연결하기</title>
      <link>https://miiml.tistory.com/15</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 단계(Provisioning flow)에서 프로비저닝 부분을 개발하면서, 사용자가 웹 UI로 인스턴스 생성 요청하면 Proxmox(실제 인프라 관리 도구)에서 리소스를 생성해주고 이후에 post-provisioning 스크립트를 실행해주는 걸 볼 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(관련 글 인덱스 : &lt;a href=&quot;https://miiml.tistory.com/18&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://miiml.tistory.com/18&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이전에 Core에 구성했던 PAM (Teleport)을 사용해서 생성된 리소스들에 접근할 수 있게 해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Teleport 구성 글 : &lt;a href=&quot;https://miiml.tistory.com/8&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://miiml.tistory.com/8&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설계에 앞서 기획 단계에서 작성해뒀던 몇 가지 요구사항들이 있는 데 좀 정리해보면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2830&quot; data-origin-height=&quot;1020&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YSieN/dJMcabYmjpr/k9JUlIVe01hlu2o4ptPyC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YSieN/dJMcabYmjpr/k9JUlIVe01hlu2o4ptPyC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YSieN/dJMcabYmjpr/k9JUlIVe01hlu2o4ptPyC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYSieN%2FdJMcabYmjpr%2Fk9JUlIVe01hlu2o4ptPyC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;216&quot; data-origin-width=&quot;2830&quot; data-origin-height=&quot;1020&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2088&quot; data-origin-height=&quot;776&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLR424/dJMcaf7uGWe/XSdAn5YWbEK5XviRBFLSkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLR424/dJMcaf7uGWe/XSdAn5YWbEK5XviRBFLSkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLR424/dJMcaf7uGWe/XSdAn5YWbEK5XviRBFLSkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLR424%2FdJMcaf7uGWe%2FXSdAn5YWbEK5XviRBFLSkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;223&quot; data-origin-width=&quot;2088&quot; data-origin-height=&quot;776&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 사용자는 우리 서비스가 Core 레이어에서 어떤 툴들을 사용하는지 몰라야 한다. (정확히는 상관없어야 됨)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템 요구사항에선 REQ-SYS-002 부분이 해당 내용과 겹치는 거 같고 (근데 사실 이건 BFF 패턴 등 프론트 영역과 더 가까운 듯)&lt;/li&gt;
&lt;li&gt;사용자 요구사항의 REQ-USR-002 부분도 맥락이 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 사용 과정에서 별도의 수동 작업 (설정 등) 없이 리소스에 접근하게 할 수 있어야 한다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 부분도 REQ-USR-001 이랑 같은 기능인 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 저런 요구사항들을 최대한 고려해서 설계를 해보긴했는데, 사실 Teleport와 연동하는 방법은 어느정도 정해져있다. (특히 JAVA Spring Boot는 SDK 지원이 안되서 옵션이 좀 더 적음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 OpenCSP에서 Teleport 연동에 신경써야 할 건 크게 2가지다.&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Teleport 인증&amp;nbsp;&lt;/li&gt;
&lt;li&gt;SSH 트래픽 중계 (SSL handshake, SSH real-time relaying)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증은 tbot과 봇계정을 사용하면 되지만 실제 리소스에 SSH 접근하는 건 ssh 라이브러리 등으로 직접 구현하긴 어렵고 (Teleport는 SSH 연결에 자체 프로토콜 등을 사용하기 때문에) 이렇게 할 경우 오픈소스 버전 업그레이드에 맞게 계속 수정도 해줘야 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 공식 도구인 tsh, tbot 그리고 Teleport가 가장 잘 지원하는 Go SDK를 잘 활용해서 개발해주기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 난이도에 따라 아래 3가지 정도로 정리해봄&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;옵션 1. BE에서 tsh(Teleport 전용 쉘 도구) 호출하기 &lt;/b&gt;(MVP 초기 방식)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 쉬운 방식이지만 백엔드에서 CLI로 외부 툴을 호출해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 개발 방식을 별로 선호하진 않지만 우선 빠르게 동작을 테스트한다는 관점에선 나쁘지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현은 백엔드랑 같은 공간에 tsh 바이너리를 같이 넣어주고, 명령어를 직접 호출해서 인증 후 중계 해주면 되지만 이렇게 할 경우 직접 tsh 명령어를 호출해서 인증서를 받아와야 하는데 인증서 자체의 유효기간이 짧고, 인증 과정에 MFA가 강제된다. (텔레포트 정책)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 테스트할 땐 인증 부분을 직접 수동으로 갱신해줬었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;858&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqWqKV/dJMcaf7uI5R/k2IMBeOZVP0hYnnL1lmpg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqWqKV/dJMcaf7uI5R/k2IMBeOZVP0hYnnL1lmpg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqWqKV/dJMcaf7uI5R/k2IMBeOZVP0hYnnL1lmpg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqWqKV%2FdJMcaf7uI5R%2Fk2IMBeOZVP0hYnnL1lmpg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;452&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;858&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;옵션 2. 인증은 tbot, 중계는 tsh 활용&lt;/b&gt; (현재 배포된 MVP에 적용된 방식)&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Teleport는 Bot 계정을 제공해주는데, 해당 계정과 Role을 설정하고 tbot으로 등록해주면 인증서를 자동으로 갱신해서 관리해준다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1330&quot; data-origin-height=&quot;936&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dYY5ya/dJMcaaLU0NW/jdJ4K55qDqc71XBWKOdDeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dYY5ya/dJMcaaLU0NW/jdJ4K55qDqc71XBWKOdDeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dYY5ya/dJMcaaLU0NW/jdJ4K55qDqc71XBWKOdDeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdYY5ya%2FdJMcaaLU0NW%2FjdJ4K55qDqc71XBWKOdDeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;936&quot; data-origin-width=&quot;1330&quot; data-origin-height=&quot;936&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너로 BE를 빌드하고 쿠버네티스에 배포도 할 거니까 BE 컨테이너 내부에 tsh 바이너리를 똑같이 넣어주고, 중계할 때 파라미터로 identity 옵션으로 인증서를 지정해주면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스 환경에서 인증서를 공유하는 방법은 2가지 정도 있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 위에 그림에 있는대로 공유 볼륨을 마운트하는 방식이 많이 쓰이지만 컨테이너가 다르므로 (사이드카) fsGroup을 같게 지정하고 그룹 읽기 권한을 주거나 (440), 컨테이너간 UID를 맞춰줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해두면 BE에서도 딜레이없이 거의 실시간으로 인증이 갱신된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 보안 좀 신경쓰면서(?) 구성 편하게 하고 싶으면 tbot이 Kubernetes Secret에 인증서를 배포하게 해두고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그걸 BE가 읽어오게 하면 되는데 이 경우에도 인증서 자체의 값을 별도로 암호화를 해두는 게 권장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;옵션 3. Go SDK로 Teleport 인증/ 중계용 백엔드를 내부에 배포하고 tbot 사이드카로 인증&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번이 내가 생각하는 제일 이상적인 방법인데 Teleport와 같은 Namespace에 별도의 Go 백엔드를 개발해서 파드로 배포해두고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증은 tbot이 하고, ssh 릴레이는 SDK를 사용해서 개발해주는 방식임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 할 경우 goroutine을 활용하면 대량의 세션 처리도 어느정도 가능할 거 같다. (파드로 배포되어서 파드 자체의 scale-out도 가능)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1436&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OXr4I/dJMcadIAk6W/cvXwUlXME5fkwoBXZUVqwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OXr4I/dJMcadIAk6W/cvXwUlXME5fkwoBXZUVqwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OXr4I/dJMcadIAk6W/cvXwUlXME5fkwoBXZUVqwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOXr4I%2FdJMcadIAk6W%2FcvXwUlXME5fkwoBXZUVqwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;251&quot; data-origin-width=&quot;1436&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 시점 기준으론 옵션 2로 개발되어 있고 tbot을 사용해 인증할 때 봇 자체(be 시스템)의 인증은 잘 갱신이 되지만 각 사용자의 Audit이 남지 않는다는 문제가 있다. (Teleport 입장에선 다 bot 이 요청한 거니까)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 IAM과 Teleport의 사용자 목록을 동기화하고 teleport-impersonate (&lt;a href=&quot;https://goteleport.com/docs/zero-trust-access/authentication/impersonation/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://goteleport.com/docs/zero-trust-access/authentication/impersonation/&lt;/a&gt;) 기능을 활용해서 Audit을 남기게 해주려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ssh 릴레이 방식도 지금은 컨테이너 내에서 tsh 프로세스를 사용하니까 동시 처리가 버거울 거 같아서 이것도 같이 고민 중임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 글: &lt;a href=&quot;https://miiml.tistory.com/8&quot;&gt;2026.03.20 - [프로젝트/OpenCSP] - k3s에 Teleport 올리고 리소스 연결하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>OpenCSP/MVP 프로젝트</category>
      <author>miiml</author>
      <guid isPermaLink="true">https://miiml.tistory.com/15</guid>
      <comments>https://miiml.tistory.com/15#entry15comment</comments>
      <pubDate>Thu, 30 Apr 2026 18:36:28 +0900</pubDate>
    </item>
    <item>
      <title>OpenCSP 프로젝트 진행 현황 및 계획 정리 (2026-04-19 기준)</title>
      <link>https://miiml.tistory.com/19</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하다보니까 신경써야할 범위들이 처음보다 훨씬 많아지기도 했고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진행상황에 대해서 어느 정도 내용 정리도 필요할 거 같아서 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 전체 프로젝트의 진행은 아래처럼 생각하고 있고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차적인 완성은 Basic 프로젝트 완료까지로 보고 있다.&amp;nbsp; (근데 가능할진 모르겠음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Basic 프로젝트&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1단계: MVP 개발&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Core 구성 및 Console 프로토타입(API 연동 방식, MSA) 개발&lt;/li&gt;
&lt;li&gt;예상 RPS : 100 ~ 500 RPS (Maximum 3k RPS)&lt;/li&gt;
&lt;li&gt;DoD(Definition of Done)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Core 연동 부분&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Provisioning Flow (FE -&amp;gt; BE -&amp;gt; Terraform -&amp;gt; Ansible)&lt;/li&gt;
&lt;li&gt;PAM (Teleport) 인증 및 SSH 릴레이&lt;/li&gt;
&lt;li&gt;IAM (Zitadel) 서비스 기반 중앙 인증 및 사용자 데이터 연동&lt;/li&gt;
&lt;li&gt;Billing 파이프라인(Lago) 연동 및 메트릭 가시화 페이지 개발&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Console UI/UX 및 백엔드 구조 부분
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;싱글 테넌트 단위 UI 및 기능 개발 및 사용자 단위 리소스 격리 (2단계에서 고도화)&lt;/li&gt;
&lt;li&gt;직관적인 UI와 UX
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;적은 레이턴시/화면 리프레시, 프로비저닝 진행 상황 모니터링, Dynamic Configuration(Integrations) 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;각 영역의 다른 오픈소스 서비스 연동이 고려된 백엔드 구조 (파사드 패턴)&lt;/li&gt;
&lt;li&gt;MSA 구조에서 데이터 안정성 및 백업/복구 정책 및 방법 구상&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CI/CD 파이프라인 구성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;github OCI로 helm chart와 버전별 컨테이너를 Core 인프라 내부에 배포&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;도메인 구매 후 서비스 오픈&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2단계: 고도화&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1단계 결과물(MVP)을 고도화함&lt;/li&gt;
&lt;li&gt;예상 RPS : 낮은 레이턴시로 10k ~ 30k 정도가 목표 (스파이크 방어)&lt;/li&gt;
&lt;li&gt;DoD(Definition of Done)&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트렌젝션 통합 처리 시스템(Temporal) 구성 및 연동 (EDA)&lt;/li&gt;
&lt;li&gt;각 레이어의 마이크로 서비스 fallback 처리&lt;/li&gt;
&lt;li&gt;멀티 테넌트 적용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Core 내부의 각 오픈소스 서비스들이 멀티 테넌트를 지원하지 않을 때도 고려해서 로직 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;테넌트 단위 격리 (격리 레벨은 고민이 좀 필요)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 레이어 격리: 테넌트 별 가상 네트워크로 격리 (Security Group/ Rule을 OVN ACL 등으로 구현)&lt;/li&gt;
&lt;li&gt;리소스 레이어 격리: 같은 테넌트 내부 사용자 단위에서의 리소스 격리&lt;/li&gt;
&lt;li&gt;데이터 레이어 격리: 테넌트별로 물리적인 레벨(DB자체)에서 분리할지, 스키마 레벨에서 분리할지, 아니면 쿼리 파라미터 등으로 논리 레벨에서 분리할지 고민&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;네트워크 가상화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BE에 IPAM (IP Address Managements) 등 네트워크 관리 기능 개발&lt;/li&gt;
&lt;li&gt;OVN, OVS 등을 인프라 노드(컨트롤 플레인, 컴퓨트 플레인 등)에 설치해서 AWS VPC 같은 테넌트별 가상 네트워크 서비스를 개발함 (대충 아래 아키텍쳐)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2663&quot; data-origin-height=&quot;2853&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x5rTp/dJMcabjDeL1/DlJ7gL9lO6MDJgfwY3g1L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x5rTp/dJMcabjDeL1/DlJ7gL9lO6MDJgfwY3g1L1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x5rTp/dJMcabjDeL1/DlJ7gL9lO6MDJgfwY3g1L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx5rTp%2FdJMcabjDeL1%2FDlJ7gL9lO6MDJgfwY3g1L1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;857&quot; data-origin-width=&quot;2663&quot; data-origin-height=&quot;2853&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Enterprise 프로젝트&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1단계: Observability Node 구성 및 연동&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위에 Basic 프로젝트에서 구현한 내용들이 클라우드 플랫폼이라고 한다면, 해당 클라우드 내부 생태계에서 무슨일이 있어나는지를 관측하고 특정 패턴들을 탐지하는 기능도 필요할 거 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선은 아래처럼 아키텍쳐를 그려봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 실제 구성에선 좀 달라질 수도 있겠지만 흐름을 보면 별도의 노드에 카프카를 중심으로 이벤트를 쌓고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석이 필요한 각 영역 (보안, 데이터, 모니터링 등)에서 해당 데이터를 전달받아 사용하는 느낌으로&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카가 pubsub 구조이고, 토픽 단위 분리가 가능하기 때문에 아래 그림이 좋을 거 같았음.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1417&quot; data-origin-height=&quot;2059&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QJTm4/dJMcaaSx7GC/hLvtmHJAnyNYUwJRoCu4n0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QJTm4/dJMcaaSx7GC/hLvtmHJAnyNYUwJRoCu4n0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QJTm4/dJMcaaSx7GC/hLvtmHJAnyNYUwJRoCu4n0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQJTm4%2FdJMcaaSx7GC%2FhLvtmHJAnyNYUwJRoCu4n0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;1162&quot; data-origin-width=&quot;1417&quot; data-origin-height=&quot;2059&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2단계: AI Node 구성 및 연동&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;앞단이 데이터 파이프라인을 기반으로한 관측 노드였다면 이번엔 해당 데이터를 활용하는 내부 AI 서비스들을 올릴 GPU Node 들을 만들고 기존 시스템들과 연동하는 프로젝트임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 붙이면 내 클라우드에 특화된 AWS Bedrock, MS GraphRAG 같은 서비스들을 오픈소스들을 사용해서 구성할 수 있을 거 같았다. (물론 성능은 다르겠지만)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1866&quot; data-origin-height=&quot;2364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8RRcK/dJMb990qaZS/cRDhyxUs7X2K6YIEucJxIk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8RRcK/dJMb990qaZS/cRDhyxUs7X2K6YIEucJxIk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8RRcK/dJMb990qaZS/cRDhyxUs7X2K6YIEucJxIk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8RRcK%2FdJMb990qaZS%2FcRDhyxUs7X2K6YIEucJxIk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;2364&quot; data-origin-width=&quot;1866&quot; data-origin-height=&quot;2364&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>OpenCSP/Index</category>
      <author>miiml</author>
      <guid isPermaLink="true">https://miiml.tistory.com/19</guid>
      <comments>https://miiml.tistory.com/19#entry19comment</comments>
      <pubDate>Sun, 19 Apr 2026 17:58:34 +0900</pubDate>
    </item>
    <item>
      <title>Lago기반 Billing 파이프라인 구조 고민</title>
      <link>https://miiml.tistory.com/17</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 글: &lt;a href=&quot;https://miiml.tistory.com/7&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2026.03.20 - [프로젝트/OpenCSP] - k3s에 lago 올려보기&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1776583241386&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;k3s에 lago 올려보기&quot; data-og-description=&quot;2026.03.20 - [프로젝트] - OpenCSP (1) 프로젝트 내용 정리 OpenCSP (1) 프로젝트 내용 정리진행 중인 오픈소스 프로젝트 OpenCSP의 깃허브 문서가 영어로만 있어서 한글로도 정리해보고 싶어졌다. https://gith&quot; data-og-host=&quot;miiml.tistory.com&quot; data-og-source-url=&quot;https://miiml.tistory.com/7&quot; data-og-url=&quot;https://miiml.tistory.com/7&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/SNQfQ/dJMb8RRTMcO/38sXfq2swRqx7kwNouJb2K/img.png?width=800&amp;amp;height=289&amp;amp;face=0_0_800_289,https://scrap.kakaocdn.net/dn/l0yg5/dJMb8UHQ3qN/ofGNkkjSIk85JKi9VJdNPK/img.png?width=800&amp;amp;height=289&amp;amp;face=0_0_800_289,https://scrap.kakaocdn.net/dn/baaccH/dJMb85vQUeP/jqtfSZ85nWX3oZXIAtJhAk/img.png?width=2912&amp;amp;height=1620&amp;amp;face=0_0_2912_1620&quot;&gt;&lt;a href=&quot;https://miiml.tistory.com/7&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://miiml.tistory.com/7&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/SNQfQ/dJMb8RRTMcO/38sXfq2swRqx7kwNouJb2K/img.png?width=800&amp;amp;height=289&amp;amp;face=0_0_800_289,https://scrap.kakaocdn.net/dn/l0yg5/dJMb8UHQ3qN/ofGNkkjSIk85JKi9VJdNPK/img.png?width=800&amp;amp;height=289&amp;amp;face=0_0_800_289,https://scrap.kakaocdn.net/dn/baaccH/dJMb85vQUeP/jqtfSZ85nWX3oZXIAtJhAk/img.png?width=2912&amp;amp;height=1620&amp;amp;face=0_0_2912_1620');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;k3s에 lago 올려보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2026.03.20 - [프로젝트] - OpenCSP (1) 프로젝트 내용 정리 OpenCSP (1) 프로젝트 내용 정리진행 중인 오픈소스 프로젝트 OpenCSP의 깃허브 문서가 영어로만 있어서 한글로도 정리해보고 싶어졌다. https://gith&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;miiml.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 관련 글에서 lago를 Core에 배포했을 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 핵심 기능들이 유료로 막혀있어서 다른 빌링 도구를 찾아보고 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 찾아봐도 스택에 kafka 등을 필수로 요구하는 경우가 많아 k3s에 올리기 너무 무겁거나 필요한 billing 기능을 일부만 제공해서&amp;nbsp;적합한게 별로 없는거 같았음 (사실 이런 경우들도 고려해서 개발이 되어야하긴 하는데..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 좀 더 찾아보면서 Lago도 UI만 막혀있고 핵심 API 기능들은 문제 없다는 걸 알게됐다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 Console에서 별도의 빌링 페이지를 개발하고 데이터만 가져와주면 기존 설계대로 구성이 가능할 거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 생각해본건 비즈니스 로직에서 유료 이벤트가 발생하면 lago의 API Endpoint에 요청을 보내주고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌링 페이지에서 조회가 필요한 부분이 있으면 Lago Web의 API로 가져와서 보여주는 방식인데 (이게 지금 시점 MVP에 적용된 방식)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 이렇게 만들면 사용자가 늘어났을 때 불필요한 내부 트래픽이 많이 발생할 거 같기 때문에 (Fan-out Exprosion) 사용자 요청이 있으면 최초에만 불러와서 데이터를 캐싱해두고 이후는 그 데이터를 참조하는 구조를 생각해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청은 확실히 줄어들겠지만 여전히 뭔가 부족한 거 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 AI한테 질문해보니까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 트랜젝션(비즈니스 로직에서)이 진행될 때 Lago와 BE DB에 동일한 트렌젝션 ID로 데이터를 미러링해두고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자에게 보여주는 데이터는 BE DB(시계열 데이터)에서 실제 청구에 필요한 데이터는 lago에서 가져가는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 같은 transactional outbox 패턴을 권장한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;python&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;def process_business_logic():
    with db.transaction():
        do_business_work()                    # 비즈니스 데이터 변경
        my_db.insert_event(...)               # 미러 (같은 DB면 원자적)
        outbox.insert(lago_payload)           # 발송 대기열에 기록
    # 트랜잭션 커밋 후 워커가 비동기로 Lago에 전송&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 패턴의 장점은&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비즈니스 DB와 미러가 같은 트랜잭션에 있어서 항상 일치&lt;/li&gt;
&lt;li&gt;Lago 전송은 별도 워커가 outbox를 폴링해서 처리 &amp;rarr; 실패 시 재시도&lt;/li&gt;
&lt;li&gt;비즈니스 로직은 Lago 장애와 무관하게 진행&lt;/li&gt;
&lt;li&gt;레이턴시가 외부 호출에 묶이지 않음&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해주면 중복 트래픽 대부분이 내부 DB 참조로 바뀌니까 위에서 고민했던 내부 트래픽 문제가 많이 해결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(MSA 내부 트래픽이 BE - DB 트래픽 구간에 합쳐지면서 관리포인트가 1개로 합쳐짐)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 아래 기능들은 Lago가 담당하게 하고 (이미 다 지원해주니까)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Billable metric 정의와 집계 로직:&amp;nbsp; sum/count/max/unique, 필터, 프로레이션 등&lt;/li&gt;
&lt;li&gt;Plan / Charge 구조: 티어, 패키지, graduated, volume pricing&lt;/li&gt;
&lt;li&gt;구독 라이프사이클: 업그레이드, 다운그레이드, proration, trial&lt;/li&gt;
&lt;li&gt;Invoice 생성과 PDF&lt;/li&gt;
&lt;li&gt;결제 게이트웨이 orchestration (Stripe, Adyen 등): 국내 기준으론 PG 사들 연동이 필요한 부분이고 사업자 등록이 필요하기 때문에 프로젝트 설계에선 별도 연동으로 분리해뒀다.&lt;/li&gt;
&lt;li&gt;Credit / Wallet / Coupon&lt;/li&gt;
&lt;li&gt;Tax 계산 연동 (Avalara 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 기능들 중에 초기화 필요한 부분은 BE에 Seeder로 만들어두고, Metric 값들(갱신이 자주 발생하지 않는)도 캐싱을 해두거나 한다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BE에도 데이터가 있으니까 lago같은 빌링 서비스가 죽었을 때의 fallback도 어느정도 가능할거 같다. (물론 이러면 fallback이 완벽하지 않고 유실되는 부분도 있을거라 좀 더 고민이 필요)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lago 기본 제공 메트릭들은 아래 문서를 보면된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2194&quot; data-origin-height=&quot;1360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oXXcF/dJMcadBy1CR/bkvAQUtkhILGEsdKOqLu2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oXXcF/dJMcadBy1CR/bkvAQUtkhILGEsdKOqLu2k/img.png&quot; data-alt=&quot;https://getlago.com/docs/guide/billable-metrics/aggregation-types/overview&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oXXcF/dJMcadBy1CR/bkvAQUtkhILGEsdKOqLu2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoXXcF%2FdJMcadBy1CR%2FbkvAQUtkhILGEsdKOqLu2k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;372&quot; data-origin-width=&quot;2194&quot; data-origin-height=&quot;1360&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://getlago.com/docs/guide/billable-metrics/aggregation-types/overview&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 BE에서 등록한 메트릭들 예시&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2870&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coW0it/dJMcabqjKwp/Cn52ecNr8rKruAovCxT1L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coW0it/dJMcabqjKwp/Cn52ecNr8rKruAovCxT1L1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coW0it/dJMcabqjKwp/Cn52ecNr8rKruAovCxT1L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoW0it%2FdJMcabqjKwp%2FCn52ecNr8rKruAovCxT1L1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;654&quot; data-origin-width=&quot;2870&quot; data-origin-height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;simple-translate&quot; class=&quot;simple-translate-system-theme&quot;&gt;
&lt;div&gt;
&lt;div class=&quot;simple-translate-button &quot; style=&quot;background-image: url('moz-extension://71910560-c071-48c9-9147-a24f91ffb22e/icons/512.png'); height: 22px; width: 22px; top: 10px; left: 10px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;simple-translate-panel isShow&quot; style=&quot;width: 106px; height: 50px; top: 1391.4px; left: 635.133px; font-size: 13px;&quot;&gt;
&lt;div class=&quot;simple-translate-result-wrapper&quot; style=&quot;overflow: hidden;&quot;&gt;
&lt;div class=&quot;simple-translate-move&quot; draggable=&quot;true&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;simple-translate-result-contents&quot;&gt;
&lt;p class=&quot;simple-translate-result&quot; dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;(팬아웃 확장)&lt;/p&gt;
&lt;p class=&quot;simple-translate-candidate&quot; dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>OpenCSP/MVP 프로젝트</category>
      <author>miiml</author>
      <guid isPermaLink="true">https://miiml.tistory.com/17</guid>
      <comments>https://miiml.tistory.com/17#entry17comment</comments>
      <pubDate>Sun, 19 Apr 2026 17:49:18 +0900</pubDate>
    </item>
    <item>
      <title>Provisioning Flow</title>
      <link>https://miiml.tistory.com/18</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://miiml.tistory.com/10&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2026.03.23 - [프로젝트/OpenCSP] - API 요청으로 Terraform CR 생성하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://miiml.tistory.com/12&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2026.03.25 - [프로젝트/OpenCSP] - API 요청으로 PVE VM 생성하기&lt;/a&gt;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://miiml.tistory.com/13&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2026.03.26 - [프로젝트/OpenCSP] - OpenCSP Console로 VM 생성해보기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://miiml.tistory.com/14&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2026.03.28 - [프로젝트/OpenCSP] - Ansible Semaphore로 VM post-provisioning하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://miiml.tistory.com/16&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2026.04.08 - [프로젝트/OpenCSP] - Ansible Semaphore로 VM post-provisioning하기(2)&amp;nbsp;&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>OpenCSP/Index</category>
      <author>miiml</author>
      <guid isPermaLink="true">https://miiml.tistory.com/18</guid>
      <comments>https://miiml.tistory.com/18#entry18comment</comments>
      <pubDate>Mon, 13 Apr 2026 14:59:49 +0900</pubDate>
    </item>
    <item>
      <title>Ansible Semaphore로 VM post-provisioning하기(2)</title>
      <link>https://miiml.tistory.com/16</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://miiml.tistory.com/18&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2026.04.13 - [프로젝트/OpenCSP] - [OpenCSP] Index - Provisioning Flow&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1776060315652&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[OpenCSP] Index - Provisioning Flow&quot; data-og-description=&quot;2026.03.23 - [프로젝트/OpenCSP] - API 요청으로 Terraform CR 생성하기 2026.03.25 - [프로젝트/OpenCSP] - API 요청으로 PVE VM 생성하기 2026.03.26 - [프로젝트/OpenCSP] - OpenCSP Console로 VM 생성해보기 2026.03.28 - [프로젝&quot; data-og-host=&quot;miiml.tistory.com&quot; data-og-source-url=&quot;https://miiml.tistory.com/18&quot; data-og-url=&quot;https://miiml.tistory.com/18&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c0oX32/dJMb8Z3sy1M/wOKY97JMvkLdUl1SEu3Hx1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dz8PvP/dJMb8RRS9jI/UCQR8kikp9KH6jg0uFjO4k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://miiml.tistory.com/18&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://miiml.tistory.com/18&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c0oX32/dJMb8Z3sy1M/wOKY97JMvkLdUl1SEu3Hx1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dz8PvP/dJMb8RRS9jI/UCQR8kikp9KH6jg0uFjO4k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[OpenCSP] Index - Provisioning Flow&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2026.03.23 - [프로젝트/OpenCSP] - API 요청으로 Terraform CR 생성하기 2026.03.25 - [프로젝트/OpenCSP] - API 요청으로 PVE VM 생성하기 2026.03.26 - [프로젝트/OpenCSP] - OpenCSP Console로 VM 생성해보기 2026.03.28 - [프로젝&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;miiml.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://miiml.tistory.com/14&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;에서 semaphore UI를 사용해서 워크 플로우를 직접 테스트 해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;semaphore는 Template 단위로 작업을 정의하고, 초기든 이후 단계든 상관없이 run으로 프로비저닝할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 template를 생성하려면 레포지토리(ansible role), 인벤토리, key store(SSH 키) 그리고 Variable Group이 필요하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이중에 VM이 생성된 이후에 알 수 있는 값들과 공통으로 사용해야 하는 값들이 섞여 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Variable group과 repository -&amp;gt; 얘네는 공통의 모듈을 사용하고 VM에 주입해야 하는 값들도 지금은 텔레포트 Join token (Mvp 개발에선 단일 토큰을 재활용 하는 방식) 이라 초기에 한번에 생성해주면 되서 FE &amp;gt; admin &amp;gt; integrations 탭에 UI를 만들고 백엔드에 간단히 CRUD 할 수 있는 API Endpoint, Service, Domain, DTO 같은걸 만들어줬다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Key Store, Inventory, Template는 VM 생성 이후 시점(Terraform 프로비저닝 이후)에 알게된 IP나 노드 이름, VM ID 같은 걸로 순서대로 만들어야 된다. (Inventory 생성하려면 key Store가 지정되어야하고, Temeplate는 위에 말한대로 다 필요하기 때문)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;백엔드 작업 내용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 세마포어에 대한 인터페이스인데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 부분은 세마포어 1개에 대한게 아니라 post provisioning 해주는 여러 툴들을 활용해서 백엔드가 공통적으로 처리해야 하는 기능을 정의해줘야 한다. (파사드 패턴?)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 이부분(AWX, Ansible Tower, Semaphore, Ansible CLI 등)을 한 단어로 뭐라고 할지 모르겠어서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 수정하기로 하고 일단 개발했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776055605804&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package io.hlab.opencsp.infrastructure.semaphore;

import java.util.Map;

public interface SemaphoreClient {
    boolean isConfigured(); // 현재 레이어의 도구(semaphore 같은)가 설정되어 있는지 여부
    
    PostProvisionResult triggerPostProvisionJob(String crName, Map&amp;lt;String, String&amp;gt; outputs); // 프로비저닝 완료 후 post-provisioning Ansible job을 실행한다.
    
    void cleanupPostProvision(int sshKeyId, int templateId, int inventoryId, int environmentId); // 프로비저닝 삭제 시 Semaphore에 생성했던 리소스(sshKey, inventory, template)를 정리한다.

    TaskResult getTaskResult(int taskId);  // Semaphore Task의 실행 상태와 로그 출력을 조회한다.

    record PostProvisionResult(int sshKeyId, int inventoryId, int templateId, int taskId, int environmentId) {}

    record TaskResult(String status, boolean success, String output) {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 위 인터페이스의 구현체는 아래처럼 만들어 지고 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;길어서 접어 뒀는데 구조를 좀 요약하면 기본적으로 Semaphore 와 연동 방식은 WebClient를 활용해서 API를 사용하고&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 공통 옵션(레포지토리, variable group 등)은 configStore(이건 DB에 저장된 설정 값을 런타임에 조회하는 클래스로 백엔드 전역에서 여러 시스템과 연동을 위해 사용됨, admin integrations)로 관리하고 나머지는 필요한 정보를 각 API 양식에 맞게 채워서 요청하는 기능들이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1776056536356&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package io.hlab.opencsp.infrastructure.semaphore;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.hlab.opencsp.domain.config.ConfigCategory;
import io.hlab.opencsp.infrastructure.config.ConfigStore;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;

/**
 * Semaphore REST API v2 클라이언트.
 *
 * &amp;lt;h3&amp;gt;Semaphore Task 흐름 (프로비저닝 1건당)&amp;lt;/h3&amp;gt;
 * &amp;lt;ol&amp;gt;
 *   &amp;lt;li&amp;gt;POST /api/project/{id}/keys     &amp;rarr; sshKeyId (Terraform output의 ssh_private_key 사용, 없으면 정적 config)&amp;lt;/li&amp;gt;
 *   &amp;lt;li&amp;gt;POST /api/project/{id}/inventory &amp;rarr; inventoryId&amp;lt;/li&amp;gt;
 *   &amp;lt;li&amp;gt;POST /api/project/{id}/templates &amp;rarr; templateId (동적 생성)&amp;lt;/li&amp;gt;
 *   &amp;lt;li&amp;gt;POST /api/project/{id}/tasks     &amp;rarr; taskId&amp;lt;/li&amp;gt;
 * &amp;lt;/ol&amp;gt;
 *
 * &amp;lt;h3&amp;gt;정리 (destroy 시)&amp;lt;/h3&amp;gt;
 * &amp;lt;ol&amp;gt;
 *   &amp;lt;li&amp;gt;DELETE /api/project/{id}/templates/{templateId}&amp;lt;/li&amp;gt;
 *   &amp;lt;li&amp;gt;DELETE /api/project/{id}/inventory/{inventoryId}&amp;lt;/li&amp;gt;
 *   &amp;lt;li&amp;gt;DELETE /api/project/{id}/keys/{sshKeyId} (동적 생성된 경우만)&amp;lt;/li&amp;gt;
 * &amp;lt;/ol&amp;gt;
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class SemaphoreHttpClient implements SemaphoreClient {

    private final ConfigStore configStore;
    private final ObjectMapper objectMapper;

    // ──────────────────────────────────────────────────────────────────────────
    // SemaphoreClient 구현
    // ──────────────────────────────────────────────────────────────────────────

    @Override
    public boolean isConfigured() {
        return configStore.get(ConfigCategory.SEMAPHORE, &quot;semaphore.url&quot;)
                .filter(v -&amp;gt; !v.isBlank())
                .isPresent();
    }

    @Override
    public PostProvisionResult triggerPostProvisionJob(String crName, Map&amp;lt;String, String&amp;gt; outputs) {
        String baseUrl     = requireUrl();
        int    projectId   = requireInt(&quot;semaphore.project.id&quot;);
        int    repositoryId = requireInt(&quot;semaphore.repository.id&quot;);
        String playbook    = require(&quot;semaphore.playbook&quot;);
        String token       = require(&quot;semaphore.api.token&quot;);

        WebClient wc = buildWebClient(baseUrl, token);

        // 1. SSH 키 &amp;mdash; BE가 인스턴스 생성 시 생성한 private key를 outputs에서 읽어 Semaphore에 등록
        String privateKey = outputs.getOrDefault(&quot;vm_ssh_private_key&quot;,
                            outputs.getOrDefault(&quot;ssh_private_key&quot;, null));
        if (privateKey == null || privateKey.isBlank()) {
            throw new IllegalStateException(&quot;outputs에 SSH private key(vm_ssh_private_key)가 없습니다: crName=&quot; + crName);
        }
        int sshKeyId = createSshKey(wc, projectId, crName, privateKey);
        log.atInfo()
                // .addKeyValue(&quot;cr_name&quot;, crName)
                .addKeyValue(&quot;semaphore_ssh_key_id&quot;, sshKeyId)
                .log(&quot;SSH 키 등록&quot;);

        // 2. 인벤토리 생성
        String inventoryContent = buildInventory(crName, outputs);
        int inventoryId = createInventory(wc, projectId, crName, inventoryContent, sshKeyId);
        log.atInfo()
                // .addKeyValue(&quot;cr_name&quot;, crName)
                .addKeyValue(&quot;semaphore_inventory_id&quot;, inventoryId)
                .log(&quot;인벤토리 생성&quot;);

        // 3. 환경 &amp;mdash; 정적 config(semaphore.environment.id) 우선, 없으면 동적 생성
        int envId;
        boolean dynamicEnv;
        Optional&amp;lt;Integer&amp;gt; staticEnvId = optionalInt(&quot;semaphore.environment.id&quot;);
        if (staticEnvId.isPresent()) {
            envId      = staticEnvId.get();
            dynamicEnv = false;
            log.atInfo()
                    // .addKeyValue(&quot;cr_name&quot;, crName)
                    .addKeyValue(&quot;semaphore_env_id&quot;, envId)
                    .addKeyValue(&quot;env_source&quot;, &quot;static&quot;)
                    .log(&quot;환경 사용&quot;);
        } else {
            envId      = createEnvironment(wc, projectId, crName);
            dynamicEnv = true;
            log.atInfo()
                    // .addKeyValue(&quot;cr_name&quot;, crName)
                    .addKeyValue(&quot;semaphore_env_id&quot;, envId)
                    .addKeyValue(&quot;env_source&quot;, &quot;dynamic&quot;)
                    .log(&quot;환경 생성&quot;);
        }

        // 4. 템플릿 동적 생성
        int templateId = createTemplate(wc, projectId, crName, repositoryId, playbook, sshKeyId, inventoryId, envId);
        log.atInfo()
                // .addKeyValue(&quot;cr_name&quot;, crName)
                .addKeyValue(&quot;semaphore_template_id&quot;, templateId)
                .log(&quot;템플릿 생성&quot;);

        // 5. Task 실행
        int taskId = runTask(wc, projectId, templateId, inventoryId, crName);
        log.atInfo()
                // .addKeyValue(&quot;cr_name&quot;, crName)
                .addKeyValue(&quot;semaphore_task_id&quot;, taskId)
                .log(&quot;Task 실행&quot;);

        return new PostProvisionResult(sshKeyId, inventoryId, templateId, taskId, envId);
    }

    @Override
    public TaskResult getTaskResult(int taskId) {
        String baseUrl   = requireUrl();
        int    projectId = requireInt(&quot;semaphore.project.id&quot;);
        String token     = require(&quot;semaphore.api.token&quot;);
        WebClient wc     = buildWebClient(baseUrl, token);

        try {
            // 태스크 상태 조회
            String taskJson = wc.get()
                    .uri(&quot;/api/project/{pid}/tasks/{tid}&quot;, projectId, taskId)
                    .retrieve().bodyToMono(String.class).block();
            JsonNode task   = objectMapper.readTree(taskJson);
            String status   = task.path(&quot;status&quot;).asText(&quot;unknown&quot;);
            boolean success = &quot;success&quot;.equals(status);

            // 태스크 출력 조회
            String outputJson = wc.get()
                    .uri(&quot;/api/project/{pid}/tasks/{tid}/output&quot;, projectId, taskId)
                    .retrieve().bodyToMono(String.class).block();
            StringBuilder sb = new StringBuilder();
            for (JsonNode line : objectMapper.readTree(outputJson)) {
                sb.append(line.path(&quot;output&quot;).asText()).append(&quot;\n&quot;);
            }

            return new TaskResult(status, success, sb.toString().stripTrailing());
        } catch (WebClientResponseException e) {
            log.atWarn()
                    .addKeyValue(&quot;semaphore_task_id&quot;, taskId)
                    .addKeyValue(&quot;http_status&quot;, e.getStatusCode().value())
                    .setCause(e)
                    .log(&quot;Task 결과 조회 실패&quot;);
            return new TaskResult(&quot;error&quot;, false, &quot;HTTP &quot; + e.getStatusCode().value());
        } catch (Exception e) {
            log.atWarn()
                    .addKeyValue(&quot;semaphore_task_id&quot;, taskId)
                    .addKeyValue(&quot;error&quot;, e.getMessage())
                    .setCause(e)
                    .log(&quot;Task 결과 조회 실패&quot;);
            return new TaskResult(&quot;error&quot;, false, e.getMessage());
        }
    }

    @Override
    public void cleanupPostProvision(int sshKeyId, int templateId, int inventoryId, int environmentId) {
        String baseUrl   = requireUrl();
        int    projectId = requireInt(&quot;semaphore.project.id&quot;);
        String token     = require(&quot;semaphore.api.token&quot;);
        WebClient wc     = buildWebClient(baseUrl, token);

        deleteTemplate(wc, projectId, templateId);
        deleteInventory(wc, projectId, inventoryId);
        if (sshKeyId &amp;gt; 0) {
            deleteSshKey(wc, projectId, sshKeyId);
        }
        // 정적 config로 지정된 공유 환경은 삭제하지 않음
        boolean isStaticEnv = optionalInt(&quot;semaphore.environment.id&quot;)
                .filter(id -&amp;gt; id == environmentId)
                .isPresent();
        if (environmentId &amp;gt; 0 &amp;amp;&amp;amp; !isStaticEnv) {
            deleteEnvironment(wc, projectId, environmentId);
        }
    }

    // ──────────────────────────────────────────────────────────────────────────
    // Ansible 인벤토리 빌더
    // ──────────────────────────────────────────────────────────────────────────

    String buildInventory(String crName, Map&amp;lt;String, String&amp;gt; outputs) {
        String hostname = outputs.getOrDefault(&quot;vm_hostname&quot;,
                          outputs.getOrDefault(&quot;vm_name&quot;, crName));
        String ip       = outputs.getOrDefault(&quot;vm_ip&quot;,
                          outputs.getOrDefault(&quot;ip_address&quot;, &quot;&quot;));
        String user     = outputs.getOrDefault(&quot;ansible_user&quot;,
                          outputs.getOrDefault(&quot;vm_user&quot;, &quot;ubuntu&quot;));

        StringBuilder sb = new StringBuilder(&quot;[test_vms]\n&quot;);
        sb.append(hostname);
        if (!ip.isBlank()) sb.append(&quot; ansible_host=&quot;).append(ip);
        sb.append(&quot; ansible_user=&quot;).append(user);
        sb.append(&quot;\n\n[test_vms:vars]\n&quot;);
        sb.append(&quot;opencsp_cr_name=&quot;).append(crName).append(&quot;\n&quot;);
        if (!ip.isBlank())       sb.append(&quot;opencsp_vm_ip=&quot;).append(ip).append(&quot;\n&quot;);
        if (!hostname.isBlank()) sb.append(&quot;opencsp_vm_hostname=&quot;).append(hostname).append(&quot;\n&quot;);
        if (!hostname.isBlank()) sb.append(&quot;node_name=&quot;).append(hostname).append(&quot;\n&quot;);
        return sb.toString();
    }

    // ──────────────────────────────────────────────────────────────────────────
    // Semaphore API 호출
    // ──────────────────────────────────────────────────────────────────────────

    private int createSshKey(WebClient wc, int projectId, String crName, String privateKey) {
        Map&amp;lt;String, Object&amp;gt; ssh = new LinkedHashMap&amp;lt;&amp;gt;();
        ssh.put(&quot;private_key&quot;, privateKey);

        Map&amp;lt;String, Object&amp;gt; body = new LinkedHashMap&amp;lt;&amp;gt;();
        body.put(&quot;name&quot;,       &quot;opencsp-&quot; + crName);
        body.put(&quot;project_id&quot;, projectId);
        body.put(&quot;type&quot;,       &quot;ssh&quot;);
        body.put(&quot;ssh&quot;,        ssh);

        try {
            String response = wc.post()
                    .uri(&quot;/api/project/{id}/keys&quot;, projectId)
                    .bodyValue(body)
                    .retrieve()
                    .bodyToMono(String.class)
                    .block();

            JsonNode root = objectMapper.readTree(response);
            return root.path(&quot;id&quot;).asInt();
        } catch (WebClientResponseException e) {
            log.atError()
                    .addKeyValue(&quot;http_status&quot;, e.getStatusCode().value())
                    .addKeyValue(&quot;response_body&quot;, e.getResponseBodyAsString())
                    .setCause(e)
                    .log(&quot;SSH 키 등록 실패&quot;);
            throw new IllegalStateException(&quot;Semaphore SSH 키 등록 실패: &quot; + e.getMessage(), e);
        } catch (Exception e) {
            throw new IllegalStateException(&quot;Semaphore SSH 키 등록 실패&quot;, e);
        }
    }

    private int createInventory(WebClient wc, int projectId, String crName,
                                String inventoryContent, int sshKeyId) {
        Map&amp;lt;String, Object&amp;gt; body = new LinkedHashMap&amp;lt;&amp;gt;();
        body.put(&quot;name&quot;,       &quot;opencsp-&quot; + crName);
        body.put(&quot;project_id&quot;, projectId);
        body.put(&quot;inventory&quot;,  inventoryContent);
        body.put(&quot;ssh_key_id&quot;, sshKeyId);
        body.put(&quot;type&quot;,       &quot;static&quot;);

        try {
            String response = wc.post()
                    .uri(&quot;/api/project/{id}/inventory&quot;, projectId)
                    .bodyValue(body)
                    .retrieve()
                    .bodyToMono(String.class)
                    .block();

            JsonNode root = objectMapper.readTree(response);
            return root.path(&quot;id&quot;).asInt();
        } catch (WebClientResponseException e) {
            log.atError()
                    .addKeyValue(&quot;http_status&quot;, e.getStatusCode().value())
                    .addKeyValue(&quot;response_body&quot;, e.getResponseBodyAsString())
                    .setCause(e)
                    .log(&quot;인벤토리 생성 실패&quot;);
            throw new IllegalStateException(&quot;Semaphore 인벤토리 생성 실패: &quot; + e.getMessage(), e);
        } catch (Exception e) {
            throw new IllegalStateException(&quot;Semaphore 인벤토리 생성 실패&quot;, e);
        }
    }

    private int createTemplate(WebClient wc, int projectId, String crName,
                               int repositoryId, String playbook, int sshKeyId, int inventoryId,
                               int environmentId) {
        Map&amp;lt;String, Object&amp;gt; body = new LinkedHashMap&amp;lt;&amp;gt;();
        body.put(&quot;project_id&quot;,    projectId);
        body.put(&quot;name&quot;,          &quot;opencsp-&quot; + crName);
        body.put(&quot;app&quot;,           &quot;ansible&quot;);
        body.put(&quot;playbook&quot;,      playbook);
        body.put(&quot;repository_id&quot;, repositoryId);
        body.put(&quot;inventory_id&quot;,  inventoryId);
        body.put(&quot;ssh_key_id&quot;,    sshKeyId);
        body.put(&quot;environment_id&quot;, environmentId);
        body.put(&quot;type&quot;,          &quot;&quot;);

        try {
            String response = wc.post()
                    .uri(&quot;/api/project/{id}/templates&quot;, projectId)
                    .bodyValue(body)
                    .retrieve()
                    .bodyToMono(String.class)
                    .block();

            JsonNode root = objectMapper.readTree(response);
            return root.path(&quot;id&quot;).asInt();
        } catch (WebClientResponseException e) {
            log.atError()
                    .addKeyValue(&quot;http_status&quot;, e.getStatusCode().value())
                    .addKeyValue(&quot;response_body&quot;, e.getResponseBodyAsString())
                    .setCause(e)
                    .log(&quot;템플릿 생성 실패&quot;);
            throw new IllegalStateException(&quot;Semaphore 템플릿 생성 실패: &quot; + e.getMessage(), e);
        } catch (Exception e) {
            throw new IllegalStateException(&quot;Semaphore 템플릿 생성 실패&quot;, e);
        }
    }

    private int runTask(WebClient wc, int projectId, int templateId,
                        int inventoryId, String crName) {
        Map&amp;lt;String, Object&amp;gt; body = new LinkedHashMap&amp;lt;&amp;gt;();
        body.put(&quot;template_id&quot;,  templateId);
        body.put(&quot;inventory_id&quot;, inventoryId);
        body.put(&quot;message&quot;,      &quot;OpenCSP post-provision: &quot; + crName);
        body.put(&quot;debug&quot;,        false);
        body.put(&quot;dry_run&quot;,      false);
        body.put(&quot;diff&quot;,         false);

        try {
            String response = wc.post()
                    .uri(&quot;/api/project/{id}/tasks&quot;, projectId)
                    .bodyValue(body)
                    .retrieve()
                    .bodyToMono(String.class)
                    .block();

            JsonNode root = objectMapper.readTree(response);
            return root.path(&quot;id&quot;).asInt();
        } catch (WebClientResponseException e) {
            log.atError()
                    .addKeyValue(&quot;http_status&quot;, e.getStatusCode().value())
                    .addKeyValue(&quot;response_body&quot;, e.getResponseBodyAsString())
                    .setCause(e)
                    .log(&quot;Task 실행 실패&quot;);
            throw new IllegalStateException(&quot;Semaphore Task 실행 실패: &quot; + e.getMessage(), e);
        } catch (Exception e) {
            throw new IllegalStateException(&quot;Semaphore Task 실행 실패&quot;, e);
        }
    }

    private void deleteTemplate(WebClient wc, int projectId, int templateId) {
        try {
            wc.delete()
                    .uri(&quot;/api/project/{projectId}/templates/{templateId}&quot;, projectId, templateId)
                    .retrieve()
                    .bodyToMono(Void.class)
                    .block();
            log.atInfo()
                    .addKeyValue(&quot;semaphore_template_id&quot;, templateId)
                    .log(&quot;템플릿 삭제&quot;);
        } catch (WebClientResponseException e) {
            if (e.getStatusCode().value() == 404) {
                log.atDebug()
                        .addKeyValue(&quot;semaphore_template_id&quot;, templateId)
                        .log(&quot;템플릿 이미 없음 (정상)&quot;);
                return;
            }
            log.atWarn()
                    .addKeyValue(&quot;semaphore_template_id&quot;, templateId)
                    .addKeyValue(&quot;http_status&quot;, e.getStatusCode().value())
                    .setCause(e)
                    .log(&quot;템플릿 삭제 실패&quot;);
        }
    }

    private void deleteInventory(WebClient wc, int projectId, int inventoryId) {
        try {
            wc.delete()
                    .uri(&quot;/api/project/{projectId}/inventory/{inventoryId}&quot;, projectId, inventoryId)
                    .retrieve()
                    .bodyToMono(Void.class)
                    .block();
            log.atInfo()
                    .addKeyValue(&quot;semaphore_inventory_id&quot;, inventoryId)
                    .log(&quot;인벤토리 삭제&quot;);
        } catch (WebClientResponseException e) {
            if (e.getStatusCode().value() == 404) {
                log.atDebug()
                        .addKeyValue(&quot;semaphore_inventory_id&quot;, inventoryId)
                        .log(&quot;인벤토리 이미 없음 (정상)&quot;);
                return;
            }
            log.atWarn()
                    .addKeyValue(&quot;semaphore_inventory_id&quot;, inventoryId)
                    .addKeyValue(&quot;http_status&quot;, e.getStatusCode().value())
                    .setCause(e)
                    .log(&quot;인벤토리 삭제 실패&quot;);
        }
    }

    private void deleteSshKey(WebClient wc, int projectId, int sshKeyId) {
        try {
            wc.delete()
                    .uri(&quot;/api/project/{projectId}/keys/{sshKeyId}&quot;, projectId, sshKeyId)
                    .retrieve()
                    .bodyToMono(Void.class)
                    .block();
            log.atInfo()
                    .addKeyValue(&quot;semaphore_ssh_key_id&quot;, sshKeyId)
                    .log(&quot;SSH 키 삭제&quot;);
        } catch (WebClientResponseException e) {
            if (e.getStatusCode().value() == 404) {
                log.atDebug()
                        .addKeyValue(&quot;semaphore_ssh_key_id&quot;, sshKeyId)
                        .log(&quot;SSH 키 이미 없음 (정상)&quot;);
                return;
            }
            log.atWarn()
                    .addKeyValue(&quot;semaphore_ssh_key_id&quot;, sshKeyId)
                    .addKeyValue(&quot;http_status&quot;, e.getStatusCode().value())
                    .setCause(e)
                    .log(&quot;SSH 키 삭제 실패&quot;);
        }
    }

    private int createEnvironment(WebClient wc, int projectId, String crName) {
        Map&amp;lt;String, Object&amp;gt; body = new LinkedHashMap&amp;lt;&amp;gt;();
        body.put(&quot;name&quot;,       &quot;opencsp-&quot; + crName);
        body.put(&quot;project_id&quot;, projectId);
        body.put(&quot;json&quot;,       &quot;{}&quot;);
        body.put(&quot;env&quot;,        null);

        try {
            String response = wc.post()
                    .uri(&quot;/api/project/{id}/environment&quot;, projectId)
                    .bodyValue(body)
                    .retrieve()
                    .bodyToMono(String.class)
                    .block();

            JsonNode root = objectMapper.readTree(response);
            return root.path(&quot;id&quot;).asInt();
        } catch (WebClientResponseException e) {
            log.atError()
                    .addKeyValue(&quot;http_status&quot;, e.getStatusCode().value())
                    .addKeyValue(&quot;response_body&quot;, e.getResponseBodyAsString())
                    .setCause(e)
                    .log(&quot;환경 생성 실패&quot;);
            throw new IllegalStateException(&quot;Semaphore 환경 생성 실패: &quot; + e.getMessage(), e);
        } catch (Exception e) {
            throw new IllegalStateException(&quot;Semaphore 환경 생성 실패&quot;, e);
        }
    }

    private void deleteEnvironment(WebClient wc, int projectId, int environmentId) {
        try {
            wc.delete()
                    .uri(&quot;/api/project/{projectId}/environment/{environmentId}&quot;, projectId, environmentId)
                    .retrieve()
                    .bodyToMono(Void.class)
                    .block();
            log.atInfo()
                    .addKeyValue(&quot;semaphore_env_id&quot;, environmentId)
                    .log(&quot;환경 삭제&quot;);
        } catch (WebClientResponseException e) {
            if (e.getStatusCode().value() == 404) {
                log.atDebug()
                        .addKeyValue(&quot;semaphore_env_id&quot;, environmentId)
                        .log(&quot;환경 이미 없음 (정상)&quot;);
                return;
            }
            log.atWarn()
                    .addKeyValue(&quot;semaphore_env_id&quot;, environmentId)
                    .addKeyValue(&quot;http_status&quot;, e.getStatusCode().value())
                    .setCause(e)
                    .log(&quot;환경 삭제 실패&quot;);
        }
    }

    private String requireUrl() {
        return configStore.get(ConfigCategory.SEMAPHORE, &quot;semaphore.url&quot;)
                .orElseThrow(() -&amp;gt; new IllegalStateException(&quot;semaphore.url 설정이 없습니다.&quot;));
    }

    private String require(String key) {
        String value = configStore.get(ConfigCategory.SEMAPHORE, key)
                .orElseThrow(() -&amp;gt; new IllegalStateException(&quot;Semaphore 설정 누락: &quot; + key));
        if (value == null || value.isBlank()) {
            throw new IllegalStateException(&quot;Semaphore 설정값이 비어있음: &quot; + key);
        }
        return value;
    }

    private Optional&amp;lt;Integer&amp;gt; optionalInt(String key) {
        return configStore.get(ConfigCategory.SEMAPHORE, key)
                .filter(v -&amp;gt; !v.isBlank())
                .map(v -&amp;gt; {
                    try { return Integer.parseInt(v.trim()); }
                    catch (NumberFormatException e) { return null; }
                });
    }

    private int requireInt(String key) {
        String value = require(key);
        try {
            return Integer.parseInt(value.trim());
        } catch (NumberFormatException e) {
            throw new IllegalStateException(&quot;Semaphore 설정값이 정수가 아님: &quot; + key + &quot;=&quot; + value, e);
        }
    }

    private WebClient buildWebClient(String baseUrl, String token) {
        return WebClient.builder()
                .baseUrl(baseUrl)
                .defaultHeader(&quot;Authorization&quot;, &quot;Bearer &quot; + token)
                .defaultHeader(&quot;Content-Type&quot;, &quot;application/json&quot;)
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스에서 정의한 triggerPostProvisionJob 구현하려면 동적으로 생성해야하는 key store, inventory, template들을 순차적으로 실행해줘야 했고 각 작업을 API 요청 단위로 정리했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 고민했었던 부분은 SSH 키 생성을 누가 언제할 거고 어떻게 주입할건지에 대한 부분과 트렌젝션 관리에 대한 부분인데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 SSH 키 생성을 Terraform에서 담당하도록 모듈을 수정했지만 tofu-controller에서 생성한 결과 키를 semaphore나 BE로 전달할 방법이 없었다. (tofu-controller가 output을 secret으로 관리하는데 private key는 생성이 안됨)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 BE가 해당 부분을 담당하게 설계 수정해서 해결했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 BE에서 teleport 랑 SSH 릴레이를 구현해보려고 JSch 의존성을 추가했었는데 (근데 SSL Handshake 중 실패해서 tsh로 변경)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 사용해서 ssh 키를 생성했고 위에 구현체에 주입해주는 방식으로 바꿨다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프론트엔드 작업 내용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트에선 매니저가 integrations 를 UI에서 할 수 있도록 아래처럼 섹션을 추가했는데 개선이 좀 필요할 거 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2386&quot; data-origin-height=&quot;1590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIi0iV/dJMcagSGd6d/lu7uwoNjhnPWr8Svf2iFp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIi0iV/dJMcagSGd6d/lu7uwoNjhnPWr8Svf2iFp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIi0iV/dJMcagSGd6d/lu7uwoNjhnPWr8Svf2iFp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIi0iV%2FdJMcagSGd6d%2Flu7uwoNjhnPWr8Svf2iFp0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;533&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2386&quot; data-origin-height=&quot;1590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 아래처럼 인스턴스에 대한 Ansible 결과를 볼 수 있게 컬럼도 추가했는데 이부분도 상태 코드로 수정해서 알려주는 방식으로 변경해야 될 거 같다. (프로젝트 설계 철학? 때문에)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2342&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q6pm6/dJMcagSGebS/rLpbjtzngXdS70jrRHulK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q6pm6/dJMcagSGebS/rLpbjtzngXdS70jrRHulK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q6pm6/dJMcagSGebS/rLpbjtzngXdS70jrRHulK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq6pm6%2FdJMcagSGebS%2FrLpbjtzngXdS70jrRHulK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;520&quot; data-origin-width=&quot;2342&quot; data-origin-height=&quot;520&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 사용자가 웹에서 프로비저닝을 요청하면 트랜젝션이 생성되고 그 과정 중에 위에 구현체를 사용해서 요청을 보내면 아래처럼 생성이 잘되는걸 볼 수 있고, 앤서블이 동작하면서 텔레포트에 노드 등록이 되는 것까지 확인했으니까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로비저닝 플로우 개발이 완료됐다고 할 수 있을 거 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2936&quot; data-origin-height=&quot;664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0mXEK/dJMcaaroGBu/QFXHQDsnKZu2KwXk2kiB0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0mXEK/dJMcaaroGBu/QFXHQDsnKZu2KwXk2kiB0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0mXEK/dJMcaaroGBu/QFXHQDsnKZu2KwXk2kiB0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0mXEK%2FdJMcaaroGBu%2FQFXHQDsnKZu2KwXk2kiB0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;664&quot; data-origin-width=&quot;2936&quot; data-origin-height=&quot;664&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 고민해볼만한건&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜젝션의 관리와 롤백을 어떻게 개선할 지 (지금은 백엔드가 해주고 있지만 중간 실패 단계에서 알림만 해주고 자동으로 롤백이나 취소처리해주지 않음),&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영중인 상태에서 BE가 죽거나 DB가 통째로 증발(?)하면 core의 각 마이크로 서비스들에 있는 파편화된 정보들을 BE가 어떻게 다시 제어할 수 있을 지&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 부분들인데 어느 정도 방향성은 있긴하지만 MVP 개발에선 신경안쓰려고 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 시간되면 하나씩 포스팅해보겠음&lt;/p&gt;</description>
      <category>OpenCSP/MVP 프로젝트</category>
      <author>miiml</author>
      <guid isPermaLink="true">https://miiml.tistory.com/16</guid>
      <comments>https://miiml.tistory.com/16#entry16comment</comments>
      <pubDate>Mon, 13 Apr 2026 14:50:34 +0900</pubDate>
    </item>
    <item>
      <title>Ansible Semaphore로 VM post-provisioning하기</title>
      <link>https://miiml.tistory.com/14</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://miiml.tistory.com/18&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2026.04.13 - [프로젝트/OpenCSP] - [OpenCSP] Index - Provisioning Flow&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1776060362953&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[OpenCSP] Index - Provisioning Flow&quot; data-og-description=&quot;2026.03.23 - [프로젝트/OpenCSP] - API 요청으로 Terraform CR 생성하기 2026.03.25 - [프로젝트/OpenCSP] - API 요청으로 PVE VM 생성하기 2026.03.26 - [프로젝트/OpenCSP] - OpenCSP Console로 VM 생성해보기 2026.03.28 - [프로젝&quot; data-og-host=&quot;miiml.tistory.com&quot; data-og-source-url=&quot;https://miiml.tistory.com/18&quot; data-og-url=&quot;https://miiml.tistory.com/18&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c0oX32/dJMb8Z3sy1M/wOKY97JMvkLdUl1SEu3Hx1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dz8PvP/dJMb8RRS9jI/UCQR8kikp9KH6jg0uFjO4k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://miiml.tistory.com/18&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://miiml.tistory.com/18&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c0oX32/dJMb8Z3sy1M/wOKY97JMvkLdUl1SEu3Hx1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dz8PvP/dJMb8RRS9jI/UCQR8kikp9KH6jg0uFjO4k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[OpenCSP] Index - Provisioning Flow&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2026.03.23 - [프로젝트/OpenCSP] - API 요청으로 Terraform CR 생성하기 2026.03.25 - [프로젝트/OpenCSP] - API 요청으로 PVE VM 생성하기 2026.03.26 - [프로젝트/OpenCSP] - OpenCSP Console로 VM 생성해보기 2026.03.28 - [프로젝&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;miiml.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://miiml.tistory.com/13&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;까지가 Terraform으로 리소스를 생성하는 거였다면 여기부턴 생성 이후 프로비저닝을 Ansible로 자동화하는 로직을 구현하는 단계다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ansible은 SSH와 CLI 기반으로 동일한 스크립트를 여러 서버에 한번에 적용하거나 구성을 관리하는 IT 자동화 도구(RedHat 꺼) 이고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Semaphore는 Ansible을 웹 UI로 편하게 관리할 수 있는 툴이다. (비슷한 포지션 도구로는 Ansible Tower(유료), AWX 등이 있음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 점은 반응형 웹 UI와 별도의 API를 지원한다는 점이고 다른 툴에 비해 가볍기 때문에 Core에 넣기 좋아보였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Semaphore도 Flux가 구성을 관리해주는데 얘는 별도의 CR을 제공하지 않고 API로 기능을 다 제어한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 이 툴의 역할은 Post-provisioning인데 (테라폼으로 리소스 생성이 끝난 후에 Agent 및 기본 패키지 설치/업데이트, 보안 스크립트 실행 등을 할 예정)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 이미지의 박스 부분을 담당하기 위해 구성했다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1718&quot; data-origin-height=&quot;972&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RdbTA/dJMcagkCYH2/oK10dcAIAypgSgFyfFf5jk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RdbTA/dJMcagkCYH2/oK10dcAIAypgSgFyfFf5jk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RdbTA/dJMcagkCYH2/oK10dcAIAypgSgFyfFf5jk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRdbTA%2FdJMcagkCYH2%2FoK10dcAIAypgSgFyfFf5jk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;453&quot; data-origin-width=&quot;1718&quot; data-origin-height=&quot;972&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 당장할 건 Teleport Agent 설치와 기본 설정 변경 정도고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얘네들은 어느정도 모듈화되어서 OpenCSP-modules/ansible에 올라가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 하나씩 정리해 보겠음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;계정 생성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 관리자 계정 생성을 바로 못해서 (방법이 있을 수도 있긴한데 모르겠음) 아래 명령어로 생성해줬다.&lt;/p&gt;
&lt;pre id=&quot;code_1774624228273&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl exec -it $(kubectl get pod -n semaphore -l app.kubernetes.io/name=semaphore -o name) -n semaphore -- semaphore user add --admin \
--name &quot;Admin&quot; --login &quot;admin&quot; --email &quot;admin@domain.com&quot; --password 'PASSWORD'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호에 특수기호가 들어가면 '' 로 묶어주면 되고 잘못 입력했으면 아래 명령어로 수정 가능&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1774625200102&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl exec -it $(kubectl get pod -n semaphore -l app.kubernetes.io/name=semaphore -o name) -n semaphore -- semaphore user change-by-login \
--login &quot;admin&quot; \
--password 'NEW_PASSWORD'&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인하면 아래처럼 UI가 보인다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2926&quot; data-origin-height=&quot;1522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xEHPE/dJMcacbtbMI/ddzdTS1VtzAVV3RitGRfSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xEHPE/dJMcacbtbMI/ddzdTS1VtzAVV3RitGRfSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xEHPE/dJMcacbtbMI/ddzdTS1VtzAVV3RitGRfSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxEHPE%2FdJMcacbtbMI%2FddzdTS1VtzAVV3RitGRfSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;416&quot; data-origin-width=&quot;2926&quot; data-origin-height=&quot;1522&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;semaphore로 vm에 teleport agent 설치하기 (modules 사용)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 console로 vm 만들어주고&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2362&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uuYj2/dJMcaarhIoq/KpC1J317WpysT95yRFdhFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uuYj2/dJMcaarhIoq/KpC1J317WpysT95yRFdhFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uuYj2/dJMcaarhIoq/KpC1J317WpysT95yRFdhFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuuYj2%2FdJMcaarhIoq%2FKpC1J317WpysT95yRFdhFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;165&quot; data-origin-width=&quot;2362&quot; data-origin-height=&quot;486&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;semaphore로 와서 git repository를 등록해줌.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ansible 실행하는 주체가 local에서 semaphore가 된거라 requirements와 site.yaml이 있는 core를 등록해주면 된다. (public이라 key는 필요없지만 private이면 github 키도 별도 등록과 연결 필요)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2908&quot; data-origin-height=&quot;1530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bs6EmA/dJMcadnXRzO/D7PeKfNPwnfSdlDrlXlOSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bs6EmA/dJMcadnXRzO/D7PeKfNPwnfSdlDrlXlOSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bs6EmA/dJMcadnXRzO/D7PeKfNPwnfSdlDrlXlOSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbs6EmA%2FdJMcadnXRzO%2FD7PeKfNPwnfSdlDrlXlOSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;421&quot; data-origin-width=&quot;2908&quot; data-origin-height=&quot;1530&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vm 접근할 ssh 키도 생성해준다. (pub 키는 해당 VM의 ~/.ssh/authorized 에 추가)&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2916&quot; data-origin-height=&quot;1528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhUUfw/dJMcabReTc4/jNJJLvUjXsZW2YVlPTpiO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhUUfw/dJMcabReTc4/jNJJLvUjXsZW2YVlPTpiO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhUUfw/dJMcabReTc4/jNJJLvUjXsZW2YVlPTpiO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhUUfw%2FdJMcabReTc4%2FjNJJLvUjXsZW2YVlPTpiO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;419&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2916&quot; data-origin-height=&quot;1528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인벤토리를 만들면서 위에서 생성한 키 연결해주고 node_name은 텔레포트에 등록해줄 이름을 담은 변수임&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2920&quot; data-origin-height=&quot;1540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A9TrH/dJMcajaDMaZ/HMoE8ryeacYhKBegr4sZjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A9TrH/dJMcajaDMaZ/HMoE8ryeacYhKBegr4sZjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A9TrH/dJMcajaDMaZ/HMoE8ryeacYhKBegr4sZjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA9TrH%2FdJMcajaDMaZ%2FHMoE8ryeacYhKBegr4sZjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;422&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2920&quot; data-origin-height=&quot;1540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 모듈에서 사용하는 변수들 채워서 등록해주고&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2930&quot; data-origin-height=&quot;1528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q0Rsb/dJMcabwVTqL/J5h1KxACVf444FXKlUrYc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q0Rsb/dJMcabwVTqL/J5h1KxACVf444FXKlUrYc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q0Rsb/dJMcabwVTqL/J5h1KxACVf444FXKlUrYc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq0Rsb%2FdJMcabwVTqL%2FJ5h1KxACVf444FXKlUrYc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;417&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2930&quot; data-origin-height=&quot;1528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿 생성하면서 site.yaml 파일을 지정해주고 변수는 Variable Group 에 설정해주면 됨&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2924&quot; data-origin-height=&quot;1538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qEgnT/dJMcahKIwU5/m6WXe8CGX2y5OR8G06bk0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qEgnT/dJMcahKIwU5/m6WXe8CGX2y5OR8G06bk0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qEgnT/dJMcahKIwU5/m6WXe8CGX2y5OR8G06bk0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqEgnT%2FdJMcahKIwU5%2Fm6WXe8CGX2y5OR8G06bk0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;421&quot; data-origin-width=&quot;2924&quot; data-origin-height=&quot;1538&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task 잘 완료됐다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2940&quot; data-origin-height=&quot;1528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mQnhP/dJMcahKIw6u/PERw6aqUL7yctTraJo0wLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mQnhP/dJMcahKIw6u/PERw6aqUL7yctTraJo0wLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mQnhP/dJMcahKIw6u/PERw6aqUL7yctTraJo0wLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmQnhP%2FdJMcahKIw6u%2FPERw6aqUL7yctTraJo0wLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;416&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2940&quot; data-origin-height=&quot;1528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텔레포트에서도 잘 생성됨&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1888&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5Swo9/dJMcaco6c9I/LXXvewBjDkjUweMZ0mUBT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5Swo9/dJMcaco6c9I/LXXvewBjDkjUweMZ0mUBT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5Swo9/dJMcaco6c9I/LXXvewBjDkjUweMZ0mUBT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5Swo9%2FdJMcaco6c9I%2FLXXvewBjDkjUweMZ0mUBT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;271&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1888&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 UI에서는 잘 생성됐으니까 curl로 요청해보고 BE에서 WebClient로 동일하게 요청만 보내주면 완성&lt;/p&gt;</description>
      <category>OpenCSP/MVP 프로젝트</category>
      <author>miiml</author>
      <guid isPermaLink="true">https://miiml.tistory.com/14</guid>
      <comments>https://miiml.tistory.com/14#entry14comment</comments>
      <pubDate>Fri, 3 Apr 2026 17:52:52 +0900</pubDate>
    </item>
    <item>
      <title>OpenCSP Console로 VM 생성해보기</title>
      <link>https://miiml.tistory.com/13</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://miiml.tistory.com/18&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2026.04.13 - [프로젝트/OpenCSP] - [OpenCSP] Index - Provisioning Flow&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1776060259771&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[OpenCSP] Index - Provisioning Flow&quot; data-og-description=&quot;2026.03.23 - [프로젝트/OpenCSP] - API 요청으로 Terraform CR 생성하기 2026.03.25 - [프로젝트/OpenCSP] - API 요청으로 PVE VM 생성하기 2026.03.26 - [프로젝트/OpenCSP] - OpenCSP Console로 VM 생성해보기 2026.03.28 - [프로젝&quot; data-og-host=&quot;miiml.tistory.com&quot; data-og-source-url=&quot;https://miiml.tistory.com/18&quot; data-og-url=&quot;https://miiml.tistory.com/18&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c0oX32/dJMb8Z3sy1M/wOKY97JMvkLdUl1SEu3Hx1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dz8PvP/dJMb8RRS9jI/UCQR8kikp9KH6jg0uFjO4k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://miiml.tistory.com/18&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://miiml.tistory.com/18&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c0oX32/dJMb8Z3sy1M/wOKY97JMvkLdUl1SEu3Hx1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dz8PvP/dJMb8RRS9jI/UCQR8kikp9KH6jg0uFjO4k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[OpenCSP] Index - Provisioning Flow&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2026.03.23 - [프로젝트/OpenCSP] - API 요청으로 Terraform CR 생성하기 2026.03.25 - [프로젝트/OpenCSP] - API 요청으로 PVE VM 생성하기 2026.03.26 - [프로젝트/OpenCSP] - OpenCSP Console로 VM 생성해보기 2026.03.28 - [프로젝&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;miiml.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://miiml.tistory.com/12&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;에서 API 요청만으로 실제 리소스를 구성하는 걸 성공했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 프론트에서 사용자 값 넣고, 백엔드에서 그 값들을 양식만 잘 맞춰서 k3s에 요청보내면 항상 일정한 리소스가 생성된다. (테라폼 기반이기 때문에 멱등성)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 아래 이미지의 빨간 박스 부분의 동작을 개발하고 테스트해보려고 함&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1718&quot; data-origin-height=&quot;972&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cj8Psb/dJMcaiCJ0Yg/KZCDzzaIAT5AYoaQfww6ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cj8Psb/dJMcaiCJ0Yg/KZCDzzaIAT5AYoaQfww6ek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cj8Psb/dJMcaiCJ0Yg/KZCDzzaIAT5AYoaQfww6ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcj8Psb%2FdJMcaiCJ0Yg%2FKZCDzzaIAT5AYoaQfww6ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;453&quot; data-origin-width=&quot;1718&quot; data-origin-height=&quot;972&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 왼쪽의 프론트엔드는 Next.js 16, 백엔드는 자바 스프링 부트로 되어 있고 인증은 다른 플로우를 사용해서 IAM과 진행한다. (나중에 포스팅 예정)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;백엔드 구조&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java Spring Boot 3.5.x를 사용중이고 여기서 k8s로 요청 보내려면 여러 방법이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 Fabric8:Kubernetes Client을 사용해서 개발했었는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글들에서 테스트하면서 key-value 구조의 vm 정보를 담았던 자유 형식 필드(additionalProperties) 부분을 Map&amp;lt;String, Object&amp;gt;로 다뤘는데, 이 부분을 역직렬화 시킬 때 Fabric8의 ObjectMapper를 사용하면 원본 타입을 못찾고 이상한 형태로 나와 JSON Parse 에러가 나는 문제가 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Spring 기본 ObjectMapper를 사용해서 원본 JSON을 그대로 다루도록 WebClient로 직접 호출했음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 위의 과정은 잘 동작했고 아래처럼 스웨거로 API를 보내면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1916&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kJlgH/dJMcacifuyS/XgY687kKbL3oZ3VtLKw8a0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kJlgH/dJMcacifuyS/XgY687kKbL3oZ3VtLKw8a0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kJlgH/dJMcacifuyS/XgY687kKbL3oZ3VtLKw8a0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkJlgH%2FdJMcacifuyS%2FXgY687kKbL3oZ3VtLKw8a0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;377&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1916&quot; data-origin-height=&quot;904&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 잘 오고&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;462&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wQwkR/dJMcacCyKcl/iK4l8dBHxXeAYXfd1ldEsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wQwkR/dJMcacCyKcl/iK4l8dBHxXeAYXfd1ldEsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wQwkR/dJMcacCyKcl/iK4l8dBHxXeAYXfd1ldEsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwQwkR%2FdJMcacCyKcl%2FiK4l8dBHxXeAYXfd1ldEsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;193&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;462&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CrName으로 상태를 조회하면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1904&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ttraG/dJMcafF2VRg/kHXKbiXwo71ZfkZps7Kczk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ttraG/dJMcafF2VRg/kHXKbiXwo71ZfkZps7Kczk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ttraG/dJMcafF2VRg/kHXKbiXwo71ZfkZps7Kczk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FttraG%2FdJMcafF2VRg%2FkHXKbiXwo71ZfkZps7Kczk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;224&quot; data-origin-width=&quot;1904&quot; data-origin-height=&quot;532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것도 잘 된다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1934&quot; data-origin-height=&quot;676&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c2e4Xh/dJMcaaEKHDk/2emfkTa6p1Pg0cboNl4sM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c2e4Xh/dJMcaaEKHDk/2emfkTa6p1Pg0cboNl4sM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c2e4Xh/dJMcaaEKHDk/2emfkTa6p1Pg0cboNl4sM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc2e4Xh%2FdJMcaaEKHDk%2F2emfkTa6p1Pg0cboNl4sM0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;280&quot; data-origin-width=&quot;1934&quot; data-origin-height=&quot;676&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 실제 pve에도 잘 위에 정의한 스펙대로 잘 생성됐다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1910&quot; data-origin-height=&quot;828&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pJZp7/dJMcai3LWpD/gtqR09RQ3gRmxpiKVrkXD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pJZp7/dJMcai3LWpD/gtqR09RQ3gRmxpiKVrkXD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pJZp7/dJMcai3LWpD/gtqR09RQ3gRmxpiKVrkXD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpJZp7%2FdJMcai3LWpD%2FgtqR09RQ3gRmxpiKVrkXD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;451&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1910&quot; data-origin-height=&quot;828&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 백엔드 API도 잘생성됐으니까 프론트에서 양식만 잘 맞춰서 요청보내주면 끝인 듯&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;프론트엔드 구조&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트는 Next.js로 구성했고 UI는 나름 일관성을 주기위해 tailwind 기반의 커스텀 UI 패키지를 만들어서 npm에 배포했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(관련 링크 : &lt;a href=&quot;https://ui-storybook-ruddy.vercel.app/?path=/docs/patterns-simplecarousel--docs&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ui-storybook-ruddy.vercel.app/?path=/docs/patterns-simplecarousel--docs&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 아직은 개발중이라 전체 UI 컴포넌트가 위에 패키지를 사용하진 않고, 일부 기능이 필요한 경우는 console/fe의 components에 별도로 정의해서 사용하고 나중에 패키지로 옮기는 방식으로 작업할 예정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 지금 개발 중인 UI는 아래처럼 되어 있다. (변경될 수 있음)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2930&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQdMXu/dJMcabDD49C/KLMBO4qKKb4K1jY9bDadPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQdMXu/dJMcabDD49C/KLMBO4qKKb4K1jY9bDadPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQdMXu/dJMcabDD49C/KLMBO4qKKb4K1jY9bDadPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQdMXu%2FdJMcabDD49C%2FKLMBO4qKKb4K1jY9bDadPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;750&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2930&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스 페이지인데 추가 버튼 누르면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2358&quot; data-origin-height=&quot;1066&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIRNo3/dJMcacP47nt/T6xK7N8qioNV3kXBqMk6WK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIRNo3/dJMcacP47nt/T6xK7N8qioNV3kXBqMk6WK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIRNo3/dJMcacP47nt/T6xK7N8qioNV3kXBqMk6WK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIRNo3%2FdJMcacP47nt%2FT6xK7N8qioNV3kXBqMk6WK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;362&quot; data-origin-width=&quot;2358&quot; data-origin-height=&quot;1066&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2378&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tAzyG/dJMcaflJ1jK/kjXWismlbfURh2GuKRbz01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tAzyG/dJMcaflJ1jK/kjXWismlbfURh2GuKRbz01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tAzyG/dJMcaflJ1jK/kjXWismlbfURh2GuKRbz01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtAzyG%2FdJMcaflJ1jK%2FkjXWismlbfURh2GuKRbz01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;390&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2378&quot; data-origin-height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 채워주고 시작 눌러주면 아래처럼 백엔드가 폴링으로 테라폼 CR 상태를 체크하면서 DB를 업데이트 해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2350&quot; data-origin-height=&quot;526&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dPhOBb/dJMb996ToIp/hVLO7jv3AER7HBncxy8iX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dPhOBb/dJMb996ToIp/hVLO7jv3AER7HBncxy8iX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dPhOBb/dJMb996ToIp/hVLO7jv3AER7HBncxy8iX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdPhOBb%2FdJMb996ToIp%2FhVLO7jv3AER7HBncxy8iX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;526&quot; data-origin-width=&quot;2350&quot; data-origin-height=&quot;526&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2342&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDFNrj/dJMcaibIi4L/sg1QRVirDUqxbe0PaiCJx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDFNrj/dJMcaibIi4L/sg1QRVirDUqxbe0PaiCJx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDFNrj/dJMcaibIi4L/sg1QRVirDUqxbe0PaiCJx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDFNrj%2FdJMcaibIi4L%2Fsg1QRVirDUqxbe0PaiCJx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;145&quot; data-origin-width=&quot;2342&quot; data-origin-height=&quot;424&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2344&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLGncW/dJMcaivXEpk/GkBUaVyQOqdzj43rjt7cDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLGncW/dJMcaivXEpk/GkBUaVyQOqdzj43rjt7cDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLGncW/dJMcaivXEpk/GkBUaVyQOqdzj43rjt7cDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLGncW%2FdJMcaivXEpk%2FGkBUaVyQOqdzj43rjt7cDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;404&quot; data-origin-width=&quot;2344&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;눌러서보면 이렇게 세부 정보도 나옴&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2384&quot; data-origin-height=&quot;1138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qsfR8/dJMcaaY1e3e/FD3ySRZDnlNnxJ6BC30KVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qsfR8/dJMcaaY1e3e/FD3ySRZDnlNnxJ6BC30KVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qsfR8/dJMcaaY1e3e/FD3ySRZDnlNnxJ6BC30KVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqsfR8%2FdJMcaaY1e3e%2FFD3ySRZDnlNnxJ6BC30KVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;1138&quot; data-origin-width=&quot;2384&quot; data-origin-height=&quot;1138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 테라폼 상태고&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2892&quot; data-origin-height=&quot;104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/luzdv/dJMcafF2WN5/NNa6zSEHTWls0TnF8u2kI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/luzdv/dJMcafF2WN5/NNa6zSEHTWls0TnF8u2kI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/luzdv/dJMcafF2WN5/NNa6zSEHTWls0TnF8u2kI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fluzdv%2FdJMcafF2WN5%2FNNa6zSEHTWls0TnF8u2kI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;29&quot; data-origin-width=&quot;2892&quot; data-origin-height=&quot;104&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pve에서 확인해보면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1904&quot; data-origin-height=&quot;787&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nmhdA/dJMcafe0uxC/mRoyjmEh69isde28XavSv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nmhdA/dJMcafe0uxC/mRoyjmEh69isde28XavSv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nmhdA/dJMcafe0uxC/mRoyjmEh69isde28XavSv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnmhdA%2FdJMcafe0uxC%2FmRoyjmEh69isde28XavSv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;345&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1904&quot; data-origin-height=&quot;787&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 생성된거 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 백엔드로 요청하는 부분은 BFF(Backend for Frontend) 패턴을 사용했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API Proxy를 두고 해당 경로로 fecth하면 서버 사이드에서 환경변수나 세션에 저장된 토큰 등 필요한 정보를 가져와서 백엔드로 요청을 보내준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 사이드에서 동작하기 때문에 클라이언트 사이드에 토큰이 노출되지 않고 백엔드 엔드포인트를 직접 노출하지 않아 보안성이 높아졌고, CORS 문제도 구조적으로 해결된다.&lt;/p&gt;</description>
      <category>OpenCSP/MVP 프로젝트</category>
      <author>miiml</author>
      <guid isPermaLink="true">https://miiml.tistory.com/13</guid>
      <comments>https://miiml.tistory.com/13#entry13comment</comments>
      <pubDate>Sat, 28 Mar 2026 17:35:59 +0900</pubDate>
    </item>
    <item>
      <title>API 요청으로 PVE VM 생성하기</title>
      <link>https://miiml.tistory.com/12</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://miiml.tistory.com/18&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2026.04.13 - [프로젝트/OpenCSP] - [OpenCSP] Index - Provisioning Flow&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1776060172155&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[OpenCSP] Index - Provisioning Flow&quot; data-og-description=&quot;2026.03.23 - [프로젝트/OpenCSP] - API 요청으로 Terraform CR 생성하기 2026.03.25 - [프로젝트/OpenCSP] - API 요청으로 PVE VM 생성하기 2026.03.26 - [프로젝트/OpenCSP] - OpenCSP Console로 VM 생성해보기 2026.03.28 - [프로젝&quot; data-og-host=&quot;miiml.tistory.com&quot; data-og-source-url=&quot;https://miiml.tistory.com/18&quot; data-og-url=&quot;https://miiml.tistory.com/18&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c0oX32/dJMb8Z3sy1M/wOKY97JMvkLdUl1SEu3Hx1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dz8PvP/dJMb8RRS9jI/UCQR8kikp9KH6jg0uFjO4k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://miiml.tistory.com/18&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://miiml.tistory.com/18&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c0oX32/dJMb8Z3sy1M/wOKY97JMvkLdUl1SEu3Hx1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dz8PvP/dJMb8RRS9jI/UCQR8kikp9KH6jg0uFjO4k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[OpenCSP] Index - Provisioning Flow&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2026.03.23 - [프로젝트/OpenCSP] - API 요청으로 Terraform CR 생성하기 2026.03.25 - [프로젝트/OpenCSP] - API 요청으로 PVE VM 생성하기 2026.03.26 - [프로젝트/OpenCSP] - OpenCSP Console로 VM 생성해보기 2026.03.28 - [프로젝&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;miiml.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://miiml.tistory.com/10&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;에서 API로 직접 Terraform CR을 생성해봤다.&lt;br /&gt;그리고 snippets 파일을 SSH가 아닌 PVE API를 사용한 전달로 변경하는 게 나을 거 같다고 생각했어서 모듈을 해당 방식으로 수정했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;먼저 기존 모듈을 사용해서 구성해둔 인프라에는 영향을 주지 않기 위해 모듈에 버전 tag랑 release note를 남겨줬고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core에서 해당 태그의 코드를 사용하도록 지정해줬다. (관련 커밋 : &lt;a href=&quot;https://github.com/h001-lab/OpenCSP-modules/commit/17f4a557571bfc9b737d47631eadd00e11709ecc&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://github.com/h001-lab/OpenCSP-modules/commit/17f4a557571bfc9b737d47631eadd00e11709ecc&lt;/span&gt;&lt;/a&gt;)&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈 수정 후 테스트 해보면서 PVE에서 API로 snippets 파일 업로드를 지원하지 않는다는걸 알게됐는데 (2019년 관련 이슈만 있고 아직 개발진행 안됐다고 함, 현재는 iso랑 vztmpl만 지원)&lt;br /&gt;&amp;nbsp;&lt;br /&gt;생각해보니까 처음 모듈을 구성할 때 SSH로 했던 이유가 이거였는데 다른 작업들을 하다보니 까먹고 한번 더 개발해버렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(그래도 덕분에 모듈에 버저닝이 추가됐으니까 장기적으로는 이득..)&lt;br /&gt;&amp;nbsp;&lt;br /&gt;아무튼 위에 작업들을 하면서 몇 가지 정리해두고 싶은게 생겨서 2번 글을 따로 작성했다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;SSH 방식에서 Terraform 컨테이너에 SSH key와 API 크레덴셜을 전달해야하니까 기존 Tofu-controller Secret에 해당 내용을 추가해준 부분, provision(wrapper) 디렉토리 구조 수정, 테라폼 모듈 버저닝, Release Note 반 자동화 같은 내용들인데&lt;br /&gt;&amp;nbsp;&lt;br /&gt;일단 여기선 2번째 까지만 적어두고 나머진 나중에 정리해봐야겠다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Modules 수정 작업&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 ssh 키 새로 생성해서 host에 등록해주고&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 키 생성
ssh-keygen -t ed25519 -f ~/.ssh/pve-tofu -C &quot;tofu-controller&quot; -N &quot;&quot; 

# host에 키 등록
ssh-copy-id -i ~/.ssh/pve-tofu.pub root@{pve ip}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;위에 생성한 pve-tofu 내용을 기존 secret.yaml에 등록해주면 된다. (sops로 암호화도 해줌)&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;---
apiVersion: v1
kind: Secret
metadata:
&amp;nbsp;&amp;nbsp;name: pve-ssh-key
&amp;nbsp;&amp;nbsp;namespace: flux-system
type: Opaque
stringData:
&amp;nbsp;&amp;nbsp;id_rsa: |
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-----BEGIN OPENSSH PRIVATE KEY-----
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;~/.ssh/pve-tofu 내용 붙여넣기 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-----END OPENSSH PRIVATE KEY-----&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;참고로 기존 secret에도 ssh 관련 값이 있으니까 거기도 맞춰주야 됨.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;저 키를 컨테이너에 마운트 해주는건 Terraform CR이 해준다.&amp;nbsp;&lt;br /&gt;secret 잘 생성 되었으면 아래처럼 curl을 보내볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;curl -v -k -X POST \
&amp;nbsp;&amp;nbsp;&quot;${PVE_K3S_API_SERVER}/apis/infra.contrib.fluxcd.io/v1alpha2/namespaces/flux-system/terraforms&quot; \
&amp;nbsp;&amp;nbsp;-H &quot;Authorization: Bearer ${TOKEN}&quot; \
&amp;nbsp;&amp;nbsp;-H &quot;Content-Type: application/json&quot; \
&amp;nbsp;&amp;nbsp;-d '{
&amp;nbsp;&amp;nbsp;&quot;apiVersion&quot;: &quot;infra.contrib.fluxcd.io/v1alpha2&quot;,
&amp;nbsp;&amp;nbsp;&quot;kind&quot;: &quot;Terraform&quot;,
&amp;nbsp;&amp;nbsp;&quot;metadata&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name&quot;: &quot;test-vm-provision&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;namespace&quot;: &quot;flux-system&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;annotations&quot;: {
	&amp;nbsp;&amp;nbsp;&quot;kustomize.toolkit.fluxcd.io/prune&quot;: &quot;disabled&quot;
	}
&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&quot;spec&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;path&quot;: &quot;./bootstrap/terraform/provisions/proxmox-vm&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;interval&quot;: &quot;10m&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;approvePlan&quot;: &quot;auto&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;destroyResourcesOnDeletion&quot;: true,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;sourceRef&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;kind&quot;: &quot;GitRepository&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name&quot;: &quot;flux-system&quot;, 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;namespace&quot;: &quot;flux-system&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&quot;runnerPodTemplate&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;spec&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;volumes&quot;: [{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name&quot;: &quot;ssh-key&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;secret&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;secretName&quot;: &quot;pve-ssh-key&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;defaultMode&quot;: 292
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}],
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;volumeMounts&quot;: [{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name&quot;: &quot;ssh-key&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;mountPath&quot;: &quot;/home/runner/.ssh&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;readOnly&quot;: true
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;varsFrom&quot;: [
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&quot;kind&quot;: &quot;Secret&quot;, &quot;name&quot;: &quot;terraform-secrets&quot;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;],
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;vars&quot;: [
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&quot;name&quot;: &quot;vm_name&quot;, &quot;value&quot;: &quot;test-vm3&quot;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&quot;name&quot;: &quot;vm_id&quot;, &quot;value&quot;: 7003},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&quot;name&quot;: &quot;cores&quot;, &quot;value&quot;: 1},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&quot;name&quot;: &quot;memory&quot;, &quot;value&quot;: 2048},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&quot;name&quot;: &quot;disk_size&quot;, &quot;value&quot;: &quot;50G&quot;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&quot;name&quot;: &quot;vm_ip&quot;, &quot;value&quot;: &quot;{IP}/24&quot;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&quot;name&quot;: &quot;vm_gw&quot;, &quot;value&quot;: &quot;{GW}&quot;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&quot;name&quot;: &quot;vm_network_bridge&quot;, &quot;value&quot;: &quot;vmbr0&quot;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&quot;name&quot;: &quot;target_node&quot;, &quot;value&quot;: &quot;pve&quot;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&quot;name&quot;: &quot;template_name&quot;, &quot;value&quot;: &quot;ubuntu-2404-template&quot;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&quot;name&quot;: &quot;storage_pool&quot;, &quot;value&quot;: &quot;local-lvm&quot;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&quot;name&quot;: &quot;snippet_storage_pool&quot;, &quot;value&quot;: &quot;local&quot;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;]
&amp;nbsp;&amp;nbsp;}
}'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;몇가지 옵션들 설명해보면&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&quot;annotations&quot;: {
&amp;nbsp;&amp;nbsp;&quot;kustomize.toolkit.fluxcd.io/prune&quot;: &quot;disabled&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 fluxcd의 prune 옵션인데, 이거 활성화시키고 git에 코드가 없으면(flux가 관리하지 않는 리소스면) 계속 제거해주는 옵션이다.&amp;nbsp;&lt;br /&gt;클러스터 관리 면에선 켜주는게 좋지만 CR을 BE가 생성해야 해서 꺼줬다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&quot;destroyResourcesOnDeletion&quot;: true,&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 리소스가 삭제될 때 destory를 진행할지에 대한 옵션이고, 이게 켜져있으면 Terraform CR이 제거될 때 생성된 리소스가 같이 제거됨.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&quot;path&quot;: &quot;./bootstrap/terraform/provisions/proxmox-vm&quot;,
&quot;sourceRef&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;kind&quot;: &quot;GitRepository&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name&quot;: &quot;flux-system&quot;, 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;namespace&quot;: &quot;flux-system&quot;
},&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 2번째 항목인데 기존에 tofu-controller가 참조하기 위해 modules/.../provision 으로 만들었던 wrapper를 core로 옮겨줬다. 이렇게 하면 GitRepository CR을 별도로 추가할 필요없고, 모듈에 두는거 보다 Core에 두는게 운영 관점에서 더 맞다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&quot;runnerPodTemplate&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;spec&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;volumes&quot;: [{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name&quot;: &quot;ssh-key&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;secret&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;secretName&quot;: &quot;pve-ssh-key&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;defaultMode&quot;: 292
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}],
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;volumeMounts&quot;: [{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name&quot;: &quot;ssh-key&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;mountPath&quot;: &quot;/home/runner/.ssh&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;readOnly&quot;: true
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;runnerPodTemplate 부분이 위에서 생성한 ssh 키를 컨테이너에 마운트 시켜주는 부분이고, 권한을 292(read)로 줘서 PVE 접근용 ssh 키를 읽을 수 있게 해줌&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;참고로 아래 명령어로 필드 구조를 알 수 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;kubectl explain terraform.spec.runnerPodTemplate.spec&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;실행하면 아래처럼 Apply가 성공하고 VM도 생성된걸 볼 수 있음&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2170&quot; data-origin-height=&quot;102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CVVsW/dJMcabcv8zc/dku41G78aFJp0X44Kx1R9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CVVsW/dJMcabcv8zc/dku41G78aFJp0X44Kx1R9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CVVsW/dJMcabcv8zc/dku41G78aFJp0X44Kx1R9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCVVsW%2FdJMcabcv8zc%2Fdku41G78aFJp0X44Kx1R9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;38&quot; data-origin-width=&quot;2170&quot; data-origin-height=&quot;102&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2914&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bavWHE/dJMcahqiNpl/7KsuPKCAsd3G2ZvAKWg5R0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bavWHE/dJMcahqiNpl/7KsuPKCAsd3G2ZvAKWg5R0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bavWHE/dJMcahqiNpl/7KsuPKCAsd3G2ZvAKWg5R0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbavWHE%2FdJMcahqiNpl%2F7KsuPKCAsd3G2ZvAKWg5R0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;168&quot; data-origin-width=&quot;2914&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;삭제도 잘 된다. (근데 참고로 finalizer를 먼저 제거하면 CR은 잘 지워지지만 VM 리소스가 제거 되지 않는다.)&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;curl -k -X DELETE \
&amp;nbsp;&amp;nbsp;&quot;${PVE_K3S_API_SERVER}/apis/infra.contrib.fluxcd.io/v1alpha2/namespaces/flux-system/terraforms/test-vm-provision&quot; \
&amp;nbsp;&amp;nbsp;-H &quot;Authorization: Bearer ${TOKEN}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>OpenCSP/MVP 프로젝트</category>
      <author>miiml</author>
      <guid isPermaLink="true">https://miiml.tistory.com/12</guid>
      <comments>https://miiml.tistory.com/12#entry12comment</comments>
      <pubDate>Wed, 25 Mar 2026 19:15:30 +0900</pubDate>
    </item>
  </channel>
</rss>