Carryduo

Carryduo | AWS S3를 이용해 롤 챔피언 이미지 버전 관리

차가운에스프레소 2023. 1. 13. 20:38

1. 개요

- Carryduo를 지속적으로 운영하면서, 이미지 관리에 대한 고민이 생겼다.

- Carryduo는 롤 듀오 데이터를 제공해주는 사이트이므로, 자연스레 롤 챔피언/스킬/티어 등 다양한 이미지들을 사용한다. 총 개수는 대략 1100개 가량 된다.

- 기존에는 그 이미지들을 라이엇에서 제공하는 이미지 url을 그대로 가져와서 사용했었다. (라이엇 이미지 url 예시)

- 롤을 플레이하면서 문득 든 생각이 "만약 라이엇 서버가 다운되면, 우리 사이트에서 보여지는 이미지들이 모두 오류 처리가 나겠구나"였다.

- 그래서 사이트의 안정적인 운영을 위해, 이미지를 사이트 자체적으로 관리할 필요성을 느껴, AWS S3에 라이엇 이미지를 자체적으로 저장하고 이를 FE에 제공하는 방식을 도입하기로 하였다.

 

* S3 관련 내용만 확인한다면, 4번 목차부터 확인하면 됩니다.


2. 작업 전 고려 사항

- 단순하게 라이엇에서 제공하는 이미지 파일들을 AWS S3에 저장해서, 이를 고정적으로 FE에 전달하면 작업이 편하겠지만, 라이엇은 주기적으로 롤 챔피언 업데이트, 리메이크, 리워크 패치를 진행한다.

- 즉, 이러한 패치 업데이트에 대응해서 주기적으로 AWS S3에 저장된 이미지들을 변경해줘야 했다.

- 이를 고려해, Carryduo에서 이미지 파일을 관리하는 프로세스는 다음과 같이 설정했다.

1. 라이엇 패치버전과 Carryduo에서 제공하고 있는 롤 데이터의 패치버전을 비교한다.
2. 라이엇 패치버전이 Carryduo에서 제공하는 패치버전보다 상위 버전이면, AWS S3에 새 패치버전 이미지를 저장한다.
3. 새 패치버전 이미지를 저장하면, 이전 패치버전 이미지는 삭제한다.
* 위 프로세스를 이탈할 상황에는 default 이미지를 삽입하여, 사이트에서 이미지 오류가 발생하지 않도록 한다.

- 이하 내용은 위 프로세스 순서에 따라 서술된다.


3. 라이엇 패치버전과 Carryduo의 패치버전 비교

 - 롤과 관련한 대부분의 정보를 제공하고 있는 라이엇 API에서 여태까지 제공한 롤 패치버전들을 JSON 형식으로 제공하고 있다.(라이엇 API 공식문서)

- 다음 코드와 같이, axios를 이용해 라이엇에서 제공하고 있는 최신 패치버전과 현재 Carryduo에서 제공하고 있는 버전을 조회해서 그 값 크기를 비교했다.

- 한편, 라이엇 패치버전은 12.23 ->  13.1 -> 13.2 ... -> 13.9 -> 13.10 과 같은 순서로 패치 업데이트를 진행한다. 즉, 13.10이 13.9보다 높은 패치버전인 것이다. 코드 상에서는 13.9가 더 높은 값이기 때문에, 이를 처리해주기 위한 로직을 구현했다. 소수점 상위 숫자를 먼저 비교하고, 이후에 소수점 이후 숫자를 비교하는 것이다.

- 아래 함수에서 return으로 받는 version은 새 패치버전 이미지를 요청하는 데, oldVersion은 이전 패치버전 S3 폴더를 삭제하는데 활용된다.

exports.checkVersion = async () => {
    try {
        let param, version, oldVersion
        // 라이엇에서 제공하는 패치 버전 조회
        const riotVersion = await axios('https://ddragon.leagueoflegends.com/api/versions.json')
        // 라이엇에서 제공하는 최신 패치버전
        const recentRiotVersion = riotVersion.data[0].slice(0, 5)
        // Carryduo에서 제공하는 패치버전 조회
        const originData = await findVersion_combination_service()
        const dbVersionList = getRecentDBversion(originData)

        const dbVersion = dbVersionList[0]

        // 패치버전 크기 비교
        const recentRiotVersion_year = Number(recentRiotVersion.split('.')[0])
        const dbVrsion_year = Number(dbVersion.split('.')[0])
        if (recentRiotVersion_year > dbVrsion_year) {
            param = 1
            version = recentRiotVersion
            oldVersion = dbVersion
        } else if (recentRiotVersion_year === dbVrsion_year) {
            const recentRiotVersion_week = Number(recentRiotVersion.split('.')[1])
            const dbVersion_week = Number(dbVersion.split('.')[1])
            if (recentRiotVersion_week > dbVersion_week) {
                param = 1
                version = recentRiotVersion
                oldVersion = dbVersion
            } else if (recentRiotVersion_week === dbVersion_week) {
                param = 0
                version = recentRiotVersion
                oldVersion = dbVersion
            } else {
                param = 2
                version = recentRiotVersion
                oldVersion = dbVersion
            }
        } else {
            param = 2
            version = recentRiotVersion
            oldVersion = dbVersion
        }
        if (version[version.length - 1] === '.') {
            version = version.slice(0, -1)
        }
        if (oldVersion[oldVersion.length - 1] === '.') {
            oldVersion = oldVersion.slice(0, -1)
        }
        
        // 패치버전 비교 상태에 따라 param과 그에 따른 패치버전들을 retrun
        return { param, version, oldVersion }
    } catch (err) {
        logger.error(err, { message: '-from checkVersion' })
        return err
    }
}

4. 새 패치버전일 경우, AWS S3에 새 패치버전 이미지 저장

4-1. 라이엇 API에 새 패치버전에서 제공하는 모든 챔피언 이미지 요청하기

- 위와 마찬가지로, 라이엇 API에서는 롤의 모든 챔피언에 대한 정보를 JSON 형식으로 제공한다. 이 때, 패치버전이 paramater로 활용된다.

- Carryduo에 필요한 이미지는 챔피언 이미지 두 종류, 챔피언의 스킬/패시브 이미지였다.

- 이미지 파일의 데이터를 저장하기 위해서는 이미지의 buffer 값을 저장해야하는데, 이를 라이엇 API로부터 응답받기 위해서는 다음과 같은 순서가 필요하다.

1. 라이엇이 제공하는 모든 챔피언별로 챔피언 이미지, 스킬/패시브 이미지 paramter 값을 뽑는다.
2. 라이엇에서 이미지를 제공하는 url에 해당 parameter 값으로 요청을 보내, 이미지 파일의 buffer 값을 받는다.

- 예시 코드는 다음과 같다. fetch를 이용할 수도 있지만, node.js에서 fetch를 사용하기 위해서는 node-fetch 패키지를 다운받아야 하며, 이미 기존에 axios를 사용하고 있었으며, 이것이 코드 가독성이 더 좋다고 느껴 axios를 활용했다.

// 1. 라이엇에 모든 챔피언에 대한 정보를 요청하여, 챔피언 상세정보 조회를 위한 parameter 뽑기.
const riotResponse = await axios(`https://ddragon.leagueoflegends.com/cdn/${version}.1/data/ko_KR/champion.json`)
const riotChampList = Object.keys(riotResponse.data.data)
 
// 2. 챔피언 상세정보 조회 parameter로 챔피언 상세정보 요청
for (let i = 0; i < riotChampList.length; i++) {
const champName = riotChampList[i]
const originData = await axios(`https://ddragon.leagueoflegends.com/cdn/${version}.1/data/ko_KR/champion/${champName}.json`)

// 3. 이미지 파일 데이터 요청을 위한 paramter 뽑기
const champCommonImgKey = originData.data.data[`${champName}`].image.full
const champMainImgKey = `${champName}_0`

// 4. 챔피언 기본 이미지 요청 url에 챔피언 이미지 paramter를 삽입하여 요청.
const champCommonImgData = await axios({ url: `http://ddragon.leagueoflegends.com/cdn/${version}.1/img/champion/${champCommonImgKey}`, responseType: 'arraybuffer' })
const champMainImgData = await axios({ url: `http://ddragon.leagueoflegends.com/cdn/img/champion/loading/${champMainImgKey}.jpg`, responseType: 'arraybuffer' })

// 5. 챔피언 이미지별 buffer 데이터
const champCommonImg = champCommonImgData.data
const champMainImg = champMainImgData.data
}

4-2. 이미지 S3에 업로드하기

1) S3 버킷 생성

- AWS S3는 정적 데이터 관리를 위해 사용되는 툴이다.

- S3에 저장되는 데이터들은 Object 개념으로 관리되는데, 이 때 Object들에 대한 엑세스 권한을 Bucket 차원에서 ACL, , IAM, Bucket Policy로 설정할 수 있다.

S3에 대한 엑세스 권한은 ACL, BUCKET POLICY, IAM을 통해 조절할 수 있다. 관련해서는 다음 링크를 참조해보면 좋다.
요약하자면, 다음과 같은 엑세스 권한 상황에서 각 방법을 이용하면 좋은 듯 하다.

- BUCKET POLICY: 버킷 단위의 엑세스 권한 제한
- ACL: 객체 단위의 엑세스 권한 제한
- IAM: 사용자 단위의 엑세스 권한 제한

- 필자는 현재 시점에서는 Bucket에 대한 다른 AWS, IAM 계정에 엑세스 권한을 부여할 필요가 없으며, 오직 Object인 이미지 파일들에 대한 불특정 유저들의 엑세스 권한만 부여하면 됐다.

- 그래서 필자는 ACL를 활용하여 Object들에 대한 read 엑세스 권한만 활성화시켰다.(Bucket Policy에서 버킷에 대한 모든 read 엑세스 권한만 활성화시켜도 된다.)

- 위와 같은 맥락으로 S3 버킷을 생성한 흐름을 보자면 다음과 같다.

 

1. 버킷 이름 작성
2. ACL 활성화 여부: ACL로 모든 유저의 read 권한을 활성화 할 것이므로, 활성화한다.
3. 퍼블릭 엑세스 설정: 모든 유저의 엑세스에 대한 ACL을 수정할 것이므로, 1,2(ACL 관련)는 선택 해제, 3,4(Policy 관련)는 선택하였다.
4. ACL 설정: 모든 유저에 대한 읽기 권한만 활성화했다. 이제 read ACL이 활성화된 객체에 대해서 모든 유저들이 Object 조회가 가능하다.

 

2) aws-sdk를 이용해 nodeJS 환경에서 S3에 파일 업로드하기

- nodeJS 환경에서 S3를 조작하기 위해서는 aws-sdk를 이용할 수 있다.

(1) S3에 접근하기

const aws = require('aws-sdk')
aws.config.update({ region: 'ap-northeast-2' })
const s3 = new aws.S3({ apiVersion: '2006-03-01' })

- 기본적으로 코드 상에서 aws 서비스에 접근하기 위해서는 접근하기 위한 계정의 access_key_id와 secret_access_key가 필요한데, 위 코드의 config에서는 이것이 없는 것을 확인할 수 있다.

- 이는 aws-sdk 모듈 자체적으로 .getenv()를 통해 프로젝트에 내장된 env에 접근하기 때문에, 굳이 config에서 이를 서술할 필요가 없는 것이다.(출처)

- 한편, region이 설정되어 있는데, 이는 추후에 설명될 aws-sdk 문법 중 하나인 await .promise()를 사용하는 중 간혹 region이 잘못 요청되는 경우를 방지하기 위해 설정해주었다.

(2) S3에 파일 업로드하기

        const params = {
            Bucket: `${process.env.BUCKET}/${version}`,
            Key: `${champMainImgKey}.jpg`,
            Body: champMainImg,
            ACL: 'public-read',
            ContentType: 'image/jpeg',
        }

        s3.upload(params, (err, result) => {
            if (err) return err
            return
        })

- s3에 파일을 업로드할 때는 upload메소드를 이용하면 된다. 이 때, 업로드할 파일의 경로, ACL, ContentType, 객체 key 값 등을 param으로 설정해주어야 한다.

  • Bucket: 파일을 저장할 S3 Bucket 이름
  • Key: 파일을 저장할 object key의 이름
  • Body: Key에 저장할 데이터(axios로 받은 데이터는 arrayBuffer 형식이다)
  • ACL: 설정할 ACL의 속성(모든 유저가 READ만 가능하게 할 것이므로, public-read로 설정했다)
  • ContentType: arrayBuffer 형식의 데이터를 ContentType을 지정하지 않으면 application/octet-stream 형태로 지정이 되는데, 이럴 경우, 파일이 열람되는 것이 아니라 default로 다운로드를 하게 된다. 이에, 이미지 파일에 맞게 image/png와 image/jpeg로 설정하였다.

5. 새 패치버전일 경우, 이전 패치버전 S3 폴더 삭제하기

- S3는 UI 상으로는 폴더와 폴더 안의 파일 개념으로 보이지만, 사실 폴더/파일 구분없이 S3 버킷에 들어있는 모든 것들은 다 Object로 간주된다.

- 이에, S3에서 특정 폴더를 삭제하기 위해서는 해당 폴더(Object)만을 삭제하는 것이 아니라, 하위 경로의 모든 Object들을 삭제해주어야 한다.

- 한편, S3에서 listObjects 메소드를 통해 조회할 수 있는 최대 Key는 1000개인 것으로 파악된다. MaxKeys라는 parameter를 listObjects에 삽입할 수 있는데, 삽입되지 않는다면 default로 1000이며, 1000 이상 숫자는 적용되지 않는 것을 확인할 수 있었다.

