Discourse 커뮤니티용 Cloudflare R2 설정 방법

Cloudflare R2 버킷은 이미지와 GIF와 같은 정적 자산을 Discourse 커뮤니티에 저장하는 데 사용할 수 있지만, 커뮤니티 백업을 저장하는 데는 사용할 수 없습니다!

소개:

Cloudflare R2 객체 스토리지는 Discourse 포럼의 업로드를 저장하기 위해 Amazon S3의 대안으로 사용할 수 있습니다. 다음 단계는 이를 설정하는 방법을 설명합니다.

설정 단계:

  1. S3 업로드 활성화: Discourse 설정에서 S3 업로드를 활성화하는 상자를 선택하세요.
  2. S3 액세스 키 ID: R2 스토리지 버킷의 API 키 ID를 입력하세요. 이는 버킷에 대한 API 토큰을 생성할 때 제공된 ID입니다.
  3. 비밀 액세스 키: 스토리지 버킷에 대한 액세스 권한을 부여하는 API 토큰을 생성할 때 제공된 비밀 키를 입력하세요. 중요: 이 비밀 키는 한 번만 표시되므로 안전하게 백업해 두세요.
  4. S3 리전: R2의 경우 리전 값은 무엇이든 입력해도 상관없습니다.
  5. S3 업로드 버킷: R2 스토리지 버킷의 이름을 입력하세요.
  6. S3 엔드포인트: https://xxxxxx.com 형식의 R2 버킷에 대한 S3 API 링크를 입력하세요. 이 링크를 찾으려면 Cloudflare R2 대시보드를 참고하세요.
  7. S3 CDN URL: 버킷의 공개 R2.dev 스토리지 버킷 URL을 입력하세요. 이 URL도 Cloudflare R2 대시보드에서 찾을 수 있습니다.

완료:

이러한 설정이 구성되면 Discourse 포럼이 스토리지로 Cloudflare R2를 사용하도록 설정됩니다.

무료 티어 정보:

Cloudflare의 R2 서비스는 월 10GB의 저장 공간, 100만 건의 업로드, 100만 건의 읽기 작업을 포함하는 무료 티어를 제공합니다.

데이터베이스가 아닌 yml 파일에 설정을 넣으시되, 업로드를 위한 S3 호환 객체 저장소 프로바이더 구성 문서의 예시를 따르시는 것을 권장합니다.

피드백을 남겨주셔서 감사합니다. 이전에 가이드를 꼼꼼히 읽었으며, Cloudflare R2에 대한 조언은 부정확하다고 생각합니다. 해당 기사는 Discourse 커뮤니티가 Cloudflare R2 버킷을 지원하지 않는다고 주장하지만, 실제로는 Cloudflare R2가 S3와 높은 호환성을 갖추고 있어 Discourse 커뮤니티의 이미지 및 파일 업로드와 다운로드를 완벽하게 처리할 수 있습니다. 이는 제 커뮤니티(starorigin.net)에서의 실전 적용을 통해 검증되었습니다.

그 기사가 작성되었을 당시에는 사실이었을 것이라고 추정됩니다.

S3 설정을 UX를 통해 구성하여 데이터베이스에 저장하는 것보다 yml 파일에 넣는 것이 훨씬 좋습니다. 데이터베이스를 새 서버로 복원해 보셨나요?

권장된 방식으로 설정을 마친 후, 해당 토픽을 편집하거나 댓글을 남겨 다른 누군가에게 요청할 수 있습니다.

맞아요, 저는 커뮤니티의 이미지, GIF 및 기타 리소스를 저장하기 위해 Cloudflare R2 스토리지 버킷을 사용하고 있습니다. 이를 통해 커뮤니티 서버에 가해지는 부하를 크게 줄이고 페이지 로딩 속도를 높일 수 있습니다.

Cloudflare R2 버킷은 압축 파일 저장을 지원하지 않기 때문에 커뮤니티 전체를 자동으로 백업해 두는 설정은 하지 않았습니다. 하지만 Cloudflare R2 스토리지에는 커뮤니티의 PDF, 이미지, GIF 및 기타 정적 리소스를 저장할 수 있어 이 부분도 매우 좋습니다.

재미있네요. 이전에 R2를 백업용으로 사용했던 것 같기도 한데, 아마도 제 기억이 맞지 않을 수도 있겠네요.

그래도 권장되는 지침을 따르시되, 백업을 거기에 저장하지 않도록 메모해 두시면 됩니다.

