AWS

EKS-NodeLess-01-CoreDNS

EKS의 관리영역중 Addon 이나 필수 컴포넌트중에 Node에서 동작해야하는 것들이 있다. 이 경우에 NodeGroup을 운영해야한다. NodeGroup에 여러 파드들이 스케줄링되고 관리형 Pod들은 다른 서비스에 운영되는 NodeGroup과 섞여서 스케줄리되어야 하는데, 이것의 가장큰 문제는 Node의 사망이 기능의 장애로 이어진다는 점이다. 따라서 Node를 전용 Node로 사용하면 좋은데 아주작은 노드를 스케줄링한다고 해도 관리되어야 하는 대상이 됨은 틀림없고, 노드를 정해서 사용해야 하는 문제점들이 생기게된다.

이러한 문제를 해결하기에 EKS에서는 Fargate가 있다. 1Node - 1Pod 라는게 아주 중요한 포인트다.

CoreDNS는 클러스터에 최저 2개의 Pod가 스케줄링되어야 한다.

https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/fargate-profile.html

eksctl를 사용하여 Fargate 프로파일을 생성하려면 다음 eksctl 명령으로 Fargate 프로파일을 생성하고 모든 example value를 고유한 값으로 바꿉니다. 네임스페이스를 지정해야 합니다. 그러나 --labels 옵션은 필요하지 않습니다.

eksctl create fargateprofile \
    --cluster my-cluster \
    --name kube-system \
    --namespace kube-system

다음과 같이 생성해 주면된다. 그럼 kube-system namespace로 스케줄링되는 Pod는 Fargate로 생성되게 된다.

그다음은 CoreDNS를 패치하고 재시작하면된다.

https://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/patterns/deploy-coredns-on-amazon-eks-with-fargate-automatically-using-terraform-and-python.html

kubectl patch deployment coredns -n kube-system --type=json -p='[{"op": "remove", "path": "/spec/template/metadata/annotations", "value": "eks.amazonaws.com/compute-type"}]'
kubectl rollout restart -n kube-system deployment coredns

이렇게 진행하면 CoreDNS를 Fargate로 실행하게 된다.

 k get pod -o wide
NAME                      READY   STATUS    RESTARTS   AGE     IP              NODE                                                       NOMINATED NODE   READINESS GATES
coredns-fd69467b9-bsh88   1/1     Running   0          5h18m   192.168.13.23   fargate-ip-192-168-13-23.ap-northeast-2.compute.internal   <none>           <none>
coredns-fd69467b9-gn24k   1/1     Running   0          5h18m   192.168.12.34   fargate-ip-192-168-12-34.ap-northeast-2.compute.internal   <none>           <none>

다음과 같이 스케줄링되면 정상적으로 배포 된것이다.

AWS-SQS-터져랏

일단 SQS를 터질때 까지 밀어넣어 보기로 했다.

목표 메시지수는 100만건.

100만건의 메시지를 100초안에 SQS에 넣는게 목표다. TPS 10000 이라는 소리다.

목표를 이루기위해선 첫번째 SQS의 TPS는 3000이다. 초당 3000의 메시지를 넣을수 있다.

먼저 큐를 4개를 만들었다. 목표수치에 가려면 TPS가 10000은 나와야한다.

그렇다면 큐를 병렬로 줄세운다 4개의 큐를 만든다.

이제 넣어봤다.

import boto3
import json
import uuid
from concurrent.futures import ThreadPoolExecutor
import random
import string

sqs = boto3.client('sqs')
queue_urls = [
    'linuxer-sqs-1',
    'linuxer-sqs-2',
    'linuxer-sqs-3',
    'linuxer-sqs-4'
]

def random_string(length):
    return ''.join(random.choices(string.ascii_letters + string.digits, k=length))

def create_dummy_data():
    return {
        'id': str(uuid.uuid4()),
        'data': f"host-{random_string(5)}-count",
        'padding': random_string(10 * 1024 - 100)  # 10KB 크기의 더미 데이터를 생성
    }

def send_message_batch(queue_url, messages):
    entries = []

    for idx, message in enumerate(messages):
        entries.append({
            'Id': str(idx),
            'MessageBody': json.dumps(message)
        })

    response = sqs.send_message_batch(
        QueueUrl=queue_url,
        Entries=entries
    )
    return response

