본문 바로가기

블로그

LG CNS 기술블로그 DX Lounge에서 최신 IT 소식을 만나보세요!

AWS Ambassador

Lambda를 활용한 S3 replication

2023.02.01

[ 머리말 ]

S3는 AWS의 Storage 서비스 중 가장 대표적인 서비스입니다. 수많은 데이터가 S3에 저장되며, 저장을 위한 구성, 연계 또한 용이합니다.
S3 자체가 매우 높은 안정성을 보장하지만, Object에 담긴 데이터의 중요성 및 각종 보안 요건에 의해 특정 버킷 또는 object의 복제는 매우 중요하며
글로벌 서비스를 대비한 데이터의 리전 분산 혹은 백업과 DR 등은 시스템 구현 시 꼭 고려해야 하는 사항입니다.
S3 replication 기능이 S3 자체에서 제공되기 전까지 가장 대표적인 object 복제 방식은 Lambda를 활용하는 것이었습니다.
Lambda는 코드만 있다면 인프라에 대한 걱정 없이 특정 기능을 구현할 수 있고 타 AWS 서비스와의 연계도 용이하다는 장점이 있습니다.
아래에서는, S3 버킷 복제가 가능한 2가지 방법 중, 람다를 통한 방법을 알아보겠습니다.

[ Architect ]

이제 구현을 위한 고려사항을 정리해보겠습니다. 적용 될 아키텍처는 아래와 같습니다.

주요 고려 사항 및 서비스는 아래와 같습니다.

1. Account A, B : 2개의 다른 Account
2. IAM : User 와 User에 부여한 권한
Service에 할당될 Role과 해당 Role에 부여한 권한(policy)
3. KMS Key A, B : CMS으로 생성된 각각의 Bucket을 암호화한 KMS Key 와 Key Policy
4. S3 bucket A , B : 각 Account 별 생성된 S3 bucket 과 Bucket 별 Policy
5. Lambda : Lambda의 구현과 그를 위한 환경 변수, Log 관리
6. Cloudwatch logs : Lambda의 수행 로그를 기록할 Cloudwatch logs

아키텍처를 보고 예상되는 연계방안 대비 실제 구현을 위해 고민해야 할 점은 많습니다. 보안을 고려하여 권한을 타이트하게 부여할 경우 더욱 그 복잡성은 증가합니다.


자 그럼 구현해 보시죠!

[ Content ]

1. IAM : User 권한 부여
첫 단계는 Account A, B에 접속이 가능한 User를 생성하고 권한을 부여하는 것입니다.
IAM을 통해 User, Service에 권한을 부여할 수 있습니다.

권한을 부여하는 방식은, 권한의 상세 내용이 정의된 Policy를 직접 user에 연계하거나,
역할(Role)에 Policy를 연계한 후 해당 Role을 User 또는 Service에 할당함으로써 적용 가능합니다.

권한에 대한 다양하고 상세한 정의가 가능한 만큼, 특정 리소스에 대한 특정 권한만을 부여하는 policy의 작성은
결코 쉽지 않습니다.

아래와 같이 생각하시면 조금 개념을 잡기 쉽습니다.

* Tips ! IAM 권한 부여의 원칙

어떤 서비스에 > 어떤 행동을 > 어떤 조건에서 > 하게 또는 못하게 할 것인가. : Resource > Action > Condition > Effect
가장 간결한 administrator 권한을 예시로 보겠습니다.

{
“Version”: “2012-10-17”,
“Statement”: [
{
“Effect”: “Allow”,
“Action”: “”,
“Resource”: “”
}
]
}

위의 개념을 빗대어 생각해보겠습니다.  어떤 서비스에 > 어떤 행동을 > 어떤 조건에서 > 하게 또는 못하게 할 것인가.  :  Resource >  Action > Condition > Effect
  *      >    *    >   * (없으면 다)> ALLOW (허가)  즉, 모든 리소스에 모든 권한을 조건 없이 다 허용하는 권한입니다.

다른 예시 하나를 더 보시죠. 아래 Policy는 User에 부여할 수 있는 정책 예시입니다.

