학습일지/K-Digital Traing

[KDT] AIaaS 마스터클래스 5주차 - 리눅스 ssh 실습

tierr 2025. 4. 24. 11:23

카카오클라우드에서 서버용/클라이언트용 VM인스턴스를 각각 생성해서 실습 진행


1. 실습: gRPC vs REST API

  • 마이크로서비스에서 gRPC가 각광받는 이유는?

환경 준비

sudo apt update
sudo apt install -y python3 python3-pip golang-go wrk apache2-utils
sudo apt install -y python3.12-venv

python3 -m venv ~/myenv
source ~/myenv/bin/activate
python3 -m pip install --upgrade pip setuptools wheel \
                    flask \
                    grpcio grpcio-tools protobuf \
                    grpcio-reflection


go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
go install github.com/bojand/ghz/cmd/ghz@latest

echo 'export PATH=$PATH:$(go env GOPATH)/bin' >> ~/.bashrc
source ~/.bashrc

grpcurl --version
 

1-1. REST API 서버 세팅 (Flask / HTTP/1.1)

더보기
from flask import Flask, request, jsonify
app = Flask(__name__)

@app.route('/echo', methods=['POST'])
def echo():
    data = request.get_json()
    return jsonify(echo=data['message'])

if __name__ == '__main__':
    # HTTP/1.1 기본 모드로 5000 포트 실행
    app.run(host='0.0.0.0', port=5000)
  • 서버 실행
python3 app.py
  • 동작 확인
curl -i -X POST <서버 IP>:5000/echo \
     -H "Content-Type: application/json" \
     -d '{"message":"hello"}'
  • Expected Response
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 20
Server: Werkzeug/2.x.x Python/3.x.x
Date: ...

{"echo":"hello"}

 

1-2. gRPC 서버 세팅 (HTTP/2 / Protobuf)

  • echo.proto 작성
더보기
syntax = "proto3";
package echo;

service Echo {
  rpc UnaryEcho(EchoRequest) returns (EchoResponse);
}

message EchoRequest  { string message = 1; }
message EchoResponse { string reply   = 1; }
  • 코드 생성
python3 -m grpc_tools.protoc \\
  -I. --python_out=. --grpc_python_out=. echo.proto
  • server.py 작성
더보기
import grpc
from concurrent import futures
import echo_pb2, echo_pb2_grpc
from grpc_reflection.v1alpha import reflection

class EchoServicer(echo_pb2_grpc.EchoServicer):
    def UnaryEcho(self, request, context):
        return echo_pb2.EchoResponse(reply=request.message)

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=4))
    echo_pb2_grpc.add_EchoServicer_to_server(EchoServicer(), server)

    # Reflection 활성화
    SERVICE_NAMES = (
        echo_pb2.DESCRIPTOR.services_by_name['Echo'].full_name,
        reflection.SERVICE_NAME,
    )
    reflection.enable_server_reflection(SERVICE_NAMES, server)

    server.add_insecure_port('[::]:50051')
    server.start()
    print("gRPC server listening on 50051")
    server.wait_for_termination()

if __name__ == '__main__':
    serve()
  • 서버 실행
python3 server.py
  • 동작 확인
grpcurl -plaintext \\
  -d '{"message":"hello"}' \\
  127.0.0.1:50051 echo.Echo/UnaryEcho
  • Expected Response
{
  "reply": "hello"
}

 


1-3. 패킷 캡처 & 애플리케이션 바이트 분석 (REST API)

  • 캡처 준비
# REST 트래픽 캡처
sudo tcpdump -i any -s0 -w rest.pcap tcp port 5000 &
# gRPC 트래픽 캡처
sudo tcpdump -i any -s0 -w grpc.pcap tcp port 50051 &

 

  • 부하/테스트 트래픽 생성
for i in {1..500}; do
  curl -s -o /dev/null -w "%{http_code}\n" -X POST 127.0.0.1:5000/echo \
    -H "Content-Type: application/json" \
    -d '{"message":"hello"}'