def generate_and_send_dummy_data(num_messages=100000, batch_size=10, num_threads=10):
    with ThreadPoolExecutor(max_workers=num_threads) as executor:
        for _ in range(num_messages // (batch_size * num_threads * len(queue_urls))):
            batch_futures = []
            for queue_url in queue_urls:
                messages = [create_dummy_data() for _ in range(batch_size)]
                future = executor.submit(send_message_batch, queue_url, messages)
                batch_futures.append(future)

            for future in batch_futures:
                future.result()

if __name__ == '__main__':
    generate_and_send_dummy_data()

대충 이코드는 TPS 100 정도이다.

5분 정도 걸려서 10만 건의 메시지를 모두 PUT했다. 분당 20000 TPS 333정도다.

병렬처리했다.

import boto3
import json
import uuid
from concurrent.futures import ThreadPoolExecutor
from multiprocessing import Process
import random
import string

sqs = boto3.client('sqs')
queue_urls = [
    'linuxer-sqs-1',
    'linuxer-sqs-2',
    'linuxer-sqs-3',
    'linuxer-sqs-4'
]

def random_string(length):
    return ''.join(random.choices(string.ascii_letters + string.digits, k=length))

def create_dummy_data():
    return {
        'id': str(uuid.uuid4()),
        'data': f"host-{random_string(5)}-count",
        'padding': random_string(10 * 1024 - 100)  # 10KB 크기의 더미 데이터를 생성
    }

def send_message_batch(queue_url, messages):
    entries = []

    for idx, message in enumerate(messages):
        entries.append({
            'Id': str(idx),
            'MessageBody': json.dumps(message)
        })

    response = sqs.send_message_batch(
        QueueUrl=queue_url,
        Entries=entries
    )
    return response

def generate_and_send_dummy_data(queue_url, num_messages=1000000, batch_size=10, num_threads=10):
    with ThreadPoolExecutor(max_workers=num_threads) as executor:
        for _ in range(num_messages // (batch_size * num_threads)):
            batch_futures = []
            for _ in range(num_threads):
                messages = [create_dummy_data() for _ in range(batch_size)]
                future = executor.submit(send_message_batch, queue_url, messages)
                batch_futures.append(future)

            for future in batch_futures:
                future.result()

def start_processes(num_processes):
    processes = []
    for queue_url in queue_urls:
        for _ in range(num_processes):
            process = Process(target=generate_and_send_dummy_data, args=(queue_url,))
            processes.append(process)
            process.start()

    for process in processes:
        process.join()

if __name__ == '__main__':
    num_processes = 4
    start_processes(num_processes)
501 17512 16975   0  9:27PM ttys001    0:00.21 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python main.py
  501 17513 17512   0  9:27PM ttys001    0:00.05 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python -c from multiprocessing.resource_tracker import main;main(6)
  501 17514 17512   0  9:27PM ttys001    0:14.41 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=7, pipe_handle=9) --multiprocessing-fork
  501 17515 17512   0  9:27PM ttys001    0:14.50 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=7, pipe_handle=11) --multiprocessing-fork
  501 17516 17512   0  9:27PM ttys001    0:14.36 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=7, pipe_handle=14) --multiprocessing-fork
  501 17517 17512   0  9:27PM ttys001    0:14.60 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=7, pipe_handle=16) --multiprocessing-fork
  501 17518 17512   0  9:27PM ttys001    0:14.55 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=7, pipe_handle=18) --multiprocessing-fork
  501 17519 17512   0  9:27PM ttys001    0:14.21 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=7, pipe_handle=20) --multiprocessing-fork
  501 17520 17512   0  9:27PM ttys001    0:14.16 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=7, pipe_handle=22) --multiprocessing-fork
  501 17521 17512   0  9:27PM ttys001    0:14.11 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=7, pipe_handle=24) --multiprocessing-fork
  501 17522 17512   0  9:27PM ttys001    0:14.46 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=7, pipe_handle=27) --multiprocessing-fork
  501 17523 17512   0  9:27PM ttys001    0:14.55 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=7, pipe_handle=29) --multiprocessing-fork
  501 17524 17512   0  9:27PM ttys001    0:14.24 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=7, pipe_handle=31) --multiprocessing-fork
  501 17525 17512   0  9:27PM ttys001    0:14.19 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=7, pipe_handle=33) --multiprocessing-fork
  501 17526 17512   0  9:27PM ttys001    0:14.28 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=7, pipe_handle=35) --multiprocessing-fork
  501 17527 17512   0  9:27PM ttys001    0:14.18 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=7, pipe_handle=37) --multiprocessing-fork
  501 17528 17512   0  9:27PM ttys001    0:14.49 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=7, pipe_handle=39) --multiprocessing-fork
  501 17529 17512   0  9:27PM ttys001    0:14.46 /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=7, pipe_handle=41) --multiprocessing-fork

