Work/개발 노트
[Istio 스터디] 1주차 - Istio 소개, 첫걸음
★용호★
2025. 4. 12. 13:02
서비스 메시 소개
- 클라우드가 확산되면서 애플리케이션 설계에 있어서도 고려할 사항들이 변화하고 있음
- 컨테이너화 된 애플리케이션들은 언제든 삭제되고 재생성될 수 있음
- 네트워크는 신뢰할 수 없으므로 더 크고 더 분산된 시스템을 구축할 때는 네트워크에 대해 재시도, 타임아웃, 서킷 브레이커와 같은 네트워크 복원력이나 관찰가능성, 애플리케이션 계층은 보안 등을 신경써야함
- 네트워킹, 보안, 메트릭 수집은 애플리케이션 구현에 있어서 필수이지만 차별화 요소는 아님
- 중요한 자원인 개발자들이 네트워크 단의 설계와 구현에 많은 시간을 투자하는 것보다는 더 가치있는 본연의 업무를 수행할 수 있도록 해야함
- 서비스 메시는 이런 공통 관심사를 애플리케이션 대신 프로세스 외부에서 투명한 방식으로 구현하기 위함
- 네트워킹, 보안, 메트릭 수집과 같은 것들을 프로그래밍 언어와 프레임워크에 구애받지 않는 방식으로 구현하고 정책을 적용하는 것
- Istio는 서비스 메시의 오픈소스 구현체
- 애플리케이션 코드를 수정하지 않고도 보안, 정책 관리, 관찰 가능성과 같은 어려운 문제를 해결할 수 있고 신뢰성있고 안전한 클라우드 네이티브 시스템을 구축할 수 있도록 함
- 애플리케이션 네트워킹을 위해 아래와 같은 구현들이 필요한데 각각의 애플리케이션들이 이런 것들을 모두 구현하기에는 반복적이고 어려움
- 클라이언트 측 로드밸런싱 : 클라이언트에게 엔드포인트 목록을 제공하고 어떤 엔드포인트를 호출할지를 클라이언트가 결정
- 서비스 디스커버리 : 특정 논리적 서비스의 주기적으로 갱신되는 정상 엔드포인트 목록을 찾는 매커니즘
- 서킷 브레이커 : 오동작하는 것으로 보이는 서비스에 일정 시간 부하를 차단
- 격벽 (Bulkheading) : 서비스 호출 시 클라이언트 리소스 사용량을 명시적 임계값으로 제한 (커넥션, 스레드, 세션 등)
- 타임아웃 : 서비스 호출 시 요청, 소켓, 활성 liveness 등에 시간 제한을 적용
- 재시도 : 실패한 요청을 재시도
- 재시도 예산(Retry budgets) : 재시도에 제한을 적용. (예. 10초 동안 호출의 50%까지만 재시도 가능)
- 데드라인 : 요청에 응답 유효 기간 지정. 데드라인을 벗어나면 요청 처리를 무시
- 서비스메시 vs API Gateway
- API GW를 사용하는 경우 모든 통신이 API GW를 거쳐서 통신하기 때문에 서비스간 통신에서 네트워크 홉이 증가하고 MSA 구조에서 트래픽 량이 증가하게 되면 API GW가 받는 부하가 커지기 때문에 스케일링에 대한 고려가 필요함. 서비스 간 direct로 통신하는 것에 대한 통제가 불가능함
- istio는 모든 서비스에 Proxy가 붙어 있기 때문에 중앙에서 트래픽을 관리하는 것이 불필요해짐
- istio는 무조건 좋을까?
- 프록시에 익숙하지 않은 경우 이 영역의 동작이 블랙박스가 되어 애플리케이션 동작을 디버깅하기 어렵게 만들 수 있음
- Envoy가 디버깅하기 쉽도록 설계가 되었지만 Envoy를 사용해보지 않은 사람에게는 복잡하게 보일 수 있음
- Istio를 적용하면 또하나의 인프라 레이어가 생기는 것으로 볼 수 있기 때문에 설정에 따라 테넌시 및 격리 모델로 인한 서비스 영향이 있을 수 있음
- 복잡성이 올라가기 때문에 누가 어떻게 담당할지에 대한 R&R에 대한 문제가 발생할 수 있음. 조직 내 거버넌스 계획이 필요
- 서비스 메시가 가져다주는 이점이 크지만 그에 따른 트레이드 오프가 발생함
- 프록시에 익숙하지 않은 경우 이 영역의 동작이 블랙박스가 되어 애플리케이션 동작을 디버깅하기 어렵게 만들 수 있음
Istio 소개
- Istio는 서비스 메시(Service Mesh) 기술을 구현한 오픈소스 플랫폼
- 마이크로서비스 아키텍처에서 서비스 간 통신과 관련된 복잡성을 줄여주는 도구
- 애플리케이션 코드를 변경할 필요 없이 서비스 간 트래픽 흐름을 관리하고, 보안 정책을 적용하며, 원격 분석 데이터를 수집함
- 클라우드 기반 애플리케이션을 구성하는 다양한 마이크로서비스 간 통신 방법을 효과적으로 관리할 수 있음
- 데이터플레인
- Envoy 프록시를 사이드카 패턴으로 각 서비스에 배포
- Envoy 프록시가 마이크로서비스 간의 모든 네트워크 통신을 조정하고 제어
- 모든 인바운드 및 아웃바운드 트래픽을 가로채서 처리
- 트래픽 라우팅, 로드 밸런싱, 보안 정책 적용 등을 담당
- 모든 메시 트래픽에 대한 원격 분석을 수집하고 보고합니다
- 컨트롤플레인
- 트래픽을 라우팅하도록 프록시를 구성하고 관리
- istiod라는 프로세스를 통해 Envoy에 대한 설정 관리
- istiod 구성 요소
- 파일럿(Pilot): 모든 Envoy 사이드카에서 프록시 라우팅 규칙을 관리하며, 서비스 디스커버리와 로드 밸런싱 설정 제공
- 갤리(Galley): Istio와 쿠버네티스(TLS 연결 및 파일럿에 필요한 설정)를 연결해 주는 역할
- 서비스 메시 구성 데이터를 검증하고 변환
- 시타델(Citadel): 보안 기능을 담당하며, TLS 인증서 발급 및 관리를 통해 서비스 간 통신의 암호화 수행
Envoy Proxy 소개
- C++로 개발한 고성능 프록시 서버
- HTTP/1.1, HTTP/2, gRPC 등 지원
- L3/L4 네트워크 프록시 및 L7 기능 제공
- 구성 요소
- Cluster : envoy 가 트래픽을 포워드할 수 있는 논리적인 서비스 (엔드포인트 세트), 실제 요청이 처리되는 IP 또는 엔드포인트의 묶음을 의미
- Endpoint : IP 주소, 네트워크 노드로 클러스터로 그룹핑됨, 실제 접근이 가능한 엔드포인트를 의미. 엔드포인트가 모여서 하나의 Cluster 가 됨
- Listener : 무엇을 받을지 그리고 어떻게 처리할지 IP/Port 를 바인딩하고, 요청 처리 측면에서 다운스트림을 조정하는 역할
- Route : Listener 로 들어온 요청을 어디로 라우팅할 것인지를 정의. 라우팅 대상은 일반적으로 Cluster 라는 것에 대해 이뤄지게 됨
- Filter : Listener 로부터 서비스에 트래픽을 전달하기까지 요청 처리 파이프라인
- UpStream : envoy 요청을 포워딩해서 연결하는 백엔드 네트워크 노드 - 사이드카일때 application app, 아닐때 원격 백엔드
- DownStream : Envoy에 연결하는 엔티티, 사이드카가 아닌 모델에서는 원격 클라이언트
- Flow : downstream으로부터 들어온 트래픽을 리스너가 수신해서 HTTP router filter로 불필요한 트래픽을 차단하고 엔드포인트에 해당하는 클러스터가 받아 로직 수행
실습 환경 구성
CloudFormation으로 vscode server를 AWS에 구축하기
Resources:
VscodeServerVPC757FAB28:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
InstanceTenancy: default
Tags:
- Key: Name
Value: VscodeServerStack/VscodeServerVPC
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/Resource
VscodeServerVPCpublicSubnet1SubnetA1A0B3AA:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-2a
CidrBlock: 10.0.0.0/24
MapPublicIpOnLaunch: true
Tags:
- Key: aws-cdk:subnet-name
Value: public
- Key: aws-cdk:subnet-type
Value: Public
- Key: Name
Value: VscodeServerStack/VscodeServerVPC/publicSubnet1
VpcId:
Ref: VscodeServerVPC757FAB28
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/publicSubnet1/Subnet
VscodeServerVPCpublicSubnet1RouteTable629F63A5:
Type: AWS::EC2::RouteTable
Properties:
Tags:
- Key: Name
Value: VscodeServerStack/VscodeServerVPC/publicSubnet1
VpcId:
Ref: VscodeServerVPC757FAB28
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/publicSubnet1/RouteTable
VscodeServerVPCpublicSubnet1RouteTableAssociation8CC5347E:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: VscodeServerVPCpublicSubnet1RouteTable629F63A5
SubnetId:
Ref: VscodeServerVPCpublicSubnet1SubnetA1A0B3AA
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/publicSubnet1/RouteTableAssociation
VscodeServerVPCpublicSubnet1DefaultRoute6AE6FDA2:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: VscodeServerVPCIGW13663343
RouteTableId:
Ref: VscodeServerVPCpublicSubnet1RouteTable629F63A5
DependsOn:
- VscodeServerVPCVPCGW8B7CC360
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/publicSubnet1/DefaultRoute
VscodeServerVPCpublicSubnet1EIP590279C8:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
Tags:
- Key: Name
Value: VscodeServerStack/VscodeServerVPC/publicSubnet1
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/publicSubnet1/EIP
VscodeServerVPCpublicSubnet1NATGateway1F5F5652:
Type: AWS::EC2::NatGateway
Properties:
AllocationId:
Fn::GetAtt:
- VscodeServerVPCpublicSubnet1EIP590279C8
- AllocationId
SubnetId:
Ref: VscodeServerVPCpublicSubnet1SubnetA1A0B3AA
Tags:
- Key: Name
Value: VscodeServerStack/VscodeServerVPC/publicSubnet1
DependsOn:
- VscodeServerVPCpublicSubnet1DefaultRoute6AE6FDA2
- VscodeServerVPCpublicSubnet1RouteTableAssociation8CC5347E
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/publicSubnet1/NATGateway
VscodeServerVPCpublicSubnet2Subnet84DD90E3:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-2b
CidrBlock: 10.0.1.0/24
MapPublicIpOnLaunch: true
Tags:
- Key: aws-cdk:subnet-name
Value: public
- Key: aws-cdk:subnet-type
Value: Public
- Key: Name
Value: VscodeServerStack/VscodeServerVPC/publicSubnet2
VpcId:
Ref: VscodeServerVPC757FAB28
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/publicSubnet2/Subnet
VscodeServerVPCpublicSubnet2RouteTable740E19CB:
Type: AWS::EC2::RouteTable
Properties:
Tags:
- Key: Name
Value: VscodeServerStack/VscodeServerVPC/publicSubnet2
VpcId:
Ref: VscodeServerVPC757FAB28
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/publicSubnet2/RouteTable
VscodeServerVPCpublicSubnet2RouteTableAssociation18FBBED4:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: VscodeServerVPCpublicSubnet2RouteTable740E19CB
SubnetId:
Ref: VscodeServerVPCpublicSubnet2Subnet84DD90E3
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/publicSubnet2/RouteTableAssociation
VscodeServerVPCpublicSubnet2DefaultRouteE31A60EC:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: VscodeServerVPCIGW13663343
RouteTableId:
Ref: VscodeServerVPCpublicSubnet2RouteTable740E19CB
DependsOn:
- VscodeServerVPCVPCGW8B7CC360
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/publicSubnet2/DefaultRoute
VscodeServerVPCprivateSubnet1SubnetB33C64E0:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-2a
CidrBlock: 10.0.2.0/24
MapPublicIpOnLaunch: false
Tags:
- Key: aws-cdk:subnet-name
Value: private
- Key: aws-cdk:subnet-type
Value: Private
- Key: Name
Value: VscodeServerStack/VscodeServerVPC/privateSubnet1
VpcId:
Ref: VscodeServerVPC757FAB28
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/privateSubnet1/Subnet
VscodeServerVPCprivateSubnet1RouteTableDE4B9197:
Type: AWS::EC2::RouteTable
Properties:
Tags:
- Key: Name
Value: VscodeServerStack/VscodeServerVPC/privateSubnet1
VpcId:
Ref: VscodeServerVPC757FAB28
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/privateSubnet1/RouteTable
VscodeServerVPCprivateSubnet1RouteTableAssociationEA535B83:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: VscodeServerVPCprivateSubnet1RouteTableDE4B9197
SubnetId:
Ref: VscodeServerVPCprivateSubnet1SubnetB33C64E0
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/privateSubnet1/RouteTableAssociation
VscodeServerVPCprivateSubnet1DefaultRoute4A41E27A:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId:
Ref: VscodeServerVPCpublicSubnet1NATGateway1F5F5652
RouteTableId:
Ref: VscodeServerVPCprivateSubnet1RouteTableDE4B9197
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/privateSubnet1/DefaultRoute
VscodeServerVPCprivateSubnet2Subnet3578E158:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-2b
CidrBlock: 10.0.3.0/24
MapPublicIpOnLaunch: false
Tags:
- Key: aws-cdk:subnet-name
Value: private
- Key: aws-cdk:subnet-type
Value: Private
- Key: Name
Value: VscodeServerStack/VscodeServerVPC/privateSubnet2
VpcId:
Ref: VscodeServerVPC757FAB28
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/privateSubnet2/Subnet
VscodeServerVPCprivateSubnet2RouteTable13687399:
Type: AWS::EC2::RouteTable
Properties:
Tags:
- Key: Name
Value: VscodeServerStack/VscodeServerVPC/privateSubnet2
VpcId:
Ref: VscodeServerVPC757FAB28
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/privateSubnet2/RouteTable
VscodeServerVPCprivateSubnet2RouteTableAssociationB55E97C6:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: VscodeServerVPCprivateSubnet2RouteTable13687399
SubnetId:
Ref: VscodeServerVPCprivateSubnet2Subnet3578E158
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/privateSubnet2/RouteTableAssociation
VscodeServerVPCprivateSubnet2DefaultRoute3FB89628:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId:
Ref: VscodeServerVPCpublicSubnet1NATGateway1F5F5652
RouteTableId:
Ref: VscodeServerVPCprivateSubnet2RouteTable13687399
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/privateSubnet2/DefaultRoute
VscodeServerVPCIGW13663343:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: VscodeServerStack/VscodeServerVPC
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/IGW
VscodeServerVPCVPCGW8B7CC360:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId:
Ref: VscodeServerVPCIGW13663343
VpcId:
Ref: VscodeServerVPC757FAB28
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/VPCGW
VscodeServerVPCRestrictDefaultSecurityGroupCustomResource699DD8BC:
Type: Custom::VpcRestrictDefaultSG
Properties:
ServiceToken:
Fn::GetAtt:
- CustomVpcRestrictDefaultSGCustomResourceProviderHandlerDC833E5E
- Arn
DefaultSecurityGroupId:
Fn::GetAtt:
- VscodeServerVPC757FAB28
- DefaultSecurityGroup
Account: "482687297408"
UpdateReplacePolicy: Delete
DeletionPolicy: Delete
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerVPC/RestrictDefaultSecurityGroupCustomResource/Default
CustomVpcRestrictDefaultSGCustomResourceProviderRole26592FE0:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: lambda.amazonaws.com
ManagedPolicyArns:
- Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: Inline
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- ec2:AuthorizeSecurityGroupIngress
- ec2:AuthorizeSecurityGroupEgress
- ec2:RevokeSecurityGroupIngress
- ec2:RevokeSecurityGroupEgress
Resource:
- Fn::Join:
- ""
- - arn:aws:ec2:ap-northeast-2:482687297408:security-group/
- Fn::GetAtt:
- VscodeServerVPC757FAB28
- DefaultSecurityGroup
Metadata:
aws:cdk:path: VscodeServerStack/Custom::VpcRestrictDefaultSGCustomResourceProvider/Role
CustomVpcRestrictDefaultSGCustomResourceProviderHandlerDC833E5E:
Type: AWS::Lambda::Function
Properties:
Code:
S3Bucket: cdk-hnb659fds-assets-482687297408-ap-northeast-2
S3Key: 7fa1e366ee8a9ded01fc355f704cff92bfd179574e6f9cfee800a3541df1b200.zip
Timeout: 900
MemorySize: 128
Handler: __entrypoint__.handler
Role:
Fn::GetAtt:
- CustomVpcRestrictDefaultSGCustomResourceProviderRole26592FE0
- Arn
Runtime: nodejs20.x
Description: Lambda function for removing all inbound/outbound rules from the VPC default security group
DependsOn:
- CustomVpcRestrictDefaultSGCustomResourceProviderRole26592FE0
Metadata:
aws:cdk:path: VscodeServerStack/Custom::VpcRestrictDefaultSGCustomResourceProvider/Handler
aws:asset:path: asset.7fa1e366ee8a9ded01fc355f704cff92bfd179574e6f9cfee800a3541df1b200
aws:asset:property: Code
VscodeServerSG6CB386E7:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow traffic to VS Code Server
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
Description: Allow all outbound traffic by default
IpProtocol: "-1"
SecurityGroupIngress:
- CidrIp: 0.0.0.0/0
Description: Allow VS Code Server traffic
FromPort: 8000
IpProtocol: tcp
ToPort: 8000
VpcId:
Ref: VscodeServerVPC757FAB28
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerSG/Resource
VscodeServerSGfromVscodeServerStackAlbSGACC5D0CA8000C6700CB5:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: Allow traffic from ALB to VS Code Server
FromPort: 8000
GroupId:
Fn::GetAtt:
- VscodeServerSG6CB386E7
- GroupId
IpProtocol: tcp
SourceSecurityGroupId:
Fn::GetAtt:
- AlbSGE6D0E5D1
- GroupId
ToPort: 8000
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerSG/from VscodeServerStackAlbSGACC5D0CA:8000
AlbSGE6D0E5D1:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow HTTP traffic to ALB
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
Description: Allow all outbound traffic by default
IpProtocol: "-1"
SecurityGroupIngress:
- CidrIp: 0.0.0.0/0
Description: Allow HTTP traffic
FromPort: 80
IpProtocol: tcp
ToPort: 80
- CidrIp: 0.0.0.0/0
Description: Allow HTTPS traffic
FromPort: 443
IpProtocol: tcp
ToPort: 443
VpcId:
Ref: VscodeServerVPC757FAB28
Metadata:
aws:cdk:path: VscodeServerStack/AlbSG/Resource
VscodeServerRole26698AC1:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: ec2.amazonaws.com
Version: "2012-10-17"
ManagedPolicyArns:
- Fn::Join:
- ""
- - "arn:"
- Ref: AWS::Partition
- :iam::aws:policy/AmazonSSMManagedInstanceCore
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerRole/Resource
VscodeServerLaunchTemplateProfile9F18C67E:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- Ref: VscodeServerRole26698AC1
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerLaunchTemplate/Profile
VscodeServerLaunchTemplate2CD546F9:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateData:
IamInstanceProfile:
Arn:
Fn::GetAtt:
- VscodeServerLaunchTemplateProfile9F18C67E
- Arn
ImageId:
Ref: SsmParameterValueawsserviceamiamazonlinuxlatestal2023amikernel61x8664C96584B6F00A464EAD1953AFF4B05118Parameter
InstanceType: t3.medium
SecurityGroupIds:
- Fn::GetAtt:
- VscodeServerSG6CB386E7
- GroupId
TagSpecifications:
- ResourceType: instance
Tags:
- Key: Name
Value: VscodeServerStack/VscodeServerLaunchTemplate
- ResourceType: volume
Tags:
- Key: Name
Value: VscodeServerStack/VscodeServerLaunchTemplate
UserData:
Fn::Base64: |-
#!/bin/bash
yum update -y
yum install -y curl wget unzip git
yum install -y nodejs npm
curl -fsSL https://code-server.dev/install.sh | sh
mkdir -p ~/.config/code-server
echo "bind-addr: 0.0.0.0:8000" > ~/.config/code-server/config.yaml
echo "auth: password" >> ~/.config/code-server/config.yaml
echo "password: $(openssl rand -base64 12)" >> ~/.config/code-server/config.yaml
echo "cert: false" >> ~/.config/code-server/config.yaml
cat > /etc/systemd/system/code-server.service << EOF
[Unit]
Description=VS Code Server
After=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/bin/code-server
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl enable code-server
systemctl start code-server
TagSpecifications:
- ResourceType: launch-template
Tags:
- Key: Name
Value: VscodeServerStack/VscodeServerLaunchTemplate
DependsOn:
- VscodeServerRole26698AC1
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerLaunchTemplate/Resource
VscodeServerASG9D8D3BF1:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
LaunchTemplate:
LaunchTemplateId:
Ref: VscodeServerLaunchTemplate2CD546F9
Version:
Fn::GetAtt:
- VscodeServerLaunchTemplate2CD546F9
- LatestVersionNumber
MaxSize: "1"
MinSize: "1"
TargetGroupARNs:
- Ref: VscodeServerTargetGroup0ADA727F
VPCZoneIdentifier:
- Ref: VscodeServerVPCprivateSubnet1SubnetB33C64E0
- Ref: VscodeServerVPCprivateSubnet2Subnet3578E158
UpdatePolicy:
AutoScalingScheduledAction:
IgnoreUnmodifiedGroupSizeProperties: true
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerASG/ASG
VscodeServerALBBD8337B1:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
LoadBalancerAttributes:
- Key: deletion_protection.enabled
Value: "false"
Scheme: internet-facing
SecurityGroups:
- Fn::GetAtt:
- AlbSGE6D0E5D1
- GroupId
Subnets:
- Ref: VscodeServerVPCpublicSubnet1SubnetA1A0B3AA
- Ref: VscodeServerVPCpublicSubnet2Subnet84DD90E3
Type: application
DependsOn:
- VscodeServerVPCpublicSubnet1DefaultRoute6AE6FDA2
- VscodeServerVPCpublicSubnet1RouteTableAssociation8CC5347E
- VscodeServerVPCpublicSubnet2DefaultRouteE31A60EC
- VscodeServerVPCpublicSubnet2RouteTableAssociation18FBBED4
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerALB/Resource
VscodeServerALBHttpListener68E8857C:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn:
Ref: VscodeServerTargetGroup0ADA727F
Type: forward
LoadBalancerArn:
Ref: VscodeServerALBBD8337B1
Port: 80
Protocol: HTTP
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerALB/HttpListener/Resource
VscodeServerTargetGroup0ADA727F:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 30
HealthCheckPath: /healthz
HealthCheckTimeoutSeconds: 5
Matcher:
HttpCode: 200-499
Port: 8000
Protocol: HTTP
TargetGroupAttributes:
- Key: stickiness.enabled
Value: "false"
TargetType: instance
VpcId:
Ref: VscodeServerVPC757FAB28
Metadata:
aws:cdk:path: VscodeServerStack/VscodeServerTargetGroup/Resource
CDKMetadata:
Type: AWS::CDK::Metadata
Properties:
Analytics: v2:deflate64:H4sIAAAAAAAA/31SwW7CMAz9lt1DtlU7bMeCJlQJbRUgrpMbTAmkSZU4IFT13+dSoGyadvJ7z6/Js5tEPr++yacHOIaRWu9HRheyWRCovWDpq0GVyGZVKzHZ2FU+EXksjFaLWFikThvQ3EXCJRQGB33Q0hCc0kDa2Zu5A+9Z3pUPoCkQHuEkcq8PDIeDM0voGV8NfZILS4mzbiu0JBaootd0mnoX63OGf4XMlh5DEDOIVm2XWNUG+kw/lVZoqGQzd/1g15rZQGAV5t5ttGEXRHJBgdG2lE3KZNGT2+2/tVaggUBaGQfrAgyfxq0D7zuta17yeVkz7o3PPfTnbPf83qcDob14rviuvwRf8gqvWe5o24o5Bhe94rliIFcNlMf9u8VTH/Qa/RgCCv63SPxmSo7ffZODhwqpD/MZqY7UCuvWKHfh8ZAk8vmFX9wuaD3y0ZKuUM77+g3MyLXujgIAAA==
Metadata:
aws:cdk:path: VscodeServerStack/CDKMetadata/Default
Parameters:
SsmParameterValueawsserviceamiamazonlinuxlatestal2023amikernel61x8664C96584B6F00A464EAD1953AFF4B05118Parameter:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64
BootstrapVersion:
Type: AWS::SSM::Parameter::Value<String>
Default: /cdk-bootstrap/hnb659fds/version
Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]
Outputs:
LoadBalancerDNS:
Description: The DNS name of the load balancer
Value:
Fn::GetAtt:
- VscodeServerALBBD8337B1
- DNSName
필요한 프로그램 설치
# docker 설치
sudo yum update
sudo yum install docker -y
# kubectl 설치
curl -LO https://dl.k8s.io/release/v1.32.0/bin/linux/amd64/kubectl
chmod +x ./kubectl
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
# kind 설치
[ $(uname -m) = x86_64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.27.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
# helm 설치
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh
kind로 k8s 설치
git clone https://github.com/AcornPublishing/istio-in-action
cd istio-in-action/book-source-code-master
pwd # 각자 자신의 pwd 경로
kind create cluster --name myk8s --image kindest/node:v1.23.17 --config - <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30000 # Sample Application (istio-ingrssgateway)
hostPort: 30000
- containerPort: 30001 # Prometheus
hostPort: 30001
- containerPort: 30002 # Grafana
hostPort: 30002
- containerPort: 30003 # Kiali
hostPort: 30003
- containerPort: 30004 # Tracing
hostPort: 30004
- containerPort: 30005 # kube-ops-view
hostPort: 30005
extraMounts:
- hostPath: /root/istio-in-action/book-source-code-master
containerPath: /istiobook
networking:
podSubnet: 10.10.0.0/16
serviceSubnet: 10.200.1.0/24
EOF
# 설치 확인
docker ps
# 노드에 기본 툴 설치
docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree psmisc lsof wget bridge-utils net-tools dnsutils tcpdump ngrep iputils-ping git vim -y'
# (옵션) kube-ops-view
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set service.main.type=NodePort,service.main.ports.http.nodePort=30005 --set env.TZ="Asia/Seoul" --namespace kube-system
kubectl get deploy,pod,svc,ep -n kube-system -l app.kubernetes.io/instance=kube-ops-view
## kube-ops-view 접속 URL 확인
open "http://localhost:30005/#scale=1.5"
open "http://localhost:30005/#scale=1.3"
# (옵션) metrics-server
helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/
helm install metrics-server metrics-server/metrics-server --set 'args[0]=--kubelet-insecure-tls' -n kube-system
kubectl get all -n kube-system -l app.kubernetes.io/instance=metrics-server
istio 설치
# myk8s-control-plane 진입 후 설치 진행
docker exec -it myk8s-control-plane bash
-----------------------------------
# 코드 파일들 마운트 확인
tree /istiobook/ -L 1
# istioctl 설치
export ISTIOV=1.17.8
echo 'export ISTIOV=1.17.8' >> /root/.bashrc
curl -s -L https://istio.io/downloadIstio | ISTIO_VERSION=$ISTIOV sh -
tree istio-$ISTIOV -L 2 # sample yaml 포함
cp istio-$ISTIOV/bin/istioctl /usr/local/bin/istioctl
istioctl version --remote=false
# default 프로파일 컨트롤 플레인 배포
istioctl x precheck # 설치 전 k8s 조건 충족 검사
istioctl profile list
istioctl install --set profile=default -y
✔ Istio core installed
✔ Istiod installed
✔ Ingress gateways installed
✔ Installation complete
# 설치 확인 : istiod, istio-ingressgateway, crd 등
kubectl get istiooperators -n istio-system
NAME REVISION STATUS AGE
installed-state 4m49s
kubectl get istiooperators -n istio-system -o yaml
...
spec:
components:
base:
enabled: true
cni:
enabled: false
egressGateways:
- enabled: false
name: istio-egressgateway
ingressGateways:
- enabled: true
name: istio-ingressgateway
istiodRemote:
enabled: false
pilot:
enabled: true
hub: docker.io/istio
meshConfig:
defaultConfig:
proxyMetadata: {}
enablePrometheusMerge: true
profile: default
...
pilot:
autoscaleEnabled: true
autoscaleMax: 5
autoscaleMin: 1
configMap: true
cpu:
targetAverageUtilization: 80
deploymentLabels: null
enableProtocolSniffingForInbound: true
enableProtocolSniffingForOutbound: true
env: {}
image: pilot
keepaliveMaxServerConnectionAge: 30m
nodeSelector: {}
podLabels: {}
replicaCount: 1
traceSampling: 1
telemetry:
enabled: true
v2:
enabled: true
metadataExchange:
wasmEnabled: false
prometheus:
enabled: true
wasmEnabled: false
stackdriver:
configOverride: {}
enabled: false
logging: false
monitoring: false
topology: false
kubectl get all,svc,ep,sa,cm,secret,pdb -n istio-system
...
NAME READY STATUS RESTARTS AGE
istio-ingressgateway-58888b4f9b-gv7r9 1/1 Running 0 2m43s
istiod-78c465d86b-tsd8l 1/1 Running 0 3m
...
kubectl get crd | grep istio.io | sort
istioctl verify-install # 설치 확인
# 보조 도구 설치
kubectl apply -f istio-$ISTIOV/samples/addons
#
kubectl get pod -n istio-system
NAME READY STATUS RESTARTS AGE
grafana-67f5ccd9d7-cgkn4 1/1 Running 0 86s
istio-ingressgateway-58888b4f9b-7t5zj 1/1 Running 0 116s
istiod-78c465d86b-pvqv6 1/1 Running 0 2m14s
jaeger-78cb4f7d4b-d8b88 1/1 Running 0 86s
kiali-c946fb5bc-4njln 1/1 Running 0 86s
prometheus-7cc96d969f-6ft4s 2/2 Running 0 86s
# 빠져나오기
exit
-----------------------------------
#
kubectl get cm -n istio-system istio -o yaml
kubectl get cm -n istio-system istio -o yaml | kubectl neat
- profile을 default로 지정했기 때문에 위 테이블과 같이 istio-ingressgateway와 istiod를 설치함
- 참고로 istio 최신 버전에서는 profile 기능을 사용하지 않지만 현재 사용하고 있는 istio 버전은 오래된 버전이라 실습 내용에는 profile을 사용하고 있음
istio가 정상적으로 설치된 경우 아래와 같이 리소스가 구성됨
- istio 구성에 CRD를 사용하며 configmap에 구성된 설정에 따라 사용 도구 및 동작 방식이 정의됨 (예를들어 지표 수집에 zipkin 사용)
- CRD(Custom Resource Definition)란? 쿠버네티스 API를 확장하여 사용자 정의 리소스를 만들고 관리할 수 있게 해주는 기능. 기본적으로 제공되는 Pod, Deployment, Service 등의 리소스 외에 사용자가 직접 새로운 리소스 유형을 정의할 수 있음
- 스크립트를 통해 보조 도구를 설치하면 위 그림과 같이 grafana, kiali, jaeger, prometheus 등 사용할 도구들이 추가됨
Envoy 주입 및 설정
- Pod에 Envoy를 sidecar로 실행하기 위해 주입하는 방법은 두가지가 있음
- 실행 중인 Pod에 강제 주입
- Pod가 실행할 때 자동으로 Envoy가 같이 실행 (편리함)
kubectl create namespace istioinaction
kubectl label namespace istioinaction istio-injection=enabled
kubectl get ns --show-labels
#
kubectl get mutatingwebhookconfiguration
NAME WEBHOOKS AGE
istio-revision-tag-default 4 9m24s # 특정 revision의 사이드카 주입 설정 관리
istio-sidecar-injector 4 9m45s # Istio는 각 애플리케이션 Pod에 Envoy 사이드카 프록시를 자동으로 주입
## 네임스페이스나 Pod에 istio-injection=enabled 라벨이 있어야 작동
kubectl get mutatingwebhookconfiguration istio-sidecar-injector -o yaml
# sample 애플리케이션 추가
cat services/catalog/kubernetes/catalog.yaml
kubectl apply -f services/catalog/kubernetes/catalog.yaml -n istioinaction
cat services/webapp/kubernetes/webapp.yaml
kubectl apply -f services/webapp/kubernetes/webapp.yaml -n istioinaction
# 접속 테스트용 netshoot 파드 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: netshoot
spec:
containers:
- name: netshoot
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
EOF
# catalog 접속 확인
kubectl exec -it netshoot -- curl -s http://catalog.istioinaction/items/1 | jq
{
"id": 1,
"color": "amber",
"department": "Eyewear",
"name": "Elinor Glasses",
"price": "282.00"
}
# webapp 접속 확인 : webapp 서비스는 다른 서비스에서 데이터를 집계해 브라우저에 시각적으로 표시한다.
## 즉 webapp은 다른 백엔드 서비스의 파사드 facade 역할을 한다.
kubectl exec -it netshoot -- curl -s http://webapp.istioinaction/api/catalog/items/1 | jq
{
"id": 1,
"color": "amber",
"department": "Eyewear",
"name": "Elinor Glasses",
"price": "282.00"
}
# 샘플 애플리케이션 접속
kubectl port-forward -n istioinaction deploy/webapp 8080:8080
- 웹 페이지에 접속해보면 Webapp이 catalog 서비스로부터 정보를 가져와서 테이블로 표시해주고 있음
- Forum 서비스는 아직 실행 전이라서 정보를 가져오지 못함
# istioctl proxy-status : 단축어 ps
docker exec -it myk8s-control-plane istioctl proxy-status
docker exec -it myk8s-control-plane istioctl ps
- Envoy 프록시 상태를 살펴보면 xDS의 동기화 상태를 확인할 수 있음
- xDS(eXtensible Discovery Services)란? Istio의 동적 구성(dynamic configuration) 관리의 핵심 요소로, Envoy 프록시에 동적으로 설정 정보를 제공하는 API 및 서비스들의 집합
istio 네트워크 흐름
Gateway와 VirtualService
cat ch2/ingress-gateway.yaml
cat <<EOF | kubectl -n istioinaction apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: outfitters-gateway
namespace: istioinaction
spec:
selector:
istio: ingressgateway # use istio default controller
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: webapp-virtualservice
namespace: istioinaction
spec:
hosts:
- "*"
gateways:
- outfitters-gateway
http:
- route:
- destination:
host: webapp
port:
number: 80
EOF
#
kubectl get gw,vs -n istioinaction
NAME AGE
gateway.networking.istio.io/outfitters-gateway 126m
NAME GATEWAYS HOSTS AGE
virtualservice.networking.istio.io/webapp-virtualservice ["outfitters-gateway"] ["*"] 126m
# istio-ingressgateway 서비스 NodePort 변경 및 nodeport 30000로 지정 변경
kubectl get svc,ep -n istio-system istio-ingressgateway
kubectl patch svc -n istio-system istio-ingressgateway -p '{"spec": {"type": "NodePort", "ports": [{"port": 80, "targetPort": 8080, "nodePort": 30000}]}}'
kubectl get svc -n istio-system istio-ingressgateway
# istio-ingressgateway 서비스 externalTrafficPolicy 설정 : ClientIP 수집 확인
kubectl patch svc -n istio-system istio-ingressgateway -p '{"spec":{"externalTrafficPolicy": "Local"}}'
kubectl describe svc -n istio-system istio-ingressgateway
curl -s http://127.0.0.1:30000/api/catalog | jq
curl -s http://127.0.0.1:30000/api/catalog/items/1 | jq
curl -s http://127.0.0.1:30000/api/catalog -I | head -n 1
# webapp 반복 호출
while true; do curl -s http://127.0.0.1:30000/api/catalog/items/1 ; sleep 1; echo; done
while true; do curl -s http://127.0.0.1:30000/api/catalog -I | head -n 1 ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; echo; done
while true; do curl -s http://127.0.0.1:30000/api/catalog -I | head -n 1 ; date "+%Y-%m-%d %H:%M:%S" ; sleep 0.5; echo; done
- Istio에서 Gateway와 VirtualService는 트래픽 관리의 핵심 구성 요소로, 각자 고유한 역할을 수행하면서 함께 작동하여 효과적인 트래픽 제어를 제공함
- Gateway : 서비스 메시의 경계에서 작동하는 트래픽 입출구 관리 리소스
- 메시에 대한 인바운드/아웃바운드 트래픽을 관리하며, 어떤 트래픽이 메시에 들어오거나 나갈지를 지정
- 애플리케이션과 함께 실행되는 사이드카 Envoy 프록시가 아닌, 메시의 엣지에서 실행되는 독립적인 Envoy 프록시에 적용
- VirtualService : 실제 트래픽 라우팅 규칙을 정의하는 리소스
- Envoy 프록시에게 어떤 목적지로 어떻게 트래픽을 전달할지 알려주는 역할
- Istio 서비스 메시 내에서 서비스로 요청을 라우팅하는 방법 구성
- HTTP URI, 헤더, 포트 등 다양한 조건에 따른 라우팅 규칙 설정
- 트래픽 비중(weight) 조절. 카나리 배포, A/B 테스팅에 활용
- Gateway : 서비스 메시의 경계에서 작동하는 트래픽 입출구 관리 리소스
디테일한 Envoy 설정 확인
docker exec -it myk8s-control-plane istioctl proxy-status
NAME CLUSTER CDS LDS EDS RDS ECDS ISTIOD VERSION
catalog-6cf4b97d-nccfj.istioinaction Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-7df6ffc78d-bj7h7 1.17.8
istio-ingressgateway-996bc6bb6-mz544.istio-system Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-7df6ffc78d-bj7h7 1.17.8
webapp-7685bcb84-c55ck.istioinaction Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-7df6ffc78d-bj7h7 1.17.8
ISTIOIGW=istio-ingressgateway-996bc6bb6-647tx.istio-system
WEBAPP=webapp-7685bcb84-nfntj.istioinaction
docker exec -it myk8s-control-plane istioctl proxy-config all $ISTIOIGW
docker exec -it myk8s-control-plane istioctl proxy-config all $WEBAPP
docker exec -it myk8s-control-plane istioctl proxy-config listener $ISTIOIGW
docker exec -it myk8s-control-plane istioctl proxy-config route $ISTIOIGW
docker exec -it myk8s-control-plane istioctl proxy-config cluster $ISTIOIGW
docker exec -it myk8s-control-plane istioctl proxy-config endpoint $ISTIOIGW
docker exec -it myk8s-control-plane istioctl proxy-config log $ISTIOIGW
docker exec -it myk8s-control-plane istioctl proxy-config listener $WEBAPP
docker exec -it myk8s-control-plane istioctl proxy-config route $WEBAPP
docker exec -it myk8s-control-plane istioctl proxy-config cluster $WEBAPP
docker exec -it myk8s-control-plane istioctl proxy-config endpoint $WEBAPP
docker exec -it myk8s-control-plane istioctl proxy-config log $WEBAPP
# envoy 가 사용하고 있는 인증서 정보 확인
docker exec -it myk8s-control-plane istioctl proxy-config secret $ISTIOIGW
docker exec -it myk8s-control-plane istioctl proxy-config secret $WEBAPP
istio 관찰 가능성
- 애플리케이션에 별도의 로직을 수정하지 않더라도 서비스 메시에 대한 메트릭들을 자동으로 수집해서 추가로 설치한 Prometheus와 Grafana를 통해 손쉽게 대시보드를 구성할 수 있음
- Kiali를 사용하면 Istio 서비스 메시의 ingressgateway부터 뒷단의 애플리케이션까지의 네트워크 흐름을 시각적으로 볼 수 있음
- 아래 그림과 같이 Kiali에서 Jaeger를 사용할 수 있어서 애플리케이션에서 별도로 Trace를 남기지 않더라도 Trace 정보를 확인할 수 있음
- Envoy에 대한 설정도 Kiali에서 손쉽게 볼 수 있는데 서비스 메시가 어려운 이유 중 하나가 Envoy의 설정이 굉장히 복잡하다는 것. Config의 라인수를 보면 1만 3천줄이 넘어감
장애 상황 감지 및 해결
git으로 clone 한 디렉토리 내 bin/chaos.sh 파일을 사용하여 의도적으로 500 에러 발생 시킴
docker exec -it myk8s-control-plane bash
----------------------------------------
# istioinaction 로 네임스페이스 변경
cat /etc/kubernetes/admin.conf
kubectl config set-context $(kubectl config current-context) --namespace=istioinaction
cat /etc/kubernetes/admin.conf
cd /istiobook/bin/
./chaos.sh 500 100 # 모니터링 : kiali, grafana, tracing
./chaos.sh 500 50 # 모니터링 : kiali, grafana, tracing
- 먼저 Grafana 대시보드에서도 성공률이 점차 떨어지고, 500 에러가 증가하는 것을 확인
- 이 후 Kiali에서 트래픽의 어느 구간에 문제가 있는지 시각적으로 확인
- Jaeger의 Trace 정보를 통해 오류에 대한 상세 내용 확인
- Client가 오류가 발생하더라도 무조건 트래픽을 보내고 있기 때문에 서버는 계속 부하를 받게됨. 애플리케이션 코드 수정 없이 오류가 발생하면 간격을 두고 retry를 하도록 수정하여 서버 부하를 완화시킬 수 있음
# catalog 3번까지 요청 재시도 할 수 있고, 각 시도에는 2초의 제한 시간이 있음.
cat <<EOF | kubectl -n istioinaction apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: catalog
spec:
hosts:
- catalog
http:
- route:
- destination:
host: catalog
retries:
attempts: 3
retryOn: 5xx
perTryTimeout: 2s
EOF
kubectl get vs -n istioinaction
NAME GATEWAYS HOSTS AGE
catalog ["catalog"] 12s
webapp-virtualservice ["outfitters-gateway"] ["*"] 3h38m
- 이 후 상대적으로 오류가 발생하는 트래픽은 재시도 간격이 있기 때문에 상대적으로 트래픽이 적어지고, 오류율도 줄어듦