“Statement”: [
        {
            “Action”: “sts:AssumeRole”,
            “Condition”: {
                “BoolIfExists”: {
                    “aws:MultiFactorAuthPresent”: “true”
                },
                “ForAnyValue:IpAddress”: {
                    “aws:SourceIp”: “접속을 허용할 IP/32”
                }
            },
            “Effect”: “Allow”,
            “Resource”: “arn:aws:iam::account_id:role/Role_이름”,
        }
    ]

위의 설명에 맞춰 보겠습니다.

Resource  –  “account_id” 계정에 생성된 “Role_이름” Role 에
Action    –  Security Token Service(sts:AssumRole)의 AssumeRole 행동을 
Condition –  사용자가 MFA를 통과하고 (MultiFactorAuthPresent) , 특정 IP를 사용하고 있는 경우(aws:SourceIp)  
Effect    –  허가한다 (Allow)  

즉, “사용자가 mfa를 사용하고 지정된 IP로 접근한 경우, 특정 역할(role)에 부여된 권한을 사용할 수 있게 허가한다” 입니다. 

분석해 보면 이해가 가능하지만, 실제 policy는 이런 문구가 여러 개 붙어 있을 수 있어 권한을 정확히 판단하기 어려울 수 있기 때문에 위와 같은 방식으로 따라가보면 이해하기가 용이합니다!

IAM에 대한 상세한 내용과 예시는 다른 Tech Diary 에서 더 설명하겠습니다.

우선, 이번 시나리오에서 구성 작업을 수행할 User는  S3, Lambda, KMS 등에 모두 권한을 보유한 AdministratorAccess 권한을 부여하겠습니다.

2. KMS를 활용한 Key 생성 
S3 bucket을 암호화할 KMS Key를 데이터를 복제할 소스 bucket과 대상 bucket 용으로 각각 계정에서 생성해야 합니다.
KMS 에서 버킷 암호화를 통해 생성할 수 있는 키의 종류는 크게 2가지 입니다.
   (1) AWS Managed key         – AWS 가 생성하고 직접 관리하는 키 
   (2) Customer Managed Key – 사용자가 생성하고 관리하는 키

*Tips! – Key의 특징

ALB accesslog 저장 등 특정 조건에서 S3 버킷 암호화는 AWS Managed Key(SSE-S3)만 지원 가능하지만, 가능한 Customer Managed Key를 사용하는 것을 권장합니다. 데이터 복제 등 Account, Region을 넘나드는 Key 활용을 위해선 Customer managed key(SSE-KMS)가 활용도가 높습니다. 보안이 강화된 아키텍처에선 KMS Key를 별도의 Account(Security)에서 분리 생성하고 관리합니다. 이 경우, 분리를 통한 보안은 강화되지만, IAM 및 Key Policy 설정이 더 복잡해진다는 단점이 있습니다.

이 시나리오에서는, Account A, B에 각각 Customer managed Key를 생성합니다. Key를 생성하면 가장 중요한 점은 “Key Policy”를 설정해야 한다는 점입니다.

Key Policy는 약간의 차이는 존재하지만 IAM Policy와 매우 흡사합니다. 보안의 측면에서 당연히, 너무 많은 권한을 부여하는 것은 좋지 않습니다.

Key 생성 시 Policy에 영향을 주는 3가지 옵션은 아래와 같습니다.
(1) Key administrators
(2) Key usage permission
(3) Other AWS accounts

위 3가지 옵션에서 하나씩 선택한 후 key를 생성하면 아래와 같이 Key Policy가 생성됩니다.

