주말에 Migrate agentic workloads to Amazon Bedrock AgentCore 글을 읽었는데, 문장 하나가 눈에 박혔습니다. "노트북에서 돌아가는 에이전트는 프로덕션 에이전트가 아니다." 정확히 지난 분기에 겪은 상황이었습니다. LangGraph로 만든 고객 지원 에이전트 데모는 잘 돌아갔는데, 실사용자가 붙기 시작하니 세션 격리, 체크포인트 지속성, 툴 인증 같은 것들이 전부 우리 코드가 되어버렸거든요. Amazon Bedrock AgentCore는 이 "에이전트 추론과 상관없는 운영 부담"을 서비스 단위로 떼어가는 플랫폼입니다. Runtime은 세션당 마이크로VM으로 컴퓨트를 대신 돌려주고, Gateway는 툴을 MCP(Model Context Protocol)로 퍼블리싱하면서 인증을 대신 처리하고, Memory는 프로세스가 죽어도 대화 상태를 유지합니다. 원문에서는 이 세 가지를 붙이는 걸 Stage 1, 루프 자체를 모델 주도 계획으로 갈아엎는 걸 Stage 2로 나눠서 진행합니다. 이 글은 원문과 공개 샘플 레포(aws-samples/sample-migrate-agents-to-amazon-bedrock-agentcore)를 뜯어보며 이관 설계를 정리하고, 핵심 주장은 제 AWS 계정에 실제로 배포해 확인한 기록입니다. 원문이 강조하는 "한 번에 변수 하나씩만 옮긴다"는 원칙이 샘플 구조에서 어떻게 표현되는지 코드 레벨로 보고, 어댑터 코드 라인 수, Gateway 툴 이름 규칙, cross-process Memory 지속, 자동 생성되는 CloudWatch 로그를 배포해서 실측했습니다. (본문에서 인용하는 examples/... 경로는 모두 위 샘플 레포 기준입니다.) 범위 명시: 코드 설계는 원문과 커밋된 샘플 기준으로 정리했고, 핵심 주장(어댑터 라인 수 45/22/85, Gateway 툴 이름의 ___ 접두사, cross-process Memory 이어받기, OTEL 설정 없이 생성되는 CloudWatch 로그)은 제 AWS 계정에 실제 배포해 확인했습니다. ARM64 휠 함정만 원문 설명을 인용했고 직접 재현은 하지 않았습니다. 결론부터 말하면, 샘플을 읽고 핵심을 직접 배포해본 기준으로 Stage 1에서 멈추는 게 대부분의 팀에 합리적이라는 판단이 섰습니다. 아래는 그 근거입니다. (A) 설계 의도와 아키텍처 먼저 원본 LangGraph 에이전트의 구조부터 짚어야 합니다. StateGraph 위에 classify_intent 노드가 모델에게 "이 사용자 문의가 어떤 종류냐"를 한 단어로 물어보고, 손으로 짠 route_intent 함수가 그 답을 읽어서 escalate 또는 assist 노드로 분기합니다. assist 노드는 lookup_order, process_return, search_faq 세 개의 @tool 함수를 바인딩한 상태로 모델 호출을 하고, 툴 결과가 다시 assist로 돌아오는 루프입니다. 상태는 MemorySaver 체크포인터에 thread_id로 저장되죠. 문제는 이 MemorySaver가 프로세스 안 딕셔너리라는 점입니다. 프로세스가 죽으면 대화가 사라지고, 두 개의 레플리카는 서로의 세션을 볼 수 없습니다. AgentCore의 접근 방식은 이 각각을 별도 서비스로 분리하는 것입니다. Runtime은 BedrockAgentCoreApp 래퍼에 @app.entrypoint 함수를 정의하면 세션당 마이크로VM으로 실행됩니다. 컨테이너를 만들 필요도, ECR에 푸시할 필요도 없습니다. 소스와 의존성을 zip으로 말아서 S3에 올리고 CreateAgentRuntime을 호출하면 됩니다. 서버리스 컴퓨트에 pip 결과물만 던지는 셈입니다. ▲ AgentCore Stage 1 아키텍처: Runtime(마이크로VM) + Gateway(MCP 툴) + Memory(체크포인터) Gateway는 관점을 바꿔야 합니다. 툴을 왜 Gateway 뒤로 옮기느냐 하면, 툴 인증이 에이전트 코드에서 빠지고, 그 툴을 다음 에이전트도 재사용할 수 있게 되기 때문입니다. Lambda 함수를 타깃으로 등록하면 Gateway가 supportTools___lookup_order 같은 이름의 MCP 툴로 퍼블리싱합니다. 밑줄 세 개(___)가 <targetName>___<toolName> 구분자인데, 이 형식은 샘플 레포의 Cedar 정책(examples/stage2_rebuild/policy/support_tools.cedar)에서 액션 이름으로 그대로 확인됩니다. 직접 배포해서 확인해보니 이 접두사는 타깃 등록 스키마나 콘솔 Target 상세에는 나타나지 않았습니다. 거기에는 원본 툴 이름(lookup_order)만 저장돼 있습니다. supportTools___lookup_order라는 접두사 붙은 이름이 실제로 나타나는 곳은 두 군데입니다. 하나는 MCP tools/list 응답이고(그래서 SigV4로 tools/list를 직접 호출해 밑줄 세 개를 확인했습니다), 다른 하나는 Lambda가 호출될 때 받는 context.client_context.custom['bedrockAgentCoreToolName']입니다. 그래서 실제 핸들러는 이 값을 ___로 split해서 원본 툴 이름을 뽑습니다. 이 구분자를 놓치면 툴 이름 매칭이 실패합니다. 흥미로운 건 search_faq는 로컬 Python 함수로 남겨둔다는 점입니다(샘플의 merge_tools가 Gateway 툴로 로컬 스텁을 덮되 search_faq는 로컬로 유지). 다른 에이전트가 재사용할 일이 없고 정책으로 게이팅할 필요도 없는 툴은 굳이 Gateway로 옮길 이유가 없다는 판단입니다. Memory는 MemorySaver를 AgentCoreMemorySaver로 바꾸는 것으로 끝나는데, 재미있는 건 이게 first-party 패키지(langgraph-checkpoint-aws)로 제공된다는 점입니다. actor_id와 thread_id가 RunnableConfig에 실려서 매 호출마다 전달되는 구조라(호출 시 thread_id 값으로 Runtime의 session_id를 넣습니다), LangGraph의 기존 인터페이스는 그대로 유지됩니다. 샘플의 saver는 생성 시 memory_id만 받고 actor_id/thread_id는 config에서 매번 읽습니다. event_expiry_days를 처음 만들 때 신중하게 정해야 한다는 부분이 중요합니다 - 체크포인트가 이 값을 상속받습니다. (B) 심층 분석: 관찰한 이점과 고려 사항 원문은 Stage 1을 붙이면 "10개 중 5개가 AWS 쪽으로 넘어간다"고 정리합니다 - OS 패칭, 오토스케일링, 세션 격리, 체크포인트 저장, 툴 인증. 우리 쪽에 남는 건 VPC 구성, WAF, IAM 정책, 시크릿 로테이션, 의존성 업데이트이고, 이 중 하나는 Stage 3까지 가야 추가로 넘어갑니다. 이 카운트는 원문이 커밋된 샘플 기준으로 제시한 값이며, 샘플에는 이를 재계산하는 스크립트(examples/validation/verify_diff_claim.py)까지 포함돼 있어 주장 대비 검증이 가능하게 돼 있습니다. 코드 변화량도 같은 스크립트로 측정하도록 돼 있습니다. 원문/샘플 기준으로 에이전트 자체 변경, SDK가 안 주는 어댑터 코드(Strands가 반환하는 MCP 툴 객체를 LangGraph BaseTool로 변환), stage 0에서 그대로 import되는 코드로 나뉩니다. 어댑터는 두 SDK에 공유 인터페이스가 없어서 필요한 유일한 접착제입니다. 실제로 verify_diff_claim.py를 돌려보니 45/22/85가 그대로 재현됐습니다. 에이전트 본문 45줄 변경, SDK가 안 주는 어댑터 22줄, stage 0에서 그대로 import되는 85줄입니다. 참고로 배포 스크립트 422줄은 agentcore CLI가 대신 배포해줘서 아무도 손으로 짤 필요가 없어 이 카운트에서 빠집니다. 세 번째는 Observability입니다. 실제로 Runtime을 배포하고 한 번 invoke하니, OTEL을 설정한 적이 없는데도 /aws/bedrock-agentcore/runtimes/... 로그 그룹이 자동으로 생겼습니다. 다만 로그와 트레이스는 구분해야 합니다. 로그 그룹 자동 생성은 기본 동작이지만, 스팬/트레이스를 보려면 Transaction Search를 계정 단위로 켜는 것에 더해 에이전트 코드에 ADOT(aws-opentelemetry-distro) 계측이 붙어야 합니다. 이번에 직접 확인한 건 로그까지이고, 스팬 자동 수집은 배포 방식과 계측 설정에 따라 달라집니다. 다만 이 로그 그룹은 Runtime을 삭제해도 남으므로 cleanup에서 별도로 처리해야 합니다. ▲ OTEL을 설정한 적이 없는데 자동 생성된 CloudWatch 로그 그룹 반대로 만만치 않은 부분도 있습니다. 첫째, pip install -t로 로컬에서 벤더링하면 Mac ARM 또는 x86 휠이 딸려 들어가는데, Runtime은 ARM64 리눅스입니다. --platform manylinux2014_aarch64 --python-version 3.12 --only-binary=:all: 옵션을 안 걸면 실제 원인인 ModuleNotFoundError 대신 Runtime initialization time exceeded ... 30s라는 엉뚱한 에러로 실패한다고 원문은 설명합니다. 이 함정은 직접 재현하지는 않았지만, 배포 시 벤더링 옵션을 반드시 확인하라는 신호로 받아들였습니다. 둘째, Stage 1의 지속성을 검증할 때 이벤트 카운트로 어서션을 걸면 안 됩니다. 원문 지적대로 동일 코드를 두 번 돌려도 이벤트 총합이 다르게 나올 수 있습니다. 대신 다른 프로세스에서 같은 memory_id, actor_id, session_id로 붙어 "이전에 알려준 적 없는 주문 번호"에 대한 답변이 나오는지로 확인하는 편이 안정적입니다. 실제로 다른 프로세스(다른 셸)에서 같은 memory_id/actor_id/session_id로 붙어 보니, 앞선 프로세스가 남긴 대화("주문번호 1001")가 그대로 복원됐습니다. 프로세스가 완전히 갈렸는데도 이어받아지는 걸 눈으로 확인한 거죠. (검증에 쓴 langgraph-checkpoint-aws 1.2.x에서는 대화 메시지 채널을 이벤트로 보존하는 동작을 관찰했는데, 공식 문서는 대화 상태에 더해 실행 컨텍스트와 상태 변수까지 blob 타입으로 저장한다고 설명하므로, 이 부분은 패키지 버전에 따라 달라질 수 있습니다.) 셋째, Stage 2에서 마주치는 트레이드오프인데, route_intent라는 결정론적이고 감사 가능한 분기가 사라집니다. 모델이 계획을 세우니 새 인텐트가 생겨도 프롬프트 한 줄이면 되지만, 반대로 "왜 이 사용자한테는 escalate로 안 갔지?"를 파이썬 코드 라인 번호로 추적할 수 없게 됩니다. 원문은 이 손실을 Cedar 기반 Policy로 데이터 플레인에서 다시 결정론을 되찾는 방식으로 보완합니다. 재밌는 건 샘플의 Cedar 정책에 forbid 룰이 하나도 없다는 점입니다. Cedar가 기본 거부라, read-only 신원이 process_return을 못 부르는 이유는 매칭되는 permit이 없어서지 명시적 forbid가 있어서가 아닙니다. 기본 거부라는 점은 IAM과 같지만, IAM처럼 넓은 wildcard allow로 접근하려다 보면 permit을 촘촘히 나눠 쓰는 Cedar 스타일이 처음엔 낯설게 느껴집니다. (C) 아키텍처 트레이드오프 가장 크게 고민한 결정은 "어디까지 갈 것인가"였습니다. Stage 1까지만 갈지, Stage 2까지 밀어붙일지, 아니면 Stage 3의 harness까지 기다릴지. 팀 상황을 대입해봤을 때 Stage 1이 정답이었습니다. 이유는 단순합니다. LangGraph 그래프 자체는 우리가 신뢰하고 있고, 하드코딩된 분기가 병목이 아니었기 때문입니다. Stage 2로 넘어가면 얻는 건 "새 인텐트를 프롬프트로 처리"인데, 잃는 건 감사 가능한 라우팅입니다. 규제 산업에서는 이 손실이 무겁습니다. 두 번째 결정은 Lambda 타깃의 인증 방식이었습니다. GATEWAY_IAM_ROLE을 쓰면 Gateway가 자기 실행 롤로 Lambda를 호출하고 툴 인자가 raw event로 들어옵니다. API Gateway 스타일 이벤트를 기대하는 기존 Lambda가 있다면 어댑팅이 필요합니다(앞서 (A)에서 짚었듯 툴 인자는 raw event로 들어오고, 호출된 툴 이름은 context.client_context로 따로 옵니다). 대안은 AgentCore Identity를 붙여서 사용자 위임 토큰을 흘려보내는 것인데, 이번 PoC에서는 사용자별 델리게이션이 필요한 툴이 없어서 IAM 롤로 갔습니다. Identity로 갈아탈 때 credential provider를 바꾸는 지점은 Gateway 타깃의 credentialProviderConfigurations입니다. 단, 이 필드 하나만 바꾸면 끝나는 건 아닙니다 - 인바운드 authorizer(예: OAuth/3LO)는 Gateway 생성 시의 별개 선택이므로, Identity 경로로 전환할 때는 아웃바운드 자격 증명과 인바운드 인증을 함께 봐야 합니다. 세 번째는 배포 방식이었습니다. AgentCore Runtime은 컨테이너 이미지 배포와 codeConfiguration(zip + S3) 배포를 모두 지원하는데, 이번엔 zip을 선택했습니다. ECR을 안 건드려도 되고, 빌드 파이프라인이 pip 하나로 끝난다는 점이 매력적이었거든요. 대신 포기한 건 커스텀 베이스 이미지에 시스템 패키지를 넣는 자유도입니다. 순수 Python 의존성만으로 충분한 에이전트라면 zip이 압도적으로 빠른 이터레이션을 줍니다. 네 번째는 Memory의 TTL 정책이었습니다. event_expiry_days를 30일로 잡을지, 90일로 잡을지, 아니면 규제 최대치까지 늘릴지 고민했는데, 결국 짧게 시작해서 필요할 때 늘리는 방향으로 갔습니다. 체크포인트가 이 값을 상속받는다는 특성 때문에, 처음에 넉넉하게 잡으면 나중에 저장 비용이 예상보다 튀어오릅니다. 반대로 짧게 잡으면 "어제 대화를 이어서 하고 싶어요"라는 사용자 요청을 못 받게 되니, 도메인의 실제 세션 지속 시간을 먼저 재봐야 합니다. 요약과 최종 체크리스트 핵심 결정을 요약하면 이렇습니다. Runtime은 zip 배포로, Gateway는 GATEWAY_IAM_ROLE로, Memory는 first-party 체크포인터로, 그래프 자체는 stage 0에서 import. Stage 1에서 멈추고 Policy를 Gateway에 붙이는 조합이 대부분의 팀에 충분합니다. 재현하려는 분들을 위한 체크리스트: Bedrock 모델 액세스 활성화, CloudWatch Transaction Search 계정 단위 활성화 로컬에서 app.run()으로 먼저 검증한 뒤 Runtime 배포 (Runtime 로그는 원인 추적이 어렵습니다) pip install -t 시 --platform manylinux2014_aarch64 --python-version 3.12 --only-binary=:all: 필수 Gateway 타깃 등록 후 툴 이름은 <targetName>___<toolName> 형식으로 접두사가 붙는다는 점 기억 (콘솔 스키마가 아니라 MCP tools/list 응답에서 확인) Memory 검증은 이벤트 카운트가 아니라 "다른 프로세스에서 이어받기 시나리오"로 Runtime sessionId는 최소 33자 이상이어야 하므로, 직접 세션 ID를 만들 때 길이를 확인 Cleanup 시 Gateway 타깃 -> Gateway 순서, GetMemory 폴링, CloudWatch 로그 그룹 별도 삭제 Stage 0의 그래프를 그대로 재사용하는 부분입니다. 이관 후에도 라우팅 로직이 stage 0의 코드라는 점, 그리고 그래프는 매 호출 재빌드가 아니라 프로세스 수명 동안 1회 빌드해 홀드한다는 점이 핵심입니다(샘플의 support_graph() 지연 싱글턴). # examples/stage1_replatform/agent_runtime.py (샘플 구조 기준, 요지 발췌) from bedrock_agentcore import BedrockAgentCoreApp from langchain_core.messages import HumanMessage from examples.stage0_langgraph.agent import build_graph # stage 0을 import app = BedrockAgentCoreApp() _graph = None # 모듈 스코프: 프로세스 수명 동안 1회만 빌드해 홀드 def support_graph(): # import 시점이 아니라 최초 호출 시 1회 빌드 (오프라인 import 가능하게). # 매 호출 재빌드하면 모델 클라이언트/ MCP 세션이 반복 생성/해제된다. global _graph if _graph is None: _graph = build_graph(llm=llm, tools=gateway_tools(), checkpointer=saver) return _graph @app.entrypoint def agent_invocation(payload, context): state = support_graph().invoke( {"messages": [HumanMessage(payload.get("prompt", ""))]}, config={"configurable": { # thread_id 값에 Runtime이 주는 session_id를 넣는다 "thread_id": context.session_id or "local-session", "actor_id": os.environ.get("AGENTCORE_ACTOR_ID", "langgraph"), # 필수 }}, ) return {"result": state["messages"][-1].text} # 샘플이 .text 사용 (확인함) Gateway 타깃 등록 코드입니다. Lambda ARN과 툴 스키마만 넘기면 MCP 툴로 자동 퍼블리싱됩니다. # examples/gateway/register_target.py (요지) response = client.create_gateway_target( gatewayIdentifier=gateway_id, name="supportTools", # 이 이름이 supportTools___lookup_order 접두사가 됨 targetConfiguration={ "mcp": { "lambda": { "lambdaArn": lambda_arn, "toolSchema": {"inlinePayload": TOOL_SCHEMA}, } } }, # Gateway가 자기 실행 롤로 Lambda 호출 credentialProviderConfigurations=[ {"credentialProviderType": "GATEWAY_IAM_ROLE"} ], ) ▲ ① 타깃 등록 스키마(콘솔). 여기엔 원본 이름 lookup_order / process_return만 저장됩니다. ▲ ② 같은 게이트웨이의 MCP tools/list 응답. supportTools___ 접두사는 퍼블리싱 시점에만 붙어서, AWS_IAM authorizer 게이트웨이라 SigV4로 직접 호출해야 밑줄 세 개가 보입니다. Memory 체크포인터 교체 부분. MemorySaver() 자리에 한 줄만 들어갑니다. # examples/stage1_replatform/agent_runtime.py 의 saver 구성 (요지) from langgraph_checkpoint_aws import AgentCoreMemorySaver # saver 는 memory_id 만 받는다. actor_id/thread_id 는 생성 시가 아니라 매 호출 config 에서 읽는다. graph = build_graph( llm=llm, tools=tools, checkpointer=AgentCoreMemorySaver(memory_id, region_name=region), ) # actor_id 와 thread_id 가 매 호출마다 config 로 전달됨 state = graph.invoke( {"messages": [HumanMessage(prompt)]}, config={"configurable": { "thread_id": session_id, # thread_id 값 = Runtime session_id "actor_id": actor_id, # 필수 (없으면 saver 가 InvalidConfigError) }}, ) Runtime 배포 시 ARM64 리눅스용 휠을 벤더링하는 명령. 여기서 옵션을 놓치면 원인 없는 30초 타임아웃이 뜬다고 원문은 경고합니다. # Runtime은 ARM64 리눅스, Python 3.12 pip install -r requirements.txt -t build/ \ --platform manylinux2014_aarch64 \ --python-version 3.12 \ --only-binary=:all: # build 디렉토리를 소스와 함께 zip으로 압축해 S3 업로드 cd build && zip -r ../agent.zip . && cd .. zip -g agent.zip agent_runtime.py aws s3 cp agent.zip s3://$BUCKET/agent.zip Cedar Policy 예시입니다. 툴 이름이 action으로, Gateway ARN이 resource로 들어가고, 신원은 AgentCore::IamEntity(Gateway가 AWS_IAM authorizer라 principal이 ARN)라는 점이 핵심입니다. forbid가 없다는 점도 그대로입니다. (샘플 examples/stage2_rebuild/policy/support_tools.cedar 기준. 플레이스홀더는 등록 시 치환되는데, 이때 들어가는 값은 IAM 역할 ARN이 아니라 STS assumed-role 형식 arn:aws:sts::<account>:assumed-role/<RoleName>입니다. Runtime이 실행 롤로 Gateway를 호출하면 항상 이 형태라, 역할 ARN을 그대로 ==에 넣으면 매칭되지 않습니다. 그래서 아래 예시는 principal == "<ARN>" 대신 principal is AgentCore::IamEntity + when { principal.id like "*:assumed-role/<Role>/*" } 패턴으로 썼습니다.) // read-only 신원은 lookup_order만 허용 (forbid 없음 - Cedar 기본 거부) // principal 은 == 로 STS ARN 을 통째로 맞추기보다, id 패턴 매칭이 안정적이다. permit( principal is AgentCore::IamEntity, action == AgentCore::Action::"supportTools___lookup_order", resource == AgentCore::Gateway::"<GATEWAY_ARN>" ) when { principal.id like "*:assumed-role/ReadOnlyRole/*" }; // support-agent 신원은 process_return도 허용 permit( principal is AgentCore::IamEntity, action in [AgentCore::Action::"supportTools___lookup_order", AgentCore::Action::"supportTools___process_return"], resource == AgentCore::Gateway::"<GATEWAY_ARN>" ) when { principal.id like "*:assumed-role/SupportAgentRole/*" }; // forbid 룰 없음. read-only의 process_return 거부 = 매칭 permit 부재. 배운 점 가장 인상적인 건 "이관 비용을 접착 코드 라인 수로 측정할 수 있게 해뒀다"는 접근이었습니다. 보통 이런 마이그레이션 글은 "쉽게 옮길 수 있다"고 말하지만 실제 접착 코드가 얼마인지 알려주지 않는데, 이 샘플은 그걸 커밋된 파일 + 계산 스크립트(verify_diff_claim.py) 수준에서 세어볼 수 있게 해뒀습니다. 실제로 돌려보니 45(에이전트)/22(어댑터)/85(그대로 import)로 원문 값이 재현됐고, 어댑터 코드의 본질은 두 SDK의 툴 타입을 서로 변환하는 것이었습니다. 이런 정직함이 있어야 견적이 가능해집니다. 두 번째는 "한 번에 변수 하나씩만 옮기라"는 원칙의 실용성입니다. Stage 1에서는 어디서 실행되는지만 바뀌고 어떻게 사고하는지는 그대로이기 때문에, 이관 후 답변이 이상해지면 원인이 인프라라는 게 좁혀집니다. Stage 2로 넘어가 라우팅 방식을 바꾸는 순간부터는 답변 차이의 원인이 모델의 계획이 되고, 회귀 테스트의 성격이 완전히 달라집니다. Stage 1과 Stage 2를 한 번에 했다면 문제 발생 시 원인 지목이 어려웠을 겁니다. 세 번째는 배포 전 반드시 넣어야 할 검증입니다. 세션 idle timeout(idleRuntimeSessionTimeout) 조정과 인메모리 상태 감사를 프로덕션 커트오버 전에 넣어야, "세션은 살아있지만 프로세스 안 딕셔너리 상태가 날아가는" 케이스(15분 이상 유휴)를 놓치지 않습니다. 모든 상태를 Memory에 넣고 그래프 객체만 프로세스에 홀드한다는 원칙(위 support_graph() 싱글턴)을 지키면 예방됩니다. Kyungmin (Lucas) Kim 주식회사 이테크시스템의 AWS Solutions Architect로서, 실무 중심의 서버리스 엔지니어링과 전략적 AI 전환(AX)의 가교 역할을 하고 있습니다. 단순한 시스템 구현을 넘어 지능적이고 자율적인 환경을 전략적으로 설계하는 데 집중하며, AWS 생태계의 지속적인 발전에 기여하고자, 실전에서 얻은 아키텍처 패턴과 기술적 통찰을 기술 커뮤니티와 적극적으로 공유하고 있습니다. Donghee (Chad) Kim 주식회사 이테크시스템의 AWS Solutions Architect이자 AI Architect입니다. 엔터프라이즈 AI Transformation(AX) 및 클라우드 아키텍처 설계를 전문으로 하며, DevSecOps 배경과 정보보안팀을 이끌어 ISMS-P 인증 취득을 달성한 실무 경험을 보유하고 있습니다. AWS Ambassador 및 AWS 13x Certified(Golden Jacket) 자격을 보유하고 있으며, AWS Summit Seoul 2026에서 AX 사례(PRT302-S)를 발표하고 AWS 커뮤니티를 직접 운영하며 생태계에 기여하고 있습니다.