호스티드에서 셀프호스팅으로 마이그레이션: 과거 업로드가 여전히 discourse 인프라를 참조함

확인 완료: Images lost when migrating to self-hosting, posts:rebake는 아무런 좋은 결과를 내지 않습니다.

문제
공식 지침을 따라 Lightsail 인스턴스를 생성하고, Discourse UI에서 데이터베이스를 다운로드하여 적용함으로써 80% 정도까지 진행했습니다. 기존 버전은 유지하면서 셀프호스팅 인스턴스로 전환하려는 것이었습니다.

이전 포럼의 라이브 복사본을 확보한 후 이미지 전환을 시작했습니다. 이를 위해 먼저 구독을 취소하고 이미지를 가져와 마이그레이션했습니다.

새 이미지는 셀프호스팅 인스턴스에 업로드되므로, 전환 날짜 이전에 호스팅 인스턴스에서 업로드된 것만 업로드하면 됩니다. 즉, 이미지가 포함된 데이터베이스 덤프는 이미 마이그레이션을 완료했으므로 만료되었기 때문에 사용하지 않았습니다.

이 시점과 관련하여 두 가지 동작을 관찰했습니다.

  1. 백업(특히 SQL 덤프)에 참조된 리소스는 Discourse 인프라를 가리킵니다.
  2. 백업 이후에 생성된 참조 리소스, 예를 들어 새 게시물의 이미지는 올바르게 참조되어 우리의 인프라에서 발견됩니다.

결과적으로, 동일한 해시로 평가되는 리소스를 다시 업로드하면 Discourse 인프라에 링크됩니다. 예를 들어, 동일한 파일로 파비콘을 수정하려는 시도는 작동하지 않습니다. 그러나 다른 임의의 이미지를 업로드하면 작동합니다.

현재 상태
제가 이해하기로는, upload://<X>는 b62 디코딩(그리고 sha1?)을 거쳐 폴더 public/uploads에 매핑됩니다. 우리는 이 이미지들을 모두 가지고 있습니다:

Discourse 팀이 제공한 덤프에는 default/original/1X가 포함된 지퍼 파일이 있으며, 현재 /var/www/discourse/public/uploads/default/original/1X에서 볼 수 있습니다. 후자의 폴더에는 현재 329개의 항목이 포함되어 있고, 제공된 덤프에는 249개의 항목이 포함되어 있었습니다—이것은 저에게는 좋은 신호입니다.

즉, 폴더에서 업로드를 직접 찾을 수 없더라도 데이터는 발견 가능해야 합니다. 저는 이 관계를 이해하여 매핑을 어떻게든 수정하고자 합니다. 처음에는 단순한 문자열 치환처럼 보였고, 일부 이미지에서는 실제로 그렇게 작동했습니다. 그러나 일부는 이제 이전에 접근할 수 없는 이미지가었던 곳에서 투명한 PNG(transparent.png)로 대체되었습니다.

리베이킹(rebake)이 실패했다면, remap을 시도하여 Discourse 인프라에 대한 모든 참조를 검색하고 상대 링크로 대체해야 합니다.

Richard님, 감사합니다!

정리하자면, 이 방법을 사용했습니다: Replace a string in all posts

아래 명령어를 사용했습니다:

rake posts:remap["find","replace","string",true]

구체적으로는 다음과 같이 실행했습니다:

rake posts:remap[
  "https://cdck-file-uploads-europe1.s3.dualstack.eu-west-1.amazonaws.com/standard21/uploads/everviz/",
  "/uploads/default/"
]

상대 경로 대신 사용할 수 있는 대안은 "https://forum.everviz.com/uploads/default/" 입니다.

말씀하신 것이 이 상대 링크를 의미하는 것인가요?

e: '/'로 시작하는 상대 URL 수정

원줄 명령어(Oneliner):

rake posts:remap["https://cdck-file-uploads-europe1.s3.dualstack.eu-west-1.amazonaws.com/standard21/uploads/everviz/", "/uploads/default/"]

나한테는 괜찮아 보여! 앞에 슬래시를 추가해야 해요

/uploads/default/