알려주셔서 감사합니다. 해당 부분을 강조하겠습니다.

Cloudflare R2 버킷은 이미지나 GIF와 같은 정적 자산을 Discourse 커뮤니티에 저장하는 데 사용할 수 있지만, 커뮤니티 백업 저장에는 사용할 수 없습니다!

이 게시글을 업데이트하면서 Cloudflare가 정상 작동하기 위해 알아야 할 몇 가지 주의사항이 있었습니다.


1. 리전


이것은 사실이 아니었습니다. "auto"를 사용하거나 제가 선택한 리전을 사용해야 했으며, auto가 더 쉬우므로 auto를 사용하는 것이 좋습니다.
사용 가능한 옵션이 무엇인지 알고 싶다면, 리전 필드에 아무 문자열이나 입력하고 다음을 실행해 보세요:

sudo -E -u discourse bundle exec rake s3:upload_assets

nixos를 사용하는 경우

sudo discourse-rake s3:upload_assets

이 명령을 실행하면 유효한 옵션 목록이 포함된 오류 메시지가 출력됩니다.


2. API 권한


제한된 권한의 API 토큰은 작동하지 않는다는 점도 중요합니다. Admin Read & Write 권한을 사용해야 합니다.
Object Read & Write 권한은 작동하지 않았습니다.

sudo -E -u discourse bundle exec rake s3:upload_assets 실행 시 오류 발생 @Eviepayne

지역을 자동으로 설정하세요.
다음도 설정해야 할 수 있습니다:
DISCOURSE_S3_INSTALL_CORS_RULE: false

위 두 가지를 모두 수행하고 app.yml을 다시 빌드했습니다:

  ## S3 구성
  DISCOURSE_USE_S3: true
  DISCOURSE_S3_REGION: auto
  DISCOURSE_S3_ACCESS_KEY_ID: XXX
  DISCOURSE_S3_SECRET_ACCESS_KEY: XXX
  DISCOURSE_S3_CDN_URL: https://pub-XXX.r2.dev
  DISCOURSE_S3_ENDPOINT: https://XXX.r2.cloudflarestorage.com/XXX
  DISCOURSE_S3_BUCKET: XXX
  DISCOURSE_S3_INSTALL_CORS_RULE: false

또한, API 키가 버킷 전용 키가 아닌 계정 API 키임을 확인했습니다(게시물에 언급된 대로). 또한 내 Discourse 인스턴스는 다음을 표시합니다:

그리고 sudo -E -u discourse bundle exec rake s3:upload_assets를 실행한 후 다음이 표시됩니다:

`/root` is not writable.
Bundler will use `/tmp/bundler20250410-2363-zj2g6x2363' as your home directory temporarily.
Installing CORS rules...
skipping
rake aborted!
Seahorse::Client::NetworkingError: Empty or incomplete response body (Seahorse::Client::NetworkingError)
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/seahorse/client/plugins/raise_response_errors.rb:17:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-s3-1.182.0/lib/aws-sdk-s3/plugins/sse_cpk.rb:24:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-s3-1.182.0/lib/aws-sdk-s3/plugins/dualstack.rb:21:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-s3-1.182.0/lib/aws-sdk-s3/plugins/accelerate.rb:43:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/plugins/checksum_algorithm.rb:169:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/plugins/jsonvalue_converter.rb:16:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/plugins/invocation_id.rb:16:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/plugins/idempotency_token.rb:19:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/plugins/param_converter.rb:26:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/seahorse/client/plugins/request_callback.rb:89:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/plugins/response_paging.rb:12:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/seahorse/client/plugins/response_target.rb:24:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/plugins/telemetry.rb:39:in `block in call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/telemetry/no_op.rb:29:in `in_span'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/plugins/telemetry.rb:53:in `span_wrapper'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/plugins/telemetry.rb:39:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/seahorse/client/request.rb:72:in `send_request'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-s3-1.182.0/lib/aws-sdk-s3/client.rb:12654:in `list_objects_v2'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-s3-1.182.0/lib/aws-sdk-s3/bucket.rb:1513:in `block (2 levels) in objects'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/plugins/user_agent.rb:69:in `metric'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-s3-1.182.0/lib/aws-sdk-s3/bucket.rb:1512:in `block in objects'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/resources/collection.rb:101:in `each'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/resources/collection.rb:101:in `each'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/resources/collection.rb:101:in `block in non_empty_batches'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/resources/collection.rb:52:in `each'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/resources/collection.rb:52:in `each'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/resources/collection.rb:52:in `block in each'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/resources/collection.rb:58:in `each'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/resources/collection.rb:58:in `each'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/aws-sdk-core-3.219.0/lib/aws-sdk-core/resources/collection.rb:58:in `each'
/var/www/discourse/lib/tasks/s3.rake:14:in `map'
/var/www/discourse/lib/tasks/s3.rake:14:in `existing_assets'
/var/www/discourse/lib/tasks/s3.rake:24:in `should_skip?'
/var/www/discourse/lib/tasks/s3.rake:36:in `upload'
/var/www/discourse/lib/tasks/s3.rake:197:in `block (2 levels) in <main>'
/var/www/discourse/lib/tasks/s3.rake:197:in `each'
/var/www/discourse/lib/tasks/s3.rake:197:in `block in <main>'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/rake-13.2.1/exe/rake:27:in `<top (required)>'
/usr/local/bin/bundle:25:in `load'
/usr/local/bin/bundle:25:in `<main>'
Tasks: TOP => s3:upload_assets
(See full trace by running task with --trace)

