⭐ 가시다(gasida) 님이 진행하는 AWS EKS Workshop 실습 스터디 게시글입니다.
AWS 키 페어 생성하기
- 필요한 이유: CloudFormation 으로 K8s 관련 실습환경을 배포할때 사용한다!
- 키페어 유형: RSA
- 프라이빗 키 파일 형식: .pem (with OpenSSH)
클라우드 스택 포메이션
- 필요: 기본실습환경 배포
- 링크: https://console.aws.amazon.com/cloudformation/home?region=ap-northeast-2#/stacks/new?stackName=myeks&templateURL=https:%2F%2Fs3.ap-northeast-2.amazonaws.com%2Fcloudformation.cloudneta.net%2FK8S%2Fmyeks-1week.yaml
- 설정:
- 키페어: 위에서 설정한 키페어
- SgIngressSshCidr: {네이버에 “내 ip주소 조회” 검색해서 나온 값}/32 넣기(공인아이피)
- MyInstanceType: 여기서 말하는 MyInstance 는 워커노드에 붙여저있는 작업용EC2 를 의미함(영어로는 bastion)
- 나머지는 건들지 말고 다음 → 다음 → 전송
CREATE_COMPLETE 라는 메세지가 보이면 배포완료된것이다.
- EC2 인스턴스 항목 접근: myeks-host 이름을 가진 instance 가 생성됨.
AWS EKS 구성
기본 K8s 구성 : Control Plane + Node Component
EKS?
EKS 는 K8s에서 Control Plane 을 관리해 주는 서비스이며, 이를 AWS Managed VPC 라고 구분하여 지칭한다.
또한 Autoscaling 기능을 제공해준다. EKS는 여러개의 AZ(Availablity Zone)으로 구성하여 사용한다.
EKS 로 kubectl 명령어를 사용한다면 네트워크플로우는
NLB 를 통해서 Control Plane 에 접근(앞단) → API Server 와 통신 → ELB 를 통해서 Etcd 에 접근
처럼 진행된다.
명령을 내리는 Control Plane 이 존재한다면, Pod가 작동하는 Node Component도 존재한다.
- 위에서 EKS 는 AWS Managed VPC 라고 지칭했는데, Node Component는 Your VPC(사용자, User) 라고 지칭한다.
AWS VPC의 AZ에 대응하는 Your(User) VPC의 AZ도 존재한다.
이때 하나 주의할점은, User VPC의 AZ에 AWS VPC 가 통신하기 위한 ENI인터페이스(Elastic Network Interface)가 있는데, 여기있는 ENI 의 소유권자는 User가 아닌, EKS 로 설정되어있다는것이다.
기본적으로 Node Component의 소유권은 User 이지만, 이유는 사용자가 Node Component에 직접 통신하지 않기 때문이다.
User는 EKS 가 제공하는 Kube Apiserver 에 통신하여, Kube Apiserver가 NodeComponent에 접근한다.
User 는 Node Component 를 소유하지만, Node Component에 직접 접근하지 않는다.
그러나 Node Component 의 ENI는 EKS소유이다.
따라서 EKS의 책임관리 영역을 구분해보자면,
Control Plane 은 AWS 의 영역
Node Component 는 User, AWS 의 영역으로 구분된다.
EKS 에서 API부하분산할때 NLB를 사용하는 이유, ALB를 사용하지 않는이유
ALB(Appplication Load Balancer)
ALB는 네트워크에서 Application Layer계층(7계층) 에서 작동하며, HTTP/HTTPS 트래픽을 처리한다.
일반적으로 고급라우팅, 트래픽 라우팅 등의 기능을 제공한다.
NLB(Network Load Balancer)
NLB는 네트워크에서 Transport Layer계층(4계층) 에서 작동하며, TCP,UDP, TLS 트래픽을 처리한다.
매우빠르다.
왜 NLB를 사용하는가?
- NLB 는 4계층에서 작동하여, ALB보다 낮은지연시간, 높은처리량을 제공한다.
- NLB는 TCP연결을 직접타겟해서 전달할 수 있으므로, Kubernetes API 서버의 트래픽을 효율적으로 라우팅할 수 있다.
결론: ALB보다 NLB가 조금 더 효율적으로 작동하기때문이다.
AWS CLI 자격증명 생성
- AWS IAM 사이트(https://us-east-1.console.aws.amazon.com/iam/home?region=ap-northeast-2#/users ) 에 들어가서 Administrator Policy 권한을 가진 IAM USER 생성하기
이렇게 선택하고 다음→다음 누르다보면 IAM 만들어지는데
여기서 액세스 키 만들기 선택 → 사용사례(Command Line Interface) → 선택
⭐여기서 나오는 ACCESS Key, PW 저장해두기!
하단 aws configure 할때 사용한다.
aws cli 자격증명
aws configure
#위에서 생성한 IAM 내용을 입력함
AWS Access Key ID [None]:
AWS Secret Access Key [None]:
Default region name [None]: ap-northeast-2
Default output format [None]: json
자격증명 잘 되었는지 확인하는 방법
aws ec2 describe-instances
이 명령어를 입력했을때
An error occurred (AuthFailure) when calling the DescribeInstances operation: AWS was not able to validate the provided access credentials 가 출력되면, 제대로 자격증명이 되지 않은것
여러가지 json 내용 이 출력되면, 제대로 자격증명이 된 것이다.
VPC정보확인
#vpc 정보확인
aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" | jq
#VPC ID확인 및, 빈번히 사용하는만큼 /etc/profile에 등록
export VPCID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" | jq -r .Vpcs[].VpcId)
echo "export VPCID=$VPCID" >> /etc/profile
echo $VPCID
#배포할 subnet 정보확인
aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet1" | jq
#서브넷 ID확인, /etc/profile에 등록
export PubSubnet1=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
echo "export PubSubnet1=$PubSubnet1" >> /etc/profile
echo "export PubSubnet2=$PubSubnet2" >> /etc/profile
echo $PubSubnet1
echo $PubSubnet2
EKSCTL 실습
EKS 를 배포할때 사용하는 CLI 툴이다.
# 변수 확인
echo $AWS_DEFAULT_REGION
echo $CLUSTER_NAME
echo $VPCID
echo $PubSubnet1,$PubSubnet2
모니터링 창 띄우기(동일한 서버에 접근해서)
while true; do aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text ; echo "------------------------------" ; sleep 1; done
(myeks-host 만 존재하는 상태)
배포 전 확인
- --dry-run 명령어를 사용하면 실제로 배포하지않은 상태에서, 배포되었을때의 결과값을 출력하여 확인할 수 있다.
eksctl create cluster --name $CLUSTER_NAME --region=$AWS_DEFAULT_REGION --nodegroup-name=$CLUSTER_NAME-nodegroup --node-type=t3.medium \
--node-volume-size=30 --vpc-public-subnets "$PubSubnet1,$PubSubnet2" --version 1.28 --ssh-access --external-dns-access --dry-run | yh
배포하기
eksctl create cluster --name $CLUSTER_NAME --region=$AWS_DEFAULT_REGION --nodegroup-name=$CLUSTER_NAME-nodegroup --node-type=t3.medium \
--node-volume-size=30 --vpc-public-subnets "$PubSubnet1,$PubSubnet2" --version 1.28 --ssh-access --external-dns-access --verbose 4
(배포중인 화면, 대략 15분 정도 걸림)
(배포완료된 화면, 하단 모니터링 화면에서 노드그룹 2개가 생성된 것을 확인가능하다)
(EKS 화면)
EKS 정보 확인
# 기존 설치된 명령어
kubectl krew list
# eks클러스터 정보 확인
kubectl cluster-info
Kubernetes control plane is running at https://9773F48197FF6031A83C14C2AA6D1164.gr7.ap-northeast-2.eks.amazonaws.com
CoreDNS is running at https://9773F48197FF6031A83C14C2AA6D1164.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
# 혹은 아래명령어로도 확인가능
aws eks describe-cluster --name $CLUSTER_NAME | jq -r .cluster.endpoint
이때 출력되는 control plane 주소는 EKS클러스터 화면에서도확인가능하다.
## dig 조회 : 해당 IP 소유 리소스는 어떤것일까요?
APIDNS=$(aws eks describe-cluster --name $CLUSTER_NAME | jq -r .cluster.endpoint | cut -d '/' -f 3)
dig +short $APIDNS
출력 결과물로 확인해보면
15.165.156.210 43.203.74.28
가 출력되는데, 해당 IP는 Public Endpoint 주소임을 명심해야한다
(Public IP 가 노출되어있다는건, EKS가 Public IP 로 배포되었다는 뜻이다)
# eks API 접속 시도 : 도메인 or 출력되는 ip 주소로 https://<IP>/version 외부에서도 접속 가능!
#실패함
curl -k -s $(aws eks describe-cluster --name $CLUSTER_NAME | jq -r .cluster.endpoint)
#성공
curl -k -s $(aws eks describe-cluster --name $CLUSTER_NAME | jq -r .cluster.endpoint)/version | jq
# eks 노드 그룹 정보 확인
eksctl get nodegroup --cluster $CLUSTER_NAME --name $CLUSTER_NAME-nodegroup
aws eks describe-nodegroup --cluster-name $CLUSTER_NAME --nodegroup-name $CLUSTER_NAME-nodegroup | jq
# 노드 정보 확인 : OS와 컨테이너런타임 확인
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
##result
NAME STATUS ROLES AGE VERSION INSTANCE-TYPE CAPACITYTYPE ZONE
ip-192-168-1-6.ap-northeast-2.compute.internal Ready <none> 26m v1.28.5-eks-5e0fdde t3.medium ON_DEMAND ap-northeast-2a
ip-192-168-2-219.ap-northeast-2.compute.internal Ready <none> 26m v1.28.5-eks-5e0fdde t3.medium ON_DEMAND ap-northeast-2c
# -v=6 으로 디버그 레벨을 높여서 확인
kubectl get node -v=6
##result
I0309 22:35:09.204142 19608 loader.go:395] Config loaded from file: /root/.kube/config
I0309 22:35:10.243696 19608 round_trippers.go:553] GET https://9773F48197FF6031A83C14C2AA6D1164.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/nodes?limit=500 200 OK in 1022 milliseconds
NAME STATUS ROLES AGE VERSION
ip-192-168-1-6.ap-northeast-2.compute.internal Ready <none> 29m v1.28.5-eks-5e0fdde
ip-192-168-2-219.ap-northeast-2.compute.internal Ready <none> 29m v1.28.5-eks-5e0fdde
EKS 의 Public API 값을 찔러서 가져오는데, /root/.kube/config 값을 먼저 로드한다.
해당 파일은 인증정보와 관련되어있다는 정보만 알아두면된다.
#인증정보
cat /root/.kube/config
#pod확인
kubectl get pod -A
이때 API-server, Controller, Scheduler 파드가 안보이는 이유
- Control Plane 영역은 User의 영역이아닌, AWS EKS의 영역이므로, 굳이 사용자에게 보여줄 필요가 없다.
컨테이너 정보확인
kubectl get pod -n kube-system -o wide
IP 대역에서 192.168.2.x 대역과 192.168.1.x 대역으로 구분되는데
On-premise의 쿠버네티스 IP 에서는 Worker node의 Instance IP, Pod의 IP 가 다른 대역을 사용한다.
EKS의 쿠버네티스 IP 에서는 Workder node의 Instance IP, Pod의 IP가 같은 대역을 사용한다.
- 이유 : AWS VPC CNI 때문(자세한 이유는 나중에 설명)
노드 정보
#현재 노드 정보 확인
aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
N1=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2a -o jsonpath={.items[0].status.addresses[0].address})
N2=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2c -o jsonpath={.items[0].status.addresses[0].address})
echo $N1, $N2
echo "export N1=$N1" >> /etc/profile
echo "export N2=$N2" >> /etc/profile
ping -c 1 $N1
ping -c 1 $N2
- ping 을 날렸을때 안되는 이유 : 접속가능한 보안그룹이 설정되어있지 않기때문이다.
현재 상황으로 설명
- Bastion(작업용) EC2 에는 워커노드의 관련 인증서, Kubectl, 기타 플러그인 등이 설치되어있다.
- 그러나 Bastion EC2 에는 워커노드로 ssh접근(TCP통신) 을 하기위한 보안그룹이 설정되어있지 않았다.
- 따라서 Bastion EC2에서 워커노드로 ssh 접근이 불가능한 상태였다.
# 노드 보안그룹 ID 확인
aws ec2 describe-security-groups --filters Name=group-name,Values=*nodegroup* --query "SecurityGroups[*].[GroupId]" --output text
NGSGID=$(aws ec2 describe-security-groups --filters Name=group-name,Values=*nodegroup* --query "SecurityGroups[*].[GroupId]" --output text)
echo $NGSGID
echo "export NGSGID=$NGSGID" >> /etc/profile
# 노드 보안그룹에 eksctl-host 에서 노드(파드)에 접속 가능하게 룰(Rule) 추가 설정
aws ec2 authorize-security-group-ingress --group-id $NGSGID --protocol '-1' --cidr 192.168.1.100/32
- 모든 프로토콜 접근이 가능하게 보안그룹 룰 설정
# eksctl-host 에서 노드의IP나 coredns 파드IP로 ping 테스트
ping -c 2 $N1
ping -c 2 $N2
# 워커 노드 SSH 접속
ssh -i ~/.ssh/id_rsa ec2-user@$N1 hostname
ssh -i ~/.ssh/id_rsa ec2-user@$N2 hostname
ssh ec2-user@$N1
exit
ssh ec2-user@$N2
exit
노드정보 확인
for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c addr; echo; done
노드그룹cgroup 버전확인
for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i stat -fc %T /sys/fs/cgroup/; echo; done
노드프로세스정보확인
#kubelet
for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i sudo systemctl status kubelet; echo; done
#process
for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i sudo pstree; echo; done
#detail하게 봐야할때
for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i ps afxuwww; echo; done
for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i ps axf |grep /usr/bin/containerd; echo; done
#manifests
for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i ls /etc/kubernetes/manifests/; echo; done
#kubelet
for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i ls /etc/kubernetes/kubelet/; echo; done
for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i cat /etc/kubernetes/kubelet/kubelet-config.json; echo; done
노드 스토리지 정보
for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i lsblk; echo; done
for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i df -hT /; echo; done
EKS Owned ENI
#kubelet 은 kube-proxy와 어떻게 통신할까?
for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i sudo ss -tnp; echo; done
##result
(jjongiam@myeks:N/A) [root@myeks-host ~]# for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i sudo ss -tnp; echo; done
>> node 192.168.1.6 <<
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
ESTAB 0 0 192.168.1.6:57874 15.165.156.210:443 users:(("kubelet",pid=2933,fd=14))
ESTAB 0 0 192.168.1.6:56938 52.95.195.109:443 users:(("ssm-agent-worke",pid=2467,fd=14))
ESTAB 0 0 192.168.1.6:36390 10.100.0.1:443 users:(("controller",pid=3799,fd=11))
ESTAB 0 0 192.168.1.6:59584 43.203.74.28:443 users:(("kube-proxy",pid=3147,fd=7))
ESTAB 0 0 192.168.1.6:53882 10.100.0.1:443 users:(("aws-k8s-agent",pid=3397,fd=7))
ESTAB 0 0 192.168.1.6:34254 52.95.194.54:443 users:(("ssm-agent-worke",pid=2467,fd=10))
ESTAB 0 56 192.168.1.6:22 192.168.1.100:51372 users:(("sshd",pid=19427,fd=3),("sshd",pid=19395,fd=3))
>> node 192.168.2.219 <<
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
ESTAB 0 0 192.168.2.219:22 192.168.1.100:34438 users:(("sshd",pid=19122,fd=3),("sshd",pid=19090,fd=3))
ESTAB 0 0 192.168.2.219:37714 43.203.74.28:443 users:(("kube-proxy",pid=3152,fd=7))
ESTAB 0 0 192.168.2.219:56812 15.165.156.210:443 users:(("kubelet",pid=2938,fd=24))
ESTAB 0 0 192.168.2.219:34338 52.95.195.109:443 users:(("ssm-agent-worke",pid=2486,fd=14))
ESTAB 0 0 192.168.2.219:59962 52.95.194.65:443 users:(("ssm-agent-worke",pid=2486,fd=10))
ESTAB 0 0 192.168.2.219:51266 10.100.0.1:443 users:(("controller",pid=3561,fd=11))
ESTAB 0 0 192.168.2.219:60188 10.100.0.1:443 users:(("aws-k8s-agent",pid=3404,fd=7))
kubelet과 kubeproxy 는 43.203.74.28:443, 15.165.156.210:443 에 연결되어있는데
이 값은 dig +short $APIDNS 로도 확인 가능한 값이다.
이건 EKS 퍼블릭 엔드포인트로 보이는 주소인
https://9773F48197FF6031A83C14C2AA6D1164.gr7.ap-northeast-2.eks.amazonaws.com
값이다.
아래그림에서 보이다시피 (Default 배포: Public only) 전체적인 network flow 는 3가지로 구분된다.
- EKS Cluster Endpoint - Public : 제어부 → (EKS owned ENI) 워커노드 kubelet, 워커노드 → (퍼블릭 도메인) 제어부, 사용자 kubectl → (퍼블릭 도메인) 제어부
kubelet,kube-proxy 는 API server 로 어떻게 통신하냐면
Internetgateway → Public IP 의 공인IP로 접근 → API Server
여기서 주목할 점 : Public 으로 배포한 EKS 는 kubelet, kube-proxy 도 Public IP로 연결된다.(굳이 그럴필요 없는데)
# [터미널] aws-node 데몬셋 파드 1곳에 bash 실행해두기
kubectl exec daemonsets/aws-node -it -n kube-system -c aws-eks-nodeagent -- bash
# exec 실행으로 추가된 연결 정보의 Peer Address는 어딘인가요? >> AWS 네트워크 인터페이스 ENI에서 해당 IP 정보 확인
for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i sudo ss -tnp; echo; done
...
ESTAB 0 0 [::ffff:192.168.1.129]:10250 [::ffff:192.168.1.7]:38238 users:(("kubelet",pid=2933,fd=9))
...
네트워크 인터페이스 항목 확인
네트워크 인터페이스의 소유자, 요청자ID가 다른것을 확인이 가능하다.
여기서 알아야하는 것(통신경로)
- kubectl exec, log 로 확인할때의 흐름: User - Public IP - API server - EKS owned ENI - pod
- 네트워크 인터페이스 소유자 : User
- 네트워크 인터페이스 요청자, 인스턴스 소유자 : AWS EKS
- ref: https://kubernetes.io/docs/concepts/architecture/control-plane-node-communication
만약 EKS Endpoint 를 Public, Private 으로 설정한다면
- EKS Cluster Endpoint - Public Private : 제어부 → (EKS owned ENI) 워커노드 kubelet, 워커노드 → (프라이빗 도메인, EKS owned ENI) 제어부, 사용자 kubectl → (퍼블릭 도메인) 제어부
worker node 에서 API Server 로 연결하는 플로우가
before: worker node(kubelet, kube-proxy) → Internal gateway → API Server
after: worker node(kubelet, kube-proxy) → EKS owned ENI
이때의 핵심은 Workernode → Control Plane 으로 접근할때는 PRIVATE 설정 되었지만
사용자가 kubectl 로 접근할때에는 PRIVATE 로 접근한다는 것이다.
만약 EKS Endpoint 를 PRIVATE으로 설정한다면
- EKS Cluster Endpoint - Private : 제어부 → (EKS owned ENI) 워커노드 kubelet, 워커노드,사용자 kubectl → (프라이빗 도메인, EKS owned ENI) 제어부
kubectl 도 EKS owned ENI 로 통신할 수 있게된다.
선언형 실습
# 터미널1 (모니터링)
watch -d 'kubectl get pod'
# 터미널2
# Deployment 배포(Pod 3개)
kubectl create deployment my-webs --image=gcr.io/google-samples/kubernetes-bootcamp:v1 --replicas=3
kubectl get pod -w
# 파드 증가 및 감소
kubectl scale deployment my-webs --replicas=6 && kubectl get pod -w
kubectl scale deployment my-webs --replicas=3
kubectl get pod
# 강제로 파드 삭제 : 바라는상태 + 선언형에 대한 대략적인 확인! ⇒ 어떤 일이 벌어지는가?
kubectl delete pod --all && kubectl get pod -w
kubectl get pod
# 실습 완료 후 Deployment 삭제
kubectl delete deploy my-webs
마리오 배포 테스트
# 터미널1 (모니터링)
watch -d 'kubectl get pod,svc'
# 수퍼마리오 디플로이먼트 배포
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/1/mario.yaml
kubectl apply -f mario.yaml
cat mario.yaml | yh
# 배포 확인 : CLB 배포 확인
kubectl get deploy,svc,ep mario
# 마리오 게임 접속 : CLB 주소로 웹 접속
kubectl get svc mario -o jsonpath={.status.loadBalancer.ingress[0].hostname} | awk '{ print "Maria URL = http://"$1 }'
⭐실습 종료 후 리소스제거
#EKS 제거
eksctl delete cluster --name $CLUSTER_NAME
#CloudFormation제거
aws cloudformation delete-stack --stack-name myeks
'외부활동' 카테고리의 다른 글
[AEWS] 2기 스터디: EKS Storage 에 대해서 (1) | 2024.03.24 |
---|---|
[AEWS] 2기 스터디: AWS VPC + ALB (3) | 2024.03.17 |
코딩테스트 준비 플랫폼 추천: 코드트리 (0) | 2024.03.03 |
[DOIK2] 스터디: Stackable 로 Airflow 배포하기 + 스터디후기 (3) | 2023.11.26 |
[DOIK2] 스터디: Strimzi로 Kafka Cluster 배포하기 (0) | 2023.11.18 |