credstore를 프로세스 밖으로 — DynamoDB에 얹다

Cloud Control Plane
  1. 1. AWS CLI는 무엇을 보내는가
  2. 2. SigV4 검증기를 Go로 밑바닥부터
  3. 3. CLI가 속아 넘어가는 순간
  4. 4. IAM과 STS를 나누다
  5. 5. credstore를 프로세스 밖으로 — DynamoDB에 얹다
  6. 6. 최초의 계정은 어디서 오는가
  7. 7. 맨 아래 거북이 — 제네시스 계정과 신뢰의 뿌리
  8. 8. 인증은 됐는데 권한이 없다 — 서명된 CreateAccount
  9. 9. 루트키를 넘기지 마라 — 역할과 AssumeRole
전체 9편
5 / 9

4편 끝에서 솔직하게 인정한 게 있다. mincloud의 credstore는 인메모리 맵 하나라, IAM이 발급한 키를 STS가 즉시 검증할 수 있는 건 “나눌 데이터가 애초에 한 벌뿐이라서"라고. 그 공짜에는 유효기간이 있었다. 프로세스를 내리면 발급한 키가 전부 증발한다.

$ aws iam create-access-key --user-name friend01 ...   # 키 발급
$ docker restart mincloud                               # 재시작
$ aws sts get-caller-identity ...                       # 방금 그 키로
An error occurred (InvalidClientTokenId) ...            # 없는 사람 취급

이번 편은 이걸 고친다. credstore를 프로세스 밖으로 빼서, docker compose down && up 을 해도 발급한 모든 자격증명이 살아있게 만든다.

AWS는 이걸 무엇에 담나

파일 하나에 JSON으로 적어도 “재시작 후 유지"는 된다. 그런데 이왕이면 AWS가 실제로 하는 방식을 따라가고 싶었다. 그러려면 먼저 물어야 한다. AWS는 IAM 같은 서비스의 자격증명을 무엇에 담을까?

관계형 데이터베이스가 아니다. 근거는 IAM의 성격 그 자체에 있다.

  • 조회가 키 하나로 끝난다. 우리가 하는 일은 accessKeyID → credential 딱 하나다. 조인이 아니라 단일 키 조회다. 이건 관계형의 강점을 하나도 안 쓰고, 오히려 key-value 스토어의 정확히 그 모양이다.
  • 글로벌 + 최종 일관성(eventual consistency). 4편에서 “IAM 변경이 전파되는 데 몇 초 걸린다"고 했다. 그 몇 초가 단일 관계형 DB의 지문이 아니다. 여러 리전에 복제되는 분산 key-value 스토어의 지문이다. 트랜잭션 하나로 끝나는 DB라면 그런 지연이 날 이유가 없다.

그 모양에 정확히 맞는 AWS 서비스가 DynamoDB다. 실제로 AWS의 여러 컨트롤플레인이 그 위에 올라가 있다. 그리고 고맙게도 AWS는 DynamoDB Local — 공식 amazon/dynamodb-local 컨테이너 — 을 배포한다. 로컬 docker-compose에 그대로 얹어, mincloud가 진짜 DynamoDB처럼 쓸 수 있다.

우리가 손으로 짜는 건 여전히 AWS의 표면(SigV4, STS/IAM 프로토콜)이다. 스토어는 “데이터베이스를 밑바닥부터"가 아니라 — 그건 이 프로젝트의 주제가 아니니까 — AWS가 실제로 쓰는 그 스토어를 그대로 쓴다.

인터페이스는 그대로, 구현만 밖으로

여기서 4편에 깔아 둔 게 값을 한다. credstore는 처음부터 인터페이스였다.

1
2
3
4
type Store interface {
    Put(accessKeyID string, c Credential)
    Lookup(accessKeyID string) (Credential, bool)
}

이 인터페이스가 작게 유지된 덕에, 뒤를 통째로 갈아도 어떤 핸들러도 눈치채지 못한다. STS도 IAM도 여전히 LookupPut만 부른다. 바뀌는 건 그 두 메서드 안쪽뿐이다.

DynamoDB 구현은 accessKeyID를 파티션 키로 삼는 단일 테이블이다. PutPutItem, LookupGetItem.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func (s *dynamoStore) Lookup(accessKeyID string) (Credential, bool) {
    out, err := s.client.GetItem(ctx, &dynamodb.GetItemInput{
        TableName: &s.table,
        Key: map[string]types.AttributeValue{
            "AccessKeyId": &types.AttributeValueMemberS{Value: accessKeyID},
        },
    })
    if err != nil {
        // 실패 시엔 닫는다: 읽지 못한 데이터로 인증을 통과시키느니 거부한다.
        return Credential{}, false
    }
    if out.Item == nil {
        return Credential{}, false
    }
    return Credential{ /* Item 의 속성들로 복원 */ }, true
}

관계형이었다면 스키마와 마이그레이션이 따라왔을 것이다. key-value라 그런 의식(儀式)이 없다. 테이블 하나, 파티션 키 하나. IAM이 이 스토어에 어울리는 이유가 코드에서도 그대로 드러난다.

바꿔 말하면, main은 이제 둘 중 하나를 고른다. DynamoDB 엔드포인트가 주어지면 그걸로 영속화하고, 없으면 예전처럼 인메모리로 돈다. 나머지 코드는 자기가 어느 쪽을 받았는지 모른다.

1
store, err := openStore(os.Getenv("MINCLOUD_DYNAMODB_ENDPOINT"))

두 서비스, 한 compose