이제 멀티프로세스로 꼽는다!!

코드보니 400만개를 넣도록 되어있어서 100만개 넣을시점에 끊었다.

10분정도에 100만개의 메시지.. TPS 1600 정도다 아직 더 올릴수 있는 가망성이 보이지만 이건이제 컴퓨팅의 문제다.
이제 병렬처리만으로도 가능함을 알았으니..producer 의 병렬성을 더 올린다. num_processes = 12 4배다! 일단 터트려 보자.

ps -ef | grep "multiprocessing.spawn" | wc -l
      49

49개의 프로세스가 미친듯이 공격을 한다. M1 진짜 좋다.

아...내 노트북으로 낼수있는 TPS는 1600이 한계다. 이제 컨슈밍을 할거다. SQS에 있는 데이터를 꺼내쓰는 속도를 확인할거다. 큐에는 100만개의 데이터가 쌓여있고 이걸 모두 소모하는 속도를 확인하려한다.

import boto3
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from multiprocessing import Process

sqs = boto3.client('sqs')
queue_urls = [
    'linuxer-sqs-1',
    'linuxer-sqs-2',
    'linuxer-sqs-3',
    'linuxer-sqs-4'
]

def receive_and_delete_message(queue_url, wait_time=20):
    while True:
        response = sqs.receive_message(
            QueueUrl=queue_url,
            AttributeNames=['All'],
            MaxNumberOfMessages=1,
            WaitTimeSeconds=wait_time
        )

        if 'Messages' in response:
            message = response['Messages'][0]
            receipt_handle = message['ReceiptHandle']
            sqs.delete_message(
                QueueUrl=queue_url,
                ReceiptHandle=receipt_handle
            )
        else:
            break

def process_messages(queue_url, num_threads=10):
    with ThreadPoolExecutor(max_workers=num_threads) as executor:
        futures = [executor.submit(receive_and_delete_message, queue_url) for _ in range(num_threads)]
        for future in as_completed(futures):
            future.result()

def start_processes(num_processes):
    processes = []
    for queue_url in queue_urls:
        for _ in range(num_processes):
            process = Process(target=process_messages, args=(queue_url,))
            processes.append(process)
            process.start()

    for process in processes:
        process.join()

if __name__ == '__main__':
    num_processes = 100
    start_time = time.time()
    start_processes(num_processes)
    end_time = time.time()

    print(f"Elapsed time: {end_time - start_time:.2f} seconds")

멀티프로세스 100개를 돌렸더니 M1이 뻣었다. 그렇지만 1분에 10만개 정도는 가볍게 뽑아가는걸 확인할수있었다.

이 결과만으로 SQS는 튼튼한 큐라는걸 알수 있었다. 그럼 이렇게 하드코어하게 넣었으니 에러레이트를 확인해 봐야했다.

지연되거나 데드레터큐에 쌓인메시지는 없었다. 모두 정상적으로 소진된것이다.

여기서 결론은 mac book M1 air 로서 낼수 있는 퍼포먼스는 TPS1600이다. 분당 10만건의 메시지를 처리할수 있는 능력이라는것.. 이다음은 컴퓨팅 자원을 마음껏 넣어서 큐를 터트려 보겠다.

밤새 머리속에서 어떻게 하면 좋을까 고민하다가 EKS에서 Job을 이용해 병렬 처리속도를 늘려보기로 했다.

도커로 말고~

CMD로 job을 실행할때 파일을 지정해서 실행하도록 했다.