호스팅 사이트에서 백업을 수행할 때 모든 업로드 항목 포함 옵션을 확인하셨나요? CDCK에서 호스팅을 사용하셨다면, 모든 업로드 항목을 포함한 백업을 수행하려면 활성화해야 하는 숨겨진 설정이 있었습니다. 현재 해당 설정이 변경되었는지 확실하지는 않지만, 이전 작업을 수행하기 전에 완전한 백업(단순한 SQL 덤프가 아닌)을 확보할 수 있도록 반드시 호스팅 제공업체와 조율하시길 권장합니다.

내 호스팅 제공업체는 Discourse였고, 월정액 요금제를 사용 중이었습니다. 호스팅된 사용자 인터페이스에서는 업로드된 파일을 받으려면 team@discourse.com으로 문의하라고 안내하고 있었습니다. 그러나 그들의 답변은 파일을 받으려면 구독을 취소해야 한다는 것이었습니다.

하지만 앞서 언급했듯이 uploads/original/1X를 받았습니다.

좋은 팁이지만, 이미 이렇게 해봤을 수도 있어요:

root@...:/var/www/discourse$ rake posts:remap["//cdck-file-uploads-europe1.s3.dualstack.eu-west-1.amazonaws.com/standard21/uploads/everviz/","/uploads/default/"]
Are you sure you want to replace all string occurrences of '//cdck-file-uploads-europe1.s3.dualstack.eu-west-1.amazonaws.com/standard21/uploads/everviz/' with '/uploads/default/'? (Y/n)
Y
Remapping
0 posts remapped!

호스팅된 포럼에서 사용되던 링크는 https://europe1.discourse-cdn.com/standard21/uploads/everviz/였어요. 물론 같은 내용이지만, CDN을 통해 게이트웨이된 것뿐입니다. 리맵핑을 시도해 보겠습니다.

1 post remapped.

이 이미지는 꽤 흥미롭네요:

물론 이것은 오늘 실행한 모든 명령어와 여기 게시글 작성 이전의 상태입니다. 몇 가지 rake 작업을 실행하기 전에 Discourse 팀에 이미 전송한 것이었습니다.

그렇게 하셨나요? S3에서 이미지를 다운로드하여 백업에 포함하도록 숨겨진 설정을 활성화해야 합니다. 일반적인 백업에는 이미지가 포함되지 않고, S3 버킷에 있는 이미지 링크만 포함됩니다. 구독을 취소하면 이 설정이 자동으로 트리거되는 것 같지만, 단순히 요청만으로도 해당 설정을 켜준 클라이언트들이 있었습니다. 구독을 취소하시거나 다시 요청해 보셔야 합니다.

그렇게 하고 싶지 않으시다면, S3에서 이미지를 다운로드하고 Discourse 데이터베이스를 그에 맞게 업데이트하는 스크립트를 작성해야 합니다.

취소 요청을 했고 파일을 받았습니다. 다만 discourse 데이터베이스의 원래 백업이 S3 경로를 참조하는 것 같습니다. 기본적으로 필요한 모든 것이 /var/www/discourse/uploads/original/1X에 있습니다.

인스턴스를 채우기 위해 파일과 함께 제공된 SQL 덤프가 아니라 수동으로 다운로드한 SQL 덤프를 사용했습니다. 후자가 이미지에 대한 수정된 경로를 제공할 수 있을 것 같아 걱정했는데, 지금은 그런 경우가 아님을 확인했습니다.

시연을 위해:


![](upload://3Qa5S9sUTcc42dT4EFAbz5K0iJP.gif) = 1aec065017da50538fe5866ae91a6396185234e1.gif

https://forum.everviz.com/uploads/default/original/1X/1aec065017da50538fe5866ae91a6396185234e1.gif

http://cdck-file-uploads-europe1.s3.dualstack.eu-west-1.amazonaws.com/standard21/uploads/everviz/original/1X/1aec065017da50538fe5866ae91a6396185234e1.gif

<img src="https://forum.everviz.com/images/transparent.png" alt="" data-orig-src="upload://3Qa5S9sUTcc42dT4EFAbz5K0iJP.gif" role="presentation" width="1" height="1" style="aspect-ratio: 1 / 1;" loading="lazy">

위와 같은 경우, cdck…에 대한 이전 참조는 단순히 transparent.png입니다. 그럼에도 불구하고 링크를 열어보면 해당 파일이 존재하는 것을 확인할 수 있습니다.

따라서 문제가 발생할 것으로 예상합니다.

제가 raw 게시글로 추정하는, 파일에 포함된 데이터베이스를 함께 사용한 부분에서 ![](upload://3Qa5S9sUTcc42dT4EFAbz5K0iJP.gif)가 로컬 스토리지를 가리킬 것으로 예상합니다. 하지만 누군가 버킷에 있는 이미지에 대한 링크를 명시적으로 붙여넣었다면, 이를 수정하기 위해 조치를 취해야 합니다. 이미지가 존재하고 ‘로컬로 다운로드’ 설정이 켜져 있다면, 해당 버킷의 이미지가 다운로드 설정 기준을 충족하는 한 다운로드됩니다.

샘플의 마지막 <img가 어떻게 생성되었는지는 정확히 알지 못합니다.

로컬 다운로드가 활성화되어 있습니다.

링크된 파일의 경우, ‘공식’ 굿바이 덤프에는 상대 경로가 포함되어 있지 않습니다.

<img src="https://europe1.discourse-cdn.com/standard21/uploads/everviz/original/1X/1aec065017da50538fe5866ae91a6396185234e1.gif" alt="" data-base62-sha1="3Qa5S9sUTcc42dT4EFAbz5K0iJP" ...

이 정확한 파일 참조는 일부 위치에서 cdck…를 가리키고 있습니다.

조금 미친 소리처럼 들릴 수 있지만, 지금 바로 백업을 수행할 수 있습니다. 그런 다음 덤프 파일 자체에서 로컬 경로를 위한 Discourse 인프라에 대한 참조를 제거하고, 그 파일을 다시 업로드할 수 있습니다.

마지막 파일은 게시물을 다시 생성(recook)했기 때문에 Discourse 인프라에서 소스 파일을 더 이상 찾을 수 없어 transparent.png를 참조하고 있을 수 있습니다. 완전한 데이터 손실은 아닌 것 같습니다.

사이트가 라이브 상태라면, 가능한 범위 내에서 rails로 들어가서 문제를 수정하면 됩니다.

하지만 그 <img는 쿡된(cooked) 게시물이지, 원본(raw) 게시물은 아닌가요?

<img 태그는 데이터베이스 덤프에서 가져온 것입니다. cooked 상태라고 추정됩니다. 원본 게시글에서는 b62를 upload://로 참조하고 있습니다.

현재 cooked 상태는 다음과 같습니다:

<img src="https://forum.everviz.com/images/transparent.png" alt="" data-orig-src="upload://3Qa5S9sUTcc42dT4EFAbz5K0iJP.gif" ...

지금까지 rake를 사용하여 missing_uploads를 찾고 수정하거나, remapping을 하거나, 게시글을 다시 rebake하는 작업에서 큰 성과를 내지 못했습니다.

Jay님, 도움 주셔서 감사합니다!

처리(cooked)된 게시글에서 참조된 파일은 정상적으로 작동합니다. 해당 파일에는 문제가 없습니다.

데이터베이스 덤프에서 처리된 게시글을 찾고 있다면, 잘못된 곳을 보고 있는 것입니다.

현재 라이브 사이트가 있으므로, 거기서부터 작업해야 합니다.

생(raw) 게시글에서는 무엇을 보고 계신가요? 해당 게시글을 다시 생성(rebake)한 후, 처리된 게시글에는 기대했던 것과 다른 내용이 무엇으로 표시되나요?

정확히 무엇을 하셨는지, 그리고 게시글(생과 처리된 것)에 어떤 내용이 포함되어 있는지 알지 못하면 도움을 드릴 방법이 거의 없습니다. 데이터가 올바르게 들어 있지 않을 것으로 예상되는 데이터베이스로 시작했기 때문에, 이 토픽은 다른 사람들에게 유용하지 않을 것입니다.

모두가 하지 말라고 한 일을 했습니다: 데이터베이스와 그 덤프 파일을 건드린 것입니다. 현재 대부분은 잘 작동하지만, 다음과 같은 경우를 제외하고는 문제가 없습니다:

<img src="https://forum.everviz.com/images/transparent.png"
alt="image" data-orig-src="upload://npqpp5O0wbL89nR9OXtP7Btu4hc.png"
width="517" height="90" style="aspect-ratio: 517 / 90;" loading="lazy">

b62를 계산하고 그 16진수를 가져오겠습니다.

npqpp5O0wbL89nR9OXtP7Btu4hc = 0x a411c90267cafca7a1cbcd7c8f4f9b8db17e51ba

이제 /var/www/discourse/public/uploads에서 find를 사용해 찾아보세요:

find . -name '*a411c90267cafca7a1cbcd7c8f4f9b8db17e51ba*'
./default/original/1X/a411c90267cafca7a1cbcd7c8f4f9b8db17e51ba.png

맞습니다!


하지만 게시글에서는 왜 transparent.png로 표시되는 것일까요? rake uploads:recover_from_tombstone과 rake posts:rebake를 실행해 보았습니다.


어떻게 이런 상황이 되었을까요?

데이터베이스의 uploads 테이블에서 url 컬럼은 여전히 이미지 소스 URL의 일부로 cdck를 표시하고 있었습니다. 컨테이너 안에서 데이터베이스로 진입했습니다:

postgres psql discourse

그 다음

UPDATE uploads
SET url = REPLACE(
           url, 
           '//cdck-file-uploads-europe1.s3.dualstack.eu-west-1.amazonaws.com/standard21/uploads/everviz/', 
           '/uploads/default/'
         )
WHERE url LIKE '//cdck-file-uploads-europe1.s3.dualstack.eu-west-1.amazonaws.com/standard21/uploads/everviz/%';

이 작업은 유망한 결과를 보여주었고, 대부분의 원본 이미지와 썸네일이 다시 나타났습니다.

한 단계 더 나아가기: 덤프 파일 수정

Discourse는 상태가 없는(stateless)* 시스템이며, 우리가 신경 써야 할 것은 데이터베이스 안에 있는 것뿐이라는 전제 하에 진행했습니다. rake 태스크나 ruby를 가지고 이 일을 하려 하지 않았습니다. 왜냐하면 저는 이 둘 모두와 Discourse 내부 구조에 익숙하지 않기 때문입니다. 저는 그저 빠르게 결과를 원했습니다.

*이미지를 포함하고 있는 public 폴더를 제외하고는 그렇습니다. 그럼에도 불구하고 우리가 필요한 모든 것을 가지고 있음을 확인할 수 있습니다.

그래서 UI에서 데이터베이스 사본을 다운로드한 후, VSCode에서 열어 cdck(버킷) 참조와 europe1(CDN의 버킷) 참조를 역순으로 대체했습니다.

역순으로라는 것은 어떤 경우엔 '//…'를, 다른 경우엔 'https://'를 볼 수 있다는 의미입니다. 따라서 파일 전체에 걸쳐 https:가 남지 않도록 먼저 '//…'를 매칭하고 대체해야 합니다.

그 다음 수정된 덤프 파일을 다시 업로드했습니다. 이 모든 것을 까다롭게 만든 부분 중 하나는 base62 단계로, 이는 원시 표현에서 실제 이미지 URL로 가는 것을 조금 더 어렵게 만듭니다.

작업 완료

업로드 테이블의 크기를 두 번 확인한 후, 수백 건의 항목이 누락되어 있음을 발견했습니다. 어떤 단계에서 누락되었는지는 알 수 없습니다. 과거의 데이터베이스 백업과 임시 테이블 간의 기본 SQL 조인을 통해 병합했습니다.

위에서 언급했듯이, 이미지 요청 시 사용되는 URL은 업로드 테이블의 url 열에 저장된 것입니다. Rails 콘솔을 통해 업로드 테이블에 대한 SQL을 실행하여 이러한 CDN 참조를 로컬 도메인으로 다시 매핑했습니다.

왜 rake 태스크를 사용하지 않는가

일부 rake 태스크는 괜찮고, 어떤 조합이든 작동할 수 있을 것입니다. 하지만 현재 동작을 관찰할 수 있고, 원하는 것이 무엇인지, 그리고 어떻게 도달할 수 있는지 알고 있다면, 그 제한은 임의적이라고 생각합니다.

해결책을 발견하는 데 필요한 모든 정보 조각을 제공해 주신 Discourse 팀과 여기의 자원봉사자들에게 감사드립니다. 결국 몇 가지 단계를 거쳐 해결책을 찾았습니다.