Lambda는 AWS에서 제공해주는 서버리스 서비스이다. 서버리스(Serverless)란 말 그대로 서버가 없다는 의미고 그 말은 진짜 서버가 아예 없다는게 아니라 관리할 서버가 없다. 즉, 관리할 필요가 없다란 의미가 된다.
개발자는 서버를 관리할 필요없이 애플리케이션을 빌드하고 실행할 수 있도록 하는 클라우드 네이티브 개발 모델을 서버리스 아키텍트라고 한다. 이 구조의 장점은 항상 대기하고 있는 전용 서버가 없어서 실행이 끝나면 자원을 반납하고 사용할 때만 자원을 가져다가 사용하는 구조라고 할 수 있다. 그러나 장점만 있는 구조는 없다. 장점과 단점을 둘 다 알아보자.
장점
서버 관리(자동 확장, 장애 방지)가 불필요
관리보다 개발에 집중이 가능
사용한 만큼 과금
급격한 트래픽 변화에 유연
단점
다른 클라우드 컴퓨팅 자원보다 비쌈
느림(호출과 동시에 서버가 세팅되기 때문)
장기적인 작업에는 적합하지 않음
함수의 처리 결과에 따라 상태를 따로 저장
단점 중 장기적인 작업에는 적합하지 않다라고 되어 있는데 이 말은 한 작업이 1시간, 2시간 또는 그 이상의 시간을 소요하는 작업이라면 그 시간만큼 과금이 되기 때문에 다른 클라우드 컴퓨팅 자원보다 비싼 과금을 내는 서버리스보단 온프레미스나 클라우드 컴퓨팅 서버를 사용하는게 더 유리할 수 있다는 소리다.
서버리스의 2가지 서비스 형태
BaaS(Backend As a Service)
클라우드 공급자가 제공하는 서비스를 이용해 백엔드 기능들을 쉽게 구현
Customizing이 어렵다
Google Firebase
FaaS(Function As a Service)
FaaS는 기능을 하나의 함수로 구현
함수가 실행할 때마다 서버 자원을 할당받아 사용
로직을 개발자가 작성하므로 Customizing이 가능
AWS Lambda
AWS Lambda 구축하기
이제 람다가 어떤것인지 알았으니 한번 만들어보고 사용해보자.
우선 AWS Console에 "Lambda"를 입력하고 나온 서비스를 클릭한다.
Create a function 버튼 클릭
설정화면은 다음과 같이 설정한다.
- Author from scratch
- Function name: cwchoiit-first-lambda
- Runtime: Python 3.12
Runtime은 어떤 환경을 사용할 것인지를 의미한다. NodeJS, Java 등등 다양하게 있다. 본인은 Python을 선택했다.
이 상태로 Create function 버튼을 클릭하면 Lambda가 만들어지면서 다음 화면이 노출된다.
아래 코드를 보자. 간단한 파이썬 코드로 된 함수 하나가 있다. 이 코드가 곧 하나의 람다 함수가 된다. 이 소스를 실행하기 위해 "Test" 버튼을 클릭해보자.
그럼 이러한 화면이 보여지는데 우선 Event name만 입력해보고 "Save" 버튼을 클릭해보자.
그러면 이렇게 내가 만든 이벤트 하나가 리스트에 보여진다. 이 이벤트를 클릭하면 아까 구성한대로 이벤트가 트리거될 준비가 된 상태이고 "Test" 버튼을 클릭하면 그 이벤트가 실행된다.
다음은 테스트 실행 결과다. 이벤트 이름, 응답 등등이 보여지고 코드에서 리턴하는 내용처럼 응답에는 statusCode와 body값이 있다.
그럼 아까 이벤트를 만들 때 이 부분이 무엇인지 알아보자.
람다 함수는 이 함수로 들어오는 데이터 같은 것들을 이벤트로 받아준다. 그 이벤트를 출력해보자.
이렇게 소스를 변경하면 Deploy 버튼을 클릭해서 새롭게 소스를 배포해야 한다.
배포가 끝나면 다시 테스트 버튼을 클릭해보자. 이 이벤트에 대한 내용이 출력될 것이다.
이렇게 이벤트를 출력하면 테스트 구성시에 작성했던 데이터가 담겨있는 것을 알 수 있다. 이렇듯 람다 함수에 뭔가를 전달할 때 이벤트라는 파라미터안에 그 데이터들이 담긴다고 생각하면 된다.
그리고 이 실행에 대한 로그를 볼 수 있는데 로그는 CloudWatch에 기록된다. 한번 확인해보자. 위에 탭 중에 Monitor 탭이 있다. 해당 탭을 누르면 다음 화면이 보이는데 여기서 View CloudWatch logs 버튼을 클릭하자.
그럼 이러한 화면이 보여진다. 내가 만든 람다 함수에 대한 로그가 기록되어있다. 그리고 저 아래 빨간 표시로 해둔것은 배포 버전별로 다르게 기록되는 로그이다. 처음 람다함수를 만들면 그게 최초 배포버전이고 내가 중간에 이벤트를 찍어보겠다고 소스를 변경하고 배포를 다시했기 때문에 저렇게 두 개의 다른 로그가 쌓인다고 보면된다. 배포를 새로 또 하면? 3개가 된다.
안으로 들어가보면 다음처럼 화면이 보여진다. 내가 찍은 "print(event)" 역시 로그로 남게 된다. 이렇게 로그도 확인이 가능하다.
이렇게 간단하게 AWS Lambda 함수를 생성해서 서버리스 서비스를 구현해보았다. 이 서버리스를 API Gateway에 붙여서 실제 서버가 있는것처럼 실행하게 만들수도 있다. 그 부분을 다음 포스팅에서 해보자.
# encrypt.py
import aws_encryption_sdk
from base64 import b64encode
from aws_encryption_sdk import CommitmentPolicy
# AWS KMS Client 생성
client = aws_encryption_sdk.EncryptionSDKClient(commitment_policy=CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT)
# Your AWS KMS Key ARN
key_arn = "your aws kms key arn"
# 암호화 하고자 하는 텍스트
source_plaintext = "hi"
kms_kwargs = dict(key_ids=[key_arn])
# 나의 CMK
master_key_provider = aws_encryption_sdk.StrictAwsKmsMasterKeyProvider(**kms_kwargs)
# 암호화 된 텍스트와 Encryptor Header
ciphertext, encryptor_header = client.encrypt(source=source_plaintext, key_provider=master_key_provider)
# 암호화 된 텍스트 출력
print(ciphertext)
# 복호화 된 텍스트와 Decryptor Header
cycled_plaintext, decrypted_header = client.decrypt(source=ciphertext, key_provider=master_key_provider)
# 복호화 된 텍스트 출력
print(cycled_plaintext)
이제 이 파일을 실행해보자.
python3 encrypt.py
실행 결과:
결과를 보면 암호화 된 텍스트는 저렇게 쭉 길게 늘어진 알 수 없는 문자열이고 복호화 된 텍스트는 빨간 박스로 친 'hi'이다.
결론
이렇게 AWS KMS를 사용해서 암호화 하고자하는 것을 암호화하고 복호화할 수 있다. 주로 사용되는 예시라고 함은 Django를 사용할 때 settings.py 파일에서 ALLOWED_HOSTS = []에 사용되는 URL을 암호화한 상태로 저장하고 복호화하는 코드만 넣어서 URL을 숨길수도 있다.
NACL과 Security group 모두 보안을 위한 녀석들이다. AWS에서 크게 인스턴스를 보호하는 방법은 두가지인데 그 두 가지가 이 NACL과 Security group이다. 이 두 녀석으로 외부의 특정 클라이언트와 통신을 허가할지 거부할지를 결정하게 된다. 그럼 이 둘의 차이는 무엇일까?
NACL (Network Access Control List)
NACL은 Stateless한 보안 체계이다. 즉, 요청을 한 번 받으면 그 요청에 대해 기억하고 있지 않다는 의미이다. 그래서 다음 그림을 보자.
NACL로 보안 체계를 걸어둔 VPC가 있는데 이 NACL이 들어오는 포트는 80으로 허가했고 나가는 포트는 아무것도 허가한게 없을 때, 외부에 있는 클라이언트에서 요청이 들어오면 클라이언트는 응답을 받을 수 있을까?
요청은 가능하지만 응답은 할 수 없다. 왜냐하면 NACL은 Outbound로 허가한 포트가 없고 NACL은 Stateless 보안 체계이기 때문이다.
그래서 NACL은 Inbound, Outbound 규칙이 명확하게 명시되어야 한다. 오로지 규칙만을 보고 요청과 응답을 허가하거나 거부하기 때문이다.
그렇다면 Security group은 어떻게 다를까?
Security group
Security group은 Stateful한 보안 체계이다. 즉, 들어오는 요청을 기억하고 있다가 응답한다는 의미이다. 결론부터 말하면 이 Security group에 위 NACL처럼 Inbound에 80, Outbound에 아무것도 허가하지 않더라도 요청을 80으로 했다면 클라이언트는 요청에 대한 응답을 받을 수 있다.
클라이언트가 특정 VPC내 80 포트에 매핑된 어떤 서버에 접속할 때 이 EC2 보안 그룹에 인바운드 아웃바운드 규칙이 그림처럼 되어있다면 아웃바운드로 아무것도 허가하지 않았지만 응답을 받을 수 있다. 이는 Security group이 Stateful 보안 체계라 들어온 요청을 기억하고 있다가 응답을 보내주기 때문이다.
NACL 설정해보기
NACL을 직접 설정해서 어떻게 동작하는지 확인해보자. 우선 VPC > Security > Network ACLs로 들어가야 한다.
이렇게 보다시피 화면 우측 상단 Create network ACL 버튼이 보인다. 클릭.
나는 아래와 같이 설정한 뒤 만들었다.
- Name: private-nacl
- VPC: 사용중인 VPC
만들고 나면 리스트에 만든 NACL이 보이는데, 클릭해서 우선 서브넷을 연동하자.
연동하고자 하는 서브넷을 선택하고 'Save changes' 클릭
이제 Inbound, Outbound 설정을 해야한다. 인바운드는 들어오는 트래픽이고 아웃바운드는 여기서 나가는 트래픽을 말한다.
예를 들어, Bastion host에서 이 private EC2로 SSH로 접속하는것을 허용하고 싶다면 Inbound에는 SSH 또는 포트(22) 선택을 한 후 Source에 Bastion host가 위치한 IPv4 CIDR를 입력하면 된다.
중요한건 이제 밖으로 나가는 Outbound인데 여기서 나가는 포트는 22가 아니다. 왜냐하면 들어오는 포트가 22인거지 나를 호출한 곳은 22가 아니란 얘기다. Outbound에 22로 설정하면 이 private EC2에서 어딘가로 SSH를 접속하는것을 의미하게 된다. 난 그것을 원하는게 아니고 나의 22번 포트로 들어온 요청에 대한 응답을 보내고자 한다. 그래서 Outbound는 임시포트(1024 - 65535) 중 아무거나를 허용해야 한다. 다음 그림처럼 말이다.
자, 이렇게 설정하고 Bastion host에서 이 private EC2에 접속해보자.
위 그림에서 10.0.0.208이 private EC2이고, 10.0.0.24가 Bastion host이다. 접속해보자.
다음과 같이 잘 접속된다.
NACL은 Stateless 보안 체계라고 했다. 그래서 규칙이 절대적인데 그 말은 이 상태에서 Outbound 규칙을 제거하면 접속이 불가능해야 한다. 한번 해보자. 보다시피 Outbound 규칙이 전부 Deny 상태이다.
다음 화면처럼 접속되지 않는다. 이런 보안 체계가 NACL이다. 만약, Security group으로 같은 행위를 했다면 정상적으로 접속이 될 것이다. 왜냐하면 Security group은 Statefull한 보안 체계니까.
위 설정으로 Docker Compose를 실행하면 마주하는 MySQL과 Spring과의 연결 에러가 나타난다. 최초에는 어떤 작업을 해줘야 하냐면 MySQL 유저를 생성하고 내가 연결하려는 데이터베이스에 접근 권한을 허용해야 한다.
다음은 모든 외부 접근을 허용하게 하는 유저를 만드는 것이다. 사실 좋은 방법은 아니다. 정확히 어디서 접근할지 판단 후 그 IP만 허용해주는게 더 좋은 방식일 것.
create user 'username'@'%' identified by 'password';
grant all privileges on dbname.* TO 'username'@'%';
flush privileges;
이렇게 유저를 만들면 이제 해당 유저를 통해 스프링에서 데이터베이스로 접근할 수 있게 된다.
다음은, 가끔 이상한 MySQL 에러가 발생하는데 이러한 에러가 그 경우 중 하나다.
java.sql.SQLException: Illegal mix of collations (latin1_swedish_ci,IMPLICIT) and (utf8mb4_general_ci,COERCIBLE) for operation '='
이건 이제 테이블 별로 인코딩이 서로 다르기 때문에 발생하는 에러라고 하는데 이를 해결하는 방법은 인코딩을 설정해주는 것.
SET collation_connection = 'utf8_general_ci';
ALTER DATABASE your_database_name CHARACTER SET utf8 COLLATE utf8_general_ci;
ALTER TABLE your_table_name CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;
나머지는 기본값으로 세팅 후 "Restore backup" 버튼을 클릭해서 복원한다. 완료가 되면 해당 스냅샷 ID를 가져와서 EC2 > Elastic Block Store > Volume 으로 간다. (해당 스냅샷 ID는 Backup vault에서 확인 가능하다. 아래 사진 참고)
Volume 리스트에 스냅샷으로부터 복원된 EBS가 보인다.
이제 이 복원한 EBS를 EC2에 붙여보자. 그러기 위해선 일단 EC2의 Volume을 떼어내야 한다. 그러기 위해선 EC2를 중지해야 한다. 먼저 중지를 하자. 중지가 완료되면 해당 인스턴스의 볼륨을 떼어낸다. 떼어내고 싶은 인스턴스를 클릭 후 하단 "Storage" 탭에서 볼륨을 클릭한다.
볼륨을 클릭하면 볼륨 리스트에서 해당 볼륨이 보이는데 선택 후 우측 Actions > Detach volume 클릭
잘 떼어내면 상태가 In-Use에서 Available로 변경된다.
이제 백업한 볼륨을 붙여보자. 다시 볼륨리스트로 가서 백업한 볼륨을 선택하고 Actions > Attach volume 클릭
그러면 복원 설정 화면에서 다음을 수정한다.
- Instance: 볼륨에 붙일 인스턴스
- Device name: /dev/xvda
Attach volume을 클릭하면 백업한 볼륨이 인스턴스에 붙는다. 붙었는지 확인하려면 상태를 보자. 해당 볼륨이 Available이 아니고 In Use로 보여진다.
인스턴스에서도 확인 가능하다. 백업한 볼륨을 붙인 인스턴스를 클릭해서 하단 Storage 탭을 클릭하면 볼륨 ID가 보인다. 백업한 볼륨의 ID인지 확인.
RDS 백업
이번에는 RDS를 백업해보자. AWS Backup 서비스로 가서 Dashboard 화면에서 "Create on-demand backup" 버튼 클릭
설정 부분은 다음과 같다.
- Resource type: RDS
- Database name: 백업할 RDS
- Backup window: Create backup now
- Total retention period: Forever
- Backup vault: 위에서 사용했던 vault
- IAM Role: Default role
이렇게 설정한 후 백업을 만든다. 그러면 Job 리스트에서 백업이 진행중인 것을 확인 가능하다.
RDS 백업은 시간이 조금 소요되므로 여유롭게 기다려보자. 시간을 좀 기다리고 나니 다음과 같이 백업이 완료됐다. Status가 Completed 상태가 된 것을 확인할 수 있다.
그리고 이 백업된 스냅샷은 RDS 서비스 내 Snapshots 화면에서도 확인할 수 있다. Backup service 탭을 눌러보면 다음과 같이 스냅샷이 보인다.
여기서 한 가지 알고 가야하는건 RDS 서비스를 만들면 시스템에서 자동으로 백업을 하는데 그 백업 시간이 있다. 이 백업 시간과 내가 직접 백업을 하려는 백업 시간이 너무 가까우면 위 작업을 따라했을 때 백업이 되지 않는다. 이를 참고하자. RDS의 백업 시간은 아래 사진처럼 확인 가능하다.
백업 시간을 확인하고자 하는 RDS를 클릭 > Maintenance & backups 탭 클릭후 Backup 섹션을 보자.
여기서 Backup window 항목을 보면 백업 시간을 확인할 수 있다. 이 시간을 변경할수도 있다. 참고하자.
이제 백업한 RDS로 복원을 해보자. 백업 볼트로 가서 백업한 RDS를 선택해서 Actions > Restore
복원 설정 화면에서 수정할 부분은 다음과 같다.
- DB Instance class: 원하는 클래스 선택
- DB Instance Identifier: 적절하게 입력
그 외는 기본으로 세팅된 원래 RDS 서비스와 동일한 설정을 따르고 복원을 해보면 된다.
근데 위 설정처럼 하면 다음과 같은 에러를 마주하게 된다.
t2.micro는 암호화가 지원이 안된다고 하니 살짝 더 큰 클래스를 선택해보자. (본인은 t2.small을 선택) 이 부분도 시간이 꽤 소요되니까 잘 기다려보자. 다 끝나면 다음과 같이 상태가 Completed로 변경된다.
그리고 RDS 서비스 내 Databases 화면으로 가면 백업한 RDS가 생성됐음을 확인할 수 있다.
백업한 RDS 세부 정보를 보면 Security group이 default로 설정되어 있다. 이것을 바꿔보자. 기존 RDS가 사용하는 보안 그룹으로.
테이블을 만들면 버킷에서 로그가 잘 쌓여있는지 확인해보자. 아까 트래픽을 발생시켰으니까 시간이 좀 지나면서 로그가 쌓일것이다.
아래 사진과 같이 오늘 날짜로 된 폴더 내 로그가 보인다.
이제 해당 로그 파일에서 원하는 쿼리를 날려보자. 다음은 위 링크에서 가져올 수 있는 예시 쿼리이다.
이 쿼리는 클라이언트 IP 주소별로 그룹화된 로드 밸런서가 수신한 HTTP GET 요청의 개수를 계산한다. 날려보자.
SELECT COUNT(request_verb) AS
count,
request_verb,
client_ip
FROM alb_logs
GROUP BY request_verb, client_ip
LIMIT 100;
쿼리 결과는 다음처럼 나온다. 내 IP는 11회 조회를 했고 그 외 나머지는 아마도 AWS의 서울 각 가용영역에서 접근한 것으로 보여진다.
ELB Access Log 중간정리
ELB의 Access Log를 Athena를 통해 쿼리문을 날려 원하는 데이터만 뽑아봤다. 이렇게 접근 관련 로그도 잘 남길 수 있게 됐다.
Amazon VPC Flow Log
이번에는 VPC Flow Log를 활성화해서 Athena에서 확인해보자.
AWS Console에서 VPC를 검색해서 서비스에 들어가고 화면 좌측 VPC를 클릭한다.
Flow log를 활성화 시킬 VPC를 선택한 다음에 하단 Flow logs 탭을 클릭해서 Create flow log 버튼을 누른다.
진입 화면에서 다음 사진에서 빨간 박스 부분을 수정해야 한다.
- Name: cwchoiit-vpc-flow-logs
- Destination: S3 bucket
- S3 bucket ARN: S3 버킷의 ARN을 입력 (S3 Bucket은 직접 만들어서 ARN 입력란에 입력해야 한다)
입력해서 만들면 Flow logs 탭에 만든 플로우 로그가 보인다. (나는 Maximum aggregation interval을 1분으로 수정했다)
이렇게 생성한 후 S3 버킷으로 가보면 폴더가 자동으로 아래처럼 만들어진다. (AWSLogs/AWSAccountID 경로)
아직 로그가 생성되지 않았고 이 로그를 생성하기 위해 트래픽을 발생시켜보자. 이 VPC 플로우 로그는 EC2에 붙어있는 네트워크 인터페이스에서 발생하는 모든 트래픽을 트래킹한다. 아래 사진이 EC2의 네트워크 인터페이스에 들어가면 보여지는 모든 네트워크 인터페이스이다. 모두 동일한 VPC(위에서 설정한 VPC)에 붙어있는 네트워크 인터페이스이고 이 네트워크를 통한 트래픽에 대한 로그를 남긴다.
트래픽을 발생시켜서 S3 버킷에 로그가 쌓이는지 확인해보자. 다음과 같이 오늘 날짜 경로에 로그가 저장된다.
이 로그를 Athena를 통해 쿼리로 데이터를 추출해보자. Athena를 통해 쿼리를 날리려면 VPC 플로우 로그에 대한 테이블이 만들어져야 한다. 테이블 만드는 쿼리는 아래 링크를 참조.
해당 링크에서 테이블 만드는 쿼리를 가져왔으면 Athena 서비스로 들어가서 테이블을 만든다. 다음이 테이블 만드는 쿼리인데 여기서 변경할 부분은 그 바로 아래와 같다.
CREATE EXTERNAL TABLE IF NOT EXISTS `vpc_flow_logs` (
version int,
account_id string,
interface_id string,
srcaddr string,
dstaddr string,
srcport int,
dstport int,
protocol bigint,
packets bigint,
bytes bigint,
start bigint,
`end` bigint,
action string,
log_status string,
vpc_id string,
subnet_id string,
instance_id string,
tcp_flags int,
type string,
pkt_srcaddr string,
pkt_dstaddr string,
region string,
az_id string,
sublocation_type string,
sublocation_id string,
pkt_src_aws_service string,
pkt_dst_aws_service string,
flow_direction string,
traffic_path int
)
PARTITIONED BY (`date` date)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY ' '
LOCATION 's3://DOC-EXAMPLE-BUCKET/prefix/AWSLogs/{account_id}/vpcflowlogs/{region_code}/'
TBLPROPERTIES ("skip.header.line.count"="1");
이렇게 해서 테이블을 생성한 다음 한 가지 더 해줄게 있다. 테이블을 수정해야 하는데 데이터를 읽을 수 있도록 파티션을 생성해야 한다. 왜냐하면 테이블 만들 때 쿼리를 잘 보면 PARTITIONED BY ('date' date)라고 입력한 부분이 있다. 날짜로 파티션을 나눴기 때문에 하단 쿼리처럼 원하는 날짜에 대한 파티션을 추가해야 그 날짜에 대한 쿼리에 대한 결과를 출력할 수 있다.
ALTER TABLE vpc_flow_logs
ADD PARTITION (`date`='YYYY-MM-dd')
LOCATION 's3://DOC-EXAMPLE-BUCKET/prefix/AWSLogs/{account_id}/vpcflowlogs/{region_code}/YYYY/MM/dd';
그래서 위 코드에서 파티션을 생성할 날짜를 입력해야 한다. 날짜는 당연히 원하는 쿼리에 해당하는 날짜를 입력해야 한다. 아래는 그 예시다.
ALTER TABLE vpc_flow_logs
ADD PARTITION (`date`='2023-01-28')
LOCATION 's3://cwchoiit-vpc-flow-logs/AWSLogs/135149110460/vpcflowlogs/ap-northeast-2/2023/01/28';
이렇게 파티션을 만들었으면 이제 예시 쿼리를 날려보면 된다. 아래는 예시 쿼리이고 지정한 날짜에 최대 100개의 흐름을 출력한다.
SELECT *
FROM vpc_flow_logs
WHERE date = DATE('2020-05-04')
LIMIT 100;
그럼 다음과 같이 결과가 쭉 나온다.
또 다른 예시는 해당 날짜에 거절된 트래픽이다. 결과가 잘 출력되는 모습이다.
VPC Flow Log 정리
VPC Flow log와 Athena를 통해 원하는 쿼리문을 작성해서 로그를 출력할 수 있었다.