CDK를 이용한 S3 프락시 API Gateway 생성

내 이 세상 도처에서 쉴 곳을 찾아보았으나, 마침내 찾아낸, 컴퓨터가 있는 구석방보다 나은 곳은 없더라.

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를 지정하는 코드도 분리되어 있다. 그리고 requestParametersresponseParameters를 매핑하고 지정하는 코드가 같은 레벨에 있지 않다.

CDK API가 이렇게 작성된 데는 분명 이유가 있겠지만, 마음에 들지는 않는다.

참고