AWS SAA 실습 문제: CloudFormation과 CDK를 활용한 Infrastructure as Code 설계
📋 문제 시나리오
📋 문제 시나리오
당신은 금융 서비스 회사의 클라우드 엔지니어로 근무하고 있습니다. 회사에서는 수동으로 관리되던 AWS 인프라를 Infrastructure as Code(IaC)로 전환하여 일관성, 재현성, 그리고 규정 준수를 보장하려고 합니다. 개발, 스테이징, 프로덕션 환경을 자동화된 방식으로 구축하고 관리해야 합니다.
비즈니스 요구사항
현재 상황:
- 수동 인프라 관리: 콘솔을 통한 수동 리소스 생성 및 관리
- 환경 불일치: 개발, 스테이징, 프로덕션 환경 간 차이점 존재
- 규정 준수: 금융 업계 보안 요구사항 및 감사 추적 필요
- 확장성: 새로운 서비스 출시 시 빠른 인프라 프로비저닝 필요
목표:
- 일관성 있는 환경 구성: 모든 환경에서 동일한 구성 보장
- 버전 관리: 인프라 변경 사항 추적 및 롤백 가능
- 자동화된 배포: CI/CD 파이프라인을 통한 인프라 배포
- 비용 최적화: 리소스 사용 최적화 및 비용 모니터링
- 보안 강화: 보안 모범 사례 및 규정 준수 자동화
기술적 제약사항
- 멀티 리전 지원: 재해 복구를 위한 다중 지역 배포
- 환경 격리: 개발, 스테이징, 프로덕션 환경 분리
- 네트워크 보안: VPC, 서브넷, 보안 그룹 세밀한 제어
- 모니터링: 인프라 상태 실시간 모니터링
- 백업 및 복구: 자동화된 백업 및 복구 전략
🎯 해결 방안
1. 전체 아키텍처 설계
[Git Repository] → [CodePipeline] → [CloudFormation/CDK] → [Multi-Environment AWS]
↓ ↓ ↓
[Version Control] → [CI/CD Pipeline] → [Infrastructure Deployment]
↓ ↓ ↓
[Branch Strategy] → [Testing/Validation] → [Environment Management]
2. 브랜치 전략 및 환경 관리
A. Git 브랜치 전략
Branch Strategy:
main:
- Production 환경 배포
- 모든 변경사항 리뷰 필수
- 태그를 통한 릴리스 관리
develop:
- Development 환경 배포
- 기능 개발 및 통합
- 자동 테스트 실행
staging:
- Staging 환경 배포
- 프로덕션 배포 전 최종 검증
- 성능 및 부하 테스트
feature/*:
- 기능별 브랜치
- 개발자 개인 환경 배포
- Pull Request 를 통한 코드 리뷰
B. 환경별 파라미터 관리
# parameters/dev.yaml
Parameters:
Environment: dev
VpcCidr: 10.0.0.0/16
InstanceType: t3.micro
MinSize: 1
MaxSize: 3
DesiredCapacity: 1
DatabaseInstanceType: db.t3.micro
DatabaseMultiAZ: false
# parameters/staging.yaml
Parameters:
Environment: staging
VpcCidr: 10.1.0.0/16
InstanceType: t3.small
MinSize: 2
MaxSize: 6
DesiredCapacity: 2
DatabaseInstanceType: db.t3.small
DatabaseMultiAZ: true
# parameters/prod.yaml
Parameters:
Environment: prod
VpcCidr: 10.2.0.0/16
InstanceType: m5.large
MinSize: 3
MaxSize: 12
DesiredCapacity: 3
DatabaseInstanceType: db.r5.large
DatabaseMultiAZ: true
3. CloudFormation 템플릿 구조
A. 네트워크 스택 (network.yaml)
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Network infrastructure for financial services application'
Parameters:
Environment:
Type: String
AllowedValues: [dev, staging, prod]
VpcCidr:
Type: String
Default: 10.0.0.0/16
Description: CIDR block for VPC
AvailabilityZones:
Type: CommaDelimitedList
Default: "us-east-1a,us-east-1b,us-east-1c"
Description: List of Availability Zones
Resources:
# VPC
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Sub "${Environment}-vpc"
- Key: Environment
Value: !Ref Environment
# Internet Gateway
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub "${Environment}-igw"
- Key: Environment
Value: !Ref Environment
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref VPC
# Public Subnets
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [0, !Ref AvailabilityZones]
CidrBlock: !Select [0, !Cidr [!Ref VpcCidr, 12, 8]]
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub "${Environment}-public-subnet-1"
- Key: Environment
Value: !Ref Environment
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [1, !Ref AvailabilityZones]
CidrBlock: !Select [1, !Cidr [!Ref VpcCidr, 12, 8]]
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub "${Environment}-public-subnet-2"
# Private Subnets
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [0, !Ref AvailabilityZones]
CidrBlock: !Select [2, !Cidr [!Ref VpcCidr, 12, 8]]
Tags:
- Key: Name
Value: !Sub "${Environment}-private-subnet-1"
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [1, !Ref AvailabilityZones]
CidrBlock: !Select [3, !Cidr [!Ref VpcCidr, 12, 8]]
Tags:
- Key: Name
Value: !Sub "${Environment}-private-subnet-2"
# Database Subnets
DatabaseSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [0, !Ref AvailabilityZones]
CidrBlock: !Select [4, !Cidr [!Ref VpcCidr, 12, 8]]
Tags:
- Key: Name
Value: !Sub "${Environment}-database-subnet-1"
DatabaseSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [1, !Ref AvailabilityZones]
CidrBlock: !Select [5, !Cidr [!Ref VpcCidr, 12, 8]]
Tags:
- Key: Name
Value: !Sub "${Environment}-database-subnet-2"
# NAT Gateways
NatGateway1EIP:
Type: AWS::EC2::EIP
DependsOn: InternetGatewayAttachment
Properties:
Domain: vpc
NatGateway1:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatGateway1EIP.AllocationId
SubnetId: !Ref PublicSubnet1
Tags:
- Key: Name
Value: !Sub "${Environment}-natgw-1"
# Route Tables
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub "${Environment}-public-rt"
DefaultPublicRoute:
Type: AWS::EC2::Route
DependsOn: InternetGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet1
PublicSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet2
PrivateRouteTable1:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub "${Environment}-private-rt-1"
DefaultPrivateRoute1:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable1
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway1
PrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable1
SubnetId: !Ref PrivateSubnet1
Outputs:
VPC:
Description: A reference to the created VPC
Value: !Ref VPC
Export:
Name: !Sub "${Environment}-VPC"
PublicSubnets:
Description: A list of the public subnets
Value: !Join [",", [!Ref PublicSubnet1, !Ref PublicSubnet2]]
Export:
Name: !Sub "${Environment}-PublicSubnets"
PrivateSubnets:
Description: A list of the private subnets
Value: !Join [",", [!Ref PrivateSubnet1, !Ref PrivateSubnet2]]
Export:
Name: !Sub "${Environment}-PrivateSubnets"
DatabaseSubnets:
Description: A list of the database subnets
Value: !Join [",", [!Ref DatabaseSubnet1, !Ref DatabaseSubnet2]]
Export:
Name: !Sub "${Environment}-DatabaseSubnets"
B. 보안 스택 (security.yaml)
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Security groups and IAM roles for financial services application'
Parameters:
Environment:
Type: String
AllowedValues: [dev, staging, prod]
Resources:
# Application Load Balancer Security Group
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for Application Load Balancer
VpcId: !ImportValue
Fn::Sub: "${Environment}-VPC"
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
Description: HTTP from anywhere
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Description: HTTPS from anywhere
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Description: All outbound traffic
Tags:
- Key: Name
Value: !Sub "${Environment}-alb-sg"
- Key: Environment
Value: !Ref Environment
# Application Security Group
ApplicationSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for application servers
VpcId: !ImportValue
Fn::Sub: "${Environment}-VPC"
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 8080
ToPort: 8080
SourceSecurityGroupId: !Ref ALBSecurityGroup
Description: HTTP from ALB
- IpProtocol: tcp
FromPort: 22
ToPort: 22
SourceSecurityGroupId: !Ref BastionSecurityGroup
Description: SSH from bastion
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Description: All outbound traffic
Tags:
- Key: Name
Value: !Sub "${Environment}-app-sg"
# Database Security Group
DatabaseSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for database
VpcId: !ImportValue
Fn::Sub: "${Environment}-VPC"
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 3306
ToPort: 3306
SourceSecurityGroupId: !Ref ApplicationSecurityGroup
Description: MySQL from application
- IpProtocol: tcp
FromPort: 3306
ToPort: 3306
SourceSecurityGroupId: !Ref BastionSecurityGroup
Description: MySQL from bastion
Tags:
- Key: Name
Value: !Sub "${Environment}-db-sg"
# Bastion Security Group
BastionSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for bastion host
VpcId: !ImportValue
Fn::Sub: "${Environment}-VPC"
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 10.0.0.0/8
Description: SSH from corporate network
Tags:
- Key: Name
Value: !Sub "${Environment}-bastion-sg"
# IAM Role for EC2 instances
EC2InstanceRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${Environment}-ec2-instance-role"
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
Policies:
- PolicyName: S3Access
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource: !Sub "arn:aws:s3:::${Environment}-app-bucket/*"
- PolicyName: SecretsManagerAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Environment}/database/*"
EC2InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Sub "${Environment}-ec2-instance-profile"
Roles:
- !Ref EC2InstanceRole
Outputs:
ALBSecurityGroup:
Description: Security group for ALB
Value: !Ref ALBSecurityGroup
Export:
Name: !Sub "${Environment}-ALB-SG"
ApplicationSecurityGroup:
Description: Security group for application
Value: !Ref ApplicationSecurityGroup
Export:
Name: !Sub "${Environment}-App-SG"
DatabaseSecurityGroup:
Description: Security group for database
Value: !Ref DatabaseSecurityGroup
Export:
Name: !Sub "${Environment}-DB-SG"
EC2InstanceProfile:
Description: IAM instance profile for EC2
Value: !Ref EC2InstanceProfile
Export:
Name: !Sub "${Environment}-EC2-InstanceProfile"
4. AWS CDK 구현 예제
A. CDK 스택 구조
// lib/financial-services-stack.ts
import * as cdk from '@aws-cdk/core';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2';
import * as autoscaling from '@aws-cdk/aws-autoscaling';
import * as rds from '@aws-cdk/aws-rds';
import * as s3 from '@aws-cdk/aws-s3';
import * as iam from '@aws-cdk/aws-iam';
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
export interface FinancialServicesStackProps extends cdk.StackProps {
environment: string;
vpcCidr: string;
instanceType: string;
minSize: number;
maxSize: number;
desiredCapacity: number;
databaseInstanceType: string;
databaseMultiAZ: boolean;
}
export class FinancialServicesStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props: FinancialServicesStackProps) {
super(scope, id, props);
// VPC 생성
const vpc = new ec2.Vpc(this, 'VPC', {
cidr: props.vpcCidr,
maxAzs: 3,
natGateways: props.environment === 'prod' ? 3 : 1,
subnetConfiguration: [
{
cidrMask: 24,
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
},
{
cidrMask: 24,
name: 'Database',
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
],
});
// 보안 그룹 생성
const albSecurityGroup = new ec2.SecurityGroup(this, 'ALBSecurityGroup', {
vpc,
description: 'Security group for Application Load Balancer',
allowAllOutbound: true,
});
albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(80),
'HTTP from anywhere'
);
albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(443),
'HTTPS from anywhere'
);
const appSecurityGroup = new ec2.SecurityGroup(this, 'AppSecurityGroup', {
vpc,
description: 'Security group for application servers',
allowAllOutbound: true,
});
appSecurityGroup.addIngressRule(
albSecurityGroup,
ec2.Port.tcp(8080),
'HTTP from ALB'
);
// IAM 역할 생성
const ec2Role = new iam.Role(this, 'EC2Role', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchAgentServerPolicy'),
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
],
});
// S3 버킷 생성
const appBucket = new s3.Bucket(this, 'AppBucket', {
bucketName: `${props.environment}-financial-app-bucket`,
versioned: true,
encryption: s3.BucketEncryption.S3_MANAGED,
publicReadAccess: false,
removalPolicy: props.environment === 'prod'
? cdk.RemovalPolicy.RETAIN
: cdk.RemovalPolicy.DESTROY,
});
// S3 버킷 접근 권한 추가
appBucket.grantReadWrite(ec2Role);
// Launch Template 생성
const launchTemplate = new ec2.LaunchTemplate(this, 'LaunchTemplate', {
instanceType: new ec2.InstanceType(props.instanceType),
machineImage: ec2.MachineImage.latestAmazonLinux(),
role: ec2Role,
securityGroup: appSecurityGroup,
userData: ec2.UserData.forLinux(),
});
// Auto Scaling Group 생성
const asg = new autoscaling.AutoScalingGroup(this, 'ASG', {
vpc,
launchTemplate,
minCapacity: props.minSize,
maxCapacity: props.maxSize,
desiredCapacity: props.desiredCapacity,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
},
healthCheck: autoscaling.HealthCheck.elb({
grace: cdk.Duration.minutes(5),
}),
});
// Application Load Balancer 생성
const alb = new elbv2.ApplicationLoadBalancer(this, 'ALB', {
vpc,
internetFacing: true,
securityGroup: albSecurityGroup,
vpcSubnets: {
subnetType: ec2.SubnetType.PUBLIC,
},
});
// Target Group 생성
const targetGroup = new elbv2.ApplicationTargetGroup(this, 'TargetGroup', {
port: 8080,
protocol: elbv2.ApplicationProtocol.HTTP,
vpc,
targets: [asg],
healthCheck: {
path: '/health',
interval: cdk.Duration.seconds(30),
timeout: cdk.Duration.seconds(5),
healthyThresholdCount: 2,
unhealthyThresholdCount: 3,
},
});
// Listener 생성
const listener = alb.addListener('Listener', {
port: 80,
defaultTargetGroups: [targetGroup],
});
// RDS 서브넷 그룹 생성
const dbSubnetGroup = new rds.SubnetGroup(this, 'DatabaseSubnetGroup', {
vpc,
description: 'Subnet group for RDS database',
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
});
// RDS 데이터베이스 생성
const database = new rds.DatabaseInstance(this, 'Database', {
engine: rds.DatabaseInstanceEngine.mysql({
version: rds.MysqlEngineVersion.VER_8_0,
}),
instanceType: new ec2.InstanceType(props.databaseInstanceType),
vpc,
subnetGroup: dbSubnetGroup,
multiAz: props.databaseMultiAZ,
credentials: rds.Credentials.fromGeneratedSecret('admin'),
backupRetention: cdk.Duration.days(7),
deletionProtection: props.environment === 'prod',
storageEncrypted: true,
});
// 데이터베이스 보안 그룹 설정
database.connections.allowDefaultPortFrom(
appSecurityGroup,
'Access from application'
);
// CloudWatch 알람 설정
const cpuAlarm = new cloudwatch.Alarm(this, 'CPUAlarm', {
metric: asg.metricCpuUtilization(),
threshold: 80,
evaluationPeriods: 2,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
// 스케일링 정책 설정
const scaleUpPolicy = asg.scaleOnMetric('ScaleUp', {
metric: asg.metricCpuUtilization(),
scalingSteps: [
{ upper: 70, change: +1 },
{ upper: 85, change: +2 },
{ upper: 100, change: +3 },
],
adjustmentType: autoscaling.AdjustmentType.CHANGE_IN_CAPACITY,
});
const scaleDownPolicy = asg.scaleOnMetric('ScaleDown', {
metric: asg.metricCpuUtilization(),
scalingSteps: [
{ upper: 30, change: -1 },
{ upper: 50, change: 0 },
],
adjustmentType: autoscaling.AdjustmentType.CHANGE_IN_CAPACITY,
});
// 출력 값 설정
new cdk.CfnOutput(this, 'LoadBalancerDNS', {
value: alb.loadBalancerDnsName,
description: 'DNS name of the load balancer',
});
new cdk.CfnOutput(this, 'DatabaseEndpoint', {
value: database.instanceEndpoint.hostname,
description: 'RDS database endpoint',
});
// 태그 추가
cdk.Tags.of(this).add('Environment', props.environment);
cdk.Tags.of(this).add('Project', 'FinancialServices');
cdk.Tags.of(this).add('ManagedBy', 'CDK');
}
}
B. CDK 앱 진입점
// bin/financial-services.ts
import * as cdk from '@aws-cdk/core';
import { FinancialServicesStack } from '../lib/financial-services-stack';
const app = new cdk.App();
// 환경별 설정
const environments = {
dev: {
environment: 'dev',
vpcCidr: '10.0.0.0/16',
instanceType: 't3.micro',
minSize: 1,
maxSize: 3,
desiredCapacity: 1,
databaseInstanceType: 'db.t3.micro',
databaseMultiAZ: false,
},
staging: {
environment: 'staging',
vpcCidr: '10.1.0.0/16',
instanceType: 't3.small',
minSize: 2,
maxSize: 6,
desiredCapacity: 2,
databaseInstanceType: 'db.t3.small',
databaseMultiAZ: true,
},
prod: {
environment: 'prod',
vpcCidr: '10.2.0.0/16',
instanceType: 'm5.large',
minSize: 3,
maxSize: 12,
desiredCapacity: 3,
databaseInstanceType: 'db.r5.large',
databaseMultiAZ: true,
},
};
// 환경별 스택 생성
Object.entries(environments).forEach(([env, config]) => {
new FinancialServicesStack(app, `FinancialServices-${env}`, {
...config,
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
});
});
5. CI/CD 파이프라인 구현
A. CodePipeline 설정
# pipeline/pipeline.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'CI/CD Pipeline for Infrastructure as Code'
Parameters:
GitHubOwner:
Type: String
Description: GitHub repository owner
GitHubRepo:
Type: String
Description: GitHub repository name
GitHubBranch:
Type: String
Default: main
Description: GitHub branch name
Resources:
# S3 Bucket for artifacts
ArtifactBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "${AWS::StackName}-artifacts-${AWS::AccountId}"
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
# CodePipeline Service Role
CodePipelineServiceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: codepipeline.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: CodePipelineServicePolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:GetBucketVersioning
Resource:
- !Sub "${ArtifactBucket}/*"
- !GetAtt ArtifactBucket.Arn
- Effect: Allow
Action:
- codebuild:BatchGetBuilds
- codebuild:StartBuild
Resource: "*"
- Effect: Allow
Action:
- cloudformation:CreateStack
- cloudformation:DescribeStacks
- cloudformation:DeleteStack
- cloudformation:UpdateStack
- cloudformation:CreateChangeSet
- cloudformation:ExecuteChangeSet
- cloudformation:DeleteChangeSet
- cloudformation:DescribeChangeSet
- cloudformation:SetStackPolicy
- cloudformation:ValidateTemplate
Resource: "*"
- Effect: Allow
Action:
- iam:PassRole
Resource: "*"
# CodeBuild Project for Testing
TestProject:
Type: AWS::CodeBuild::Project
Properties:
Name: !Sub "${AWS::StackName}-test"
ServiceRole: !GetAtt CodeBuildServiceRole.Arn
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:5.0
Source:
Type: CODEPIPELINE
BuildSpec: |
version: 0.2
phases:
install:
runtime-versions:
nodejs: 14
commands:
- npm install -g aws-cdk
- pip install cfn-lint
- pip install taskcat
pre_build:
commands:
- echo Logging in to Amazon ECR...
- aws --version
- cfn-lint --version
build:
commands:
- echo Build started on `date`
- echo Testing CloudFormation templates...
- cfn-lint templates/*.yaml
- echo Testing CDK application...
- cd cdk && npm install
- npm run build
- npm run test
- cdk synth
post_build:
commands:
- echo Build completed on `date`
# CodeBuild Service Role
CodeBuildServiceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: CodeBuildServicePolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: "*"
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource: !Sub "${ArtifactBucket}/*"
# CodePipeline
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: !Sub "${AWS::StackName}-pipeline"
RoleArn: !GetAtt CodePipelineServiceRole.Arn
ArtifactStore:
Type: S3
Location: !Ref ArtifactBucket
Stages:
- Name: Source
Actions:
- Name: SourceAction
ActionTypeId:
Category: Source
Owner: ThirdParty
Provider: GitHub
Version: '1'
Configuration:
Owner: !Ref GitHubOwner
Repo: !Ref GitHubRepo
Branch: !Ref GitHubBranch
OAuthToken: !Ref GitHubToken
OutputArtifacts:
- Name: SourceOutput
- Name: Test
Actions:
- Name: TestAction
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: '1'
Configuration:
ProjectName: !Ref TestProject
InputArtifacts:
- Name: SourceOutput
OutputArtifacts:
- Name: TestOutput
- Name: Deploy-Dev
Actions:
- Name: DeployDev
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: '1'
Configuration:
ActionMode: CREATE_UPDATE
StackName: financial-services-dev
TemplatePath: TestOutput::templates/main.yaml
TemplateConfiguration: TestOutput::parameters/dev.json
Capabilities: CAPABILITY_IAM
RoleArn: !GetAtt CloudFormationRole.Arn
InputArtifacts:
- Name: TestOutput
Region: !Ref AWS::Region
RunOrder: 1
B. GitHub Actions 워크플로우
# .github/workflows/infrastructure.yml
name: Infrastructure Deployment
on:
push:
branches: [main, develop, staging]
pull_request:
branches: [main]
env:
AWS_REGION: us-east-1
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: |
npm install -g aws-cdk
pip install cfn-lint taskcat
- name: Lint CloudFormation templates
run: |
cfn-lint templates/*.yaml
- name: Test CDK application
run: |
cd cdk
npm install
npm run build
npm run test
cdk synth
- name: Security scan
run: |
# Checkov 보안 스캔
pip install checkov
checkov -d templates/ --framework cloudformation
deploy-dev:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
environment: development
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Deploy to Development
run: |
aws cloudformation deploy \
--template-file templates/main.yaml \
--stack-name financial-services-dev \
--parameter-overrides file://parameters/dev.json \
--capabilities CAPABILITY_IAM \
--no-fail-on-empty-changeset
deploy-staging:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/staging'
environment: staging
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Deploy to Staging
run: |
aws cloudformation deploy \
--template-file templates/main.yaml \
--stack-name financial-services-staging \
--parameter-overrides file://parameters/staging.json \
--capabilities CAPABILITY_IAM \
--no-fail-on-empty-changeset
deploy-prod:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Deploy to Production
run: |
aws cloudformation deploy \
--template-file templates/main.yaml \
--stack-name financial-services-prod \
--parameter-overrides file://parameters/prod.json \
--capabilities CAPABILITY_IAM \
--no-fail-on-empty-changeset
6. 모니터링 및 알림 구성
A. CloudWatch 대시보드
# monitoring/dashboard.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'CloudWatch Dashboard for Infrastructure Monitoring'
Parameters:
Environment:
Type: String
AllowedValues: [dev, staging, prod]
Resources:
InfrastructureDashboard:
Type: AWS::CloudWatch::Dashboard
Properties:
DashboardName: !Sub "${Environment}-infrastructure-dashboard"
DashboardBody: !Sub |
{
"widgets": [
{
"type": "metric",
"x": 0,
"y": 0,
"width": 12,
"height": 6,
"properties": {
"metrics": [
[ "AWS/ApplicationELB", "RequestCount", "LoadBalancer", "${Environment}-alb" ],
[ ".", "TargetResponseTime", ".", "." ],
[ ".", "HTTPCode_Target_4XX_Count", ".", "." ],
[ ".", "HTTPCode_Target_5XX_Count", ".", "." ]
],
"period": 300,
"stat": "Sum",
"region": "${AWS::Region}",
"title": "ALB Metrics"
}
},
{
"type": "metric",
"x": 12,
"y": 0,
"width": 12,
"height": 6,
"properties": {
"metrics": [
[ "AWS/EC2", "CPUUtilization", "AutoScalingGroupName", "${Environment}-asg" ],
[ ".", "NetworkIn", ".", "." ],
[ ".", "NetworkOut", ".", "." ]
],
"period": 300,
"stat": "Average",
"region": "${AWS::Region}",
"title": "EC2 Metrics"
}
},
{
"type": "metric",
"x": 0,
"y": 6,
"width": 12,
"height": 6,
"properties": {
"metrics": [
[ "AWS/RDS", "CPUUtilization", "DBInstanceIdentifier", "${Environment}-database" ],
[ ".", "DatabaseConnections", ".", "." ],
[ ".", "ReadLatency", ".", "." ],
[ ".", "WriteLatency", ".", "." ]
],
"period": 300,
"stat": "Average",
"region": "${AWS::Region}",
"title": "RDS Metrics"
}
},
{
"type": "log",
"x": 12,
"y": 6,
"width": 12,
"height": 6,
"properties": {
"query": "SOURCE '/aws/lambda/financial-services-${Environment}' | fields @timestamp, @message | sort @timestamp desc | limit 20",
"region": "${AWS::Region}",
"title": "Recent Log Events"
}
}
]
}
# 알람 설정
HighCPUAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "${Environment}-high-cpu-utilization"
AlarmDescription: "Alarm when CPU exceeds 80%"
MetricName: CPUUtilization
Namespace: AWS/EC2
Statistic: Average
Period: 300
EvaluationPeriods: 2
Threshold: 80
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: AutoScalingGroupName
Value: !Sub "${Environment}-asg"
AlarmActions:
- !Ref SNSTopic
DatabaseConnectionAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "${Environment}-database-connections"
AlarmDescription: "Alarm when database connections exceed 80"
MetricName: DatabaseConnections
Namespace: AWS/RDS
Statistic: Average
Period: 300
EvaluationPeriods: 2
Threshold: 80
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: DBInstanceIdentifier
Value: !Sub "${Environment}-database"
AlarmActions:
- !Ref SNSTopic
# SNS 토픽
SNSTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: !Sub "${Environment}-infrastructure-alerts"
DisplayName: !Sub "${Environment} Infrastructure Alerts"
SNSSubscription:
Type: AWS::SNS::Subscription
Properties:
Protocol: email
TopicArn: !Ref SNSTopic
Endpoint: !Sub "${Environment}-ops@company.com"
7. 보안 및 규정 준수
A. AWS Config 규칙
# compliance/config-rules.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'AWS Config rules for compliance monitoring'
Resources:
ConfigRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: config.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/ConfigRole
Policies:
- PolicyName: ConfigBucketPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetBucketAcl
- s3:ListBucket
Resource: !GetAtt ConfigBucket.Arn
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource: !Sub "${ConfigBucket}/*"
ConfigBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "config-bucket-${AWS::AccountId}-${AWS::Region}"
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
ConfigurationRecorder:
Type: AWS::Config::ConfigurationRecorder
Properties:
Name: default
RoleARN: !GetAtt ConfigRole.Arn
RecordingGroup:
AllSupported: true
IncludeGlobalResourceTypes: true
DeliveryChannel:
Type: AWS::Config::DeliveryChannel
Properties:
Name: default
S3BucketName: !Ref ConfigBucket
# 보안 그룹 규칙 - SSH 접근 제한
SSHRestrictedRule:
Type: AWS::Config::ConfigRule
DependsOn: ConfigurationRecorder
Properties:
ConfigRuleName: ssh-restricted
Description: Checks whether security groups allow unrestricted SSH access
Source:
Owner: AWS
SourceIdentifier: INCOMING_SSH_DISABLED
# RDS 암호화 규칙
RDSEncryptionRule:
Type: AWS::Config::ConfigRule
DependsOn: ConfigurationRecorder
Properties:
ConfigRuleName: rds-storage-encrypted
Description: Checks whether RDS instances are encrypted
Source:
Owner: AWS
SourceIdentifier: RDS_STORAGE_ENCRYPTED
# S3 버킷 공개 액세스 금지
S3PublicAccessRule:
Type: AWS::Config::ConfigRule
DependsOn: ConfigurationRecorder
Properties:
ConfigRuleName: s3-bucket-public-access-prohibited
Description: Checks that S3 buckets do not allow public access
Source:
Owner: AWS
SourceIdentifier: S3_BUCKET_PUBLIC_ACCESS_PROHIBITED
# ELB 로깅 활성화
ELBLoggingRule:
Type: AWS::Config::ConfigRule
DependsOn: ConfigurationRecorder
Properties:
ConfigRuleName: elb-logging-enabled
Description: Checks whether ELB has logging enabled
Source:
Owner: AWS
SourceIdentifier: ELB_LOGGING_ENABLED
8. 재해 복구 및 백업 전략
A. 크로스 리전 백업
# backup/cross-region-backup.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Cross-region backup and disaster recovery'
Parameters:
Environment:
Type: String
AllowedValues: [dev, staging, prod]
BackupRegion:
Type: String
Default: us-west-2
Description: Region for backup storage
Resources:
# Backup Vault
BackupVault:
Type: AWS::Backup::BackupVault
Properties:
BackupVaultName: !Sub "${Environment}-backup-vault"
EncryptionKeyArn: !GetAtt BackupKey.Arn
# KMS Key for Backup
BackupKey:
Type: AWS::KMS::Key
Properties:
Description: KMS key for backup encryption
KeyPolicy:
Version: '2012-10-17'
Statement:
- Sid: Enable IAM User Permissions
Effect: Allow
Principal:
AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
Action: "kms:*"
Resource: "*"
- Sid: Allow AWS Backup
Effect: Allow
Principal:
Service: backup.amazonaws.com
Action:
- kms:Decrypt
- kms:GenerateDataKey
Resource: "*"
BackupKeyAlias:
Type: AWS::KMS::Alias
Properties:
AliasName: !Sub "alias/${Environment}-backup-key"
TargetKeyId: !Ref BackupKey
# Backup Plan
BackupPlan:
Type: AWS::Backup::BackupPlan
Properties:
BackupPlan:
BackupPlanName: !Sub "${Environment}-backup-plan"
BackupPlanRule:
- RuleName: DailyBackup
TargetBackupVault: !Ref BackupVault
ScheduleExpression: "cron(0 2 * * ? *)"
StartWindowMinutes: 60
CompletionWindowMinutes: 120
Lifecycle:
DeleteAfterDays: 30
MoveToColdStorageAfterDays: 7
RecoveryPointTags:
Environment: !Ref Environment
BackupType: Daily
- RuleName: WeeklyBackup
TargetBackupVault: !Ref BackupVault
ScheduleExpression: "cron(0 3 ? * SUN *)"
StartWindowMinutes: 60
CompletionWindowMinutes: 180
Lifecycle:
DeleteAfterDays: 90
MoveToColdStorageAfterDays: 14
CopyActions:
- DestinationBackupVaultArn: !Sub "arn:aws:backup:${BackupRegion}:${AWS::AccountId}:backup-vault:${Environment}-backup-vault-${BackupRegion}"
Lifecycle:
DeleteAfterDays: 365
MoveToColdStorageAfterDays: 30
RecoveryPointTags:
Environment: !Ref Environment
BackupType: Weekly
# Backup Role
BackupRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: backup.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup
- arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores
# Backup Selection
BackupSelection:
Type: AWS::Backup::BackupSelection
Properties:
BackupPlanId: !Ref BackupPlan
BackupSelection:
SelectionName: !Sub "${Environment}-backup-selection"
IamRoleArn: !GetAtt BackupRole.Arn
Resources:
- !Sub "arn:aws:rds:${AWS::Region}:${AWS::AccountId}:db:${Environment}-database"
- !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:volume/*"
Conditions:
StringEquals:
"aws:ResourceTag/Environment": !Ref Environment
💡 모범 사례 및 최적화
1. 스택 분할 전략
Stack Architecture:
Foundation Stack:
- VPC, Subnets, Route Tables
- Internet Gateway, NAT Gateway
- Security Groups (기본)
Security Stack:
- IAM Roles and Policies
- KMS Keys
- Secrets Manager
Database Stack:
- RDS Instance
- Database Subnet Groups
- Database Security Groups
Application Stack:
- Auto Scaling Group
- Application Load Balancer
- Launch Templates
Monitoring Stack:
- CloudWatch Dashboards
- CloudWatch Alarms
- SNS Topics
2. 비용 최적화
Cost Optimization Strategies:
Tagging Strategy:
- Environment: dev/staging/prod
- Project: ProjectName
- Owner: TeamName
- CostCenter: DepartmentCode
Resource Scheduling:
- Dev environments: Stop after hours
- Staging: Scale down during weekends
- Prod: Reserved instances for predictable workloads
Storage Optimization:
- S3 Lifecycle policies
- EBS volume optimization
- RDS storage auto-scaling
3. 보안 모범 사례
Security Best Practices:
Least Privilege Access:
- IAM roles with minimal permissions
- Resource-specific policies
- Cross-account access controls
Network Security:
- Private subnets for applications
- Security groups with specific rules
- NACLs for additional protection
Data Protection:
- Encryption at rest and in transit
- KMS key rotation
- Secrets Manager for credentials
Monitoring:
- CloudTrail for API logging
- Config for compliance monitoring
- GuardDuty for threat detection
📊 성능 및 비용 분석
1. 환경별 예상 비용
월간 예상 비용:
Development:
- EC2 (t3.micro): $8
- RDS (db.t3.micro): $12
- ALB: $16
- NAT Gateway: $32
- 총 비용: ~$70/월
Staging:
- EC2 (t3.small): $16
- RDS (db.t3.small): $24
- ALB: $16
- NAT Gateway: $32
- 총 비용: ~$90/월
Production:
- EC2 (m5.large): $70
- RDS (db.r5.large): $150
- ALB: $16
- NAT Gateway: $96 (3개)
- 총 비용: ~$350/월
2. 리소스 사용량 최적화
Resource Optimization:
Auto Scaling:
- CPU 기반 스케일링
- 예측 스케일링 (프로덕션)
- 스케줄 기반 스케일링
Database:
- Read Replica for read-heavy workloads
- Connection pooling
- Query optimization
Storage:
- GP3 volumes for better price/performance
- EBS optimization
- S3 Intelligent Tiering
🚀 실습 과제
1. 기본 구현 (1-2주)
- CloudFormation 템플릿 작성
- 네트워크 스택 구현
- 보안 스택 구현
- 애플리케이션 스택 구현
- CDK 애플리케이션 개발
- TypeScript CDK 앱 생성
- 환경별 설정 구현
- 단위 테스트 작성
2. CI/CD 파이프라인 구축 (2-3주)
- GitHub Actions 워크플로우 구현
- 브랜치별 배포 전략
- 테스트 자동화
- 보안 스캔 통합
- CodePipeline 구성
- 멀티 스테이지 파이프라인
- 승인 프로세스
- 롤백 전략
3. 모니터링 및 규정 준수 (1-2주)
- CloudWatch 모니터링
- 대시보드 구성
- 알람 설정
- 로그 집계
- AWS Config 규칙 구현
- 규정 준수 모니터링
- 자동 교정
- 보고서 생성
4. 재해 복구 구현 (2-3주)
- 백업 전략 구현
- 크로스 리전 백업
- 자동화된 복구
- 테스트 절차
- 멀티 리전 배포
- 액티브-패시브 구성
- 데이터 복제
- 장애 조치 테스트
이 포스트는 AWS SAA 시험에서 핵심적으로 다뤄지는 Infrastructure as Code 개념을 실제 금융 서비스 환경의 요구사항과 연결하여 제시했습니다. CloudFormation과 CDK를 모두 활용하여 실무에서 바로 적용할 수 있는 완전한 IaC 솔루션을 제공합니다.
관련 포스트: