개요
이번 포스팅에서는 public/private s3를 생성하고 각각 어떻게 접근하는지, 그리고 코드로서는 어떻게 구현하는지에 대해 알아보겠다.
IAM User 생성
AWS Root user는 모든 서비스에 대한 권한을 가지고 있기 때문에 at least privilege
의 원칙에 어긋난다. 따라서 IAM User
를 생성해 S3에 대한 접근 권한만을 부여하도록 한다.
Public/Private S3 생성
Public
모든 퍼블릭 엑세스 차단 체크박스를 해제하고 생성한다.
추가로, Bucket Policy
에 다음과 같이 작성하여, url을 통해 bucket에 접근할 수 있도록 한다.
1
2
3
4
5
6
7
8
9
10
11
12
| {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AddPerm",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "[생성한 S3 ARN]/*"
}
]
}
|
private
모든 퍼블릭 액세스 차단 체크박스를 체크하고 생성한다.
구현
config
1
2
3
4
5
6
7
8
9
| import { ConfigService } from '@nestjs/config';
import { config } from 'aws-sdk';
const configService = app.get(ConfigService);
config.update({
accessKeyId: configService.get('AWS_ACCESS_KEY_ID'),
secretAccessKey: configService.get('AWS_SECRET_ACCESS_KEY'),
region: configService.get('AWS_REGION'),
});
|
엔티티
private
1
2
3
4
5
6
7
8
9
10
11
| @Entity()
export class PrivateFile{
@PrimaryGeneratedColumn()
public id: number;
@Column()
public key: string;
@ManyToOne(() => User, (owner: User) => owner.files)
public owner?: User;
}
|
public
1
2
3
4
5
6
7
8
9
10
11
| @Entity()
class PublicFile {
@PrimaryGeneratedColumn()
public id: number;
@Column()
public url: string;
@Column()
public key: string;
}
|
업로드 기능
거의 차이는 없지만 주목할 부분은, public에서는 해당 entity에 location을
저장하고 private에서는 해당 객체의 소유자를 저장하는 모습을 볼 수 있다.
private
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| async uploadPrivateFiles(dataBuffer : Buffer, ownerId : number, filename : string){
const s3 = new S3();
const uploadResult = await s3.upload({
Bucket : this.configService.get("AWS_PRIVATE_BUCKET_NAME"),
Body: dataBuffer,
Key: `${uuid()}-${filename}`
}).promise();
const newFile = await this.privateFilesRepository.create({
key: uploadResult.Key,
owner : {
id : ownerId
}
});
await this.privateFilesRepository.save(newFile);
return newFile;
}
|
public
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| async uploadPublicFile(dataBuffer : Buffer, filename : string){
const s3 = new S3();
const uploadResult = await s3.upload({
Bucket: this.configService.get("AWS_PUBLIC_BUCKET_NAME"),
Body: dataBuffer,
Key:`${uuid()}-${filename}`
}).promise();
const newFile = this.publicFilesRepository.create({
key : uploadResult.Key,
url: uploadResult.Location
});
await this.publicFilesRepository.save(newFile);
return newFile;
}
|
promise()를 붙여주는 이유
aws-sdk는 node.js 환경에서 비동기적으로 작동하며, callback 함수를 사용하는 것보다 더 가독성 있게 코드를 만들 수 있기 때문
Fetch 기능
public s3의 업로드된 파일을 fetch하는 과정은 필요없다. 왜냐하면 url로 해당 객체에 대한 접근을 할 수 있기 때문이다.
만약 파일이 브라우저에서 해석할 수 있는 포맷이라면 바로 보여주고, 그렇지 않다면 다운로드를 하게 된다.
private
- presigned url 을 생성해서 가져오기
1
2
3
4
5
6
7
8
| public async generatePresignedUrl(key: string) {
const s3 = new S3();
return s3.getSignedUrlPromise('getObject', {
Bucket: this.configService.get('AWS_PRIVATE_BUCKET_NAME'),
Key: key
})
}
|
2. readable stream을 생성하기
AWS SDK에서 얻은 readable stream을 사용하여 파일을 다운로드하지 않고도 서버의 메모리를 절약할 수 있다는 것을 설명하고 있습니다. '스트림'은 데이터를 일정한 크기의 '조각'으로 나누어 전송하는 방식을 말하며, 이는 대용량의 데이터를 처리할 때 효율적인 방법입니다. '파이프'는 두 개의 스트림을 연결하여 데이터의 이동을 용이하게 하는 방식을 말합니다. 따라서, 이 문장은 스트림을 사용하여 데이터를 처리하고 서버의 메모리를 절약하는 방법을 제시하고 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public async getPrivateFile(fileId: number) {
const s3 = new S3();
const fileInfo = await this.privateFilesRepository.findOne({ id: fileId }, { relations: ['owner']});
if (fileInfo) {
const stream = await s3.getObject({
Bucket: this.configService.get('AWS_PRIVATE_BUCKET_NAME'),
Key: fileInfo.key
})
.createReadStream();
return {
stream,
info: fileInfo,
}
}
throw new NotFoundException();
}
|
컨트롤러
@UseInterceptors(FileInterceptor('file'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| @Delete('avatar')
@UseGuards(JwtAuthenticationGuard)
async deleteAvatar(@Req() request: RequestWithUser) {
return this.usersService.deleteAvatar(request.user.id);
}
@Get('files')
@UseGuards(JwtAuthenticationGuard)
async getAllPrivateFiles(@Req() request: RequestWithUser) {
return this.usersService.getAllPrivateFiles(request.user.id);
}
@Post('files')
@UseGuards(JwtAuthenticationGuard)
@UseInterceptors(FileInterceptor('file'))
async addPrivateFile(@Req() request: RequestWithUser, @UploadedFile() file: Express.Multer.File) {
return this.usersService.addPrivateFile(request.user.id, file.buffer, file.originalname);
}
@Get('files/:id')
@UseGuards(JwtAuthenticationGuard)
async getPrivateFile(
@Req() request: RequestWithUser,
@Param() { id }: FindOneParams,
@Res() res: Response
) {
const file = await this.usersService.getPrivateFile(request.user.id, Number(id));
file.stream.pipe(res)
}
|
마무리