공부하고 기록하는, 경제학과 출신 개발자의 노트

학습일지/kubernetes

CKA 대비 kubernetes 스터디 - 8. Networking (2)

inspirit941 2022. 4. 8. 10:17
반응형

Networking Cluster Nodes

 

스크린샷 2022-04-03 오후 3 12 52



  • k8s cluster는 여러 개의 노드 - 마스터 노드, 워커 노드 - 가 network interface로 연결되어 있는 형태.
  • 각각의 노드에는 ip address가 매핑되어 있어야 하고, unique hostName이 있어야 하며, 고유한 Mac Address 값이 있어야 한다.
    • 기존 VM을 복사해서 노드를 생성할 때 특히 유의할 부분임.

스크린샷 2022-04-03 오후 3 52 00

 

스크린샷 2022-04-03 오후 3 52 18

  • kubernetes 컴포넌트를 실행하고, 컴포넌트 간 통신을 위해 열려 있어야 하는 포트들.

 

스크린샷 2022-04-03 오후 3 57 24

https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/high-availability/#steps-for-the-first-control-plane-node

Pod Networking

스크린샷 2022-04-03 오후 4 00 43



컨테이너 단위로 통신하고, 컨테이너와 호스트를 연결하는 작업 외에도 k8s에서는 pod layer Networking이 있다.

  • pod 주소는 어떻게 할당되며, 통신은 어떻게 할 것인가?
  • 클러스터 내에 있는 service를 사용해서 internal 통신, 외부 통신은 어떻게 동작하도록 만들 것인가?

가 시험에 나옴.

 

스크린샷 2022-04-03 오후 4 08 13



  • k8s에서 제공하는 빌트인 패키지가 따로 있는 건 아니고, networking solution을 사용해서 해결하면 된다.
  • k8s가 pod networking에서 요구하는 사항은 명확하다.
    • 모든 pod는 고유한 ip주소가 있어야 한다. (range / subnet 여부에 관계없이)
    • 같은 노드에 있는 pod, 다른 노드에 있는 pod과 NAT 없이 통신이 가능해야 한다.

이 기능을 제공해주는 솔루션 (ex. cilium)도 많지만, 이전까지 배운 내용을 토대로 직접 세팅해볼 예정.

같은 말을 반복하는 것 같지만, 그만큼 중요하다는 걸로 이해하면 된다.

스크린샷 2022-04-03 오후 4 08 13

 

스크린샷 2022-04-03 오후 6 04 39

  • 각 노드에 bridge network를 생성한 뒤, ip link set dev v-net-0 up 과 같은 명령어로 network를 실행한다.
  • 각 personal network는 subnet을 가지고 있어야 한다. 따라서 bridge network의 subnet range를 설정해주자.
    • 예시의 경우 10.244.1.0/24 처럼 24 range를 지정했다. ip addr add 10.244.1.1/24 dev v-net-0

스크린샷 2022-04-03 오후 6 07 28



스크린샷 2022-04-03 오후 6 57 44



아래의 작업은 컨테이너를 생성할 때마다 실행해야 하는 작업들이다. 따라서 스크립트를 만드는 편이 효율적이다.

  • virtual Cable 생성하기. ip link add ...
  • container / bridge에 각각 연결한다. ip link set ...
  • ip주소를 할당하고, route에 default gateway를 설정한다.
    • ip -n <ns> addr add ...
    • ip -n <ns> route add ...
    • ip주소 정보는 어디서 얻어와서 할당해야 하나? -> 우리가 직접 관리하거나, DB에 저장해두는 식으로.
    • 예시의 경우 10.244.1.1 / 10.244.2.1 / 10.244.3.1 (free ip in subnet)
  • ip -n <ns> link set ... 로 인터페이스를 호스트와 연결한다.

 

스크린샷 2022-04-03 오후 7 46 03



이제 같은 노드 안에서 pod끼리 통신은 가능해졌다. 다른 노드에 있는 pod과 통신하기 위해서는

  • 각 노드별로 route의 업데이트를 일일이 다 해주거나.
  • default gateway - network routing 정보를 매핑하는 table을 router에서 관리하거나. [X]

이 과정까지 끝나면, 다른 노드에 있는 pod에도 접근할 수 있는 Large Network가 생성된다.

CNI는 지금까지 manual하게 실행한 과정에서

  • net-script.sh 라는 스크립트로 컨테이너가 실행될 때 네트워크 세팅을 자동화하는 영역을 의미한다.
  • 위에서 설명한 영역은 ADD 메소드가 담당해야 할 부분이고
  • ip link del ... 명령어들의 집합인 DEL 메소드,

스크린샷 2022-04-03 오후 7 56 40



container를 생성하는 kubelet에서

  1. 실행 시점에서 --cni-conf-dir=/etc/cni/net.d 옵션에 정의된 경로에서 CNI configuration을 찾고, 스크립트 이름을 확인한다.
  2. --cni-bin-dir=/etc/cni/bin 옵션값에 설정된 디렉토리에서 sh파일을 찾는다.
  3. ADD command를 실행한다. 명령어의 옵션값으로 < container 이름>, < namespace> 를 넣고 실행한다.

CNI in Kubernetes

 

스크린샷 2022-04-03 오후 8 01 03



스크린샷 2022-04-03 오후 8 01 33



  • CNI 옵션 확인하는 방법 1. 각 노드의 kubelet.service file에서 옵션값 확인하기

 

스크린샷 2022-04-03 오후 8 07 56

  • ps -aux | grep kubelet 명령어로 kubelet 프로세스에서 옵션 확인하기.
  • /opt/cni/bin 디렉토리에서 supported cni plugin을 확인할 수 있다.
  • /etc/cni/net.d 에서 configuration file 값을 확인할 수 있다. kubelet에서 '어떤 플러그인을 사용해야 할지' 확인하는 경로.

 

스크린샷 2022-04-03 오후 8 09 21



  • /etc/cni/net.d 에서 볼 수 있는 파일의 예시.
  • 이전에 cli로 세팅했던 bridging, routing, masquerading 등의 옵션이 설정되어 있다.

CNI Solution: WeaveWorks

스크린샷 2022-04-04 오후 9 42 31

Routing table 사용해서 연결하는 작업은 클러스터 규모가 커지면 한계를 맞이함. 테이블 크기를 무한정 늘릴 수 있지도 않고, 조회에 걸리는 시간도 점점 늘어남.

 

스크린샷 2022-04-04 오후 11 32 27

Solution의 도입. WeaveWork를 예시로 들면

  • 각 노드마다 네트워크를 담당하는 weavework service가 생성되고, 이 pod끼리는 서로 통신하면서 서로의 위치를 안다.
  • pod에서 다른 pod로 요청을 보낼 때, 패킷을 중간에서 가져간 뒤 weavework 노드에서 인식할 수 있도록 wrapping한다.
  • 목적지에 도달했을 때, 해당 노드에 있는 weavework service가 unwrapping을 담당한다.

 

스크린샷 2022-04-04 오후 11 41 46

  • weaveworks service는 자체적인 bridge network를 생성한다.
    • pod는 multiple bridge에 붙을 수 있다 (docker에서 생성한 bridge / weavework에서 생성한 bridge 두 개에 전부 붙을 수 있음)
  • pod의 요청이 들어오면, weavework는 'destination 노드에 있는 weavework service'로 요청을 전달하는 역할을 수행한다.
    • packet을 intercept -> 다른 노드에 있는 pod로 보내는 요청인지 확인
    • encapsulate packet with a new one, with a new source / destination
    • 목적지에 도착하면, weavework service에서 패킷을 decapsulate / route to the right pod.

 

스크린샷 2022-04-05 오전 7 36 03

DaemonSet 형태로 설치됨 -> 모든 node에 반드시 하나의 pod는 존재하도록.

 

스크린샷 2022-04-05 오전 7 36 53

IPAM - IP Address Management (CNI)

 

스크린샷 2022-04-05 오전 8 58 58

  • 노드 내의 virtual network / ip subnet은 어떻게 결정되고 누가 관리하는가?
  • pod는 어떻게 ip주소가 할당되는가?
  • ip주소 정보는 어디에 저장되고, 중복이 없는지는 누가 체크하는가?

CNI 정의에 따르면 assigning IP to the container는 CNI Plugin이 담당해야 한다. kubernetes가 관리하는 영역이 아님.

 

스크린샷 2022-04-05 오후 7 52 19

