내부적으로 팀 구성원과 함께 진행한 "AWS GenAI(Bedrock)을 활용한 회의록 요약 홈페이지를 구축했던 사례를 공유하고자 합니다. 시작하기 전에 AWS GenAI(Bedrock)에 대해 정확히 이해하고 진행하는 것이 필요한데요.
AWS 홈페이지와 Document 내용을 종합하여 정리하여 작성한 내용을 보면 다음과 같습니다.
1. AWS Bedrock 개요
AWS Bedrock은 생성형 AI 애플리케이션을 빠르게 개발·배포할 수 있도록 지원하는 완전관리형(fully managed) 서비스입니다.
기존에는 대규모 언어 모델(LLM) 또는 생성형 AI 모델을 사용하려면 모델을 직접 학습·배포하고 인프라를 관리해야 했지만, Bedrock은 AWS 콘솔/API를 통해 다양한 파운데이션 모델(Foundation Model, FM)을 인프라 구축 없이 API로 호출할 수 있게 해줍니다.
주요 포인트
- 모델 제공자 선택 가능: Amazon, Anthropic, AI21 Labs, Cohere, Meta, Mistral, Stability AI 등 다수 모델 제공자(FM Provider) 지원
- 자체 데이터로 커스터마이징 가능: Fine-tuning 또는 Retrieval-Augmented Generation (RAG) 적용 가능
- 서버리스(Serverless): 모델 호스팅, 스케일링, 인프라 관리를 AWS가 모두 담당
- 네이티브 AWS 통합: S3, SageMaker, Lambda, API Gateway, CloudWatch, GuardDuty 등과 연동 가능
2. AWS Bedrock 주요 구성 요소
구성 요소설명
Foundation Model (FM) | 미리 학습된 대규모 생성형 AI 모델. 텍스트, 이미지, 멀티모달 등 지원 |
FM Provider | 모델 제공 업체(Amazon Titan, Anthropic Claude, Stability AI 등) |
API / SDK | AWS SDK 또는 REST API를 통해 FM 호출 |
Customization | 모델 파인튜닝 또는 벡터 데이터 기반의 RAG 적용 |
Agent 기능 | Bedrock Agent를 사용해 외부 데이터와 API를 연결, 멀티스텝 작업 수행 |
Guardrails | 응답 제어 및 콘텐츠 필터링 기능으로 안전한 AI 서비스 제공 |
3. AWS Bedrock의 주요 특장점
3.1 인프라 관리 불필요
- 모델 서버 구축, GPU 관리, 스케일링 작업 없이 API 호출만으로 모델 사용 가능
- SLA 보장, AWS 보안·컴플라이언스 기본 제공
3.2 다양한 모델 선택
- 멀티모델·멀티벤더 전략 가능 → 특정 벤더 락인(lock-in) 최소화
- 예:
- Amazon Titan → 텍스트 생성·요약·임베딩
- Anthropic Claude → 긴 문맥 처리에 강점
- Stability AI → 이미지 생성(Stable Diffusion)
- Mistral, Meta Llama → 오픈소스 친화 모델
3.3 데이터 프라이버시 및 보안
- Bedrock 사용 시 고객 데이터는 모델 학습에 사용되지 않음
- AWS KMS 기반 암호화, VPC 엔드포인트 연동, IAM 권한 제어 지원
3.4 빠른 커스터마이징
- Fine-tuning: 모델을 고객 도메인에 맞게 재학습
- RAG: S3·DynamoDB·OpenSearch 등에서 가져온 벡터 데이터로 지식 확장
- 비즈니스 전용 QA, 요약, 검색 서비스 구축에 유리
3.5 AWS 에코시스템과 통합
- SageMaker → 데이터 전처리·평가
- Lambda & API Gateway → AI API 서비스화
- CloudWatch → 모니터링
- GuardDuty → AI 호출 보안 로깅
3.6 비용 효율성
- 사용한 만큼만 과금(온디맨드 방식)
- 모델/엔드포인트별 가격 투명
- GPU 클러스터 상시 운영 대비 TCO 절감
아래 표에 기재된 서비스는 실제 구현단계에서 사용한 리소스에 대한 명칭과 용도입니다.
S3 | 정적 컨텐츠로 사용하는 Object와, Report Summary가 저장되는 Object로 구성되며, 홈페이지는 index.html형태인 Object가 호출된다. |
Route53 | 도메인을 사용하여 Web서버에 접속할 수 있도록 진행하기 위해 Route53을 사용한다. |
Lambda | AWS Bedrock의 Claude 모델 ID 기반으로 람다함수를 구성하고, API Gateway에서 요청온 Method에 따라 함수가 호출된다. 요청받은 txt파일 요약본과, 해당 txt파일의 Obejct URI를 호출할 수 있도록 리소스 결과물들을 S3 Object에 저장한다. |
CloudWatch | 해당 Lambda의 Log 수집과 에러 확인을 위해 실시간으로 로그를 수집 후 분석한다. |
CloudFront | S3를 정적페이지로 사용한다. 하지만 S3의 경우 HTTP만 지원하는데, HTTPS 적용을 위해 CloudFront를 배치한다. HTTP 요청을 HTTPS 요청으로 Redirection한다. |
Bedrock | 회의록 요약을 위해 사용하는 Claude 모델을 사용하며, 해당 모델은 us-west-2 지역에서만 제공하므로 해당 지역에서 인프라를 구성한다. |
API Gateway | S3의 index.html에서 API Gateway로 Invoke URL 호출을 진행하는데, 이때 연결된 Lambda로 POST 요청을 진행한다. |
ACM | SSL 인증서 등록을 위해 사용하는 서비스이다. CloudFront의 경우 us-east-1 지역의 ACM만을 사용하므로, 해당 Region에 ACM을 구비한다. |
자 이제 실제 구현했던 아래 내용을 참고해보도록 합니다.
1) AI Model 선택
사용하기 전에 AWS Bedrock에서 해당 모델을 활성화 진행합니다. Claude 모델 사용할 예정입니다.
해당 모델 설명 읽고, 사용해보고 절차는 아래 캡처된 화면 진행 순서대로 진행하였습니다.
실제 구현 단계에서 완료까지 총 5분정도 소요되었고 완료 표시가 확인되었을 때 앞으로 가능합니다.
1-1) AI Model Test
테스트 가능 모델을 선택하여 CHAT / TEXT를 진행합니다.
오늘 사용할 LLM 모델인 Claude v2의 설명입니다.
온도 (Temperature)
- 확률로 단어를 순서대로 읽는다. (빈도수)
- 0에 가깝게 설정하면 모델이 더 높은 확률의 단어를 선택 (결정론적)
- 1에 가까우면 더 낮은 확률로 단어를 선택
- 1일경우 : 세계 최고의 축구 선수는? 호날두 / 펠레 / 메시
- 0일경우 : 세계 최고의 축구 선수는? 호날두 / 호날두 / 호날두
상위 P (Top P)
- 잠재적 선택지 (모델이 다음 토큰을 고려할 가능성이 가장 높은 후보의 비율)
- 모델은 가능성이 가장 높은 선택지만 고려하고 가능성이 덜 높은건 무시
- 1일경우 : 말발굽 소리를 들려줄때 유니콘의 말발굽 소리 (가능성이 낮은 값)
- 0일경우 : 말발굽 소리를 들려줄때 조랑말의 말발굽 소리 (가능성이 높은 값)
상위 K (TOP K)
- 잠재적 단어의 확률분포 (모델이 다음 토큰을 고려할 가능성이 가장 높은 후보의 수)
- 해당 숫자로 예상되는 답을 표출한다.
- 1일경우 : 사과의 색깔은 빨갛다. (가능성이 높은 값)
- 60일경우 : 사과의 색깔은 갈색일수도 있다. (가능성이 낮은 값)
2) Lambda 생성
API Gateway가 호출되면 람다함수를 호출되도록 진행
1) lambda functions create function
2) Author from scratch -> "name" -> "python 3.11"
- name : bedrock_meeting_summary
3) 생성
4) configuration - General configuration - Edit
5) Timeout 4min 으로 변경
런타임 시간 변경
관리자 IAM Role을 Lambda 함수에 추가
1) Lambda -> Configuration -> Permissions
2) Role name 선택
3) Permissions -> Add Permissions -> AttachPolicies
4) Administrator 선택
2-1) Web - 정적페이지로 사용할 S3 Bucket 생성하기
1) S3 - Create bucket #(추후 사용할 URL 이름으로)
- name : ktdscloud.com
2) Block Public access (모든 퍼블릭 액세스) = Off
3) Bucket Versioning (버킷 버전 관리) = Enabled
4) Bucket key (기본 암호화) = Enabled
5) 정책
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::버킷이름/*"
}
]
}
6) "속성" - "정적 웹 사이트 호스팅" - "편집"
7) "정적 웹 사이트 호스팅 "활성화" - "정적 웹 사이트 호스팅" - 인덱스 문서, 오류문서 = index.html
3) API Gateway 생성 후 Lambda 연결하기
- api를 생성, 유지할 수 있음
- Post Route를 지정하고, endpoint로 가면 호출
- API 를 만들기 위한 코드는 필요 없다. (API Gateway 사용하면)
1) api gateway - Create API
2) http API Bulid - "bedrock_course_api"(이름) - create
route 생성
- POST 요청이 올때, 해당 람다 함수가 호출되도록
1) Routes -> Create
- POST
- /meeting-summary
attach integration
- 해당 루트로 갈때, 인증
1) /meeting-summary:POST
2) Attach integration -> Create and attach an integration
3) lambda function
4) 방금 만든 람다 선택
5) 생성
이렇게 되면 API와 POST Method 설정 완료
배포를 위한 Stage 생성
- 개발환경/운영환경 등 선택해서 배포할 수 있다.
1) Deploy -> Stages -> Create
2) Name : dev
3) 자동배포 안함
4) 생성
새 경로를 배포하자
1) Routes - POST - Deploy
2) 방금만든 Stage 선택 - Deploy
그러면 devAPI가 만들어진다.
Bedrock을 호출하는 람다 함수를 호출할 수 있어야 한다.
이런식으로 만들면되니 실질적으로 사용할 API Route를 생성하자
1) APIs 선택 -> Routes -> Create
2) POST : /meeting-summary -> Create
3) 방금 만든 POST 선택
4) Attach integration
5) Create - lambda - 방금 만든 lambda -> Create
# Route를 배포해줘야 한다.
6) Deploy
7) dev 선택
8) API 가보면, dev에 URL 생성이 되어 있음
CORS 체크 해주자
1) API gateway - CORS - * 추가 - 재배포
4) Lambda 코드 생성
import boto3
import botocore.config
import json
import base64
from datetime import datetime
from email import message_from_bytes
# 다중 데이터일 경우 이 함수를 이용하여 텍스트 추출 (업로드 파일)
def extract_text_from_multipart(data):
msg = message_from_bytes(data)
text_content = '' # 이부분은 모델이 분석
# 번호, 줄거리 등 필요없는 부분 있는지 확인
if msg.is_multipart():
for part in msg.walk():
# 만일 순수 텍스트 부분일 경우 (다중 파트일 경우)
if part.get_content_type() == "text/plain":
text_content += part.get_payload(decode=True).decode('utf-8') + "\n"
else:
# 다중 파트가 아닐 경우
if msg.get_content_type() == "text/plain":
text_content = msg.get_payload(decode=True).decode('utf-8')
# 문자가 없으면 답변이 없고, 있으면 리턴 값
return text_content.strip() if text_content else None
# 요약 생성하기를 클릭할 때 트리거되는 함수 (bedrock 모델 적용)
# 문자열인지 확인하고 반환 (Human은 아래 stop_sequences의 변수명과 동일해야 )
def generate_summary_from_bedrock(content: str) -> str:
prompt_text = f"""Human: 금일 회의를 요약하겠습니다.: {content}
Assistant:"""
body = {
"prompt": prompt_text,
"max_tokens_to_sample": 5000,
"temperature": 0.1,
"top_k": 250,
"top_p": 0.2,
"stop_sequences": ["\n\nHuman:"]
}
# bedrock API를 호출하는 코드 (사용 모델은 anthropic.claude 모델)
# response 값에 대해 utf-8로 해독
try:
bedrock = boto3.client("bedrock-runtime", region_name="us-west-2", config=botocore.config.Config(read_timeout=300, retries={'max_attempts': 3}))
response = bedrock.invoke_model(body=json.dumps(body), modelId="anthropic.claude-v2") #수정
response_content = response.get('body').read().decode('utf-8')
response_data = json.loads(response_content)
summary = response_data["completion"].strip()
return summary
except Exception as e:
print(f"Error generating the summary: {e}")
return ""
# 요약본을 s3에 저장
def save_summary_to_s3_bucket(summary, s3_bucket, s3_key):
s3 = boto3.client('s3')
try:
# ContentDisposition='attachment' 추가
s3.put_object(Bucket=s3_bucket, Key=s3_key, Body=summary, ContentDisposition='attachment; filename="summary.txt"')
print("Summary saved to s3")
except Exception as e:
print("Error when saving the summary to s3:", e)
# aws lambda 핸들러
def lambda_handler(event, context):
# 람다 처리기가 위의 함수를 호출, 여기서 모두 코드 진행
# 포스트맨에서 받은 base64형식 부분에 대해서 디코딩 진행
decoded_body = base64.b64decode(event['body'])
# 텍스트 컨텐츠 추출
text_content = extract_text_from_multipart(decoded_body)
# 텍스트 컨텐츠가 없을 경우
if not text_content:
return {
'statusCode': 400,
'body': json.dumps("Failed to extract content")
}
# bedrock LLM을 통해서 요약 진행
summary = generate_summary_from_bedrock(text_content)
if summary:
current_time = datetime.now().strftime('%H%M%S') # UTC TIME
s3_key = f'summary-output/{current_time}.txt'
s3_bucket = 'ktdscloud2.com' # 본인 버킷 이름명
#s3_url = f"http://{s3_bucket}.s3.amazonaws.com/{s3_key}" # 바로 다운로드 할 수 있는 URL
#s3_url = f"http://s3.us-west-2.amazonaws.com/{s3_bucket}/{s3_key}"
s3_url = f"https://d2onoxzvgcelcr.cloudfront.net/{s3_key}" #cloudfront 보류
save_summary_to_s3_bucket(summary, s3_bucket, s3_key)
# HTTP로 변환
#s3_url = s3_url.replace('https://', 'http://')
# else:
# print("No summary was generated")
# return {
# 'statusCode': 500,
# 'body': json.dumps("No summary was generated")
# }
return {
'statusCode': 200,
'headers': {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'OPTIONS,POST,GET'
},
'body': json.dumps({
's3_url': s3_url,
'message': '위의 파일을 다운로드해주세요.'
})
}
5) Homepage 꾸미기
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>회의록 요약 마술사</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Comic+Neue:wght@300&display=swap');
body {
font-family: 'Comic Neue', cursive;
text-align: center;
background: linear-gradient(to bottom, #e0f7fa, #fff);
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
.container {
background: rgba(255, 255, 255, 0.8);
border-radius: 16px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
padding: 30px;
max-width: 600px;
width: 100%;
box-sizing: border-box;
position: relative;
}
h1 {
font-size: 2.5em;
margin-bottom: 10px;
color: #333;
}
p {
font-size: 0.8em; /* 크기 1/3로 줄임 */
color: #555;
margin-bottom: 30px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* 추가된 부분 */
}
#result {
margin-top: 20px;
font-size: 1.2em;
color: #34495e;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* 추가된 부분 */
}
.spacer {
margin-bottom: 20px;
}
.spacer-large {
margin-bottom: 30px;
}
.spacer-small {
margin-bottom: 10px;
}
input[type="file"] {
display: none;
}
.file-label-button {
display: inline-block;
background-color: #66bb6a;
color: white;
padding: 10px 20px;
border-radius: 20px;
cursor: pointer;
transition: background-color 0.3s;
margin-right: 10px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.file-label-button:hover {
background-color: #4caf50;
}
button[type="submit"] {
display: inline-block;
background-color: #42a5f5;
color: white;
border: none;
padding: 10px 20px;
cursor: pointer;
border-radius: 20px;
font-size: 1em;
transition: background-color 0.3s;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
button[type="submit"].processing {
background-color: red;
}
button[type="submit"]:hover {
background-color: #1e88e5;
}
button[type="button"] {
background-color: #2ecc71;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
margin-top: 10px;
border-radius: 4px;
font-size: 0.9em;
transition: background-color 0.3s;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* 추가된 부분 */
}
button[type="button"]:hover {
background-color: #27ae60;
}
.summary-complete {
font-size: 1.1em; /* 크기를 줄임 */
font-weight: bold;
margin-top: 20px;
color: #27ae60;
font-family: 'Arial', sans-serif;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* 추가된 부분 */
}
.header {
margin-bottom: 5px;
}
.footer {
margin-top: 20px;
font-size: 0.8em;
color: #95a5a6;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* 추가된 부분 */
}
.timer {
position: absolute;
bottom: 10px;
right: 10px;
font-size: 0.8em;
color: #34495e;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* 추가된 부분 */
}
#file-name {
margin-top: 10px;
font-size: 0.9em;
color: #2c3e50;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* 추가된 부분 */
}
.result-link {
font-size: 0.9em; /* 크기를 줄임 */
font-family: 'Arial', sans-serif;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* 추가된 부분 */
}
</style>
</head>
<body>
<h1>What would you like to summarize?</h1>
<p>회의록 요약 마법사✨</p>
<div class="container">
<form id="upload-form" class="spacer-large">
<input type="file" id="file-input" accept=".txt" class="spacer-small" />
<label for="file-input" class="file-label-button">파일 선택</label>
<button type="submit" id="submit-button">회의록 요약 시작</button>
<div id="file-name"></div>
</form>
<div id="result"></div>
<div class="footer">© ktds cloud컨설팅팀 성민</div>
<div class="timer" id="timer"></div>
</div>
<script src="script.js"></script>
</body>
</html>
js 파일
// script.js 파일 생성
const fileInput = document.getElementById('file-input');
const fileNameDiv = document.getElementById('file-name');
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
fileNameDiv.textContent = `선택된 파일: ${file.name}`;
} else {
fileNameDiv.textContent = '';
}
});
document.getElementById('upload-form').addEventListener('submit', async (event) => {
event.preventDefault();
const submitButton = document.getElementById('submit-button');
const timerDiv = document.getElementById('timer');
const form = document.getElementById('upload-form');
const fileLabel = document.querySelector('.file-label-button');
const file = fileInput.files[0];
if (!file) {
alert('파일을 업로드해주세요!');
return;
}
// Hide the form elements
fileLabel.style.display = 'none';
submitButton.style.display = 'none';
fileNameDiv.style.display = 'none';
submitButton.classList.add('processing');
const formData = new FormData();
formData.append('document', file);
const resultDiv = document.getElementById('result');
let loadingMessage = '생성형 AI가 열심히 요약중입니다 ';
let icons = ['📋', '📋✏️', '📋✏️🤖'];
let iconIndex = 0;
// 타이머 시작 시 표시되도록 설정
timerDiv.innerText = '예상 완료시간 : 11초';
const loadingInterval = setInterval(() => {
resultDiv.innerHTML = loadingMessage + icons[iconIndex];
iconIndex = (iconIndex + 1) % icons.length;
}, 500);
let timeLeft = 11;
const timerInterval = setInterval(() => {
if (timeLeft <= 0) {
clearInterval(timerInterval);
timerDiv.innerText = "완료 중...";
} else {
if (timeLeft <= 6) {
timerDiv.style.color = 'red';
}
if (timeLeft > 7) {
timerDiv.style.color = 'blue';
}
timerDiv.innerText = `예상 완료시간 : ${timeLeft}초`;
timeLeft -= 1;
}
}, 1000);
try {
const response = await fetch('https://rrmmm9pt71.execute-api.us-west-2.amazonaws.com/dev/meeting-summary', {
method: 'POST',
body: formData
});
clearInterval(loadingInterval);
clearInterval(timerInterval);
submitButton.classList.remove('processing');
timerDiv.innerText = "완료";
if (!response.ok) {
throw new Error('파일 업로드 실패');
}
const result = await response.json();
const s3Url = result.s3_url;
const message = result.message;
resultDiv.innerHTML = `
<div class="summary-complete">-파일 요약본 완성-</div>
<br><span class="result-link">요약된 파일 URL: <a href="${s3Url}" target="_blank">다운로드</a></span>
<br><button type="button" onclick="location.reload();">새로고침</button>
`;
} catch (error) {
clearInterval(loadingInterval);
clearInterval(timerInterval);
submitButton.classList.remove('processing');
resultDiv.innerText = `오류: ${error.message}`;
}
});
3) 위의 파일을 확장자 변경이후 S3에 넣어주기
여기서 틀려보기
(S3 에서 다운로드 되는지 확인 -> 결국 람다 수정)
6) CloudFront 생성해서 S3 연결하기 - Route53과 연결하기
해당 과정에서는 원하는 도메인으로 홈페이지에 들어오도록 만들 것이다.
1) CloudFront 생성 클릭
2) Origin domain = 해당 S3 선택
3) Viewer protocol policy "Redirect HTTP to HTTPS" 선택
4) Allowed HTTP methods "GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE" 선택 #필자는 Post 사용
5) Cache policy "CachingOptimized" 선택
6) Alternate domain name (CNAME) - optional = "사용할 도메인 명 입력"
7) Custom SSL certificate - optional = "버즈니아 북부에서 만든 ACM 적용"
8) 기본값 루트 객체 - index.html
9) 원본 액세스 제어 설정
10) Create distribution
그리고 생기는 권한 붙여넣기
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::ktdscloud2.com/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::050752615490:distribution/E3D1VVSGBKIJIW"
}
}
}
]
}
그럼 cloudfront로 https 적용 완료
Route53에서 설정
1) 레코드 생성
2) 위에서 만든 URL
3) AType
4) 별칭
정상적으로 홈페이지가 열리는지 확인하여 기능을 점검할 수 있습니다.
내부적으로 핸즈온 기반 기능 테스트 구현을 통한 개별 프로젝트가 활발하게 진행되는데 보다 복합적이고 선도적인 기술 기반으로 진행했으면 하는 욕심입니다.
'AWS 클라우드' 카테고리의 다른 글
AWS WorkSpace에 대한 아키텍처 이해(A-Z 까지 따라잡기) (5) | 2025.08.08 |
---|---|
AWS Control Tower 기반 Landing Zone 구축 사례 (0) | 2025.02.01 |
AWS CodePipeline를 이용한 사내 CI/CD 환경 구현 사례 (0) | 2024.12.27 |