done

for i in {1..500}; do
  grpcurl -plaintext -d '{"message":"hello"}' 127.0.0.1:50051 echo.Echo/UnaryEcho
done

 (예: hey/ghz 각각 100회 실행)

  • 캡처 종료
sudo killall tcpdump

 

  • 패킷 바이트 합계
sudo apt update
sudo apt install tshark
# REST
# 5000의 총 바이트(frame.len) 합계
tshark -r rest.pcap -q \\
  -z io,stat,0,"SUM(frame.len)frame.len&&tcp.port==5000"

# gRPC
tshark -r grpc.pcap -q \\
  -z io,stat,0,"SUM(frame.len)frame.len&&tcp.port==50051"

 

 

1-4. 패킷 캡처 & 애플리케이션 바이트 분석 2

캡처 준비

# REST 트래픽 캡처
sudo tcpdump -i any -s0 -w rest.pcap tcp port 5000 &
# gRPC 트래픽 캡처
sudo tcpdump -i any -s0 -w grpc.pcap tcp port 50051 &

 

  • 부하/테스트 트래픽 생성
# ───────────────────────────────────────────────────────────────
# 1) ab (ApacheBench)
# 총 500회, 동시 1개 연결로 POST 요청
# payload 파일을 미리 만들어두고(-p), Content-Type 지정(-T)
echo '{"message":"hello"}' > payload.json

ab -n 500 -c 1 \
   -p payload.json \
   -T application/json \
   http://127.0.0.1:5000/echo
# ───────────────────────────────────────────────────────────────

# 1채널을 열고 500회 호출
ghz --insecure \
    --proto echo.proto \
    --call echo.Echo.UnaryEcho \
    -d '{"message":"hello"}' \
    -n 500 -c 1 127.0.0.1:50051
  • 캡처 종료
sudo killall tcpdump

 

패킷 바이트 합계

# REST
# 포트 5000의 총 바이트(frame.len) 합계
tshark -r rest.pcap -q \\
  -z io,stat,0,"SUM(frame.len)frame.len&&tcp.port==5000"

# gRPC
tshark -r grpc.pcap -q \\
  -z io,stat,0,"SUM(frame.len)frame.len&&tcp.port==50051"

페이로드(tcp) 바이트 합계

# REST 순수 페이로드 합계
tshark -r rest.pcap -q \\
  -z io,stat,0,"tcp.payload && tcp.port==5000"

# gRPC 순수 페이로드 합계
tshark -r grpc.pcap -q \\
  -z io,stat,0,"tcp.payload && tcp.port==50051"

 


2. 실습: I/O 읽기 쓰기

리눅스 커널에 간단한 문자(Character) 디바이스 드라이버를 모듈로 작성하고, insmod/rmmod로 로드·언로드한 뒤 /dev/example 장치 파일로 read·write 동작을 테스트하는 실습 자료입니다.

  • 문자 디바이스 드라이버의 기본 구조 이해
  • 커널 모듈 빌드·설치 과정 숙지
  • /dev 하위에 장치 파일 생성 및 권한 설정
  • 사용자 공간에서 read(), write()로 데이터 송수신
  • 리눅스 개발용 머신 (커널 헤더 설치된 상태)
  • gcc, make, sudo 권한 필요

2-1. 드라이버 코드 예제 (example.c)

더보기
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "example"
#define BUFFER_SIZE 1024

static int    major_num;
static char   device_buffer[BUFFER_SIZE];
static size_t data_size;

static int example_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "example: opened\n");
    return 0;
}

static int example_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "example: closed\n");
    return 0;
}

static ssize_t example_read(struct file *file, char __user *buf, size_t len, loff_t *offset)
{
    ssize_t to_copy = min(len, data_size - (size_t)*offset);
    if (to_copy <= 0)
        return 0;
    if (copy_to_user(buf, device_buffer + *offset, to_copy))
        return -EFAULT;
    *offset += to_copy;
    printk(KERN_INFO "example: read %zu bytes\n", to_copy);
    return to_copy;
}