단순하게 생각할 수 있는 방법 중 하나로, 노드의 available IP주소를 각 노드에 저장해서 사용하는 방법이 있다.

  • 노드마다 ip available list를 저장해두고
  • ip를 할당하는 스크립트에서 free ip를 찾아서 할당하는 식.
    • free ip를 찾아서 할당하는 방법으로 built-in plugin이 제공된다. DHCP와 host-local 두 가지.
    • 위 예시그림의 경우, host-local 방식을 사용해서 할당한 것.
  • 어쨌든 이 방식으로는 script에서 DHCP이건 host-local이건 사용자가 직접 invoke를 해줘야 한다.

 

스크린샷 2022-04-05 오후 7 57 46

사용자가 직접 invoke하는 대신, different kinds of plugin을 사용할 수 있도록 동적으로 스크립트를 생성할 수 있다.

  • cat /etc/cni/net.d/net-script.conf 파일의 IPAM 필드에서 확인할 수 있다.
  • subnet / routes 정보를 제공하면, 해당 플러그인 (DHCP / host-local)을 사용해서 ip 할당을 진행한다.

 

스크린샷 2022-04-05 오후 8 01 39

3rd party 프로덕트의 경우 방법이 좀 다를 수 있다. Weavework를 예로 들면

  • weave는 전체 네트워크에서 ip주소로 할당하는 디폴트 값의 범위가 10.32.0.0/12 이다.
  • 이 범위는 1,048,574개의 ip주소를 생성할 수 있고, 이 중 특정 range의 ip주소가 각 노드에 배정된다. 해당 노드의 subnet으로 pod의 ip주소를 할당하는 식으로 동작함.

Service Networking

지금까지 pod에 ip주소를 할당했지만, 실제로 pod가 다른 pod의 ip에 직접 접근하는 경우는 잘 없다.

  • 보통 service라는 k8s object를 사용해서 접근함.

 

ClusterIP

 

스크린샷 2022-04-05 오후 8 04 55

특정 노드 하나에 배포되지만, 클러스터 단위로 service에 접근이 가능함. 클러스터 외부에서는 접근 불가능한 형태의 service.

  • ex) 오렌지색 pod가 클러스터 외부에서는 접근 불가능한 DB 서비스일 경우 오랜지색 service는 ClusterIP.

 

NodePort

 

스크린샷 2022-04-05 오후 8 27 48

클러스터 내부에서도 접근 가능하고, 클러스터 외부에서도 접근할 수 있는 service.

 

학습할 내용

  • Service는 어떻게 ip를 할당받는지
  • 어떻게 service는 여러 노드에서 접근할 수 있는 구조로 되어 있는지
  • 어떻게 노드와 포트를 사용해서 외부 사용자가 접근할 수 있도록 만들 수 있는지
  • 위 작업은 누가 담당하고, 어떻게 수행하며, 어디서 확인할 수 있는지?

 

스크린샷 2022-04-05 오후 8 34 34


기본적으로 pod를 생성하는 역할은 각 노드의 Kubelet이 담당한다. - kube-apiserver에게서 요청을 받으면 pod와 service를 생성한다. - 이 과정에서 CNI를 사용하고, configuring network 과정도 진행된다.

각 노드에 있는 kube-proxy

  • kube-apiserver에게서 요청을 받는 컴포넌트
  • 노드에 새 pod 또는 service가 생성되면 동작한다.

기본적으로 service는 cluster-wide 한 개념이지만, 정확히는 'There is no server or service really listening on the IP of the service'.

  • service에는 pod와 달리 networking interface / namespace / process 와 같은 개념이 존재하지 않는다.
  • Virtual Object

 

스크린샷 2022-04-05 오후 8 50 03

Service 오브젝트를 생성하면, "ip address와 port 조합" 하나가 주어진다.

  • 각 노드의 kube-proxy는 이 ip주소+port (service에 할당된 듯 보이는 ip주소) 와 실제 pod을 연결하는 forwarding rules를 생성한다.
    • 이 주소로 들어오는 모든 트래픽은 해당 노드의 대응되는 Pod으로 전부 보내는 것.

 

스크린샷 2022-04-05 오후 9 33 09

kube-proxy가 이 forwarding rule을 생성하는 방법엔 여러 가지가 있다.

  • userspace : kube-proxy listens on a port
  • ipvs : proxies connections to the pods by rules
  • iptable : 디폴트 옵션이자 가장 익숙한 방법

