<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>DynamoDB on J2eff's Garage</title><link>https://blog.j2eff.me/tags/dynamodb/</link><description>Recent content in DynamoDB on J2eff's Garage</description><generator>Hugo -- gohugo.io</generator><language>ko-KR</language><lastBuildDate>Mon, 06 Jul 2026 09:00:00 +0900</lastBuildDate><atom:link href="https://blog.j2eff.me/tags/dynamodb/index.xml" rel="self" type="application/rss+xml"/><item><title>credstore를 프로세스 밖으로 — DynamoDB에 얹다</title><link>https://blog.j2eff.me/p/credstore%EB%A5%BC-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EB%B0%96%EC%9C%BC%EB%A1%9C-dynamodb%EC%97%90-%EC%96%B9%EB%8B%A4/</link><pubDate>Mon, 06 Jul 2026 09:00:00 +0900</pubDate><guid>https://blog.j2eff.me/p/credstore%EB%A5%BC-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EB%B0%96%EC%9C%BC%EB%A1%9C-dynamodb%EC%97%90-%EC%96%B9%EB%8B%A4/</guid><description>&lt;!--
제목 후보 (Jeff 선택용):
 1. credstore를 프로세스 밖으로 — DynamoDB에 얹다 ← 현재 가안
 2. 재시작을 견디는 자격증명
 3. AWS는 이걸 무엇에 담나