static ssize_t example_write(struct file *file, const char __user *buf, size_t len, loff_t *offset)
{
    ssize_t to_copy = min(len, BUFFER_SIZE - 1);
    if (copy_from_user(device_buffer, buf, to_copy))
        return -EFAULT;
    device_buffer[to_copy] = '\0';
    data_size = to_copy;
    printk(KERN_INFO "example: wrote %zu bytes\n", to_copy);
    return to_copy;
}

static const struct file_operations fops = {
    .owner   = THIS_MODULE,
    .open    = example_open,
    .release = example_release,
    .read    = example_read,
    .write   = example_write,
};

static int __init example_init(void)
{
    major_num = register_chrdev(0, DEVICE_NAME, &fops);
    if (major_num < 0) {
        printk(KERN_ALERT "example: failed to register\n");
        return major_num;
    }
    printk(KERN_INFO "example: registered with major %d\n", major_num);
    return 0;
}

static void __exit example_exit(void)
{
    unregister_chrdev(major_num, DEVICE_NAME);
    printk(KERN_INFO "example: unregistered\n");
}

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple character device example");

module_init(example_init);
module_exit(example_exit);

 

2-2. Makefile

obj-m += example.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

 

2-3. 모듈 빌드와 설치

드라이버 소스와 Makefile이 있는 디렉토리로 이동

  • 빌드
make
  • 모듈 로드
sudo insmod example.ko
  • 로드 확인
lsmod | grep example
sudo dmesg | tail -n 100 | grep example

 

2-4. 장치 파일 생성 & 읽기·쓰기 테스트

  • 모듈 로드 시 출력된 major 번호 확인 (dmesg 메시지 참조)
  • /dev/example 생성
sudo mknod /dev/example c <major> 0
sudo chmod 666 /dev/example
  • 쓰기
echo "Hello, kernel!" > /dev/example
  • 읽기
cat /dev/example

 

2-5. 커널 로그 확인 & 모듈 언로드

dmesg | tail -n 5
sudo rmmod example
lsmod | grep example   # 사라진 것 확인
dmesg | tail -n 5

 

🏎️
커널 영역에서는 CPU, 메모리, 디스크, 네트워크 등 시스템 자원에 직접 접근할 수 있음.
반면 사용자 영역에서는 이러한 자원에 직접 접근이 불가능 반드시 시스템 콜(system call)이라는 인터페이스를 통해 커널에 요청해야 함

3. 실습: auditd 실습

📜 Auditd + Email 알림 시스템 구축 문서

✅ 1. 패키지 설치 및 서비스 활성화

sudo apt update
sudo apt install -y auditd audispd-plugins jq mailutils postfix

# 서비스 시작 및 부팅 시 자동 실행
sudo systemctl enable --now auditd
✅ 2. 감시 룰 설정
sudo tee /etc/audit/rules.d/secure.rules << 'EOF'
-w /etc/passwd   -p wa -k passwd_changes
-w /etc/shadow   -p wa -k shadow_changes
-w /opt/project  -p wa -k project_watch
EOF

# 룰 적용
sudo augenrules --load
sudo systemctl restart auditd
✅ 3. 이메일 알림 스크립트 작성
sudo tee /usr/local/bin/audit-email-alert.sh << 'EOF'
#!/bin/bash
RECIPIENT="ubuntu"  # 로컬 사용자로 설정
SUBJECT="[Audit Alert] $(date '+%F %T')"
while read -r LINE; do
  echo "$LINE" | mail -s "$SUBJECT" "$RECIPIENT"
  echo -e "\a"
done
EOF

sudo chmod +x /usr/local/bin/audit-email-alert.sh
✅ 4. 이벤트 파이프라인 구성
sudo pkill -f audit-email-alert.sh  # 기존 스크립트 종료

