Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions src/content/showcase/coolrestore.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
title: 'Cool Restore'
version: 'v1'
description: 'coolrestore는 장애가 난 뒤 Coolify 스토리지를 안전하게 복구하는 CLI 도구입니다.'
publishedAt: '2026-10-05T13:40:00Z'
---

import MissionBox from '../../components/MissionBox.astro';
import DocLink from '../../components/DocLink.astro';

## 1. 개요 (Overview)

coolrestore는 장애가 난 뒤 Coolify 스토리지를 안전하게 복구하는 CLI 도구입니다. S3 호환 스토리지(RustFS 등)에 보관한 Coolify 스토리지 백업(gzip tar 아카이브)이나 로컬 아카이브 파일을 읽어서 대상 디렉터리에 복구합니다. REPL Works 방식으로 개발했고, 상용 서비스 [와이파이 노트](/showcase/wifinote)의 스토리지 복구에 실제로 쓰입니다.

<MissionBox text="먼저 변경 계획을 보고, 명시적으로 확인했을 때만 적용합니다." />

S3 접두사에서 `list`로 아카이브를 찾고, `diagnose`로 접근을 점검한 뒤, `--latest`로 가장 최근 아카이브를 골라 복구합니다. `list`와 `diagnose`는 읽기 전용이라 대상 디렉터리를 건드리지 않습니다.