# Dockerfile
FROM python:3.8-slim

WORKDIR /app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY send.py receive.py ./

CMD ["python", "receive.py"]
apiVersion: batch/v1
kind: Job
metadata:
  name: sqs-test
spec:
  parallelism: 20
  template:
    spec:
      containers:
      - name: sqs-test
        image: sqs_test:latest
        command: ["python3", "send.py"] // ["python", "receive.py"] 로 변경할수 있다.
        resources:
          limits:
            cpu: "2"
            memory: "2Gi"
          requests:
            cpu: "1500m"
            memory: "1Gi"
      restartPolicy: Never
  backoffLimit: 4

컨테이너 말고 parallelism 으로 20의 pod를 예약했다.

Send 는 분당 26만개 TPS 대략 4300

receive 는 32만개 대략 TPS 5000 정도이다.

5분에 100만개를 처리할수 있는 능력이라 보이고 이 플로우의 장점은 job에서 모든 데이터를 다꺼내쓰면 Completed로 컨테이너가 완료되므로 시간에 따른 큐에 대한 스케줄링이 가능하다는 뜻이다.

job을 스케줄링할때 큐에 쌓인 지연시간+큐에쌓인 갯수를 모니터링하고 job을 이용해 큐를 비우는 방식의 아키텍처를 설계할수 있다는 이야기다.

 k get pod
NAME                  READY   STATUS      RESTARTS   AGE
sqs-test-send-2s6h4   0/1     Completed   0          4m23s
sqs-test-send-5276z   0/1     Completed   0          4m23s
sqs-test-send-72ndr   0/1     Completed   0          4m22s
sqs-test-send-c24kn   0/1     Completed   0          4m22s
sqs-test-send-ccz5r   0/1     Completed   0          4m23s
sqs-test-send-fjfnk   0/1     Completed   0          4m23s
sqs-test-send-h2jhv   0/1     Completed   0          4m22s
sqs-test-send-k7b8q   0/1     Completed   0          4m22s
sqs-test-send-ljbv5   0/1     Completed   0          4m23s
sqs-test-send-mjvh9   0/1     Completed   0          4m23s
sqs-test-send-n8wh4   0/1     Completed   0          4m23s
sqs-test-send-ngskk   0/1     Completed   0          4m22s
sqs-test-send-qj9ks   0/1     Completed   0          4m22s
sqs-test-send-r87hf   0/1     Completed   0          4m22s
sqs-test-send-rr58h   0/1     Completed   0          4m23s
sqs-test-send-sf2bd   0/1     Completed   0          4m23s
sqs-test-send-svn8d   0/1     Completed   0          4m22s
sqs-test-send-tqfg4   0/1     Completed   0          4m23s
sqs-test-send-tv68j   0/1     Completed   0          4m22s
sqs-test-send-w99hx   0/1     Completed   0          4m23s

결과가 놀라운데 4분23초 만에 pod의 스케줄링+job(120만건의 메시징컨슘)이 모두 완료된건이다.

이아키텍처에는 카펜터가 사용되었는데, 카펜터의 노드는 0에서 시작하여 새로 노드를 프로비저닝해서 깔끔하게 모두 완료된것이다. 스케줄링이 놀랍다.

재미있는 테스트였다.

이다음은 KaFka를 테스트 하겠다.

T101-AWS-To-SLACK-Noti - EventBridge

이제야 블로그가 손에 잡혀서 오랜만에 글을 쓰기위해 책상앞에 앉았다. 이게다 내 게으름 때문이다.

맨날 이 뻔한 핑계를 치면서 한번 웃고야 말았다.

이번에 쓸 블로깅은 T101에서 한번 발표한 적인 있는 내용이다.

이 포스팅에선 EventBridge와 CloudTrail을 집중적으로 다룬다.

https://nyyang.tistory.com/126 이블로그를 보고 작업을 시작했다.

먼저 시작하기전에 EventBridge Bus 규칙에서 Trail에서 패턴을 감지하기위해선 이벤트버스는 무조건 Default여야한다. 다른 버스에 만들면 버스 지나간 다음 손 흔들어야 한다. 패턴을 감지할수 없다는 이야기다.

골자는 이렇다.

