CDK를 이용한 S3 프락시 API Gateway 생성
CDK의 API는 AWS 콘솔의 입력 항목과 직관적 대응 관계가 있어야 할 것 같은데, 꼭 그렇지만은 않은 듯 하다. 여기서는 S3 버킷에 저장되어 있는 파일을 API Gateway를 통해 접근할 수 있도록 프락시 API Gateway를 CDK로 만들어보려 한다.
S3 버킷을 public으로 만들면 API Gateway를 통하지 않아도 웹을 통해 접근할 수 있다. 그러나 나중에 추가할 람다 함수를 웹에서 접근하게 하려면 API Gateway가 필요하다. 모든 웹 접근을 API Gateway를 통하도록 하는 게 나을 것이다.
S3 버킷 생성
S3 버킷은 다음과 같이 간단히 생성할 수 있다.
const s3 = require('@aws-cdk/aws-s3')
const s3deploy = require('@aws-cdk/aws-s3-deployment')
class TweetbookStack extends cdk.Stack {
constructor(scope, id, props) {
...
const bucket = new s3.Bucket(this, 'Tweetbook-Bucket', {
bucketName: 'tweetbook-bucket',
removalPolicy: cdk.RemovalPolicy.DESTROY
})
...
테스트할 때는 스택을 생성하고 제거하길 반복할테니 S3 버킷 삭제 정책을 RemovalPolicy.DESTROY
로 설정했다. 그러나 S3 버킷을 삭제하려면 버킷이 비어있어야 한다. 버킷에 비어있지 않으면 삭제할 때 에러가 발생하므로 CloudFormation AWS 콘솔이나 cdk destroy
명령으로 스택을 제거할 때 S3 버킷이 비어있지 않으면 스텍 제거에 실패한다. 스택을 깔끔하게 제거하려면 먼저 S3 버킷을 비우는 게 상책이다.
S3 버킷에 파일 업로드
CDK로 버킷을 생성한 다음 바로 파일을 업로드할 수 있다. 다음 코드는 CDK 프로젝트 루트 밑에 있는 web
디렉터리의 파일을 S3 버킷으로 업로드한다.
...
new s3deploy.BucketDeployment(this, 'DeployFiles, {
sources: [s3deploy.Source.asset('web')],
destinationBucket: bucket,
destinationPrefix: 'web/html'
})
...
Role 생성
S3 버킷은 API Gateway를 통해서만 접근이 가능하게 만들고 싶다. 따라서 S3 버킷을 public으로 만들지 않고 다음과 같이 IAM Role을 정의한다.
...
const role = new iam.Role(this, 'Tweetbook-apigw-role', {
roleName: 'Tweetbook-ApiGw-S3-ReadOnly',
assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
})
role.addToPolicy(new iam.PolicyStatement({
resources: [`${bucket.bucketArn}/*`],
actions: ['s3:GetObject']
}))
...
GetObject
액션의 적용 대상은 버킷 안의 객체라는 점에 유의해야 한다. Policy를 정의할 때 리소스에 버킷을 지정해놓고는 API Gateway에서 S3 객체를 읽지 못해 한참을 헤맸다.
API Gateway 정의
다음과 같이 RestApi
를 이용해 API Gateway를 만든다.
const apigw = require('@aws-cdk/aws-apigateway')
class TweetbookStack extends cdk.Stack {
constructor(scope, id, props) {
...
const api = new apigw.RestApi(this, 'Tweetbook-Web')
...
API Gateway에서 S3에 접근하려면 AwsIntegration
을 사용할 수 있다. AwsIntegration
을 생성할 때 위에서 정의한 IAM Role을 지정해준다.
api.root
.addResource('{file}')
.addMethod(
'GET',
new apigw.AwsIntegration({
service: 's3',
integrationHttpMethod: 'GET',
path: `${bucket.bucketName}/{file}`,
options: {
credentialsRole: role,
requestParameters: {
'integration.request.path.file': 'method.request.path.file'
},
integrationResponses: [{
statusCode: '200',
selectionPattern: '2..',
responseParameters: {
'method.response.header.Content-Type': 'integration.response.header.Content-Type'
},
}, {
statusCode: '403',
selectionPattern: '4..'
}]
}
}), {
requestParameters: {
'method.request.path.file': true
},
methodResponses: [{
statusCode: '200',
responseParameters: {
'method.response.header.Content-Type': true
}
}, {
statusCode: '404'
}]
})
코드가 조금 길다. 대부분은 addMethod
메서드의 인자가 차지한다. 코드가 이렇게 늘어지는 게 보기 싫다면 addMethod
의 두 인자를 변수로 뽑아낼 수도 있겠다.
어떻게 하든 코드 모양이 조금 마음에 들지 않는다. requestParameters
를 매핑하는 코드와 requestParameters
를 지정하는 코드가 분리되어 있다. responseParameters
를 매핑하는 코드와 responseParameters
를 지정하는 코드도 분리되어 있다. 그리고 requestParameters
와 responseParameters
를 매핑하고 지정하는 코드가 같은 레벨에 있지 않다.
CDK API가 이렇게 작성된 데는 분명 이유가 있겠지만, 마음에 들지는 않는다.