--proxy-mode 옵션으로 설정해줘야 하며, 설정하지 않을 경우 iptable이 기본으로 설정된다.

 

스크린샷 2022-04-05 오후 9 34 21

  • Service를 생성했을 때 할당되는 Ip주소는 kube-api-server의 --service-cluster-ip-range 값에 따라 결정된다.
  • ps aux | grep kube-api-server 로 확인할 수 있음
  • 디폴트 범위는 10.96.0.0 ~ 10.111.255.255
    • cf. pod의 범위는 10.244.0.0 ~ 10.244.255.255
    • 범위가 어떻게 설정되건, 절대 겹치지 않는다는 게 핵심.

 

스크린샷 2022-04-05 오후 9 38 55

iptable은 아래 명령어로 확인할 수 있다.

  • iptable -L -t nat | grep db-service
  • 결과를 보면 10.103.132.104:3306 으로 오는 요청은 전부 10.244.1.2:3306 으로 포워딩하는 걸 볼 수 있다.
  • NodePort의 경우도 동일한 방식으로 iptable을 설정한다.

 

스크린샷 2022-04-05 오후 9 41 35

  • iptable을 생성할 때 kube-proxy 자체적으로도 로그를 남기는데, 로그 경로는 installation에 따라 다를 수 있다.
  • 강의의 경우 로그는 cat /var/log/kube-proxy.log 에 저장됨.

DNS in kubernetes

 

스크린샷 2022-04-05 오후 9 43 44스크린샷 2022-04-05 오후 9 48 38스크린샷 2022-04-05 오후 10 11 56

k8s 클러스터 내부에서 사용하는 built-in DNS Server가 존재한다. (설치를 바닥부터 직접 했다면, 이것도 직접 설치해야 한다)

  • service가 생성될 때, DNS server는 해당 service의 이름과 ip주소를 저장한다.
  • 이 값이 있다면, 클러스터 내에서는 service 이름만 있어도 해당 ip에 접근이 가능하다.
  • 같은 Namespace에 있다면, service 이름만으로도 통신이 가능함.
  • 서로 다른 namespace에 있다면, DNS server는 해당 namespace로 subdomain을 생성한다.
    • 위 예시의 경우, default의 pod가 app에 있는 web-service에 접근하려면 http://web-service.app 으로 접근해야 한다. namespace가 subdomain이 되는 셈.
    • namespace 말고도, 모든 service는 svc라는 또다른 subdomain으로 그룹화된다.

 

스크린샷 2022-04-05 오후 10 14 54

정확한 FQDN은 service-name.namespace.type.root -> ip address로 연결된다.

  • service일 경우 svc라는 type으로 그룹화되며, root는 디폴트값이 cluster.local이다.
  • 따라서 기본 옵션에서의 FQDN은 web-service.apps.svc.cluster.local

 

스크린샷 2022-04-05 오후 10 17 47

  • pod의 경우 ip주소의 dot을 dash로 변경하면 hostName이 된다.

CoreDNS - how kubernetes implements DNS?

 

스크린샷 2022-04-05 오후 10 19 59

DNS 서버가 왜 등장했는지를 설명할 때, 각 서버 (또는 pod)에서 직접 /etc/hosts를 관리하기 어렵기 때문이라고 했다.

  • kubernetes의 DNS도 이 원리에서 크게 벗어나지 않는다. 단지 pod가 ephemeral이라서 pod주소를 직접 관리하지 않을 뿐.
  • pod 대신 service를 관리한다고 보면 된다.

k8s 1.12버전부터는 CoreDNS를 기본으로 사용함.

 