CloudTrail 에서 발생하는 이벤트를 EventBridge 는 특정 패턴을 감지해서 이벤트를 발생시킬수 있다.

아래 예가 그렇다.

{
  "source": ["aws.iam", "aws.ec2"],
  "detail-type": ["AWS API Call via CloudTrail"],
  "detail": {
    "eventSource": ["iam.amazonaws.com", "ec2.amazonaws.com"],
    "eventName": ["AttachGroupPolicy", "AttachRolePolicy", "AttachUserPolicy", "ChangePassword", "CreateAccessKey", "CreateGroup", "CreatePolicy", "CreateRole", "CreateUser", "DeleteAccessKey", "DeleteGroup", "DeleteGroupPolicy", "DeletePolicy", "DeleteRole", "DeleteRolePolicy", "DeleteUser", "DeleteUserPolicy", "DetachGroupPolicy", "DetachRolePolicy", "DetachUserPolicy", "PutGroupPolicy", "PutRolePolicy", "PutUserPolicy", "AuthorizeSecurityGroupIngress", "AuthorizeSecurityGroupEgress", "RevokeSecurityGroupIngress", "RevokeSecurityGroupEgress"]
  }
}

AWS ec2와 iam에서 발생하는 특정 패턴을 감지하여 이벤트를 발생시키는것이다.

여기에서 내가 굉장히 많은시간 고민을했다. 이유는 패턴 때문이다. 내가 감지하고 싶은 패턴은 AWSConsoleLogin 이다. 이 API가 속하는 source 와 detail-type 이 정리된 곳이 없었기 때문이다. 또한 EventBridge에서 템플릿으로 제공하는 이벤트 패턴으로 테스트했을 땐 잘되지 않았다. 고민했던 부분은 총 3가지 였다.

첫번째로 이벤트 패턴을 감지하기위해서 일반적으로 source 와 detail-type 을 지정해줘야했는데 모든예제는 Source 를 무조건 사용하도록 되어있었다. EventBridge 에선 3가지 이벤트 패턴을 사용할수 있는데 그중 하나만 사용해도 문제가 없다.
source / detail-type / detail 이렇게 세가지이다.

두번째 문제는 Trail에 찍히는 로그와 EventBridge 에 전달되는 이벤트의 내용이 다르다.

{
'version':'0',
'id':'1',
'detail-type':'AWS Console Sign In via CloudTrail',
'source':'aws.signin',
'account':'1',
'time':'2022-12-17T01:09:08Z',
'region':'ap-northeast-2',
'resources':[
],
'detail':{
'eventVersion':'1.08',
'userIdentity':{
'type':'IAMUser',
'principalId':'1',
'accountId':'1',
'accessKeyId':'',
'userName':'1'
},
'eventTime':'2022-12-17T01:09:08Z',
'eventSource':'signin.amazonaws.com',
'eventName':'CheckMfa',
'awsRegion':'ap-northeast-2',
'sourceIPAddress':'58.227.0.134',
'userAgent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/106.0.5249.114 Safari/537.36', 'requestParameters':None,
'responseElements':{
'CheckMfa':'Success'
},
'additionalEventData':{
'MfaType':'Virtual MFA'
},
'eventID':'1',
'readOnly':False,
'eventType':'AwsConsoleSignIn',
'managementEvent':True,
'recipientAccountId':'1',
'eventCategory':'Management',
'tlsDetails':{
'tlsVersion':'TLSv1.2',
'cipherSuite':'ECDHE-RSA-AES128-GCM-SHA256',
'clientProvidedHostHeader':'ap-northeast-2.signin.aws.amazon.com'
}
}
}
{
    "eventVersion": "1.08",
    "userIdentity": {
        "type": "IAMUser",
        "principalId": "1",
        "arn": "arn:aws:iam::1:",
        "accountId": "1",
        "accessKeyId": ""
    },
    "eventTime": "2022-12-17T02:29:28Z",
    "eventSource": "signin.amazonaws.com",
    "eventName": "ConsoleLogin",
    "awsRegion": "ap-northeast-2",
    "sourceIPAddress": "58.227.0.134",
    "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5249.207 Safari/537.36",
    "requestParameters": null,
    "responseElements": {
        "ConsoleLogin": "Success"
    },
    "additionalEventData": {
        "LoginTo": "https://ap-northeast-2.console.aws.amazon.com/console/home?hashArgs=%23&isauthcode=true&region=ap-northeast-2&state=hashArgsFromTB_ap-northeast-2_b149694953e40e5b",
        "MobileVersion": "No",
        "MFAIdentifier": "arn:aws:iam::1:mfa/root-account-mfa-device",
        "MFAUsed": "Yes"
    },
    "eventID": "1",
    "readOnly": false,
    "eventType": "AwsConsoleSignIn",
    "managementEvent": true,
    "recipientAccountId": "1",
    "eventCategory": "Management",
    "tlsDetails": {
        "tlsVersion": "TLSv1.2",
        "cipherSuite": "1",
        "clientProvidedHostHeader": "signin.aws.amazon.com"
    }
}