--&gt;
&lt;p&gt;4편 끝에서 솔직하게 인정한 게 있다. mincloud의 credstore는 인메모리 맵 하나라, IAM이 발급한 키를 STS가 즉시 검증할 수 있는 건 &amp;ldquo;나눌 데이터가 애초에 한 벌뿐이라서&amp;quot;라고. 그 공짜에는 유효기간이 있었다. &lt;strong&gt;프로세스를 내리면 발급한 키가 전부 증발한다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ aws iam create-access-key --user-name friend01 ... # 키 발급
$ docker restart mincloud # 재시작
$ aws sts get-caller-identity ... # 방금 그 키로
An error occurred (InvalidClientTokenId) ... # 없는 사람 취급
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이번 편은 이걸 고친다. credstore를 프로세스 밖으로 빼서, &lt;code&gt;docker compose down &amp;amp;&amp;amp; up&lt;/code&gt; 을 해도 발급한 모든 자격증명이 살아있게 만든다.&lt;/p&gt;
&lt;h2 id="aws는-이걸-무엇에-담나"&gt;AWS는 이걸 무엇에 담나
&lt;/h2&gt;&lt;p&gt;파일 하나에 JSON으로 적어도 &amp;ldquo;재시작 후 유지&amp;quot;는 된다. 그런데 이왕이면 &lt;strong&gt;AWS가 실제로 하는 방식&lt;/strong&gt;을 따라가고 싶었다. 그러려면 먼저 물어야 한다. AWS는 IAM 같은 서비스의 자격증명을 무엇에 담을까?&lt;/p&gt;
&lt;p&gt;관계형 데이터베이스가 아니다. 근거는 IAM의 성격 그 자체에 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;조회가 키 하나로 끝난다.&lt;/strong&gt; 우리가 하는 일은 &lt;code&gt;accessKeyID → credential&lt;/code&gt; 딱 하나다. 조인이 아니라 단일 키 조회다. 이건 관계형의 강점을 하나도 안 쓰고, 오히려 key-value 스토어의 정확히 그 모양이다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;글로벌 + 최종 일관성(eventual consistency).&lt;/strong&gt; 4편에서 &amp;ldquo;IAM 변경이 전파되는 데 몇 초 걸린다&amp;quot;고 했다. 그 몇 초가 단일 관계형 DB의 지문이 아니다. 여러 리전에 복제되는 &lt;strong&gt;분산 key-value 스토어&lt;/strong&gt;의 지문이다. 트랜잭션 하나로 끝나는 DB라면 그런 지연이 날 이유가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그 모양에 정확히 맞는 AWS 서비스가 &lt;strong&gt;DynamoDB&lt;/strong&gt;다. 실제로 AWS의 여러 컨트롤플레인이 그 위에 올라가 있다. 그리고 고맙게도 AWS는 &lt;strong&gt;DynamoDB Local&lt;/strong&gt; — 공식 &lt;code&gt;amazon/dynamodb-local&lt;/code&gt; 컨테이너 — 을 배포한다. 로컬 docker-compose에 그대로 얹어, mincloud가 진짜 DynamoDB처럼 쓸 수 있다.&lt;/p&gt;
&lt;p&gt;우리가 손으로 짜는 건 여전히 AWS의 &lt;strong&gt;표면&lt;/strong&gt;(SigV4, STS/IAM 프로토콜)이다. 스토어는 &amp;ldquo;데이터베이스를 밑바닥부터&amp;quot;가 아니라 — 그건 이 프로젝트의 주제가 아니니까 — &lt;strong&gt;AWS가 실제로 쓰는 그 스토어&lt;/strong&gt;를 그대로 쓴다.&lt;/p&gt;
&lt;h2 id="인터페이스는-그대로-구현만-밖으로"&gt;인터페이스는 그대로, 구현만 밖으로
&lt;/h2&gt;&lt;p&gt;여기서 4편에 깔아 둔 게 값을 한다. credstore는 처음부터 인터페이스였다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accessKeyID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Credential&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accessKeyID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Credential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;이 인터페이스가 작게 유지된 덕에, 뒤를 통째로 갈아도 &lt;strong&gt;어떤 핸들러도 눈치채지 못한다.&lt;/strong&gt; STS도 IAM도 여전히 &lt;code&gt;Lookup&lt;/code&gt;과 &lt;code&gt;Put&lt;/code&gt;만 부른다. 바뀌는 건 그 두 메서드 안쪽뿐이다.&lt;/p&gt;
&lt;p&gt;DynamoDB 구현은 accessKeyID를 파티션 키로 삼는 단일 테이블이다. &lt;code&gt;Put&lt;/code&gt;은 &lt;code&gt;PutItem&lt;/code&gt;, &lt;code&gt;Lookup&lt;/code&gt;은 &lt;code&gt;GetItem&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;dynamoStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accessKeyID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Credential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GetItemInput&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AttributeValue&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;AccessKeyId&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AttributeValueMemberS&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;accessKeyID&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 실패 시엔 닫는다: 읽지 못한 데이터로 인증을 통과시키느니 거부한다.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Credential&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Credential&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Credential&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* Item 의 속성들로 복원 */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;관계형이었다면 스키마와 마이그레이션이 따라왔을 것이다. key-value라 그런 의식(儀式)이 없다. 테이블 하나, 파티션 키 하나. IAM이 이 스토어에 어울리는 이유가 코드에서도 그대로 드러난다.&lt;/p&gt;
&lt;p&gt;바꿔 말하면, &lt;code&gt;main&lt;/code&gt;은 이제 둘 중 하나를 고른다. DynamoDB 엔드포인트가 주어지면 그걸로 영속화하고, 없으면 예전처럼 인메모리로 돈다. 나머지 코드는 자기가 어느 쪽을 받았는지 &lt;strong&gt;모른다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;openStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;MINCLOUD_DYNAMODB_ENDPOINT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="두-서비스-한-compose"&gt;두 서비스, 한 compose
&lt;/h2&gt;&lt;p&gt;이제 mincloud는 자기 스토어를 프로세스 안에 두지 않는다. 그래서 실행 단위가 둘로 늘어난다 — mincloud, 그리고 그 자격증명을 담는 DynamoDB. docker-compose가 이 둘을 묶는다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;amazon/dynamodb-local:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-jar&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;DynamoDBLocal.jar&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-sharedDb&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-dbPath&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/home/dynamodblocal/data&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;dynamodb-data:/home/dynamodblocal/data &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 여기가 &amp;#39;유지&amp;#39;의 핵심&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;mincloud&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;MINCLOUD_DYNAMODB_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;http://dynamodb:8000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;9900:9900&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# STS&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;9910:9910&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# IAM&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;dynamodb-data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;핵심은 &lt;code&gt;dynamodb-data&lt;/code&gt; &lt;strong&gt;이름 볼륨&lt;/strong&gt;이다. DynamoDB Local은 &lt;code&gt;-sharedDb -dbPath&lt;/code&gt;로 상태를 파일에 쓰고, 그 디렉토리를 이름 볼륨에 얹어 뒀다. 컨테이너는 &lt;code&gt;down&lt;/code&gt;으로 사라져도 볼륨은 남는다. 그래서 다음 &lt;code&gt;up&lt;/code&gt;에서 DynamoDB가 예전 데이터를 그대로 들고 올라온다.&lt;/p&gt;
&lt;p&gt;작은 정직함 하나. mincloud가 DynamoDB보다 먼저 떠서 첫 연결이 거부당하는 순간이 있다. 그래서 &lt;code&gt;restart: on-failure&lt;/code&gt;를 걸어, 스토어가 응답할 때까지 프로세스가 죽고 다시 뜨게 뒀다. 로그에 그 찰나가 그대로 찍힌다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mincloud-1 | open credstore: ... dial tcp 172.18.0.2:8000: connect: connection refused
mincloud-1 | persisting credentials in DynamoDB at http://dynamodb:8000 (table &amp;#34;mincloud-credentials&amp;#34;)
mincloud-1 | mincloud sts listening on :9900, iam listening on :9910
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="죽었다-살아나도-기억한다"&gt;죽었다 살아나도 기억한다
&lt;/h2&gt;&lt;p&gt;이제 증명. 먼저 &lt;code&gt;friend01&lt;/code&gt;에게 키를 발급한다(IAM :9910).&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ aws iam create-access-key --user-name friend01 \
 --endpoint-url http://localhost:9910
{
 &amp;#34;AccessKey&amp;#34;: {
 &amp;#34;UserName&amp;#34;: &amp;#34;friend01&amp;#34;,
 &amp;#34;AccessKeyId&amp;#34;: &amp;#34;AKIA8KM1NAU3S7PFZQHM&amp;#34;,
 &amp;#34;Status&amp;#34;: &amp;#34;Active&amp;#34;,
 &amp;#34;SecretAccessKey&amp;#34;: &amp;#34;HfhFEYv1KiUrMAcs3jCDUSCYmUk6d1UPkcQxpSR4&amp;#34;,
 &amp;#34;CreateDate&amp;#34;: &amp;#34;2026-07-06T11:34:10+00:00&amp;#34;
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(여기 값은 로컬에서 방금 만든 가짜다. 어디에도 통하지 않는다.)&lt;/p&gt;
&lt;p&gt;그 키로 STS(:9900)에 신원을 물으면 friend01로 돌아온다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ AWS_ACCESS_KEY_ID=AKIA8KM1NAU3S7PFZQHM \
 AWS_SECRET_ACCESS_KEY=HfhFEYv1KiUrMAcs3jCDUSCYmUk6d1UPkcQxpSR4 \
 aws sts get-caller-identity --endpoint-url http://localhost:9900
{
 &amp;#34;UserId&amp;#34;: &amp;#34;AIDABJDQJDVSDYOKZX2L&amp;#34;,
 &amp;#34;Account&amp;#34;: &amp;#34;123456789012&amp;#34;,
 &amp;#34;Arn&amp;#34;: &amp;#34;arn:aws:iam::123456789012:user/friend01&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;여기까진 4편과 같다. 이제 &lt;strong&gt;스택을 통째로 내렸다 다시 올린다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ docker compose down
$ docker compose up -d
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;컨테이너는 새로 만들어졌다. mincloud의 인메모리 맵은 당연히 비었다. 그런데 &lt;strong&gt;아까 그 키를 그대로 다시 쓰면:&lt;/strong&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ AWS_ACCESS_KEY_ID=AKIA8KM1NAU3S7PFZQHM \
 AWS_SECRET_ACCESS_KEY=HfhFEYv1KiUrMAcs3jCDUSCYmUk6d1UPkcQxpSR4 \
 aws sts get-caller-identity --endpoint-url http://localhost:9900
{
 &amp;#34;UserId&amp;#34;: &amp;#34;AIDABJDQJDVSDYOKZX2L&amp;#34;,
 &amp;#34;Account&amp;#34;: &amp;#34;123456789012&amp;#34;,
 &amp;#34;Arn&amp;#34;: &amp;#34;arn:aws:iam::123456789012:user/friend01&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;!-- screenshot: down → up 사이에도 같은 키가 friend01 로 인증되는 터미널 --&gt;
&lt;p&gt;똑같이 friend01이 돌아온다. 프로세스가 죽었다 살아났는데도 기억한다. friend01의 자격증명은 mincloud의 메모리가 아니라 DynamoDB의 볼륨에 있었기 때문이다. 4편에서 &amp;ldquo;곧바로 검증 가능한 자격증명&amp;quot;이라던 그 키가, 이제 &lt;strong&gt;재시작을 건너뛰고도&lt;/strong&gt; 검증된다.&lt;/p&gt;
&lt;h2 id="공짜였던-게-값을-치르기-시작한다"&gt;공짜였던 게 값을 치르기 시작한다
&lt;/h2&gt;&lt;p&gt;4편에서 &amp;ldquo;글로벌이 공짜&amp;quot;라고 했다. 프로세스가 하나고 맵이 하나라, 복제도 지연도 불일치도 없다고. credstore를 밖으로 빼는 순간, 그 공짜가 값을 치르기 시작한다. 이제 자격증명은 네트워크 건너 다른 프로세스에 있다. 그래서 스토어가 안 뜨면 인증이 실패할 수 있고(그래서 &lt;code&gt;restart: on-failure&lt;/code&gt;), 조회 하나에 왕복이 든다.&lt;/p&gt;
&lt;p&gt;물론 DynamoDB Local은 단일 노드라, 진짜 AWS의 리전 간 복제 지연은 아직 겪지 않는다. 하지만 &lt;strong&gt;모양은 이제 맞다.&lt;/strong&gt; 스토어가 프로세스 밖 독립 서비스가 됐고, 인메모리 맵이라면 못 했을 &amp;ldquo;여러 mincloud 인스턴스가 한 스토어를 공유&amp;quot;가 이제 가능한 그림이 됐다. 4편이 예고한 그 세계로 한 발 들어선 것이다.&lt;/p&gt;
&lt;p&gt;정직하게 덧붙이면, 지금 DynamoDB에는 &lt;strong&gt;시크릿이 평문으로&lt;/strong&gt; 들어간다. mincloud가 던지는 시크릿이 어차피 버려도 되는 가짜라 넘어가는 것이지, 진짜 컨트롤플레인이라면 자격증명을 이렇게 담지 않는다. 이것도 언젠가 갚아야 할 빚으로 적어 둔다.&lt;/p&gt;
&lt;p&gt;인증 골격이 이제 재시작을 견딘다. 다음 편에서는, 이 단단해진 골격 위에 지금까지와 &lt;strong&gt;완전히 다른 종류의 서비스&lt;/strong&gt;를 얹어 본다 — 신원이 아니라 컴퓨트, EC2다. 골격이 정말 값을 하는지 거기서 드러난다.&lt;/p&gt;</description></item></channel></rss>