이제 mincloud는 자기 스토어를 프로세스 안에 두지 않는다. 그래서 실행 단위가 둘로 늘어난다 — mincloud, 그리고 그 자격증명을 담는 DynamoDB. docker-compose가 이 둘을 묶는다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
services:
  dynamodb:
    image: amazon/dynamodb-local:latest
    command: ["-jar", "DynamoDBLocal.jar", "-sharedDb", "-dbPath", "/home/dynamodblocal/data"]
    volumes:
      - dynamodb-data:/home/dynamodblocal/data   # 여기가 '유지'의 핵심

  mincloud:
    build: .
    environment:
      MINCLOUD_DYNAMODB_ENDPOINT: http://dynamodb:8000
    ports:
      - "9900:9900"   # STS
      - "9910:9910"   # IAM

volumes:
  dynamodb-data:

핵심은 dynamodb-data 이름 볼륨이다. DynamoDB Local은 -sharedDb -dbPath로 상태를 파일에 쓰고, 그 디렉토리를 이름 볼륨에 얹어 뒀다. 컨테이너는 down으로 사라져도 볼륨은 남는다. 그래서 다음 up에서 DynamoDB가 예전 데이터를 그대로 들고 올라온다.

작은 정직함 하나. mincloud가 DynamoDB보다 먼저 떠서 첫 연결이 거부당하는 순간이 있다. 그래서 restart: on-failure를 걸어, 스토어가 응답할 때까지 프로세스가 죽고 다시 뜨게 뒀다. 로그에 그 찰나가 그대로 찍힌다.

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 "mincloud-credentials")
mincloud-1  | mincloud sts listening on :9900, iam listening on :9910

죽었다 살아나도 기억한다

이제 증명. 먼저 friend01에게 키를 발급한다(IAM :9910).

$ aws iam create-access-key --user-name friend01 \
    --endpoint-url http://localhost:9910
{
    "AccessKey": {
        "UserName": "friend01",
        "AccessKeyId": "AKIA8KM1NAU3S7PFZQHM",
        "Status": "Active",
        "SecretAccessKey": "HfhFEYv1KiUrMAcs3jCDUSCYmUk6d1UPkcQxpSR4",
        "CreateDate": "2026-07-06T11:34:10+00:00"
    }
}

(여기 값은 로컬에서 방금 만든 가짜다. 어디에도 통하지 않는다.)

그 키로 STS(:9900)에 신원을 물으면 friend01로 돌아온다.

$ AWS_ACCESS_KEY_ID=AKIA8KM1NAU3S7PFZQHM \
  AWS_SECRET_ACCESS_KEY=HfhFEYv1KiUrMAcs3jCDUSCYmUk6d1UPkcQxpSR4 \
  aws sts get-caller-identity --endpoint-url http://localhost:9900
{
    "UserId": "AIDABJDQJDVSDYOKZX2L",
    "Account": "123456789012",
    "Arn": "arn:aws:iam::123456789012:user/friend01"
}

여기까진 4편과 같다. 이제 스택을 통째로 내렸다 다시 올린다.

$ docker compose down
$ docker compose up -d

컨테이너는 새로 만들어졌다. mincloud의 인메모리 맵은 당연히 비었다. 그런데 아까 그 키를 그대로 다시 쓰면:

$ AWS_ACCESS_KEY_ID=AKIA8KM1NAU3S7PFZQHM \
  AWS_SECRET_ACCESS_KEY=HfhFEYv1KiUrMAcs3jCDUSCYmUk6d1UPkcQxpSR4 \
  aws sts get-caller-identity --endpoint-url http://localhost:9900
{
    "UserId": "AIDABJDQJDVSDYOKZX2L",
    "Account": "123456789012",
    "Arn": "arn:aws:iam::123456789012:user/friend01"
}

똑같이 friend01이 돌아온다. 프로세스가 죽었다 살아났는데도 기억한다. friend01의 자격증명은 mincloud의 메모리가 아니라 DynamoDB의 볼륨에 있었기 때문이다. 4편에서 “곧바로 검증 가능한 자격증명"이라던 그 키가, 이제 재시작을 건너뛰고도 검증된다.

공짜였던 게 값을 치르기 시작한다

4편에서 “글로벌이 공짜"라고 했다. 프로세스가 하나고 맵이 하나라, 복제도 지연도 불일치도 없다고. credstore를 밖으로 빼는 순간, 그 공짜가 값을 치르기 시작한다. 이제 자격증명은 네트워크 건너 다른 프로세스에 있다. 그래서 스토어가 안 뜨면 인증이 실패할 수 있고(그래서 restart: on-failure), 조회 하나에 왕복이 든다.

물론 DynamoDB Local은 단일 노드라, 진짜 AWS의 리전 간 복제 지연은 아직 겪지 않는다. 하지만 모양은 이제 맞다. 스토어가 프로세스 밖 독립 서비스가 됐고, 인메모리 맵이라면 못 했을 “여러 mincloud 인스턴스가 한 스토어를 공유"가 이제 가능한 그림이 됐다. 4편이 예고한 그 세계로 한 발 들어선 것이다.

정직하게 덧붙이면, 지금 DynamoDB에는 시크릿이 평문으로 들어간다. mincloud가 던지는 시크릿이 어차피 버려도 되는 가짜라 넘어가는 것이지, 진짜 컨트롤플레인이라면 자격증명을 이렇게 담지 않는다. 이것도 언젠가 갚아야 할 빚으로 적어 둔다.

인증 골격이 이제 재시작을 견딘다. 다음 편에서는, 이 단단해진 골격 위에 지금까지와 완전히 다른 종류의 서비스를 얹어 본다 — 신원이 아니라 컴퓨트, EC2다. 골격이 정말 값을 하는지 거기서 드러난다.