스크린샷 2022-04-05 오후 10 32 17

  • CoreDNS는 kube-system Namespace에 pod 형태로 생성된다.
    • redundency를 위해 Deployment로 두 개의 pod가 만들어져 있음.
  • cat /etc/coredns/Corefile 에 configuration 값이 기록되어 있다.
    • 주황색으로 표시된 것들이 plugins. error / health check, monitoring metrics, cache 등이 있다.
    • 이 중 coreDNS를 kubernetes에서 구동할 수 있게 해주는 것이 kubernetes plugin. -> top level of Domain name이 만들어지는 지점.
    • 예시의 경우 cluster.local 값이 확인된다. 이게 root 값이 됨.
    • pods 옵션 : what is responsible for creating a record for Pods in cluster. pod ip주소를 변형해서 hostName 생성하는 걸 말하는데, 디폴트는 disabled.
    • proxy : 만약 pod가 google.com 같이 DNS 외부로 연결을 시도할 경우, /etc/resolv.conf 파일에 정의되어 있는 namespace로 연결해준다.
  • 이 configuration file은 kube-system ns에 configmap 형태로도 저장되어 있다. 따라서 옵션값을 변경하려면 configmap을 수정하면 된다.

 

스크린샷 2022-04-05 오후 10 40 59

CoreDNS pod도 다른 pod 요청을 받기 위해 service를 생성한다.

  • kube-dns라는 이름의 service로, kube-system namespace에 만들어져 있다.
  • 각 pod는 처음 생성될 때 /etc/resolv.conf 파일에 kube-dns의 service ip주소가 nameserver로 등록되어 있다.
  • 이런 사전작업은 전부 kubelet 이 담당한다.

 

스크린샷 2022-04-05 오후 10 42 51

그러면, web-service 라는 이름만 갖고 FQDN을 어떻게 찾아가는 걸까?

  • nameserver 말고 search로 아래 세 개의 도메인을 추가해뒀기 때문.
    • default.svc.cluster.local
    • svc.cluster.local
    • cluster.local
    • 따라서 service의 경우에는 serviceName만으로도 FQDN을 만들 수 있다.
  • 만약 pod의 hostName만으로 DNS 서버에 요청을 보낼 경우 제대로 찾아내지 못함. FQDN을 제대로 입력해야 ip주소를 확인할 수 있다.

Ingress

스크린샷 2022-04-06 오전 4 55 16스크린샷 2022-04-06 오전 5 20 28

service

  • nodeport 형태로 생성해서 http;//node-ip:38080 과 같은 형태로 외부 트래픽을 받을 수 있게 해 줌.
  • 트래픽 증가로 pod 개수가 많아질 경우 load balancing 수행.

하지만 실 서비스의 경우

  • node ip를 직접 연결하는 대신 DNS를 통해 사용자가 접근할 수 있도록 변경한다.
  • port 30000 이상의 번호를 직접 쓰는 대신, 80포트를 사용하는 프록시 서버를 사용하는 편.
  • 만약 on-prem이 아니라 AWS, GCP의 경우 service의 타입을 LoadBalancer로 설정할 수 있다.
    • 이 경우 Cloud Provider가 loadbalancer를 붙여준다.

 

스크린샷 2022-04-06 오전 5 23 50

여기서 새로운 앱이 만들어졌고, 같은 도메인을 공유한 채 path만 다르게 해서 사용한다고 가정하자.

  • ex) wear: http://domain.com/wear, video : http://domain.com/watch 같은 형태
    • 새로운 service (loadbalancer or nodeport) 를 생성하면 별도의 service ip가 만들어진다.
    • 여러 개의 service로 들어가는 트래픽을 적절히 split 하는 another load balancer가 필요해짐.
    • another load balancer에 ssl 통신을 활성화하면 https를 사용할 수도 있음.



스크린샷 2022-04-06 오전 5 27 03

Ingress

  • end user가 외부에 공개된 하나의 url로 접근했을 때, path 기준으로 같은 클러스터 내에 있는 different service에 routing해주는 역할을 담당한다.
  • ssl Security를 담당한다.

 

스크린샷 2022-04-06 오전 5 33 06

 

스크린샷 2022-04-06 오전 5 36 25

k8s에서 제공하는 Layer 7 LoadBalancer라고 이해하면 됨.

  • 물론 ingress도 하나의 service인 만큼 nodeport / loadbalancer 설정을 해줘야하지만, 여기에서만 외부 연결에 관련된 설정을 해주면 됨.
    • loadbalancing, ssl, url based routing 같은 역할을 ingress controller가 담당한다.
  • 리버스 프록시를 가능하게 하는 솔루션 - nginx, haproxy 등 - 을 배포한 다음, 이 솔루션이 적용할 configuration setting을 만들어준다.
    • 여기서 nginx같은 솔루션을 ingress controller, 해당 솔루션의 configuration을 ingress resources라고 한다.
    • ingress resources는 definition file (yaml file)로 만들어진다.
  • ingress controller는 k8s의 built-in으로 제공되지 않는다.