민감 정보는 지웠다. 이렇게 두가지 내용이 다르다. 처음에 Trail Log를 보고서 패턴을 작성하다가 놀랐다. 그리고 이 이벤트를 보려면 이벤트 브릿지에선 넘어온 데이터를 볼수없다. 이벤트 카운트 뿐이다.

세번째는 리전에 대한 이야기다.

우리의 Console 로그인은 리전기반이다. 이게 나를 오랜시간 고민하게 하고 괴롭혔다.

Trail은 글로벌 서비스 이벤트가 있다.

https://docs.aws.amazon.com/ko_kr/awscloudtrail/latest/userguide/cloudtrail-concepts.html#cloudtrail-concepts-global-service-events

이 글로벌 서비스중 sts에 우리는 주목해야한다. 로그인할때 STS 를 호출하기 때문이다. 그럼 STS 를 설명하기 전에 Console Login 부터 알아야한다.

로그인을 시도할때 우리는 AWS Console 을 통해 그냥 로그인한다고 생각하지만, 그렇지 않다. AWS Console은 로그인 할때 이런 URL 을 가지고 있다.

https://signin.aws.amazon.com/signin?redirect_uri=https%3A%2F%2Fconsole.aws.amazon.com%2Fconsole%2Fhome%3FhashArgs%3D%2523%26isauthcode%3Dtrue%26state%3DhashArgsFromTB_ap-northeast-1_6b240714978b3994&client_id=arn%3Aaws%3Asignin%3A%3A%3Aconsole%2Fcanvas&forceMobileApp=0&code_challenge=U8A4YkTPRIIvi-8Gj7-tIx4RB_PR9IT-4fVs7diVUoc&code_challenge_method=SHA-256

이 URL로 로그인하면 Console Login log는 ap-northeast-1 로 연결된다. 그러니까 우리는 도쿄로 연결되는 로그인때문에 이것을 재대로 트래킹 할수 없다는 이야기다. 놓치는 로그인들을 해결하고 싶었다.

글로벌 서비스를 추적하면 도쿄로 연결되는 로그인을 추적할수 있을까?

정답은 "그렇다" 하지만 문제가 생길수도 있다.

슬프게도 글로벌서비스 추적이란 그냥 글로벌 엔드포인트를 이용하면 그 로그가 us-east-1 에 쌓일 뿐 모든 리전의 STS로그가 글로벌서비스 추적에 쌓이는건 아니다.

그렇기에 로그인 추적은 어렵다. 그렇다고 해서 아주 못하는것은 아니다. 로그인은 반드시 STS를 호출한다. 극단적으로 가기로 했다.

IAM > 계정설정 > 엔드포인트

위에서 로그인 URL에 도쿄리전으로 파라미터가 들어가있는데 그 대로 로그인 해보겠다. 그전에 나의 계정에선 도쿄의 STS 엔드포인트를 비활성화하였다.

