CloudFront 이미지 CORS 이슈

wwlee94·2022년 10월 29일
1

트러블 슈팅

목록 보기
1/2
post-thumbnail

CloudFront의 도입

기존 서비스의 구조

운영하던 서비스에서는 CDN 도입을 하지 않은 채로 WAS 서버에서 S3에서 파일을 직접 받아 byte 데이터로 내린 후 브라우저 캐싱을 이용하여 이미지를 처리하고 있는 구조로 개발이 되었다.

CloudFront 도입의 필요성

이는 매번 오리진인 S3의 비용을 증가 시킬 뿐 아니라 통신 환경이 안좋거나 origin 리전에 비해 지리적 거리가 멀어지는 경우에 성능 저하와 사용성을 떨어뜨리게 된다.
백엔드 입장에서는 WAS에서 이미지를 처리하게 되었다는 구조 자체가 이미지 처리할때 서버의 자원을 사용한다는 것이고 특히 어떤 이미지를 불러 올 것인지에 대해 URL로 관리 되지 않고 UID로 저장되고 관리되니 프론트에서 특정 이미지 조회 요청이 들어왔을 경우 DB 커넥션을 이용 + 매번 S3 원본을 조회하게 되는 비효율적인 구조로 개발이 되어있었다.

그리하여 위와 같은 구조를 개선하기 위해, 네트워크 병목을 줄이기 위해, 매번 S3에서 원본 데이터를 가져오는 구조가 아닌 사용자와 서버간의 중간 프록시 서버의 도입이 필요하여 운영하는 서비스에 CloudFront를 도입하게 되었다.

추후 이미지 리사이징시에도 활용 될 수 있기에 반드시 필요하다고 생각했다.

CloudFront 도입 이후 이슈 발생

배경

서비스에 CloudFront를 도입하고 페이지가 로드 되는 이미지 조회시에는 전혀 문제 없이 잘 동작하였다.

운영중인 서비스에서는 프로필 이미지에 대한 크롭 기능이 존재하는데
이미지를 업로드하면 서버에서는 이미지를 일정 시간 동안만 접근이 가능하게 끔 제한하기 위해 CloudFront의 SignedURL을 내려준다.

프론트에서는 받은 이미지 URL을 가지고 크롭 기능을 위해 Canvas를 이용하여 이미지를 불러오게 되는데 이때 프론트에서는 아래와 같은 에러가 발생하며

Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.

최종적으로 아래와 같이 blocked by CORS policy 이슈가 발생한다.

흔히들 발생하는 CORS는 보통 S3에서 Permission 관련 정책 이슈 때문에 발생하는 케이스가 많은데 Devops팀에 요청하여 확인해보니 아래처럼 정상적으로 관련 설정을 모두 했음에도 cors 이슈가 발생했다.

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET",
            "HEAD"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": [
            "x-amz-server-side-encryption",
            "x-amz-request-id",
            "x-amz-id-2"
        ],
        "MaxAgeSeconds": 3000
    }
]

기본적인 CORS 관련 설정은 아래 블로그에서 자세히 설명 되어있다.
https://solitas0817.tistory.com/3

원인 파악

에러 메시지에 보이듯이 Access-Control-Allow-Origin 관련 헤더가 없어서 발생한다는 뜻인데 분명 S3와 CloudFront 단에서 접근 권한 관련 설정을 모두 했다.

원인 파악을 위해 테스트 환경에서의 요청 헤더 정보를 가지고 이미지를 Postman으로 요청값을 바꿔가며 조회 해봤는데 결과는 다음과 같았다.
2번의 테스트 모두 CloudFront에 캐싱되기 전 이미지를 가지고 테스트했다.

  1. Origin 정보가 없는 경우

  2. Origin 정보가 있는 경우

결과는 다음과 같았다.
클라이언트의 요청 헤더에 Origin 헤더가 있는 경우엔 응답 헤더에 Access-Control-Allow-Origin 헤더를 내려주었고
Origin 헤더가 없는 경우에는 Access-Control-Allow-Origin 헤더를 내려주지 않았다.

더욱 난감했던 것은 테스트 시점에 CloudFront에서 아직 캐싱 되지 않는 이미지로 테스트를 하였는데
Origin을 한번 누락되서 요청하여 CloudFront에서 캐싱된 경우는 캐싱이 만료될 때까지 이후 요청에 Origin을 넣더라도 모두 응답 헤더에 Access-Control-Allow-Origin 가 내려오지 않았다.