엔드포인트에서 버킷 이름을 제거해야 할 것 같습니다.
끝의 /xxx를 제거해서 .com만 남도록 해야 합니다.

재구축을 진행할 것이며 명령어를 다시 실행하겠습니다. 이 문제를 도와주셔서 감사합니다!

현재 제 app.yml은 다음과 같습니다:

  ## S3 Configuration
  DISCOURSE_USE_S3: true
  DISCOURSE_S3_REGION: auto
  DISCOURSE_S3_ACCESS_KEY_ID: XXX
  DISCOURSE_S3_SECRET_ACCESS_KEY: XXX
  DISCOURSE_S3_CDN_URL: https://pub-XXX.r2.dev
  DISCOURSE_S3_ENDPOINT: https://XXX.r2.cloudflarestorage.com
  DISCOURSE_S3_BUCKET: XXX
  DISCOURSE_S3_INSTALL_CORS_RULE: false

모든 내용이 정확하다고 생각합니다.
CDN_URL(https://pub-xxx.r2.dev)이 익명 사용자도 에셋을 볼 수 있도록 공개 읽기(public read) 접근 권한이 설정되어 있는지 확인해 주세요.
브라우저 개발자 도구(dev tools)에서 무슨 일이 일어나고 있는지 확인할 수 있습니다. 권한이 잘못 설정되어 있으면 네트워크 탭에 403 오류와 빨간색으로 표시된 요청이 여러 개 표시됩니다.

네, 맞습니다:

이 설정이 맞나요:

그렇게 할 수는 있지만, 권장되는 방식은 아니며 문제가 발생할 수 있습니다.
이미 도메인을 보유하고 있고 Cloudflare가 이미 DNS를 담당하고 있다고 가정하면:

Cloudflare는 해당 도메인에 대해 자동으로 프록시를 처리하고 캐싱을 수행합니다.
이후 CDN_URL을 해당 사용자 지정 도메인으로 변경할 수 있습니다.

아, 커스텀 도메인을 버킷에 연결해야 하나요?

S3 버킷 설정에는 공개 액세스 설정이 있습니다.
이것에 고유한 서브도메인을 지정하세요. (Cloudflare이 DNS 레코드를 자동으로 생성해 주며, 프록시 및 캐싱도 수행합니다.)

내가 맞은 것 같나요?

Cloudflare R2로 백업이 작동하도록 설정하셨나요? 그리고 Cloudflare R2로 백업이 가능하다면, 로컬과 Cloudflare R2 모두에 백업하도록 설정할 수 있을까요?

또한 모든 에셋을 업로드하는 스크립트가 로컬의 에셋을 삭제(저장 공간 확보)하는 것인가요? 아니면 그 작업을 위해 별도로 수행해야 하는 절차가 있나요?

이 문제로 도움을 주셔서 감사합니다 :slight_smile:

저는 개인적으로 직접 해본 적이 없습니다.
제 포럼은 데이터베이스가 외부에 있고, 포럼이 사용하는 pg_dumps와 다른 백업 전략을 사용하고 있기 때문에 “지원되지 않음” 범주에 속합니다.
들리는 바로는 클라우드플레어에서는 백업이 작동하지 않는다고 하지만, 시도해 볼 수 없도록 막는 것은 아무것도 없습니다.