{
    "eventVersion": "1.08",
    "userIdentity": {
        "type": "1",
        "principalId": "1",
        "arn": "arn:aws:iam::1:1",
        "accountId": "1",
        "accessKeyId": ""
    },
    "eventTime": "2022-12-17T04:02:18Z",
    "eventSource": "signin.amazonaws.com",
    "eventName": "ConsoleLogin",
    "awsRegion": "us-east-1",
    "sourceIPAddress": "1",
    "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36",
    "requestParameters": null,
    "responseElements": {
        "ConsoleLogin": "Success"
    },
    "additionalEventData": {
        "LoginTo": "https://console.aws.amazon.com/console/home?hashArgs=%23&isauthcode=true&state=hashArgsFromTB_ap-northeast-1_6b240714978b3994",
        "MobileVersion": "No",
        "MFAIdentifier": "arn:aws:iam::1:mfa/1-account-mfa-device",
        "MFAUsed": "Yes"
    },
    "eventID": "1",
    "readOnly": false,
    "eventType": "AwsConsoleSignIn",
    "managementEvent": true,
    "recipientAccountId": "1",
    "eventCategory": "Management",
    "tlsDetails": {
        "tlsVersion": "TLSv1.2",
        "cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
        "clientProvidedHostHeader": "signin.aws.amazon.com"
    }
}

로그 보면 이렇다. LoginTo 에서는 ap-northeast-1 로 로그인했으나, 실제 리전은 us-east-1 로 연결되었다. 글로벌서비스 STS로 연결된것이다. 아마 가까운 리전엔드포인트를 제공해주는것으로 보는데 실제로는 잘모른다.

이렇게 세가지의 고민을 끝내고 Trail의 추적을 생성하였는데, 문제를 찾을수 있었다.

우리는 필연적으로 버지니아 북부와 실제사용리전에서 Trail의 추적을 생성해야하는데 이걸 콘솔에선 생성해선 안된다.

이전에는 Trail 에서는 콘솔에서 다중추적의 온오프를 옵션으로 제공했는데 이젠 그렇지 않다. 이전의 기억만 믿고 진행했다가 다중추적이 여러군데 생성되었다.

https://aws.amazon.com/ko/premiumsupport/knowledge-center/remove-duplicate-cloudtrail-events/

다중추적의 중복은 비용이 발생한다.

PaidEventsRecorded 이 지표가 증가한다면 다중추적이 여러개가 생성된거다.

그렇기에 추적을 생성할땐 주요사용리전에만 다중리전 추적을 생성하고 버지니아 북부에서는 글로벌서비스 추적만 활성화 해야한다. 그러면 비용이 추가되지 않는다.

글로벌 서비스 추적을 만들려면 AWSCLI를 이용해서 만들어야 한다.

# aws cloudtrail update-trail --name my-trail --no-include-global-service-events

https://docs.aws.amazon.com/ko_kr/awscloudtrail/latest/userguide/cloudtrail-create-and-update-a-trail-by-using-the-aws-cli-update-trail.html#cloudtrail-create-and-update-a-trail-by-using-the-aws-cli-examples-gses

EventBridge 와 Trail에 대한 삽질기를 이렇게 정리해둔다.

조금이나마 도움이 되길 빈다.

T101-Study-4Week

가시다님과 스터디를 한지도 5번째 이번엔 테라폼이다.

오늘 블로그를 쓰게된건 중간과제를 설명하기 위해서다.

바로 본론으로 들어간다. 내 GIT 이다

https://github.com/Cloud-Linuxer/T101/tree/main/4week

variable "availability_zone" {
        description = "Seoul region availability zone"
        type = list
        default = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c", "ap-northeast-2d"]
}

variable "subnet_numbers" {
  type    = list
  default = [10, 20, 30, 40]
}
variable "az_count" {
  type    = list
  default = ["A", "B", "C", "D"]
}

나의 Variables는 이런식으로 구성되어있다. 모든 타입을 List로 선언하여 사용한다. 5주차에 할 테라폼 의 반복문을 사용하기 위한 형태다. 가장 중요한 부분은 subnet_numbers 부분이다. 10, 20, 30, 40 이 핵심이다.

resource "aws_subnet" "pub-common" {
        count = "${length(var.availability_zone)}"
        vpc_id = "${aws_vpc.default.id}"
        cidr_block = [
                for num in var.subnet_numbers:
                cidrsubnet(aws_vpc.default.cidr_block, 8, num)
                ][count.index]
        availability_zone = "${element(var.availability_zone, count.index)}"
        tags = {
                Name = "Linuxer-Dev-Pub-Common-${element(var.az_count, count.index)}"
        }
}