sudo bash -c 'tail -F /var/log/audit/audit.log \
| grep --line-buffered project_watch \
| /usr/local/bin/audit-email-alert.sh \
>> /var/log/audit-email-alert.log 2>&1 &'
✅ 5. 동작 테스트
sudo mkdir -p /opt/project
sudo touch /opt/project/test-$(date +%s).txt
✅ 6. 로컬 메일 확인
mail  # 로컬 메일함 열기 (ubuntu 사용자 기준)

 

mail 명령어 단축키

기능
Enter 현재 메일 읽기
n 다음 메일
d 메일 삭제
q 종료

 

📝 참고 사항
  • /var/mail/ubuntu 에 메일 저장됨
  • postfix는 기본적으로 로컬 메일 전송만 가능 (외부 SMTP 사용하려면 별도 설정 필요)
  • mail 명령어가 작동하지 않으면 mailutils 설치 여부 확인

4. 실습: ssh 공개키 등록해두기

실습 목적

  • SSH 키 쌍을 생성해 VM의 authorized_keys에 등록
  • 비밀번호 없이 바로 접속하도록 SSH 서버를 키 인증만 허용하도록 강화

준비 사항

  • 로컬 머신에 OpenSSH 클라이언트 설치 (대부분 기본 포함)
  • 원격 VM에 OpenSSH 서버 설치·실행 중이며, 관리자 또는 sudo 권한 계정 접근 가능
  • VM의 퍼블릭 IP 주소 또는 도메인 이름 (<VM_IP>, <VM_USER> 대체)
  • Windows 환경에서는 PowerShell로 진행

4-1. SSH 키 페어 생성

ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
  • 프롬프트에 따라 파일 경로(~/.ssh/id_rsa)와 passphrase 입력


4-2. 공개키를 VM에 복사

  • ssh-copy-id 사용
# Windows
type $env:USERPROFILE\.ssh\id_rsa.pub

# Mac
#ssh-copy-id -i ~/.ssh/id_rsa.pub <VM_USER>@<VM_IP>

  • 수동 등록
# 원격 서버(<VM_USER>@<VM_IP>)에 접속 가능한 방식(비밀번호나 콘솔 등)으로 로그인
ssh <VM_USER>@<VM_IP>

# 홈 디렉터리로 이동
cd ~

# .ssh 디렉터리가 없으면 생성
mkdir -p ~/.ssh

# 공개키를 authorized_keys에 붙여넣기
echo "ssh-rsa AAAA…(여기에 id_rsa.pub 전체 내용)" >> ~/.ssh/authorized_keys

# 권한 설정
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys


4-3. 키 기반 로그인 테스트

ssh <VM_USER>@<VM_IP>
  • 비밀번호 없이 셸 프롬프트 진입 확인


4-4. SSH 서버 설정 강화 (VM 상에서 수행)

ssh <VM_USER>@<VM_IP>
sudo vi /etc/ssh/sshd_config
  • 설정에서 다음 항목 확인·수정
PubkeyAuthentication yes
PasswordAuthentication no
PermitRootLogin prohibit-password

  • 저장 후 SSH 서버 재시작
sudo systemctl restart sshd

4-5. 검증 및 확인

  • 정상 접속 시 비밀번호 입력 없이 로그인됨
  • 비밀번호 인증만 시도해 “Permission denied”가 뜨면 성공 확인
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no <VM_USER>@<VM_IP>

 

 

4-6. 추가 과제

  • ~/.ssh/config에 호스트 별칭 설정해 간편 접속
# Windows 환경 테스트 C:\Users\<사용자>\.ssh\config
Host myvm
  HostName 203.0.113.10
  User ubuntu
  IdentityFile ~/.ssh/id_rsa


5. 실습: 언어별 비동기 처리

실습 환경 준비

  • 필수 도구 설치
sudo apt update
sudo apt install -y curl htop git build-essential
sudo apt install -y python3 python3-pip openjdk-11-jdk nodejs npm golang-go
sudo apt install -y python3.12-venv

python3 -m venv ~/myenv
source ~/myenv/bin/activate

  • 터미널 여러 개 열어두기 (서버·클라이언트 모니터링용)

