Cloudfront를 사용하여 콘텐츠 가속화하기#1

이번 글에서는 아래 아키텍처에 따라 Amazon S3 및 Amazon EC2 인스턴스에서 각각 호스팅되는 정적 및 동적 콘텐츠가 있는 애플리케이션을 시작하도록 CloudFront를 설정해보겠습니다.


Cloudformation 템플릿을 사용하여 S3 및 EC2를 생성하고 애플리케이션도 배포하겠습니다. Cloudformation 콘솔로 이동하여 스택을 생성합니다.

AWSTemplateFormatVersion: '2010-09-09'
 
Parameters:
  # EC2 인스턴스의 AMI 이미지를 파라미터로 정의합니다.
  imageId:
    Description: Linux AMI image
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    AllowedValues:
      - /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
      - /aws/service/ami-amazon-linux-latest/amzn2-ami-kernel-5.10-hvm-x86_64-gp2
      - /aws/service/ami-amazon-linux-latest/al2022-ami-kernel-5.10-x86_64
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
 
  myIPAddress:
    Type: String
    Description: My IP address (  https://checkip.amazonaws.com 에서 공인 IP 확인 가능 ) + /32
    Default: 0.0.0.0/32
 
Mappings:
  # aws ec2 describe-managed-prefix-lists --query 'PrefixLists[?PrefixListName==`com.amazonaws.global.cloudfront.origin-facing`]' --region <REGION>
  # 매핑을 이용하면 해당 리전에 맞게 값을 자동으로 설정하도록 할 수 있다.
  # Ex. EC2 AMI의 ID는 리전마다 다르지만 Mappings를 이용하면 리전 선택 시 자동으로 해당 리전의 AMI ID가 선택되도록 한다.
 
  AWSRegions2PrefixListID:
   ap-northeast-1:
      PrefixList: pl-58a04531
    ap-northeast-2:
      PrefixList: pl-22a6434b
    ap-northeast-3:
      PrefixList: pl-31a14458
    ap-south-1:
      PrefixList: pl-9aa247f3
    ap-southeast-1:
      PrefixList: pl-31a34658
    ap-southeast-2:
      PrefixList: pl-b8a742d1
    ca-central-1:
      PrefixList: pl-38a64351
    eu-central-1:
      PrefixList: pl-a3a144ca
    eu-north-1:
      PrefixList: pl-fab65393
    eu-west-1:
      PrefixList: pl-4fa04526
    eu-west-2:
      PrefixList: pl-93a247fa
    eu-west-3:
      PrefixList: pl-75b1541c
    sa-east-1:
      PrefixList: pl-5da64334
    us-east-1:
      PrefixList: pl-3b927c52
    us-east-2:
      PrefixList: pl-b6a144df
    us-west-1:
      PrefixList: pl-4ea04527
    us-west-2:
      PrefixList: pl-82a045eb
 
 
 
Resources:
  securityGroup:
    # EC2 인스턴스에 연결할 SecurityGroup을 생성합니다.
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: CloudFront
      SecurityGroupIngress:
        - Description: Allow HTTP
          IpProtocol: "tcp"
          FromPort: "80"
          ToPort: "80"
          SourcePrefixListId:  !FindInMap [AWSRegions2PrefixListID, !Ref 'AWS::Region', PrefixList]
        - Description: Allow HTTP from my IP address for testing
          IpProtocol: "tcp"
          FromPort: "80"
          ToPort: "80"
          CidrIp: !Ref myIPAddress
      Tags:
        - Key: Name
          Value: CloudFront
        - Key: StackName
          Value: !Sub ${AWS::StackName}
        - Key: StackId
          Value: !Sub ${AWS::StackId}
 
  # EC2 인스턴스에 부여할 역할 정보입니다. AWS에서 관리하는 관리형 정책인 "AmazonSSMManagedInstanceCore"을 사용합니다.
  instanceIamRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: [ec2.amazonaws.com]
            Action: ['sts:AssumeRole']
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
      Tags:
        - Key: Name
          Value: CloudFront
        - Key: StackName
          Value: !Sub ${AWS::StackName}
        - Key: StackId
          Value: !Sub ${AWS::StackId}
 
  # 인스턴스 프로파일이란 IAM 역할을 위한 컨테이너로서 인스턴스 시작 시 Amazon EC2 인스턴스에 역할 정보를 전달하는 데 사용됩니다.
  instanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
      - !Ref instanceIamRole
 
  # EC2 인스턴스를 생성합니다. AMI는 파라미터에서 가져옵니다.
  instance:
    Type: AWS::EC2::Instance
    CreationPolicy:
      ResourceSignal:
        Timeout: PT15M
    Metadata:
      Comment: Install    
      AWS::CloudFormation::Init:    
        configSets:
          InstallFiles:
          - "setup-1"
        setup-1:
          packages:
            yum:
              tmux: []      
    Properties:
      ImageId: !Ref imageId
      InstanceType: t2.micro
      SecurityGroups:
        - !Ref securityGroup
      IamInstanceProfile: !Ref instanceProfile
      Tags:
        - Key: Name
          Value: CloudFront
        - Key: StackName
          Value: !Sub ${AWS::StackName}
        - Key: StackId
          Value: !Sub ${AWS::StackId}
 
      # UserData 사용하여 EC2 인스턴스가 처음 시작될 때 스크립트를 실행하도록 합니다.
      # EC2 인스턴스의 80번 포트에서 HTTP 요청을 수신하는 Node.js 애플리케이션을 배포하는 스크립트입니다.
      UserData:
        Fn::Base64:
          !Sub |          
            #!/bin/bash
 
            yum install -y aws-cfn-bootstrap
 
            /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource instance --region ${AWS::Region} -c InstallFiles
 
            yum update -y
            curl -sL https://rpm.nodesource.com/setup_14.x | sudo bash -
            yum install -y nodejs
           
            npm install pm2@latest -g
            npm install express --save
 
            cat <<'EOF' >> app.js
            let express = require('express');
            let app = express();
 
            app.get('/api', (req, res) => {
              console.log(JSON.stringify(req.headers));
              let message = {
                timestamp: new Date().toISOString(),
                headers: req.headers,
              };
              res.json(message);
            });
 
            app.listen(80, () => {
              console.log('api is up!');
            });
            EOF
 
            sudo pm2 start ./app.js            
            sudo pm2 startup systemd
            sudo pm2 save
            systemctl enable --now pm2-root.service
 
            /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource instance --region ${AWS::Region}
 
  # AWS S3 버킷을 생성합니다. Public Access Block 옵션를 모두 활성화 하여 생성합니다. (Public Access 불가능)
  s3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: Name
          Value: CloudFront
        - Key: StackName
          Value: !Sub ${AWS::StackName}
        - Key: StackId
          Value: !Sub ${AWS::StackId}
 
# Cloudformation 스택 생성 후 결과값(출력값)을 정의합니다.
Outputs:
  ApiURL:
    Description: URL to API
    Value: !Sub 'http://${instance.PublicDnsName}/api'
  S3BucketName:
    Value: !Ref s3Bucket
    Description: Name of the S3 origin
  S3BucketConsole:
    Description: S3 Bucket Console
    Value: !Sub 'https://console.aws.amazon.com/s3/buckets/${s3Bucket}?region=${AWS::Region}'
  EC2instanceDNSname:
    Description: EC2 instance DNS name
    Value: !GetAtt instance.PublicDnsName
  EC2Console:
    Description: EC2 Instance Console
    Value: !Sub "https://console.aws.amazon.com/ec2/home?region=${AWS::Region}#Instances:search=${instance}"

[S3 버킷에 index.html 생성]

텍스트 편집기를 사용하여 index.html 파일을 생성합니다. 이 HTML은 iframe 태그를 사용하여 동적 콘텐츠를 호출합니다. 실제로 사용자가 index.html을 요청하면 브라우저는 후속 요청을 /api로 보냅니다.

<!DOCTYPE html>
<html lang="en">
  <body>
    <table border="1" width="100%">
      <thead>
        <tr>
          <td><h1>CloudFront Lab</h1></td>
        </tr>
      </thead>
      <tfoot>
        <tr>
          <td>Edge Services</td>
        </tr>
      </tfoot>
      <tbody>
        <tr>
          <td>Response sent by API</td>
        </tr>
      </tbody>
      <tbody>
        <tr>
          <td>
            <iframe src="/api" style="width: 100%; height: 100%"></iframe>
          </td>
        </tr>
      </tbody>
    </table>
  </body>
</html>

CloudFormation에서 생성한 버킷에 index.html 파일을 업로드합니다. 모든 설정은 기본값으로 둡니다. S3 객체 URL을 사용하여 index.html을 요청하면 객체가 Public으로 구성되지 않았기 때문에 액세스가 거부됩니다.


CloudFormation 템플릿은 EC2 인스턴스의 80번 포트에서 HTTP 요청을 수신하는 Node.Js 애플리케이션을 배포했습니다. 요청을 받으면 애플리케이션은 요청에 수신된 헤더를 포함하는 JSON 응답을 다시 보냅니다. 또한 쿼리 문자열 정보를 검사하고 쿼리 문자열 값을 기반으로 웹 서버에서 일부 데이터를 반환합니다. 애플리케이션 코드는 다음과 같습니다.

let express = require("express");
let app = express();
 
app.get("/api", (req, res) => {
  console.log(JSON.stringify(req.headers));
  let message = {
    timestamp: new Date().toISOString(),
    headers: req.headers,
  };
  if (req.query.info) {
    let { exec } = require("child_process");
    exec(`cat ${req.query.info}`, (err, data) => {
      message.data = data;
      res.json(message);
    });
  } else {
    res.json(message);
  }
});
 
app.listen(8080, () => {
  console.log("api is up!");
});

브라우저에 http://[EC2-DNS-name]/api를 입력하여 애플리케이션이 정상적으로 동작하는지 확인합니다.


[CloudFront 배포 생성]

이전에 생성한 S3 버킷에 대한 기본 Origin을 구성하고 오리진 액세스 ID 설정을 사용하여 CloudFront에 버킷에 대한 권한을 부여합니다.

– Origin domain: 이전에 생성한 domain 이름
– S3 bucket access: Yes use OAI (버킷은 Cloudfront에 대한 액세스만 제한할 수 있음)
– Origin access identity: Create new OAI 클릭 후 팝업창에서 Create 클릭
– Bucket policy: Yes, update the bucket policy


다음과 같이 기본 캐시 동작을 구성합니다.

– Viewer protocol policy: HTTP를 HTTPS로 리디렉션
– Cache key and origin requests: 캐시 정책 및 Origin 요청 정책
– Cache policy: CachingOptimized

이번 실습에서는 Cloudfront에서 제공하는 도메인 이름을 사용하지만 소유한 도메인 이름을 사용하려는 경우 Alternate domain name 옵션을 통해서 고유한 도메인 이름을 사용할 수 있습니다.


Default root object에 index.html을 입력하고 배포 만들기를 클릭합니다. Cloudfront에서 배포 생성을 시작하고 완료까지 5~10분 소요됩니다. 왼쪽 창에서 배포 메뉴를 클릭하여 상태를 확인할 수 있습니다. CloudFront 배포는 전파가 310개 이상의 모든 접속 지점에 도달하면 Status가 배포됨으로 변경되기 때문에 Status가 아직 진행 중인 경우에도 로컬에서 사용할 수 있습니다.


배포 콘솔에서 배포 ID를 클릭한 다음 Origin 탭으로 이동하여 API에 대한 또 다른 Origin을 만듭니다. Create origin 버튼을 클릭합니다.


EC2 인스턴스 DNS 이름을 Origin domain 이름으로 입력하고 연결 유지 제한 시간을 60초로 늘립니다. Origin에서 TLS 오버헤드를 줄이기 위해 HTTP 연결을 유지하려고 합니다.


/api에 대한 새 동작을 만듭니다. 동작 탭을 선택한 다음 동작 만들기 버튼을 클릭합니다.


CloudFront를 프록시로 사용하고 캐싱 계층을 우회하도록 다음 파라미터와 함께 EC2 오리진을 사용하도록 두 번째 캐시 동작을 구성합니다.

– Path pattern: /api
– Origin and origin groups: 이전에 생성한 EC2 Origin
– Viewer protocol policy: HTTP를 HTTPS로 리디렉션
– Cache key and origin requests: CachingDisabled
– Origin request policy: AllViewer

위 설정에서 두 가지 관리형 정책을 사용하고 있습니다. CachingDisabled 는 관리형 캐시 정책 입니다. 이 정책은 캐싱을 비활성화하고 동적 콘텐츠 및 캐시할 수 없는 요청에 유용합니다. AllViewer 는 관리되는 원본 요청 정책 입니다. 이 정책은 Viewer 요청의 모든값(헤더, 쿠키 및 쿼리 문자열)을 포함합니다.


Generl 탭에서 Cloudfront가 배포에 연결한 고유한 도메인 이름을 확인할 수 있습니다.


[Cloudfront에서 애플리케이션 테스트]

배포를 로컬에서 사용할 준비가 되었는지 테스트하기 위해 nslookup 명령으로 CloudFront 도메인 이름을 조회할 수 있습니다. 

배포가 완료되면 CloudFront URL(http://dxxxx.cloudfront.net.)을 사용하여 브라우저에서 웹 페이지를 테스트할 수 있습니다..


웹 페이지를 새로고침하면 요청 ID가 어떻게 변경되는지 확인할 수 있습니다. 또한 이 ID는 모든 최종 사용자 요청으로 다시 전송되고 CloudFront 액세스 로그로 전송됩니다. 문제를 디버깅해야 하는 경우 support ticket을 열고 요청 ID를 제공할 수 있습니다.


웹 브라우저의 개발자 도구를 사용하면 CloudFront에서 보낸 응답 헤더를 확인할 수 있습니다.

x-amz-cf-id: CloudFront에서 할당한 요청 ID를 보유합니다.

x-amz-cf-pop: 요청을 처리한 CloudFront 엣지 로케이션을 나타냅니다. 각 에지 위치는 3자리 코드와 임의로 할당된 번호로 식별됩니다. 3자리 코드는 일반적으로 엣지 로케이션 근처 공항의 국제항공운송협회 공항 코드와 일치합니다.

x-cache: 요청이 Cache hit인지 캐시 Cache miss인지 나타냅니다. 일반적으로 html 파일의 경우 후속 요청에서 ‘Hit from Cloudfront’ 값을 받지만 이 동작에 대해 캐싱이 비활성화되어 있으므로 /api 요청에 대해서는 항상 ‘Miss from CloudFront’입니다.


[마무리]

Amazon CloudFront는 .html, .css, .js 및 이미지 파일과 같은 정적 및 동적 웹 콘텐츠를 사용자에게 더 빨리 배포하도록 지원하는 CDN 서비스입니다. Cloudfront에서 무효화를 실행하여 새로 업데이트된 콘텐츠 배포, 사용자 정의 오류 페이지 구성, 장애 조치 동안 경로 재지정을 제공하도록 Origin 그룹 구성 등 다양한 기능을 사용할 수 있습니다.


[참고자료]

Leave a Comment