OpenShift에서 제공하는 CoreDNS Operator의 기능적 한계에 대한 자체 구현이 주 목적이며,
CoreDNS Container 이미지를 통해 외부 질의에 대한 것들을 제어 하도록 한다.
이 글을 통해 Production 레벨에서 적용 후 문제가 발생하는 부분에 대해서는 책임을 지지 않는다.
- Bastion VM (NAT)
- RHEL 8.x
- Podman / SKOPEO / CRI-O
OpenShift를 외부와 단절된 네트워크 환경(disconnected)에서 구성하는 경우
외부 도메인에 대한 질의(Query)가 허용되지 않는다.
이 문제를 해결하기 위해서는 별도 Upstream DNS를 통해 질의가 될 수 있도록 설정해야 한다.
여기서 Upstream DNS라는 것은 외부 도메인에 대한 질의가 가능한 DNS 서버를 의미한다.
OpenShift에서 제공하는 CoreDNS는 Forwarding 기능만을 제공한다.
즉, 별도의 DNS 서버가 존재하고 이를 가져다 사용할 수 밖에 없다는 것이다.
Operator에서는 아래와 같이 Forwarding 기능만 사용 가능하기 때문에, 별도의 DNS Record를 추가할 수 없는 한계가 발생한다.
[root@bastion ~]# oc edit dns.operator/default
apiVersion: operator.openshift.io/v1
kind: DNS
metadata:
name: default
spec:
servers:
- name: test-dns
zones:
- test.com
forwardPlugin:
upstreams:
- 192.168.0.2
- 192.169.0.2:5353
- name: redhat-dns
zones:
- redhat.io
- redhat.com
forwardPlugin:
upstreams:
- 8.8.8.8
- 8.8.4.4
[1]: GitHub - CoreDNS Opearator CRD Source
개인이나 소규모 환경에서는 DNS 서버에 관련 도메인을 별도로 만들어 직접 제공해주면 된다.
다만, 이렇게 관리가 되는 경우에는 DNS 서버에 추가 요청사항이 있는 경우 관리 주체가 분명하지 않다.
따라서, 조직이 큰 기업에서는 대부분 단계별(waterfall) 적용 방식으로 일 처리가 진행 되므로
요구사항이 수시로 변경되면 변경에 대한 이유, 작업, 완료까지의 시간이 상당히 많이 지체 된다.
이렇게 잦은 요구사항이 변경되고 시간이 지체되다 보면, 협업 분위기가 상당히 까칠하게 변경될 수 있다.
그래서, Self Upstream DNS Pod를 통해 OpenShift 클러스터 내에서 처리를 하고자 한다.
자체 구현하는 Container 이미지는 CoreDNS이며 Daemonset으로 동작하도록 설계 되었다.
아래는 기본적인 Kubernetes에서의 Pod 배포에 대한 Scheduling workflow 이다.
(DaemonSet은 Label을 기반으로 모든 노드에 스케줄러가 배포하는 방식이나, 기본적인 Pod 배포 절차와 같다.)
Self Upstream DNS는 CoreDNS를 Container 이미지 형태로 만들어 DaemonSet Pod로 배포하여 구성한다.
이 Pod는 인프라 환경에서 최소한의 변경과 격리, 성능, 효율성에 중점을 두고 설계 되었다.
- DNS 서버를 만들 필요가 없다.
- 방화벽 구성이 필요없다.
- DNS 질의 요청은 노드의 로컬호스트로 내부에서 처리 된다.
- DaemonSet 배포 전략으로 노드가 추가/삭제 되어도 Pod가 자동 관리가 된다.
- 외부 레지스트리로 태깅 되어있는 Container 이미지들에 대해서
수작업으로 Image Contents Policy를 별도로 추가 작업을 하지 않아도 된다. - ConfigMap/DaemonSet 수정을 통하여 도메인 Zone 파일 생성 및 record 종류를 실시간으로 추가/삭제가 가능하다.
- Primary DNS 서버에 없는 요청은 CoreDNS Pod로 내부에서, 처리하기 때문에 외부로 트래픽이 나갈 이유가 없다.
- CoreDNS 이미지 사이즈는 50Mbyte 이하이며, Pod가 사용하는 CPU 및 Memory 리소스 소비량은 아주 적게 사용 된다.
CoreDNS를 빌드하기 위해서 사용되는 패키지를 설치 한다.
[root@bastion ~]# dnf group install "Development Tools"
[root@bastion ~]# dnf install podman skopeo
Golang 1.12+ 버전 이상으로 구성한다.
[root@bastion ~]# curl -LO https://golang.org/dl/go1.16.3.linux-amd64.tar.gz
[root@bastion ~]# tar -xzvf go1.16.3.linux-amd64.tar.gz -C /opt
[root@bastion ~]# vi /etc/profile
# Go Language
export GO_DIR=/opt
export GO_HOME=$GO_DIR/go
export GOPATH=$GO_DIR/gopath
export PATH=$PATH:$GO_HOME/bin
[root@bastion ~]# git clone https://github.com/coredns/coredns.git /opt/coredns/
[root@bastion ~]# cd /opt/coredns
[root@bastion ~]# make
Dockerfile에서 scratch 이미지를 사용하여 Layer 없는 구조로 CoreDNS Container 이미지를 만든다.
DNS 서비스 포트는 TCP/53, UDP/53번이 오픈이 된다.
[root@bastion ~]# mkdir /opt/dockerfile
[root@bastion ~]# vi /opt/dockerfile/Dockerfile
FROM scratch
COPY coredns /usr/bin/coredns
EXPOSE 53 53/udp
ENTRYPOINT ["/usr/bin/coredns"]
컴파일 된 CoreDNS Binary 파일을 Dockerfile이 존재하는 위치에 복사 후 podman을 통해 Container 이미지를 빌드한다.
[root@bastion ~]# cp /opt/coredns/coredns /opt/dockerfile/
[root@bastion ~]# cd /opt/dockerfile/
[root@bastion ~]# podman build -t registry.ybkim.ocp4.local/coredns/insecure-registry-only:latest .
생성한 Container 이미지를 Private Registry 서버로 업로드 한다.
[root@bastion ~]# podman push --tls-verify=false registry.ybkim.ocp4.local/coredns/insecure-registry-only:latest
Redhat 및 커뮤니티에 사용되는 외부 레지스트리 주소 태깅을 가진 모든 도메인들에 대해서,
내부에서 사용하는 Private Registry 주소로 CNAME을 설정하였다.
즉, 관련 도메인으로 오는 모든 요청은 내부 Private Registry 주소로 요청 된다.
[root@bastion ~]# vi /opt/coredns-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
labels:
coredns: insecure-registry-only
name: coredns-config
namespace: openshift-dns
data:
Corefile: |
.:53 {
errors
log
health
file /opt/quay.io.db quay.io
file /opt/redhat.io.db redhat.io
file /opt/redhat.com.db redhat.com
file /opt/docker.io.db docker.io
file /opt/gcr.io.db gcr.io
file /opt/nvcr.io.db nvcr.io
file /opt/elastic.co.db elastic.co
cache 30
reload
}
quay.io.db: |
$ORIGIN quay.io.
@ IN SOA quay.io. admin.quay.io. 20210101 21600 3600 604800 86400
IN A 192.168.0.2
* IN CNAME quay.io.
redhat.io.db: |
$ORIGIN redhat.io.
@ IN SOA redhat.io. admin.redhat.io. 20210101 21600 3600 604800 86400
IN A 192.168.0.2
* IN CNAME redhat.io.
docker.io.db: |
$ORIGIN docker.io.
@ IN SOA docker.io. admin.docker.io. 20210101 21600 3600 604800 86400
IN A 192.168.0.2
* IN CNAME docker.io.
gcr.io.db: |
$ORIGIN gcr.io.
@ IN SOA gcr.io. admin.gcr.io. 20210101 21600 3600 604800 86400
IN A 192.168.0.2
* IN CNAME gcr.io.
nvcr.io.db: |
$ORIGIN nvcr.io.
@ IN SOA nvcr.io. admin.nvcr.io. 20210101 21600 3600 604800 86400
IN A 192.168.0.2
* IN CNAME nvcr.io.
elastic.co.db: |
$ORIGIN elastic.co.
@ IN SOA elastic.co. admin.elastic.co. 20210101 21600 3600 604800 86400
IN A 192.168.0.2
* IN CNAME elastic.co.
redhat.com.db: |
$ORIGIN redhat.com.
@ IN SOA redhat.com. admin.redhat.com. 20210101 21600 3600 604800 86400
IN A 192.168.0.2
* IN CNAME redhat.com.
생성된 ConfigMap을 명령어를 통해 생성한다.
[root@bastion ~]# oc create -f /opt/coredns-configmap.yaml
[2]: Custom DNS Entries For Kubernetes
앞서 만들었던 ConfigMap을 DaemonSet에 포함하여 해당 Zone 파일 DB가 마운트 되어 사용될 수 있도록 만든다.
[root@bastion ~]# vi /opt/coredns-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
labels:
coredns: insecure-registry-only
namespace: openshift-dns
name: coredns-insecure-registry-only
spec:
selector:
matchLabels:
coredns: insecure-registry-only
template:
metadata:
creationTimestamp: null
labels:
coredns: insecure-registry-only
spec:
restartPolicy: Always
nodeSelector:
kubernetes.io/os: linux
hostNetwork: true
containers:
- name: coredns-insecure-registry-only
image: registry.ybkim.ocp4.local/coredns/insecure-registry-only:latest
command:
- coredns
args:
- '-conf'
- /opt/Corefile
ports:
- containerPort: 53
name: dns
protocol: UDP
securityContext:
privileged: true
runAsUser: 0
livenessProbe:
httpGet:
path: /health
port: 8080
scheme: HTTP
initialDelaySeconds: 60
timeoutSeconds: 5
periodSeconds: 10
successThreshold: 1
failureThreshold: 5
readinessProbe:
httpGet:
path: /health
port: 8080
scheme: HTTP
initialDelaySeconds: 10
timeoutSeconds: 3
periodSeconds: 3
successThreshold: 1
failureThreshold: 3
volumeMounts:
- name: coredns-config-volume
readOnly: true
mountPath: /opt
volumes:
- name: coredns-config-volume
configMap:
name: coredns-config
defaultMode: 420
items:
- key: Corefile
path: Corefile
- key: quay.io.db
path: quay.io.db
- key: redhat.io.db
path: redhat.io.db
- key: docker.io.db
path: docker.io.db
- key: gcr.io.db
path: gcr.io.db
- key: nvcr.io.db
path: nvcr.io.db
- key: elastic.co.db
path: elastic.co.db
- key: redhat.com.db
path: redhat.com.db
생성된 Daemonset YAML을 OC CLI를 통해 생성한다.
[root@bastion ~]# oc create -f /opt/coredns-daemonset.yaml
Image controller에서 Registry Sources에 대한 Insecure Registry 설정을 한다.
Insecure Registry 설정은 SSL 인증서를 사용하지 않고 접근시에 사용 된다.
즉, 목록에 없는 주소는 SSL 인증서를 사용한다고 보면되며, "domain:port"로 구분한다.
또한, 이 작업을 수행시 Worker 노드의 /etc/containers/registries.conf 파일의 내용을 수정하기 때문에,
Machine Config Operator가 이를 반영하기 위해 노드를 재부팅 하므로, 반드시 일정을 잡고 진행하는 것이 좋다.
[root@bastion ~]# oc edit images.config.openshift.io cluster
apiVersion: config.openshift.io/v1
kind: Image
metadata:
name: cluster
spec:
registrySources:
insecureRegistries:
- quay.io
- docker.io
- gcr.io
- nvcr.io
- docker.elastic.co
- registry.redhat.io
- registry-1.docker.io
- registry.connect.redhat.com
- registry2.ybkim.ocp4.local:5000
[3]: OpenShift Docs - Image configuration resources
OpenShift의 Worker 노드를 기준으로 작업을 진행한다.
본 작업은 RedHat CoreOS 기반에서 nmcli 명령어를 통해 수작업으로 진행 한다.
각 노드별 네트워크 설정시 DNS를 127.0.0.1로 로컬호스트를 등록한다.
[root@bastion ~]# ssh [email protected]
[core@worker01 ~]$ sudo -i
[root@worker01 ~]# nmcli connection mod 'ens3' \
ipv4.method manual \
ipv4.addresses 192.168.0.11/24 \
ipv4.gateway 192.168.0.1 \
ipv4.dns 192.168.0.3 \
+ipv4.dns 127.0.0.1 \
connection.autoconnect yes
[root@worker01 ~]# systemctl restart NetworkManager
[root@bastion ~]# ssh [email protected]
[core@worker02 ~]$ sudo -i
[root@worker02 ~]# nmcli connection mod 'ens3' \
ipv4.method manual \
ipv4.addresses 192.168.0.12/24 \
ipv4.gateway 192.168.0.1 \
ipv4.dns 192.168.0.3 \
+ipv4.dns 127.0.0.1 \
connection.autoconnect yes
[root@worker02 ~]# systemctl restart NetworkManager
특정 Container 이미지를 받아서 내부 용도로 사용하는 Registry에 업로드 한다.
[root@bastion ~]# skopeo copy --dest-tls-verify=false \
docker://docker.io/library/nginx:stable docker://registry.ybkim.ocp4.local/library/nginx:stable
생성시 외부 Registry 이름으로 태깅된 이미지 주소로 선언 후 배포한다.
[root@bastion ~]# vi /opt/nginx-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
web: nginx
namespace: default
spec:
containers:
- name: web
image: docker.io/library/nginx:stable
ports:
- name: web
containerPort: 80
protocol: TCP
[root@bastion ~]# oc create -f /opt/nginx-pod.yaml
실제 namespace에 Nginx Pod가 해당 외부 도메인으로 태깅 된 이미지로 실행된 것을 확인 가능하다.
[root@bastion ~]# oc describe pod nginx -n default
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m27s default-scheduler Successfully assigned default/nginx to worker01.ybkim.ocp4.local
Normal AddedInterface 2m25s multus Add eth0 [172.40.4.18/24]
Normal Pulling 2m22s kubelet Pulling image "docker.io/library/nginx:stable"
Normal Pulled 110s kubelet Successfully pulled image "docker.io/library/nginx:stable" in 32.703255413s
Normal Created 109s kubelet Created container web
Normal Started 108s kubelet Started container web
Scheduler가 worker01 노드에 Pod가 구동 되도록 요청을 하였고,
이후 "docker.io/library/nginx:stable" 이미지를 다운로드 요청 하였다.
Worker01 노드에서 127.0.0.1번의 로컬호스트로 DNS 질의 요청이 진행 된것을 확인해 볼 수 있다.
[root@bastion ~]# oc project openshift-dns
[root@bastion ~]# oc get pod -o wide | grep -v 'dns-default' | grep 'worker01.ybkim.ocp4.local'
coredns-insecure-registry-only-wq52t 1/1 Running 0 4d12h 7.7.7.16 worker01.ybkim.ocp4.local <none> <none>
CoreDNS 로그를 확인 해보면 쿼리가 정상적으로 수행된 것을 확인 가능하다.
[root@bastion ~]# oc logs -f coredns-insecure-registry-only-wq52t
[INFO] 127.0.0.1:57289 - 32163 "A IN docker.io. udp 27 false 512" NOERROR qr,aa,rd 52 0.006971549s
[INFO] 127.0.0.1:47821 - 20144 "AAAA IN docker.io. udp 27 false 512" NOERROR qr,aa,rd 96 0.005625527s
[INFO] 127.0.0.1:49749 - 15625 "AAAA IN registry-1.docker.io. udp 38 false 512" NOERROR qr,aa,rd 81 0.000425007s
[INFO] 127.0.0.1:44583 - 5258 "A IN registry-1.docker.io. udp 38 false 512" NOERROR qr,aa,rd 106 0.002122852s
[INFO] 127.0.0.1:35541 - 14083 "AAAA IN registry-1.docker.io. udp 38 false 512" NOERROR qr,aa,rd 81 0.000206334s
[INFO] 127.0.0.1:58193 - 57438 "A IN registry-1.docker.io. udp 38 false 512" NOERROR qr,aa,rd 106 0.000106337s
[INFO] 127.0.0.1:42592 - 41777 "A IN docker.io. udp 27 false 512" NOERROR qr,aa,rd 52 0.00019376s
[INFO] 127.0.0.1:52830 - 56624 "AAAA IN docker.io. udp 27 false 512" NOERROR qr,aa,rd 96 0.000279241s
[INFO] 127.0.0.1:38889 - 34090 "AAAA IN registry-1.docker.io. udp 38 false 512" NOERROR qr,aa,rd 81 0.000256258s
[INFO] 127.0.0.1:47402 - 27894 "A IN registry-1.docker.io. udp 38 false 512" NOERROR qr,aa,rd 106 0.000090793s
[INFO] 127.0.0.1:37396 - 27076 "AAAA IN registry-1.docker.io. udp 38 false 512" NOERROR qr,aa,rd 81 0.00031906s
[INFO] 127.0.0.1:45730 - 6400 "A IN registry-1.docker.io. udp 38 false 512" NOERROR qr,aa,rd 106 0.001418448s
[INFO] 127.0.0.1:33962 - 38558 "AAAA IN registry-1.docker.io. udp 38 false 512" NOERROR qr,aa,rd 81 0.000363738s
[INFO] 127.0.0.1:39435 - 44017 "A IN registry-1.docker.io. udp 38 false 512" NOERROR qr,aa,rd 106 0.000115647s
[INFO] 127.0.0.1:57224 - 44615 "A IN registry-1.docker.io. udp 38 false 512" NOERROR qr,aa,rd 106 0.005924397s
[INFO] 127.0.0.1:34249 - 32450 "AAAA IN registry-1.docker.io. udp 38 false 512" NOERROR qr,aa,rd 81 0.006413477s
[INFO] 127.0.0.1:41334 - 51187 "AAAA IN registry-1.docker.io. udp 38 false 512" NOERROR qr,aa,rd 81 0.00022242s
[INFO] 127.0.0.1:43200 - 46667 "A IN registry-1.docker.io. udp 38 false 512" NOERROR qr,aa,rd 106 0.000349816s
외부와 단절된 네트워크 환경(disconnected)에서는 격리, 성능, 효율성을 생각하면 충분히 시도 해볼만한 구성이라 생각한다.