- Carryduo에서 한 패치버전 S3 폴더에 저장되는 이미지는 약 1100개 이상이고, 롤 패치가 지속적으로 이루어진다면, 개수는 더 늘어날 것이다.

- 이에, 필자는 다음과 같은 프로세스로 이전 패치버전의 S3 폴더를 삭제시키는 로직을 구현했다.

1. 패치버전의 S3 폴더에 있는 Object를 조회한다.
2. Object의 Key값을 paramater로 활용하여, 삭제시킨다.
3. 패치버전의 S3 폴더에 남은 Object가 있는지 한번 더 조회한다.
4. 남은 Object가 있다면 함수를 재실행하고, 없다면 마무리한다.

- 코드 상으로는 다음과 같다.

exports.deleteOutdatedS3Bucket = async (oldVersion) => {
    try {
    // 버킷에 들어있는 Object 조회
        const data = await getObjectsFromS3Bucket(oldVersion)
        
        // Object Key 값을 params에 넣어 delete 실행
        for (let i = 0; i < data.Contents.length; i++) {
            const params = {
                Bucket: `${process.env.BUCKET}`,
                Key: data.Contents[i].Key
            }
            s3.deleteObject(params, (err, result) => {
                if (err) return err
            })
        }

// 남은 Object 체크
        const status = await getObjectsFromS3Bucket(oldVersion)

// 남은 Object가 없다면 종료, 있다면 함수 재실행
        if (status.Contents.length !== 0) {
            logger.info(`${oldVersion} 폴더 삭제를 재실행합니다`)
            await this.deleteOutdatedS3Bucket(oldVersion)
        } else {
            return
        }
    } catch (err) {
        logger.error(err, { message: '- from deleteOutdatedS3Bucket' })
        return err
    }
}

// 버킷에 들어있는 Object 조회하는 함수
async function getObjectsFromS3Bucket(oldVersion) {
    return await s3.listObjectsV2({ Bucket: `${process.env.BUCKET}`, Prefix: `${oldVersion}/` }).promise()
}

-  여기서 유의할 문법은 aws-sdk의 await .promise() 문법이다. aws-sdk는 aws에 요청을 보내는 모듈이기 때문에, 상황에 따라 비동기 처리를 해주어야 한다.

- 이를 위해 필자는 단순히 await만 붙였는데, 적용되지 않는 것을 확인할 수 있었다.

- aws-sdk에서 비동기 처리를 위해서는 이것이 제공하는 .promise()를 사용해야 했다.

- await .promise()와 관련되어 두 가지 오류를 만날 수 있었는데, 다음과 같다.

  • AuthorizationHeaderMalformed: The authorization header is malformed; the region 'us-east-1' is wrong; expecting 'ap-northeast-2

    => aws confg 당시에 region을 설정해주지 않았을 때 발생한다.

  • Cannot read properties of undefined (reading 'push')

   => promise()를 사용하는데, 또 다른 콜백함수가 코드에 포함되어 있어서 발생했다. 예시 코드는 다음과 같다.

   => 이미 promise() 처리를 했는데도 또 다른 콜백함수를 호출해서 발생한 문제인 것으로 추론된다.

await s3.listObjects(params, (err, data) => { 
if(err) return err
}).promise()

6. 출처

- Riot API

 

Riot Developer Portal

League of Legends Developer API Policy Before you begin, read through the Terms of Use and Legal Notices. Developers must adhere to policy changes as they arise. When developing using the API, you must abide by the following: Products cannot violate any la

developer.riotgames.com

- AWS S3

 

[AWS] 📚 S3 개념 정리 & 버킷 생성 · 권한(Policy / ACL) 설정하기

S3 (Simple Storage Service) 개념 AWS S3는 업계 최고의 확장성과 데이터 가용성 및 보안과 성능을 제공하는 온라인 오브젝트(객체) 스토리지 서비스이다. (참고로 S 앞글자가 3개라서 S3 이라고 한다.) 쉽

inpa.tistory.com

- AWS S3 환경 자격 증명(env)

 

환경 변수에서 가져온 자격 증명 사용 - AWS SDK for PHP

이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.

docs.aws.amazon.com

- AWS S3에서 특정 폴더 삭제하기

 

How can I delete folder on s3 with node.js?

Yes, I know. There is no folder concept on s3 storage. but I really want to delete a specific folder from s3 with node.js. I tried two solutions, but both didn't work. My code is below: Solution 1:

stackoverflow.com

- AWS S3 버킷 관리

 

Amazon S3 버킷 생성 및 사용 - AWS SDK for JavaScript

이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.

docs.aws.amazon.com