ingress Controller

 

스크린샷 2022-04-06 오전 5 40 33

ingress controller로 사용 가능한 솔루션들. GCE와 nginx는 k8s 프로젝트에서 maintain중인 솔루션들이다.

 

스크린샷 2022-04-06 오전 5 43 12

k8s에서 제공하는 ingress-controller 이미지를 Deployment로 사용할 때

  • args는 반드시 /nginx-ingress-controller 를 붙여야 한다.
  • nginx의 config option (error log path, keep-alive option, ssl-protocol 같은)은 configmap에 별도로 생성한다.
    • blank object로도 충분하다고 함. 만들어두는 편이 나중에 값을 수정할 때 용이해서 만드는 거라고 함.
    • --configmap=$(POD_NAMESPACE)/nginx-configuration 옵션을 넣어준다.
  • deployment가 사용하는 환경변수 - pod_name, pod_namespace - 를 등록해준다.
  • nginx가 사용할 포트인 80, 443도 등록해준다.

 

스크린샷 2022-04-06 오전 5 43 24

nginx controller가 외부로부터 요청을 받을 수 있는 service를 NodePort로 생성한다.

  • service의 port를 설정하고, 어느 deployment에 붙을 것인지 selector로 지정한다.

 

스크린샷 2022-04-06 오전 7 24 31

  • nginx controller는 필요에 따라 k8s object를 관리할 수 있는 권한이 필요하다. 따라서 이 권한을 위한 ServiceAccount를 생성해준다.
  • nginx-ingress-serviceaccount 라는 이름으로 만들고, 적절한 role / clusterRole / RoleBinding 으로 권한을 부여한다.

다시말해

  • layer 7 loadBalancer 역할을 할 nginx-ingress controller (Deployment)
  • controller가 외부로 노출될 수 있게 하는 service
  • configuration을 위한 configmap
  • 동작에 필요한 k8s 권한을 설정하는 Auth (serviceAccount)

가 기본 형태.

ingress resources

 

스크린샷 2022-04-06 오전 7 30 12

Set of rules / configurations applied on ingressController.

  • 예컨대 모든 요청을 한 종류의 deployment (pod)로 전부 forward하거나
  • routing traffic based on url path / domain name itself

 

스크린샷 2022-04-06 오전 7 31 13스크린샷 2022-04-06 오전 8 16 10

spec.backend로 이 ingress가 forwarding할 서비스는 어디인지 설정할 수 있고, 포트도 지정할 수 있다.

  • kubectl create -f ingress-wear.yml 로 리소스 생성이 가능함.

 

스크린샷 2022-04-06 오전 8 18 03

resources로 rule을 지정할 수 있다.

  • 특정 도메인을 기준으로 rule을 지정하는 게 가능함. 해당 rule에 맞게 특정 service를 호출할 수 있다.
  • 예컨대 www.my-online-store.com 도메인의 path에 따라 접근해야 할 pod를 다르게 설정할 수 있음.

 

스크린샷 2022-04-06 오전 8 21 33

위 예시처럼 ingress에서 spec.rules를 사용해서 특정 path -> 특정 backend로 넘어가도록 세팅할 수 있음.

 

스크린샷 2022-04-06 오전 8 23 14스크린샷 2022-04-06 오전 8 24 55



  • kubectl describe ingress ingress-wear-watch 로 ingress 설정을 확인할 수 있다.
  • default backend 항목: 아래의 rules에 전부 해당되지 않는 요청은 어디로 보낼 것인지를 결정하는 필드.
    • 예시의 경우 default-http-backend 라는 service로 이동한다.
    • 없는 path나 응답할 수 없는 request가 왔을 때 대응할 수 있는 적절한 메시지를 위한 용도로 사용하면 좋다.

 

스크린샷 2022-04-06 오전 8 26 04

  • request url별로 (host별로) 설정하려면 spec.rules 하위항목에 host를 지정하면 된다.
  • host를 지정하지 않았을 경우에는 기본적으로 asterisk (*), 즉 도메인 상관없이 들어오는 모든 트래픽에 적용된다.

 

스크린샷 2022-04-06 오전 9 08 07

반응형