Skip to content

Commit 1c0e570

Browse files
docs(blog): 검색 유입용 기술 팁 포스트 5차 배치 (5개, 3개 언어)
- AWS CDK SES 이메일 전송 Lambda - DynamoDB ConditionExpression 동시성 제어 - S3 Presigned URL 일회용 다운로드 링크 - AWS Lambda 배포 쉘 스크립트 (CDK 함수명 자동 조회) - requestAnimationFrame FPS 모니터링
1 parent d95d1e5 commit 1c0e570

15 files changed

Lines changed: 809 additions & 0 deletions
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
layout: post
3+
title: "AWS CDK - Build a SES Email Lambda in Minutes"
4+
date: 2026-01-25 09:00:00 +0900
5+
categories: [Development, Tips]
6+
tags: [AWS, CDK, SES, Lambda, email, TypeScript]
7+
author: "Kevin Park"
8+
lang: en
9+
excerpt: "Set up a serverless email sending endpoint with AWS CDK: API Gateway + Lambda + SES."
10+
---
11+
12+
## Problem
13+
14+
Need email sending for a contact form, but running a dedicated server is overkill. Serverless AWS is the answer.
15+
16+
## Solution
17+
18+
```typescript
19+
import * as cdk from 'aws-cdk-lib';
20+
import * as lambda from 'aws-cdk-lib/aws-lambda';
21+
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
22+
import * as ses from 'aws-cdk-lib/aws-ses';
23+
import * as iam from 'aws-cdk-lib/aws-iam';
24+
25+
export class ContactApiStack extends cdk.Stack {
26+
constructor(scope: Construct, id: string) {
27+
super(scope, id);
28+
29+
new ses.EmailIdentity(this, 'Sender', {
30+
identity: ses.Identity.email('contact@example.com'),
31+
});
32+
33+
const fn = new lambda.Function(this, 'ContactFn', {
34+
runtime: lambda.Runtime.NODEJS_20_X,
35+
handler: 'index.handler',
36+
code: lambda.Code.fromAsset('lambda/contact'),
37+
});
38+
39+
fn.addToRolePolicy(new iam.PolicyStatement({
40+
actions: ['ses:SendEmail'],
41+
resources: ['*'],
42+
}));
43+
44+
const api = new apigateway.RestApi(this, 'ContactApi', {
45+
defaultCorsPreflightOptions: {
46+
allowOrigins: apigateway.Cors.ALL_ORIGINS,
47+
allowMethods: ['POST', 'OPTIONS'],
48+
},
49+
});
50+
api.root.addResource('contact').addMethod('POST',
51+
new apigateway.LambdaIntegration(fn));
52+
}
53+
}
54+
```
55+
56+
## Key Points
57+
58+
- SES starts in sandbox mode — you can only send to verified emails. Request production access from AWS for unrestricted sending.
59+
- Setting `resources: ['*']` for `ses:SendEmail` allows sending from any identity. For tighter security, restrict to specific identity ARNs.
60+
- Infrastructure as code with CDK means environment replication is a single `cdk deploy`. Far more reproducible than console clicking.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
layout: post
3+
title: "AWS CDKでSESメール送信Lambdaを作る"
4+
date: 2026-01-25 09:00:00 +0900
5+
categories: [Development, Tips]
6+
tags: [AWS, CDK, SES, Lambda, email, TypeScript]
7+
author: "Kevin Park"
8+
lang: ja
9+
excerpt: "AWS CDKでSESメール送信Lambdaを構成するコード。API Gateway + Lambda + SESの組み合わせをご紹介します。"
10+
---
11+
12+
## 問題
13+
14+
お問い合わせフォームからメールを送信する機能が必要ですが、専用サーバーを運用するのは負担が大きいです。AWSのサーバーレスで解決したいところです。
15+
16+
## 解決方法
17+
18+
```typescript
19+
import * as cdk from 'aws-cdk-lib';
20+
import * as lambda from 'aws-cdk-lib/aws-lambda';
21+
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
22+
import * as ses from 'aws-cdk-lib/aws-ses';
23+
import * as iam from 'aws-cdk-lib/aws-iam';
24+
25+
export class ContactApiStack extends cdk.Stack {
26+
constructor(scope: Construct, id: string) {
27+
super(scope, id);
28+
29+
new ses.EmailIdentity(this, 'Sender', {
30+
identity: ses.Identity.email('contact@example.com'),
31+
});
32+
33+
const fn = new lambda.Function(this, 'ContactFn', {
34+
runtime: lambda.Runtime.NODEJS_20_X,
35+
handler: 'index.handler',
36+
code: lambda.Code.fromAsset('lambda/contact'),
37+
});
38+
39+
fn.addToRolePolicy(new iam.PolicyStatement({
40+
actions: ['ses:SendEmail'],
41+
resources: ['*'],
42+
}));
43+
44+
const api = new apigateway.RestApi(this, 'ContactApi', {
45+
defaultCorsPreflightOptions: {
46+
allowOrigins: apigateway.Cors.ALL_ORIGINS,
47+
allowMethods: ['POST', 'OPTIONS'],
48+
},
49+
});
50+
api.root.addResource('contact').addMethod('POST',
51+
new apigateway.LambdaIntegration(fn));
52+
}
53+
}
54+
```
55+
56+
## ポイント
57+
58+
- SESは最初サンドボックスモードのため、認証済みメールにのみ送信できます。プロダクション移行にはAWSへの申請が必要です。
59+
- `ses:SendEmail`権限の`resources``'*'`にするとすべてのメールから送信可能です。セキュリティが重要な場合は、特定のidentity ARNに制限するのが良いです。
60+
- CDKでインフラをコードで管理すれば、環境複製は`cdk deploy`の1行で完了です。コンソールでクリックするよりはるかに再現性があります。
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
layout: post
3+
title: "AWS CDK로 SES 이메일 전송 Lambda 만들기"
4+
date: 2026-01-25 09:00:00 +0900
5+
categories: [Development, Tips]
6+
tags: [AWS, CDK, SES, Lambda, email, TypeScript]
7+
author: "Kevin Park"
8+
lang: ko
9+
excerpt: "AWS CDK로 SES 이메일 전송 Lambda를 구성하는 코드. API Gateway + Lambda + SES 조합."
10+
---
11+
12+
## 문제
13+
14+
문의 폼에서 이메일을 보내는 기능이 필요한데, 서버를 따로 운영하기는 부담스럽다. AWS 서버리스로 해결하고 싶다.
15+
16+
## 해결
17+
18+
```typescript
19+
// infra/lib/contact-api-stack.ts
20+
import * as cdk from 'aws-cdk-lib';
21+
import * as lambda from 'aws-cdk-lib/aws-lambda';
22+
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
23+
import * as ses from 'aws-cdk-lib/aws-ses';
24+
import * as iam from 'aws-cdk-lib/aws-iam';
25+
26+
export class ContactApiStack extends cdk.Stack {
27+
constructor(scope: Construct, id: string) {
28+
super(scope, id);
29+
30+
// SES 이메일 인증
31+
new ses.EmailIdentity(this, 'Sender', {
32+
identity: ses.Identity.email('contact@example.com'),
33+
});
34+
35+
// Lambda 함수
36+
const fn = new lambda.Function(this, 'ContactFn', {
37+
runtime: lambda.Runtime.NODEJS_20_X,
38+
handler: 'index.handler',
39+
code: lambda.Code.fromAsset('lambda/contact'),
40+
});
41+
42+
// SES 전송 권한 부여
43+
fn.addToRolePolicy(new iam.PolicyStatement({
44+
actions: ['ses:SendEmail'],
45+
resources: ['*'],
46+
}));
47+
48+
// API Gateway
49+
const api = new apigateway.RestApi(this, 'ContactApi', {
50+
defaultCorsPreflightOptions: {
51+
allowOrigins: apigateway.Cors.ALL_ORIGINS,
52+
allowMethods: ['POST', 'OPTIONS'],
53+
},
54+
});
55+
api.root.addResource('contact').addMethod('POST',
56+
new apigateway.LambdaIntegration(fn));
57+
}
58+
}
59+
```
60+
61+
## 핵심 포인트
62+
63+
- SES는 처음에 샌드박스 모드라서 인증된 이메일로만 보낼 수 있다. 프로덕션으로 전환하려면 AWS에 요청해야 한다.
64+
- `ses:SendEmail` 권한의 `resources``'*'`로 하면 모든 이메일로 보낼 수 있다. 보안이 중요하면 특정 identity ARN으로 제한하는 게 좋다.
65+
- CDK로 인프라를 코드로 관리하면 환경 복제가 `cdk deploy`한 줄이다. 콘솔에서 클릭하는 것보다 훨씬 재현 가능하다.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
layout: post
3+
title: "DynamoDB ConditionExpression for Concurrency Control"
4+
date: 2026-02-01 09:00:00 +0900
5+
categories: [Development, Tips]
6+
tags: [AWS, DynamoDB, concurrency, conditional update, race condition]
7+
author: "Kevin Park"
8+
lang: en
9+
excerpt: "Prevent race conditions in DynamoDB using ConditionExpression for atomic conditional updates."
10+
---
11+
12+
## Problem
13+
14+
Incrementing a download counter — concurrent requests can exceed the max limit. The `GET → check → UPDATE` pattern is vulnerable to race conditions.
15+
16+
## Solution
17+
18+
```typescript
19+
import { UpdateCommand } from '@aws-sdk/lib-dynamodb';
20+
21+
await docClient.send(new UpdateCommand({
22+
TableName: 'tokens',
23+
Key: { PK: `TOKEN#${token}`, SK: 'TOKEN' },
24+
UpdateExpression: 'SET downloadCount = downloadCount + :inc',
25+
ConditionExpression: 'downloadCount < :max',
26+
ExpressionAttributeValues: {
27+
':inc': 1,
28+
':max': maxDownloads,
29+
},
30+
}));
31+
// Throws ConditionalCheckFailedException if condition fails
32+
```
33+
34+
## Key Points
35+
36+
- `ConditionExpression` checks the condition before executing the update. If the condition fails, the update doesn't execute. This is atomic — race conditions are impossible.
37+
- "Read → check → write" in application code allows other requests to slip in between. Delegating the check to DynamoDB eliminates that gap.
38+
- Catch `ConditionalCheckFailedException` to return "download limit exceeded" responses. This error is a normal part of business logic.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
layout: post
3+
title: "DynamoDB ConditionExpressionで同時実行制御を行う"
4+
date: 2026-02-01 09:00:00 +0900
5+
categories: [Development, Tips]
6+
tags: [AWS, DynamoDB, concurrency, conditional update, race condition]
7+
author: "Kevin Park"
8+
lang: ja
9+
excerpt: "DynamoDBのConditionExpressionを活用して同時リクエスト時のデータ競合(race condition)を防止する方法をご紹介します。"
10+
---
11+
12+
## 問題
13+
14+
ダウンロードカウンターを増加させる際、同時に複数のリクエストが来ると最大回数を超過する可能性があります。`GET → 確認 → UPDATE`パターンはrace conditionに脆弱です。
15+
16+
## 解決方法
17+
18+
```typescript
19+
import { UpdateCommand } from '@aws-sdk/lib-dynamodb';
20+
21+
await docClient.send(new UpdateCommand({
22+
TableName: 'tokens',
23+
Key: { PK: `TOKEN#${token}`, SK: 'TOKEN' },
24+
UpdateExpression: 'SET downloadCount = downloadCount + :inc',
25+
ConditionExpression: 'downloadCount < :max',
26+
ExpressionAttributeValues: {
27+
':inc': 1,
28+
':max': maxDownloads,
29+
},
30+
}));
31+
// 条件不一致時はConditionalCheckFailedExceptionが発生
32+
```
33+
34+
## ポイント
35+
36+
- `ConditionExpression`はアップデート実行前に条件をチェックします。条件が合わなければアップデート自体が実行されません。これがアトミックなのでrace conditionは不可能です。
37+
- 「読み取り → 確認 → 書き込み」をコードで行うと、その間に他のリクエストが割り込む可能性があります。DynamoDBに条件チェックを委譲すれば、その隙間がなくなります。
38+
- `ConditionalCheckFailedException`をcatchして「ダウンロード制限超過」のレスポンスを返せます。このエラーは正常なビジネスロジックの一部です。
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
layout: post
3+
title: "DynamoDB ConditionExpression으로 동시성 제어하기"
4+
date: 2026-02-01 09:00:00 +0900
5+
categories: [Development, Tips]
6+
tags: [AWS, DynamoDB, concurrency, conditional update, race condition]
7+
author: "Kevin Park"
8+
lang: ko
9+
excerpt: "DynamoDB의 ConditionExpression을 활용해 동시 요청 시 데이터 충돌(race condition)을 방지하는 방법."
10+
---
11+
12+
## 문제
13+
14+
다운로드 카운터를 증가시키는데, 동시에 여러 요청이 들어오면 최대 횟수를 초과할 수 있다. `GET → 확인 → UPDATE` 패턴은 race condition에 취약하다.
15+
16+
## 해결
17+
18+
```typescript
19+
import { UpdateCommand } from '@aws-sdk/lib-dynamodb';
20+
21+
// 원자적 증가 + 조건부 업데이트
22+
await docClient.send(new UpdateCommand({
23+
TableName: 'tokens',
24+
Key: { PK: `TOKEN#${token}`, SK: 'TOKEN' },
25+
UpdateExpression: 'SET downloadCount = downloadCount + :inc',
26+
ConditionExpression: 'downloadCount < :max',
27+
ExpressionAttributeValues: {
28+
':inc': 1,
29+
':max': maxDownloads, // 예: 3
30+
},
31+
}));
32+
33+
// ConditionExpression 실패 시 ConditionalCheckFailedException 발생
34+
// → 이미 최대 횟수에 도달했다는 뜻
35+
```
36+
37+
## 핵심 포인트
38+
39+
- `ConditionExpression`은 업데이트를 실행하기 전에 조건을 체크한다. 조건이 맞지 않으면 업데이트 자체가 실행되지 않는다. 이게 원자적이라서 race condition이 불가능하다.
40+
- "읽기 → 확인 → 쓰기"를 코드에서 하면 그 사이에 다른 요청이 끼어들 수 있다. DynamoDB에게 조건 체크를 위임하면 그 사이가 없다.
41+
- `ConditionalCheckFailedException`을 catch해서 "다운로드 제한 초과" 같은 응답을 보내면 된다. 이 에러는 정상적인 비즈니스 로직의 일부다.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
layout: post
3+
title: "S3 Presigned URLs - Build One-Time Download Links"
4+
date: 2026-02-08 09:00:00 +0900
5+
categories: [Development, Tips]
6+
tags: [AWS, S3, presigned URL, download, security]
7+
author: "Kevin Park"
8+
lang: en
9+
excerpt: "Combine S3 presigned URLs with DynamoDB TTL to create download links with expiration and download count limits."
10+
---
11+
12+
## Problem
13+
14+
Need to provide file download links, but unlimited downloads by anyone is not acceptable. Both time limits and download count limits are required.
15+
16+
## Solution
17+
18+
```typescript
19+
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
20+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
21+
22+
const s3 = new S3Client({ region: 'ap-northeast-2' });
23+
24+
async function generateDownloadUrl(bucket: string, key: string) {
25+
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
26+
const url = await getSignedUrl(s3, command, {
27+
expiresIn: 300, // 5 minutes
28+
});
29+
return url;
30+
}
31+
32+
// Store token in DynamoDB (1-hour TTL, max 1 download)
33+
const token = crypto.randomUUID();
34+
await docClient.send(new PutCommand({
35+
TableName: 'download-tokens',
36+
Item: {
37+
PK: `TOKEN#${token}`,
38+
downloadCount: 0,
39+
maxDownloads: 1,
40+
ttl: Math.floor(Date.now() / 1000) + 3600,
41+
},
42+
}));
43+
```
44+
45+
## Key Points
46+
47+
- Presigned URLs embed signing info in the URL, enabling direct download without AWS credentials. After expiration, requests return 403.
48+
- DynamoDB TTL auto-deletes expired tokens. No cron jobs needed for cleanup.
49+
- `expiresIn` controls the URL's validity; DynamoDB TTL controls the token's validity. Separating these provides more flexibility.

0 commit comments

Comments
 (0)