배경
운영 환경의 Windows FTP 서버에서 FileReceiver(Java 프로세스)가 실행 중이어야 파일 수신이 정상 동작한다. 그러나 서버 재부팅이나 프로세스 비정상 종료 시 FileReceiver가 내려간 상태로 방치될 수 있어, 이를 자동으로 감지하는 모니터링이 필요했다.
Windows 서버는 CloudWatch Agent만으로는 특정 Java 프로세스의 실행 여부를 확인할 수 없으므로, AWS Systems Manager(SSM)를 통한 원격 PowerShell 실행 방식을 채택했다.
아키텍처
EventBridge (5분 주기: rate(5 minutes))
↓
Lambda (Python)
├── SSM SendCommand → Windows 서버에 PowerShell 원격 실행
├── SSM GetCommandInvocation → 실행 결과 수집
├── DynamoDB → 직전 상태 저장 (연속 실패 카운트)
│
└── 연속 실패 N회 이상 시 알람 발송
├── Slack (Incoming Webhook)
├── SMS (SNS Topic, Tokyo Region)
└── Email (SES SendEmail)
SSM 원격 PowerShell 실행
Windows 서버 사전 준비
대상 Windows 서버에 SSM Agent가 설치/등록되어 있어야 한다.
# SSM Agent 설치 확인
Get-Service AmazonSSMAgent실행할 PowerShell 스크립트
javaw.exe 프로세스 중 FileReceiver.jar를 실행 중인 프로세스를 탐지한다.
$procs = Get-WmiObject Win32_Process -Filter "Name = 'javaw.exe'" |
Select-Object ProcessId, CommandLine |
Where-Object { $_.CommandLine -match "FileReceiver.jar" }
if ($procs) {
Write-Output "UP: FileReceiver detected. PIDs: $($procs.ProcessId -join ', ')"
exit 0
} else {
Write-Output "DOWN: FileReceiver not detected."
exit 2
}exit 0→ UP (정상)exit 2→ DOWN (프로세스 미감지)
명확한 종료 코드를 반환하도록 래핑하여 Lambda에서 판단할 수 있게 한다.
Lambda 핵심 로직
SSM 명령 실행 및 결과 수집
import boto3, time
ssm = boto3.client('ssm')
def check_instance(instance_id, command):
# 1. SSM SendCommand로 PowerShell 원격 실행
cmd = ssm.send_command(
InstanceIds=[instance_id],
DocumentName='AWS-RunPowerShellScript',
Parameters={'commands': [command]},
TimeoutSeconds=60,
)
command_id = cmd['Command']['CommandId']
# 2. 결과 수집 (터미널 상태까지 폴링)
inv = wait_for_terminal(command_id, instance_id)
status = inv.get('Status')
response_code = inv.get('ResponseCode')
output = inv.get('StandardOutputContent', '')
is_up = (response_code == 0 and 'UP:' in output)
return is_up, status, response_code, output폴링 (SSM 전파 지연 고려)
def wait_for_terminal(command_id, instance_id, max_wait=45):
deadline = time.time() + max_wait
while time.time() < deadline:
try:
inv = ssm.get_command_invocation(
CommandId=command_id,
InstanceId=instance_id,
)
if inv['Status'] in ('Success', 'Failed', 'TimedOut', 'Cancelled'):
return inv
time.sleep(1.0)
except ssm.exceptions.InvocationDoesNotExist:
time.sleep(1.0) # SSM 전파 지연
raise TimeoutError("SSM command timed out")연속 실패 감지 (DynamoDB)
DynamoDB에 인스턴스별 직전 상태와 연속 실패 횟수를 저장한다.
ddb = boto3.client('dynamodb')
def update_state(table, instance_id, is_up):
prev = ddb.get_item(
TableName=table,
Key={'host': {'S': instance_id}}
).get('Item', {})
prev_fails = int(prev.get('fails', {}).get('N', '0'))
fails = 0 if is_up else prev_fails + 1
ddb.put_item(
TableName=table,
Item={
'host': {'S': instance_id},
'status': {'S': 'up' if is_up else 'down'},
'fails': {'N': str(fails)},
'ts': {'S': datetime.utcnow().isoformat() + 'Z'},
}
)
return fails연속 실패 N회(기본 2회) 이상일 때만 알람을 발송하여 일시적 오탐을 방지한다.
다중 알람 채널
Slack (Incoming Webhook)
def notify_slack(webhook_url, message):
payload = json.dumps({"text": message}).encode()
req = urllib.request.Request(
webhook_url,
data=payload,
headers={'Content-Type': 'application/json'},
)
urllib.request.urlopen(req, timeout=5)SMS (SNS - Tokyo Region)
def send_sms(topic_arn, message):
sns = boto3.client('sns', region_name='ap-northeast-1')
sns.publish(
TopicArn=topic_arn,
Message=message[:120], # SMS 길이 제한
MessageAttributes={
'AWS.SNS.SMS.SMSType': {
'DataType': 'String',
'StringValue': 'Transactional',
}
},
)Email (SES)
def send_email(sender, recipients, subject, body):
ses = boto3.client('ses')
ses.send_email(
Source=sender,
Destination={'ToAddresses': recipients},
Message={
'Subject': {'Data': subject, 'Charset': 'UTF-8'},
'Body': {'Text': {'Data': body, 'Charset': 'UTF-8'}},
},
)알람 조건
- UP 상태일 때는 알람을 전송하지 않음
- DOWN이 연속 N회 이상일 때만 Slack/SMS/Email 발송
- SMS는 120자 이내로 truncate
Lambda 환경 변수
| 변수 | 설명 |
|---|---|
INSTANCE_IDS | 대상 Windows 인스턴스 ID (쉼표 구분) |
SLACK_WEBHOOK_URL | Slack Incoming Webhook URL |
SNS_TOPIC_ARN_SMS | SMS용 SNS Topic ARN (Tokyo Region) |
SES_FROM | 발신 이메일 주소 |
SES_TO_CSV | 수신 이메일 주소 (쉼표 구분) |
DDB_TABLE | DynamoDB 테이블 이름 |
FAIL_THRESHOLD | 연속 실패 임계치 (기본 2) |
SSM_TIMEOUT | SSM 명령 타임아웃 (기본 60초) |
MAX_WAIT_SEC | 결과 폴링 최대 대기 (기본 45초) |
IAM 역할
Lambda 실행 역할에 필요한 권한:
{
"Effect": "Allow",
"Action": [
"ssm:SendCommand",
"ssm:GetCommandInvocation",
"dynamodb:GetItem",
"dynamodb:PutItem",
"ses:SendEmail",
"sns:Publish",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
}Standby 서버 자동 실행 설정
Active 서버뿐 아니라 Standby 서버에서도 재부팅 후 FileReceiver가 자동 실행되도록 Windows 작업 스케줄러를 설정한다.
수동 실행 확인
# 관리자 PowerShell
cd C:\java\FileReceiver
C:\java\jre1.8.0_161\bin\javaw.exe -jar FileReceiver.jar
# 프로세스 확인
Get-Process javaw | Where-Object { $_.Path -like "*jre1.8.0_161*" }작업 스케줄러 자동 실행
작업 스케줄러 (taskschd.msc) > 작업 만들기
일반 탭:
이름: FileReceiver
"가장 높은 권한으로 실행" 체크
"사용자 로그온 여부와 관계없이 실행" 선택
트리거 탭:
"컴퓨터 시작 시" 선택
동작 탭:
프로그램: C:\java\jre1.8.0_161\bin\javaw.exe
인수: -jar FileReceiver.jar
시작 위치: C:\java\FileReceiver
재부팅 후 자동 실행 확인:
Get-Process javaw | Where-Object { $_.Path -like "*jre1.8.0_161*" }트러블슈팅
Standby 서버 원격 접속 불가
보안 소프트웨어(방화벽)가 SSM Agent의 아웃바운드 통신을 차단하는 경우가 있었다. 방화벽 설정에서 SSM 관련 엔드포인트 통신을 허용하여 해결했다.
정리
- SSM을 활용한 Windows 서버 Java 프로세스 원격 모니터링
- EventBridge 5분 주기 트리거 → Lambda → SSM PowerShell 실행
- DynamoDB로 연속 실패 카운트 관리, 오탐 방지
- Slack/SMS/Email 다중 알람 채널 구성
- Standby 서버 자동 실행 설정 (작업 스케줄러)
- UP 상태에서는 알람 미발송으로 알림 피로 최소화
배운 점
모니터링에서 가장 어려운 대상은 복잡한 시스템이 아니라 표준 경로가 없는 시스템이다. Windows 서버의 Java 프로세스 하나를 보기 위해 SSM, Lambda, DynamoDB, EventBridge가 동원됐다. 에이전트를 심을 수 없는 환경에서는 "무엇으로 볼 것인가"보다 "어디까지 우회할 것인가"가 설계가 된다.
DynamoDB에 직전 상태를 저장한 이유도 결국 오탐 때문이었다. 한 번 실패했다고 알리면 재기동 중에도 알림이 울린다. 상태를 기억해야 연속 실패를 셀 수 있고, 연속 실패를 세야 알림이 신뢰를 얻는다. 같은 고민을 EDI Agent 중앙 모니터링 시스템 구축에서도 6회 연속 조건으로 풀었다.
SSM 통신이 보안 소프트웨어에 막혔던 일은 따로 기억에 남는다. 감시 도구 자체가 보안 정책에 걸려 조용히 죽을 수 있다는 뜻이라서, 그 뒤로는 감시 경로가 살아 있는지도 같이 확인하게 됐다.
이어 읽기
- EDI Agent 중앙 모니터링 시스템 구축 (AgentMonitor): 연속 감지로 오탐을 줄인 같은 방식
- SNS + Lambda 기반 Slack 알림 구축: 이 알림이 나가는 Slack 경로