{"id":"IL-0580","org":"Recruit","date":"2019-12-18","docs":[],"title":{"en":"Deploys on a ConcourseCI pipeline failed intermittently with DNS lookup timeouts when pulling images from Amazon ECR","ko":"ConcourseCI 의 배포가 Amazon ECR 이름 해석 타임아웃으로 어떤 날에만 실패했다"},"domain":["networking","aws"],"impact":{"scope":"Deploys through the ConcourseCI pipeline failed on some days","users":"unknown","region":"ap-northeast-1"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":null,"related":[],"sources":[{"url":"https://techblog.recruit.co.jp/article-204/","kind":"blog","archived":"https://web.archive.org/web/20260607143536/https://techblog.recruit.co.jp/article-204/"}],"summary":{"en":"Recruit Technologies runs the product on containers and uses ConcourseCI, set up with docker-compose on EC2, for CI/CD. The engineer found a Go and Docker issue that described the same symptom. The Docker in the Concourse job was 17.03.2-ce built with go1.7.5, the Go version named in the issue, which traced the failure to a bug in the Go standard library that Docker was built with. Fixing that meant updating Docker, either by upgrading Concourse, which needed verification and could not be done quickly, or by rewriting the resource-type.\n\nThe answers seen in the CI job were larger than the same answers seen from a local machine. As an interim fix, the engineer changed the nameserver in resolv.conf on the CI worker to a different DNS cache server, and deploys worked again. The engineer then looked into why the sizes differed: the answers in the CI job were larger because they were not compressed. DNS has a message compression feature (RFC section 4.1.4. Message compression). Packet captures on each network showed that the Concourse worker had its own DNS proxy, which forwarded responses without compressing them. The engineer then filed an issue and a pull request to Concourse. In short, the size of ECR's DNS responses varied, the Concourse internal DNS proxy forwarded them uncompressed, and together they hit the Go bug.\n\nIt took more than a month from the failure to the pull request. Much of that time went to finding which DNS server expanded the compressed messages: the engineer first suspected Docker's internal DNS, read the libnetwork code, built patched Docker and DNS libraries in a Vagrant reproduction, and analysed the iptables rules that Concourse and Docker create, all of which turned out to be beside the point.","ko":"Recruit Technologies 의 한 서비스는 컨테이너 환경에서 돌고, CI/CD 에 ConcourseCI 를 쓴다. CI 는 EC2 위에 docker-compose 로 띄운 단순한 구성이다. 2019년 어느 날 배포 중 docker build 가 캐시 이미지를 ECR 에서 pull 하다가 ECR 이름 해석의 i/o timeout 으로 실패했다. 다음 날 재시도하자 성공했지만, 며칠 뒤 같은 오류가 다시 났다.\n\nECR 이름은 ELB 를 가리키는 CNAME 으로 해석된다. ELB 는 요청량에 따라 유닛 수가 늘고 줄며, 늘면 응답의 A 레코드도 많아진다. 배포가 된 날은 A 레코드가 2개, 실패한 날은 4개 이상이었다. 엔지니어는 Go 의 이름 해석 버그를 다룬 이슈를 찾았고, CI 작업에서 쓰던 Docker 17.03.2-ce 가 그 이슈의 문제 버전인 go1.7.5 로 빌드된 것을 확인했다. Docker 를 업그레이드하려면 Concourse 를 업그레이드하거나 resource-type 을 바꿔야 해서 곧바로 할 수 없었다. 그러다 같은 이름을 조회한 응답이 개발자 컴퓨터에서는 218 바이트, CI 작업 안에서는 580 바이트인 것을 발견했고, CI worker 의 resolv.conf 에서 nameserver 를 바꾸자 배포가 다시 됐다.\n\n이 조치는 임시였으므로 엔지니어는 크기 차이의 원인을 더 팠다. 패킷 캡처로 DNS 메시지 압축이 어딘가에서 풀리고 있다는 것을 알았고, 네트워크마다 캡처해 Concourse worker 안의 DNS Proxy 가 응답을 보낼 때 메시지를 압축하지 않는다는 것을 찾았다. 이 장애는 ECR 의 DNS 응답 크기가 때때로 바뀌는 것과 Concourse 의 내부 DNS Proxy 가 응답을 압축하지 않고 전달하는 것이 겹쳐, 당시 Go 에 있던 이름 해석 버그가 드러난 경우였다. 엔지니어는 Concourse 에 이슈와 PR 을 올렸고, 장애가 난 뒤 PR 을 올리기까지 한 달 넘게 걸렸다."},"trigger":{"en":"The Elastic Load Balancer behind the ECR endpoint returned more A records, as an ELB does when its units grow with request volume, which made the DNS responses larger.","ko":"ECR 엔드포인트인 ELB 의 유닛이 늘어 DNS 응답에 담긴 A 레코드가 4개 이상이 된 날에 DNS 응답이 커졌다."},"severity":"unknown","symptoms":["dns-resolution-failures","image-pull-failures"],"timeline":{"detected_after":null,"resolved_after":null},"date_kind":"published","detection":{"en":"The deploy failed with the DNS lookup timeout in the CI job when an engineer pressed the deploy button.","ko":"엔지니어가 서비스를 배포하려고 배포 버튼을 눌렀을 때 CI 의 Docker 가 ECR 이름 해석 타임아웃 오류를 냈다."},"components":["ci-system","registry","dns-resolver"],"root_cause":"software-bug","remediation":[{"text":{"en":"Changed the nameserver in resolv.conf on the CI worker to another DNS cache server as an interim fix","ko":"CI worker 의 resolv.conf 에서 nameserver 를 다른 DNS 캐시 서버로 바꿔 배포가 다시 되게 했다."}}],"action_items":[{"text":{"en":"Filed an issue and a pull request to Concourse to fix its DNS proxy forwarding uncompressed responses","ko":"Concourse 의 DNS Proxy 가 응답을 압축하지 않는 버그를 이슈로 설명하고, 그 이슈에 연결한 PR 을 Concourse 에 보냈다."}}],"contributing":["known-bug-unpatched"],"recurrence_of":null,"symptoms_text":{"en":"When an engineer at Recruit Technologies pressed the deploy button, Docker running in a ConcourseCI job failed to pull an image from Amazon ECR that docker build uses as cache. The error was: Error response from daemon: Get https://xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/v2/: dial tcp: lookup xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com on 172.18.0.3:53: read udp 10.254.0.122:48372->172.18.0.3:53: i/o timeout\n\nThe deploy flow was git clone, docker build, docker push, then rewriting and deploying the Kubernetes manifest. The error had never appeared since the service was released, and no configuration had changed recently. Resolving the ECR name from a local machine and from a CI job worked, and the CI also resolved the name of GitHub Enterprise without trouble.\n\nThe engineer first took the error for temporary network instability, perhaps in Route53 or the VPC. A retry that night also failed. A retry the next day succeeded, but the same error came back a few days later, shortly before a release whose date could not move.\n\nComparing past results, the ECR name, a CNAME to an Elastic Load Balancer, resolved to 2 A records on days that worked and to 4 or more on days that failed. dig from a CI job showed MSG SIZE rcvd: 580, while the same query from a local machine showed MSG SIZE rcvd: 218.","ko":"배포 버튼을 누르자 ConcourseCI 위에서 도는 Docker 가 ECR 에서 이미지를 pull 하다가 실패했다. 이 서비스는 출시 뒤 이런 오류가 한 번도 없었고, 최근에 구성을 바꾼 일도 없었다.\n\n배포 흐름은 git clone, docker build, docker push, 쿠버네티스 매니페스트 수정과 배포 순서였다. docker build 중 캐시로 쓸 이미지를 pull 하는 단계에서 다음 오류가 났다. Pulling xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/xxxxx:latest (attempt 3 of 3)... Error response from daemon: Get https://xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/v2/: dial tcp: lookup xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com on 172.18.0.3:53: read udp 10.254.0.122:48372->172.18.0.3:53: i/o timeout\n\n엔지니어는 통신이 잠깐 불안정했다고 보고 재시도했지만 다시 실패했다. 밤이 늦었으므로 엔지니어는 Route53 이나 VPC 네트워크가 잠깐 불안정했다고 여기고 다음 날 다시 시도했고, 그대로 성공했다. 며칠 뒤 다시 배포하려 하자 같은 오류가 났다.\n\n개발자 컴퓨터와 CI 작업 안에서 ECR 의 이름 해석을 직접 해 보면 성공했다. CI 는 GitHub Enterprise 에도 접속했는데, 그쪽의 이름 해석은 문제가 없었다.\n\ndig 결과를 비교하자 ECR 이름이 ELB 를 가리키는 CNAME 으로 해석됐고, 배포가 된 날은 A 레코드가 2개, 안 된 날은 4개 이상이었다. 같은 이름을 개발자 컴퓨터에서 조회하면 MSG SIZE rcvd: 218 이었고, CI 작업 안에서 조회하면 MSG SIZE rcvd: 580 이었다.\n\nDNS 메시지 압축을 어디서 풀고 있는지 찾는 데 시간이 오래 걸렸다. 엔지니어는 처음에 Docker 의 내부 DNS 를 의심해 libnetwork 구현을 읽었다. docker 와 dns 라이브러리에 패치를 넣어 Vagrant 로 재현 환경을 만들고, Concourse 와 Docker 가 만든 iptables 도 분석했지만 원인은 거기에 없었다."},"updated_at":"2026-11-14T12:34:22.603743Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0580","org_slug":"recruit"}