———————————< Key Policy >———————————————————–
{
“Id”: “key-consolepolicy-3”,
“Version”: “2012-10-17”,
“Statement”: [
{
“Sid”: “Enable IAM User Permissions”,
“Effect”: “Allow”,
“Principal”: {
“AWS”: “arn:aws:iam::KEY_OWNER_ACCOUT_ID:root”
},
“Action”: “kms:”,
“Resource”: “”

},
————————————————————————————————————–

* Tips!

Action에 kms:* , Resource “*” 즉 모든 리소스를 대상으로 KMS의 모든 action을 허용한다는 것입니다.
Principal의 “arn:aws:iam::KEY_OWNER_ACCOUT_ID:root” 은 무슨 의미일까요?

이 조건은 “Key_owner 계정 내 user는 모든 권한이 가능하다” 가 아닌, “Key_owner 계정 내 설정된 IAM 정책대로 권한을 모두 허가한다”
즉, IAM에 설정된 kms 권한 대로 key의 사용 권한을 허가한다는 것입니다.

다수 계정 기반에선 KEY_OWNER_ID뿐 아니라 Security, Production 등 다양한 계정의 ID를 해당 룰에 포함하여 키 권한을
IAM 정책 기반으로 활용할 수 있습니다. 반대로, 위 정책을 사용하지 않는 경우 IAM의 key 권한과 Key policy 두 가지를 활용하여 권한을 더욱 타이트하게 부여, 관리할 수 있습니다. 단 구축 시 그만큼 수많은 고통이… 수반됩니다.

———————————< Key Policy >———————————————————–
{
“Sid”: “Allow access for Key Administrators”,
“Effect”: “Allow”,
“Principal”: {
“AWS”: “arn:aws:iam::KEY_OWNER_ACCOUT_ID:user/(1)에서선택한admin_user”
},
“Action”: [
“kms:Create”,
“kms:Describe”,
“kms:Enable”,
“kms:List”,
“kms:Put”,
“kms:Update”,
“kms:Revoke”,
“kms:Disable”,
“kms:Get”,
“kms:Delete”,
“kms:TagResource”,
“kms:UntagResource”,
“kms:ScheduleKeyDeletion”,
“kms:CancelKeyDeletion”
],
“Resource”: “*”
},
** TIPS! – KEY_OWNER 계정에서 Admin으로 지정된 계정(1) 은 kms의 관련 대부분 action의 권한을 사용할 수 있습니다.
{
“Sid”: “Allow use of the key”,
“Effect”: “Allow”,
“Principal”: {
“AWS”: [
“arn:aws:iam::KEY_OWNER_ACCOUT_ID:user/(2)에서_선택한_user”,
“arn:aws:iam::OTHER_ACCOUNT_ID:root”
]
},
“Action”: [
“kms:Encrypt”,
“kms:Decrypt”,
“kms:ReEncrypt”,
“kms:GenerateDataKey”,
“kms:DescribeKey”
],
“Resource”: “*”
},

* Tips!

KEY_OWNER 계정에서 key의 usage를 허용 받은 계정(2)는 (1) 대비 제한적 권한을 사용할 수 있습니다.
(3)에서 지정된 OTHER_ACCOUNT_ID 역시 동일한 권한을 부여받게 되며, Principal의 설정은 맨 위 KEY_OWNER_ID와 같습니다.

참고할 만한 사항은, 위 허용된 5가지 kms 액션은 실제 Key를 활용한 encryption을 적용 시 대부분 사용되는 대표적 action이라는 점입니다. 즉 OTHER_ACCOUNT_ID는 IAM의 KMS 설정에 따라 key 사용 권한을 부여받지만, 그 권한의 범위는 encryption 구성 가능 수준이며 key의 삭제, 변경 등은 불가하도록 설정됩니다.

  {
            “Sid”: “Allow attachment of persistent resources”,
            “Effect”: “Allow”,
            “Principal”: {
                “AWS”: [
                    “arn:aws:iam::KEY_OWNER_ACCOUT_ID:user/(2)에서 선택한_user”,
                    “arn:aws:iam::OTHER_ACCOUNT_ID:root”
                ]
            },
            “Action”: [
                “kms:CreateGrant”,
                “kms:ListGrants”,
                “kms:RevokeGrant”
            ],
            “Resource”: “*”,
            “Condition”: {
                “Bool”: {
                    “kms:GrantIsForAWSResource”: “true”
                }
            }
        }

** Tips!

위와 동일한 Principal입니다 단, Action이 Grant 관련 값으로 지정되어 있고, Condition이 지정되어 있습니다.
Condition의 의미는 “AWSResource가 대상일 경우 허용”이라는 의미입니다. Grant는 kms 권한을 사용하도록 “허가” 하는 형태의 권한이며, 자세한 항목은 아래 url을 참고해 주십시오.
참고 : https://docs.aws.amazon.com/kms/latest/developerguide/grants.html

]

}