macOS는 [Homebrew](https://brew.repl.net), Linux는 [APT](https://apt.repl.net) 저장소에서 설치합니다. 소스는 [GitHub](https://github.com/replworks/coolrestore)에 공개되어 있고 MIT 라이선스입니다.

## 2. 사용된 표준 문서 (Documents Used)

coolrestore도 REPL Works 표준 문서와 공통 규칙 [`AGENTS.md`](https://github.com/replworks/coolrestore/blob/main/AGENTS.md)를 따라 개발했습니다. 표준 문서는 저장소의 [`.replworks/`](https://github.com/replworks/coolrestore/tree/main/.replworks) 디렉터리에서 모두 볼 수 있고, 코딩형 AI가 읽는 문서는 다음과 같습니다.

- [PRODUCT_SPEC.md](https://github.com/replworks/coolrestore/blob/main/.replworks/PRODUCT_SPEC.md): 무엇을 만드는가. 입력, 출력, 요구사항과 안전 규칙
- [TECH_STACK.md](https://github.com/replworks/coolrestore/blob/main/.replworks/TECH_STACK.md): 기술 스택과 구현 제약
- [ARCHITECTURE.md](https://github.com/replworks/coolrestore/blob/main/.replworks/ARCHITECTURE.md): 어떻게 동작하는가. 구성 요소, 책임 경계, 불변식
- [TASKS.md](https://github.com/replworks/coolrestore/blob/main/.replworks/TASKS.md): 다음에 무엇을 할지

사람이 읽는 문서는 [`.replworks/docs/`](https://github.com/replworks/coolrestore/tree/main/.replworks/docs)에 둡니다. 사람은 이 프로젝트를 왜 만들었고 무엇이 중요한지를 나중에 잊기 때문에 [`IDEAS.md`](https://github.com/replworks/coolrestore/blob/main/.replworks/docs/IDEAS.md)와 `PITCHING_SCRIPT.md`를 쓰는데, 코딩형 AI는 이 문서를 보지 않아서 실제 제품과 달라질 수 있습니다. 실제로 `IDEAS.md`에는 처음 정한 구현 요구사항이 남아 있는데, 거기 있는 `--dry-run` 옵션은 최종 `PRODUCT_SPEC.md`에서 사라지고 옵션 없이 실행하면 곧 미리보기가 되도록 바뀌었습니다. coolrestore는 판매하는 제품이 아니어서 `PITCHING_SCRIPT.md`는 만들지 않았습니다.

## 3. 적용 워크플로 (Workflow Usage)

```text
대화형 AI (복구 도구의 요구사항과 안전 규칙 정의)
↓
PRODUCT_SPEC.md → TECH_STACK.md → ARCHITECTURE.md (복구 방식과 모드 확정)
↓
코딩형 AI (TASKS.md의 TASK 단위 구현)
↓
Human Review (macOS와 Linux에서 복구 시험, Release)
```

복구 도구는 잘못 만들면 정상 데이터를 덮어쓸 수 있어서 안전 규칙이 제품 정의의 중심입니다. 이 규칙은 코드보다 먼저 문서에 들어갑니다. 실제로 저장소의 첫 PR은 복구 동작과 안전 장치를 다룬 문서 수정이었고, 두 번째 PR이 실행 안전 기준선이었습니다.

- **미리보기가 기본**: 아무 옵션 없이 실행하면 복구 계획만 출력합니다. 대상을 바꾸려면 `--confirm`이 필요하고, `replace` 모드는 `--confirm` 없이는 거부됩니다.
- **외부 경계 검증**: `AGENTS.md`는 외부 시스템에 닿는 코드를 목(mock)만으로 검증하지 않게 합니다. coolrestore가 읽는 S3 호환 스토리지가 바로 외부 경계이고, 실제 RustFS와 NAS 환경에서 복구를 시험했습니다.

## 4. 사용 기술 (Tools Used)

- **Go**: 정적으로 빌드되는 단일 바이너리 CLI
- **AWS SDK**: S3 호환 스토리지 접근 (RustFS 등)
- **golangci-lint, GoReleaser, GitHub Actions**: 정적 검사, 릴리즈 패키징, 자동 검증과 배포
- **RustFS, Tailscale**: 복구 시험에 쓰는 S3 호환 스토리지와 사설 네트워크

## 5. 안전 설계와 배포 (Safety & Release)

### 안전 설계

coolrestore는 애플리케이션 수준 검증이 아니라 인프라 복구를 위한 도구입니다. 그래서 아카이브를 신뢰하지 않는 방향으로 설계했습니다.

- 아카이브 경로가 상위 디렉터리로 벗어나거나 절대 경로이면 거부합니다. 대상 밖으로 나가는 심볼릭 링크, 하드 링크, 일반 파일과 디렉터리 외의 항목도 거부합니다.
- 아카이브 항목을 먼저 검증하고, 통과한 것만 격리된 staging 디렉터리에 풀어 변경 계획을 만듭니다. 미리보기에 나온 계획과 실제로 적용되는 계획은 같은 계산 결과입니다.
- 같은 대상 디렉터리에 대한 복구가 동시에 실행되지 않도록 잠급니다.
- 자격 증명은 명령행 옵션으로 받지 않습니다. 명시적으로 지정한 `--env-file`, 프로세스 환경 변수, AWS SDK 기본 자격 증명 체인에서만 읽고, 현재 디렉터리의 환경 파일은 자동으로 읽지 않으며 셸 문법도 실행하지 않습니다.

복구 모드는 두 가지입니다. `merge`는 없는 파일을 추가하고 같은 경로의 파일을 덮어쓰며 대상에만 있는 파일은 남깁니다. `replace`는 대상을 아카이브 내용과 같게 만들고, 적용 중 실패하면 원자적 rename으로 이전 디렉터리 항목을 되돌립니다. 이 방식은 staging과 대상이 같은 파일시스템일 때만 가능해서, 아니면 변경 전에 거부합니다. `merge`는 실패해도 이미 적용된 변경이 남을 수 있습니다. 이 한계는 숨기지 않고 실패 출력에도 대상 상태를 알려 줍니다. 실행 결과 예시는 README의 [Example](https://github.com/replworks/coolrestore#example)에서 볼 수 있습니다.

### 배포

릴리즈는 GoReleaser로 macOS용 Homebrew와 Linux용 apt에 배포합니다. 검증과 릴리즈 workflow는 저장소에서 직접 볼 수 있습니다: [ci.yml](https://github.com/replworks/coolrestore/blob/main/.github/workflows/ci.yml), [release.yml](https://github.com/replworks/coolrestore/blob/main/.github/workflows/release.yml).

### 상태와 복구

coolrestore 자체는 데이터를 저장하지 않는 CLI라서 백업할 상태가 없습니다. 복구 시험은 Tailscale로 서버, NAS, 개발용 Mac을 연결한 뒤 복구 명령으로 백업된 파일이 모두 있는지 확인하는 방식으로 했고, macOS와 Linux 두 환경에서 모두 진행했습니다. 150MB 정도의 아카이브는 복구 명령이 10초 안에 끝났고, 실행 시간은 파일 양에 따라 달라집니다. 이 시험을 마친 뒤 v1.0.0으로 릴리즈했습니다.

## 6. 학습된 레슨 (Lessons Learned)

파괴적인 작업은 기본이 미리보기여야 합니다. 복구 도구는 실수하면 정상 데이터를 덮어쓰므로, 아무 옵션 없이 실행했을 때 아무것도 바꾸지 않는 것을 제품 정의의 첫 줄에 두었습니다.

실패했을 때의 상태를 문서에 적습니다. `merge` 실패 시 변경이 일부 남을 수 있다는 것을 숨기지 않고, 실패 출력에 대상 상태를 알립니다. 한계를 밝혀 두어야 복구 도구를 믿고 쓸 수 있습니다.

복구 수단은 장애가 나기 전에 시험합니다. 와이파이 노트의 운영에서 필요해진 절차를 도구로 분리했고, 두 운영체제에서 복구를 시험한 뒤에야 v1.0.0으로 릴리즈했습니다.

<DocLink
href="https://github.com/replworks/coolrestore"
text="coolrestore GitHub 저장소 이동"
/>
21 changes: 17 additions & 4 deletions src/data/showcase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ export const showcaseProjects: ShowcaseProject[] = [
featured: true,
order: 3,
},
{
slug: 'coolrestore',
name: 'Cool Restore',
description:
'장애가 난 뒤 Coolify 스토리지를 안전하게 복구하는 CLI 도구입니다.',
tags: ['Tooling', 'CLI'],
lesson:
'복구 도구는 실수하면 정상 데이터를 덮어쓰므로, 아무 옵션 없이 실행했을 때 아무것도 바꾸지 않는 것을 제품 정의의 첫 줄에 두었습니다.',
detailUrl: '/showcase/coolrestore',
github: 'https://github.com/replworks/coolrestore',
featured: true,
order: 4,
},
{
slug: 'claytube',
name: '클래이튜브 (ClayTube)',
Expand All @@ -64,7 +77,7 @@ export const showcaseProjects: ShowcaseProject[] = [
website: 'https://www.palgle.com/claytube/',
github: 'https://github.com/eternops/claytube',
featured: true,
order: 4,
order: 5,
},
{
slug: 'eternops',
Expand All @@ -76,7 +89,7 @@ export const showcaseProjects: ShowcaseProject[] = [
'프로젝트 기억이 세션 기억보다 우월합니다: 일시적인 대화 내역에 의존하지 않고 Git에 명세화된 시스템이 운영의 안정성을 제공합니다.',
detailUrl: '/showcase/eternops',
featured: false,
order: 5,
order: 6,
},
{
slug: 'etern-labs',
Expand All @@ -89,7 +102,7 @@ export const showcaseProjects: ShowcaseProject[] = [
detailUrl: '/showcase/etern-labs',
website: 'https://www.etern.co.kr/labs',
featured: false,
order: 6,
order: 7,
},
{
slug: 'mma',
Expand All @@ -101,6 +114,6 @@ export const showcaseProjects: ShowcaseProject[] = [
'공유 기억의 필수성: 팀과 에이전트가 완벽히 동일한 문서를 참조하지 않으면 오버 스코프와 상충된 구현이 발생합니다.',
detailUrl: '/showcase/mma',
featured: false,
order: 7,
order: 8,
},
];
6 changes: 3 additions & 3 deletions src/site-invariants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ describe('REPL Works content invariants', () => {
});

it('keeps the shared showcase data complete for home and Showcase', () => {
expect(showcaseProjects).toHaveLength(7);
expect(showcaseProjects).toHaveLength(8);
expect(new Set(showcaseProjects.map((project) => project.slug)).size).toBe(
7,
8,
);
expect(showcaseProjects.filter((project) => project.featured)).toHaveLength(
4,
5,
);

for (const project of showcaseProjects) {
Expand Down
Loading