CloudFront단 설정에 Origin 헤더에 대해서도 캐싱이 포함된 것 같았다.

어떻게 해결하면 좋을까?

처음엔 아래와 같이 생각했다.
그러면 프론트에서 조회 할 때 매번 Origin 헤더를 설정해서 요청하면 되는 거 아닐까?

생각해보면 정말 간단하다.
없으니까 채워서 넣으면 그만 아닌가? 싶었다.

하지만, 프론트 단에서 이미지를 로드할 때 <img src=""> 형식이라던지 CSS의 background-image: url(""); 형식이라던지로 첫 이미지를 로드하고 있는 상황이라 헤더 컨트롤이 어렵다.

게다가 크롬 브라우저 기준으로 위와 같은 형식으로 이미지 요청시에 Sec-Fetch-Mode 라는 헤더에 no-cors라는 설정 값을 자동으로 세팅해주어 요청한다. (img 태그는 crossorigin attribute 사용 안한 경우)

Sec-Fetch-Mode

no-cors로 요청이 되면, 브라우저는 cors 관련 설정을 확인하지 않겠다는 의미이다.
그래서 Access-Control-Allow-Origin 가 있든 말든 기본적으로 이미지 조회시에는 전혀 문제가 없었던 것이다.

반대로 canvas를 그리는 동작의 경우에는 아래와 같은 설명이 되어있다.

Security and tainted canvases

요약하자면 CORS를 통하지 않고, 다른 origin으로 부터 가져온 데이터들은 더 이상 안전하지 않은 것으로 여겨져, canvas 이미지에서 데이터를 가져오려는 어떤 시도든 exception이 발생한다고 설명이 되어있다.

즉, canvas로 이미지 로딩하는 경우는 반드시 crossorigin 속성이 적용되어 Sec-Fetch-Mode가 cors로 요청이 되므로 반드시 Access-Control-Allow-Origin 관련 응답이 내려져야 브라우저에서 정상적인 요청이라 판단하여 이미지를 불러올 수 있다는 의미다.

해결 방법

아주 친절하게 AWS 문서에 잘 적혀있다.

왜 안되는지도 파악이 안되었기 때문에 위 문서 찾는데도 오래걸렸다..

CloudFront의 경우 오리진(S3)으로 요청을 보낼때 항상 Origin 헤더를 추가하도록 구성이 가능하다고 적혀있다.

Origin 헤더가 포함되므로 응답에 Access-Control-Allow-Origin 헤더 또한 받을 수 있다.

추후에 추가로 알게 된 내용으로 Canvas를 이용하다 직면하는 CROS 이슈의 경우 Origin 헤더가 이미지 로드 시점에 캐싱되어 버리는 상황이 발생해서 Canvas를 이용할때 Origin 헤더를 보내도 이전 응답이 캐싱되어 Access-Control-Allow-Origin 헤더를 못 받는 상황을 추가로 인지했다.
이런 경우엔 Origin 헤더를 CloudFront 캐싱 키 정책에 포함하면 된다.

위 설정을 적용 시키면 위에서 했던 모든 동작이 해결이 된다.

느낀점

기존 서버 기반 이미지 URL에서 CloudFront로 마이그레이션을 한 뒤
CORS 에러가 발생 안하고 이미지 조회는 잘 되는데 정상적으로 동작하던 크롭 기능이 안된다고 프론트 개발자에게 연락이 왔을 때는 처음에는 엄청 당황스러웠다.

나도 재현하면서 왜 안되는지 몰랐기 때문에,, 😭

하지만, 백엔드 관련 개발하면서 인프라적인 설정 내용이나 브라우저의 상세 동작, MDN Web Docs의 소중함, CORS 관련 내용 등등 백 분야뿐 아니라 다른 분야측의 내용도 깊이 알아갈 수 있는 기회이자 경험이었다고 생각한다.

두서 없는 글이고 경험을 기반으로 작성 된 글임을 감안해주시고 봐주시기 바랍니다.
감사합니다. 🙇🏻‍♂️

profile
우엉이의 개발 블로그 📝

0개의 댓글