———————————————————————————————————————————————————–

자 그럼 실제 Key를 생성 작업을 확인하겠습니다.
(1) A, B account에서 각각 A, B Key를 생성합니다.
(2) Key admin / Key usage에는 실제 배포 작업을 수행하는 계정을 지정해 주십시오.
(3) other account에 A, B는 서로의 account ID를 입력해 주십시오
A,B Account는 서로의 S3 암호화 key에 대한 사용 권한을 보유하게 됩니다.

* Tips!

A, B 양 계정은 각각 다른 kms key로 암호화됩니다. 따라서 object 이동시 요구되는 암/복호화 작업은
A key로 암호화된 bucket에서 이동 > 복호화 > B key로 암호화되어야 하니,
각 KMS 키는 두 계정의 key 사용 권한이 필요합니다.

3. 암호화된 S3 bucket 생성
A account, B account에서 각각 소스 bucket과 대상 bucket을 생성해야 합니다. 2가지의 필수 체크 요건이 있습니다.
(1) 2단계에서 생성한 kms key로 암호화
(2) s3 access policy 확인
생성 절차는 아래와 같습니다.
(1) s3 생성 단계 시 – Default encryption > Server-side encryption “Enable” > AWS Key Management Service key (SSE-KMS) 선택 > Choose from your AWS KMS keys > 2단계에서 생성한 키 선택!

* Tips!

타 계정에서 생성한 key를 선택하는 경우 “Choose from.. 대신 Enter AWS KMS key ARN” 을 선택해 주시면 됩니다. Key ARN이 검색되지 않는 만큼, 미리 key arn을 확인해두십시오.

(2) 다른 옵션은 건드리지 않은 상태에서 (1)번 수행 후 생성된 bucket의 Bucket Policy에는 아무 값도 지정되지 않습니다. 보안의 관점에서, S3 bucket Policy 역시 IAM 및 KMS key policy처럼 명확한 정의를 가지고 있는 것이 좋습니다.
아래의 내용을 참고해 주십시오

Bucket A – Object를 보내기 때문에 별도의 policy 지정은 불필요함
Bucket B