이 코드만 봐서는 이게 무엇을 뜻하는지 한눈에 보기 어렵다. 그럼 하나씩 설명하겠다. 하시코프에서는 cidrsubnet 이라는 Function 을 지원한다. 이 함수를 통해서 나는 /16비트의 서브넷을 24비트로 자를거다.

간단히 보여주자면 이렇다

terraform console
> cidrsubnet("10.0.0.0/16",8,10)
"10.0.10.0/24"
> cidrsubnet("10.0.0.0/16",8,20)
"10.0.20.0/24"
> cidrsubnet("10.0.0.0/16",8,30)
"10.0.30.0/24"
> cidrsubnet("10.0.0.0/16",8,40)
"10.0.40.0/24"

for로 list 에 담긴 subnet_numbers를 가져다가 CIDR 을 반환한다. 위처럼 24비트의 4개 서브넷이다.
위와같이 24비트로 나뉜 4개의 서브넷을 테라폼은 생성한다.
위의 리소스선언 한줄로 Subnet 4개를 생성하는 것이다.

서울 리전의 4개 AZ를 모두 사용하고, A zone은 10대역대 B Zone은 20대역대 C Zone은 30대역 D Zone은 40 대역인것이다.

이렇게 사용하면 장점이 있다. 한개의 존이 문제가 생긴것을 파악하기 쉽고, 아이피 대역대 만으로 서비스의 역할을 파악할수 있는 장점이 있는 것이다.

처음엔 리스트로 서브넷 선언도 모두 입력해서 하나의 리소스 선언으로 모든 서브넷을 생성하려했지만 그렇게 사용할 경우 리스트가 변경되면 모든 서브넷이 영향을 받는 이슈가 있어서 각 서브넷별 리소스 선언을 하는 방향으로 수정했다.

AWS-FinOps-S3-incomplete-multipart-uploads-MPU

S3는 청크 단위로 파일을 잘라서 업로드 할수있는 기능을 제공한다.

이 기능의 정식명칭은 multipart upload 이다.

https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html

MPU라고 줄여서 부른다.

MPU는 업로드 속도를 빠르게 해줄수있는 아주 좋은 기능이지만, 업로드에 실패할 경우 완성되지 않은 청크단위의 파일들이 S3스토리지에 저장되게 된다. 업로드가 정상적으로 이루어진 경우 청크단위로 나뉜 파일을 하나의 파일로 합쳐서 객체로 보이게 되지만, 그렇지 않은 파일은 우리의 눈에 보이지 않지만 S3의 스토리지에 비용만 발생시키며, 하등 쓸모없는 상태로 저장만 되어있는다. 이런 경우를 "incomplete multipart uploads" 라 부른다.

불완전 멀티파트 업로드/완료되지 않은 멀티파트 업로드 는 Lifecycle 를 통해 삭제 할수있다. 간단한 정책을 만들어서 보여주고자 한다.

설정은 S3 버킷 에서 관리로 가면 수명주기 규칙으로 설정할수 있다.

이설정은 모든 버킷에서 통용적으로 사용할수 있는 규칙이므로 버킷을 생성할때 무조건 넣어도 좋다.

위와같이 "만료된 객체 삭제 마커 또는 완료되지 않은 멀티파트 업로드 삭제" 체크후 "불완전 멀티파트 업로드 삭제" 를 체크하면 된다. 일수는 1일이 최소값이다.

정상적으로 삭제가 동작하면 이런식으로 S3 dashboard에서 불완전한 멀티파트업로드 바이트 차트가 0B로 변경되는것을 확인할수 있다.

불완전 MPU는 대표적으로 이런경우 생성된다.

MPU 업로드 실패.
Athena 쿼리 실패
Redshift UNLOAD 실패등

AWS 서비스에서 S3로 저장하는 액션을 취하다 실패하는경우가 있다면 대부분 "불완전 MPU"가 생성될것이다.

AWS S3 대시보드를 확인하여 "불완전한 MPU" 를 확인하고 삭제해보자.

바닥에 흘리고 다니던 눈먼 동전 줍기가 될것이다.

읽어주셔서 감사하다!
앞으로도 FinOps 시리즈로 찾아 뵙겠다.