Python asyncio 로 간단한 Echo 서버

  • echo_async.py 작성
더보기
import asyncio
from aiohttp import web

async def handle(request):
    msg = request.rel_url.query.get('message', 'none')
    await asyncio.sleep(1)          # 비동기 대기 예시
    return web.Response(text=f"echo: {msg}")

app = web.Application()
app.router.add_get('/', handle)

if __name__ == '__main__':
    web.run_app(app, host='0.0.0.0', port=8888)
  • 실행
pip3 install aiohttp
python3 echo_async.py

  • 테스트 및 동시성 체험
# 테스트를 위해 100번 요청 보내기
for i in {1..100}; do
  curl "http://<서버 IP>:8888/?message=req$i" &
done
  • 모니터링
    100개 요청을 비동기로 처리중일 때 CPU가 급상승하지만 프로세스 수가 늘어나진 않는다.

 

✅ 동기 vs 비동기 관점에서 보면

상황 동기(Sync) 서버 비동기(Async) 서버 (현재 실습)
curl 요청 100개 동시에 전송 요청 1 → 끝나야 다음 100개 모두 대기 없이 동시 처리
처리 방식 blocking I/O non-blocking I/O (await)
CPU 사용 순차적으로 증가 동시에 사용량 증가

 

Node.js Express 비동기 핸들러

  • server.js 작성
더보기
const express = require('express');
const app = express();

app.get('/', async (req, res) => {
  const msg = req.query.message || 'none';
  await new Promise(r => setTimeout(r, 1000));  // 비동기 대기
  res.send(`echo: ${msg}`);
});

app.listen(3000, () => console.log('Listening on 3000'));
  • 실행
npm install express
node server.js
  • 테스트
curl "<http://localhost:3000/?message=hello>" &
  • 모니터링
    100개 요청을 비동기로 처리중일 때 CPU가 급상승하지만 프로세스 수가 늘어나진 않는다.

 

Java ExecutorService 로 스레드풀 체험

  • ThreadPoolDemo.java 작성
더보기
import java.util.concurrent.*;

public class ThreadPoolDemo {
  public static void main(String[] args) throws InterruptedException {
    ExecutorService pool = Executors.newFixedThreadPool(4);
    for (int i = 1; i <= 8; i++) {
      final int id = i;
      pool.submit(() -> {
        System.out.println("작업 " + id + " 시작: " + Thread.currentThread().getName());
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        System.out.println("작업 " + id + " 완료");
      });
    }
    pool.shutdown();
    pool.awaitTermination(5, TimeUnit.MINUTES);
  }
}
  • 컴파일·실행
javac ThreadPoolDemo.java
java ThreadPoolDemo

  • 검증
    • jps, jstack 으로 스레드 상태 보기
# jps로 프로세스 PID 확인
jps -l
# 예시 - jps명령어로 프로세스 PID 가 12345라는 것을 확인
# 12345 ThreadPoolDemo

# jstack으로 스레드 상태 확인
jstack 12345

 

Python concurrent.futures 스레드풀

  • threadpool_demo.py 작성
더보기
from concurrent.futures import ThreadPoolExecutor, as_completed
import time

def task(id):
    print(f"작업 {id} 시작")
    time.sleep(1)
    print(f"작업 {id} 완료")
    return id

if __name__ == '__main__':
    with ThreadPoolExecutor(max_workers=3) as exe:
        futures = [exe.submit(task, i) for i in range(1, 7)]
        for f in as_completed(futures):
            print(f.result(), "응답 수신")
  • 실행
python3 threadpool_demo.py

 

  •  확인
    • 동시에 3개씩 처리되는 로그 패턴 관찰

스레드·프로세스 모니터링 팁

  • top 혹은 htop 에서 Threads 칼럼 토글


본 후기는 [카카오엔터프라이즈x스나이퍼팩토리] 카카오클라우드로 배우는 AIaaS 마스터 클래스 (B-log) 리뷰로 작성 되었습니다.