————————————————————–< Bucket Policy >—————————————————————
{     
  “version”: “2012-10-17”,
  “Id”: “S3-Replication-Policy”
  “Statement”: [
      {
          “Sid”: “S3 replication with Lambda”,
          “Effect”: “Allow”
          “Principal”: {
               “AWS”: [
                   “arn:aws:iam:A_Account_ID:role/A_Account_Lambda_Role”
            ]
          },
          “Action”: [
             “s3:PutObject”,
             “s3:PutObjectAcl”
             ],
             “Resource”: “arn:aws:s3:::B_Account_S3_Bucket_Name/*”
          }
    ]
}

* Tips!

Lambda가 생성될 A account의 Role이 B Account의 Bucket에 PutObject, PutObjectAcl 권한을 허용하는 것입니다.
하지만 실제 시도해 보셨다면 눈치채셨을 것입니다. 에러가 납니다. 어떤 에러가 날까요?

해당 작업까지 완료했을 때 완성된 Architecture는 아래와 같습니다.

위에서 S3 bucket policy를 입력함에 있어 에러가 나게 된다는 점을 설명했습니다. 혹시 그 이유를 알고 계신가요?

이유는 단순합니다. 우리는 아직 Lambda를 위한 IAM Role을 생성하지 않았고 따라서 그 Name을 입력하면 S3 bucket policy에선 에러가 발생하게 됩니다.

*Tips!

Terraform 과 같은 IaC 기반의 리소스 배포를 수행할 때, 이런 Role의 Name 등을 먼저 변수에 모두 정의하고 코드에 반영하기 때문에
실제 Role이 우선 생성되기 전 S3 생성이 진행되면 계속 리소스 생성이 실패하고 에러의 내용은 분명하게 나타나지 않아 트러블 슈팅에 많은 시간을 소비하게 됩니다. 의외로 단순한 정보인 “생성되지 않은 내용의 resource 명이 Policy에 정의되면 에러가 발생한다”라는 점을 꼭 유념해 주십시오.

4. IAM : Lambda를 위한 Role 생성
Lambda는 S3 bucket 간 Object를 실제 옮겨주는, 핵심 요소입니다. 그리고 Lambda가 다른 AWS의 권한을 필요로 하는 만큼,
Role 및 권한의 부여는 필수적입니다.

(1) Policy 생성
Role 생성 시 Create Policy를 수행할 수 있으나, 어떤 권한을 필요로 하는지 정확하게 이해하기 위해, 먼저 Policy를 생성하겠습니다.
총 3가지의 Policy가 Role에 부여되어야 합니다. 2가지는 AWS Managed Policy, 1가지는 아래와 같이 직접 생성한 Policy입니다.

{     
  “version”: “2012-10-17”,
  “Statement”: [
      {
          “Effect”: “Allow”
          “Action”: [
             “s3:PutObject”,
             “s3:PutObject“,              “s3:ListBucket”              ],           “Resource”: [                 “arn:aws:s3:::B_Account_S3_Bucket_Name/“,
                “arn:aws:s3:::B_Account_S3_Bucket_Name”
            ]
        },
      {
          “Effect”: “Allow”
          “Action”: [
             “s3:GetObject”,
             “s3:GetObject“,              “s3:ListBucket”              ],           “Resource”: [                 “arn:aws:s3:::A_Account_S3_Bucket_Name/“,
                “arn:aws:s3:::A_Account_S3_Bucket_Name”
            ]
        },

Object를 받을 B account의 Put 및 List 허가, Object를 보낼 A account의 Get 및 List를 허가 합니다. 

      {
            “Effect”: “Allow”
          “Action”: [
              “kms:Encrypt”,
              “kms:Decrypt”,
              “kms:ReEncrypt“,               “kms:GenerateDataKey“,
              “kms:DescribeKey”
             ],
          “Resource”: [
                “arn:aws:kms:ap-northeast-2:A_Account_ID:Key-ID*”,
                “arn:aws:kms:ap-northeast-2:B_Account_ID:Key-ID”
            ]
       }
    ]
}

A, B bucket은 encryption 되어 있기 때문에 각 Key 대한 사용 권한을 부여합니다

추가로 부여되는 AWS managed policy는 아래 2가지입니다

“AWSLambdaBasicExecutionRole” – Cloudwatch logs에 Lambda 수행 로그를 남기기 위한 policy
“AmazonSNSFullAccess” – Lambda 수행 결과에 따른 메일 발송을 위함

(2) Role 생성
IAM > Create > Select trusted entity에서 “AWS Service, Lambda”를 선택해 주십시오.
Trusted Entity에 Lambda를 선택한다는 것은 Role의 Trust relationships에 아래와 같은 내용이 입력됩니다.

{
    “Version”: “2012-10-17”,
    “Statement”: [
        {
            “Effect”: “Allow”,
            “Principal”: {
              “Service”: [
                  “lambda.amazonaws.com”
                  ]
                },
            “Action”: “sts:AssumRole”
        }
    ]
}

* Tips!

Lambda가 수행됨에 있어 Role의 권한을 부여받을 수 있도록 trust를 맺는 것입니다.
이후 위에서 정의한 3가지 Policy를 Role에 선택해 주시면 됩니다.
이 Role이 생성된 이후 S3의 Bucket policy를 만들면 됩니다!

5. Lambda 생성
Lambda는 Account A에서 생성합니다.
아래 코드는 python 3.6 기반입니다. 해당 버전은 depricated 되고 있기 때문에 Runtime은 Python 3.8로 사용하시면 됩니다.
(1) Change default execution role > Use an existing role > (1) 에서 생성한 Role 선택
여기까지 수행하면 권한이 부여된 껍데기의 Lambda가 완성됩니다
(2) lambda_function.py에 아래 내용을 저장해 주십시오.

————————-<  Lambda Code > ————————————————————————————————
import os
import urllib
import boto3
import json
import base64
import botocore
import time
import logging

print(‘Replication Starting’)

def lambda_handler(event, context):
    s3 = boto3.client(‘s3’)

    tgt_bucket = os.environ[‘dst_bucket’]
    src_bucket = event[‘Records’][0][‘s3’][‘bucket’][‘name’]
    key = urllib.parse.unquote(event[‘Records’][0][‘s3’][‘object’][‘key’])
  
    
    try: 
        copy_source = {‘Bucket’:src_bucket, ‘Key’:key}
        s3.copy_object(Bucket=tgt_bucket, Key=key, CopySource=copy_source, ACL=’bucket-owner-full-control’)
        print(‘Replication Done!’)
    except botocore.exceptions.ClientError as e:
        if e.response[‘Error’][‘Code’] == “NoSuchKey”:
            time.sleep(30)
            print(“Error – 30 sec waiting!”)
            s3.copy_object(Bucket=tgt_bucket, Key=key, CopySource=copy_source)
        else:
            logging.error(“Received error: {0}”.format(e), exc_info=True)
            return e.response[‘Error’][‘Code’]
    except Exception as e:
        logging.error(“Received error: {0}”.format(e), exc_info=True)
        return e.response[‘Error’][‘Code’]

    return {
        ‘statusCode’: 200,
        ‘body’: json.dumps(‘Object Copy OK’) 
  }
——————————————————————————————————————————————————————————–

(3) Lambda > Configuration에서 아래 내용을 입력해 주십시오.
Environment variable > dst_bucket : B account의 S3 Name 입력

* Tips!

VPC를 지정하면 Lambda가 수행될 시 특정 VPC 내부에서 수행되도록 subnet 및 Ip를 지정할 수 있습니다.
Lambda 수행 시에도 보안이 요구될 경우 사용할 수 있습니다.
단 가용한 IP 에 대한 사전 점검을 수행해주십시오.

이제 Lambda의 수행을 위한 필수 조건이 세팅되었습니다. 한 가지만 추가되면 됩니다. Lambda가 언제 실행되어야 하는지_Trigger 가 설정되지 않았습니다.

이 값은 S3의 콘솔에서 수행할 수 있습니다.
Source bucket > Properties > Event Notification > Event types > All object create events 선택

Destination : Lambda function > Choose from your Lambda functions 에서 위 Lambda 생성

이후 Lambda가 콘솔에 가보면 Trigger에 S3가 추가된 것을 볼 수 있습니다.
여기까지 수행하고 나면, 아래와 같은 아키텍트가 구현된 것이며, 오브젝트가 정상 복제됨을 알 수 있습니다.


[ 결론 ]

많은 구성이 요구되며, 구현하는 아키텍트와 구현되는 서비스에 대한 이해도가 있어야 합니다. 중요한 고려 사항은 아래와 같습니다.

(1) AWS에서 구현되는 시스템은 언제나 Role 및 Policy, 즉 권한에 대한 고민이 선행되어야 하며,
(2) EBS, S3, EFS 와 같은 저장 매체는 항상 암호화하는 것을 추천합니다. 그럼 KMS 와 Key의 사용권한도 역시 고민해야 할 대상에 추가됩니다.
(3) 추가로 S3 는 bucket policy를 통해 사용 권한을 통제할 수 있습니다.
(4) Lambda code 역시 적절히 작성되어 있어야 잘 동작합니다.

S3 object가 어디서 어디로 이동하는지, 누가 이동 시키는지, 어떤 권한이 필요한지 이런 복합적인 고민이 모두 효율적, 효과적으로 구현될 때
시스템은 안정적으로 보호되고, 정상적으로 운영됨을 기억해 주세요!

챗봇과 대화를 할 수 있어요