Migrate & Upgrade GitLab 15.6.1 lên 19.2: dựng máy mới, đi private, cutover sau lưng production

Toàn bộ tên công ty, domain, IP, account ID, instance ID, fingerprint trong bài đã thay bằng giá trị giả lập cho mục đích minh hoạ. Công ty gọi là Acme (một sàn ecommerce), domain acme.io, codename nền tảng là core. IP public dùng dải tài liệu
203.0.113.0/24, IP private đã đổi dải, account ID là1111...,2222.... Cấu trúc và cách làm thì thật.
Bài này kể lại một đêm migrate GitLab EE 15.6.1 lên 19.2. Không chỉ nâng version. Mình còn đưa GitLab từ public về private (chỉ vào được qua VPN và PrivateLink), với một ràng buộc cứng:
Không đụng máy cũ. Máy cũ phục vụ production tới tận lúc flip DNS. Mọi thứ đọc-only (backup, đọc host key), không sửa config, không restart.
Kết quả cuối: máy mới gitlab.acme.io chạy 19.2, private hoàn toàn, ArgoCD và runner và VPN đều vào được, rollback bằng đúng một bản ghi DNS. Đường tới đó thì nhiều màu.
1. Bối cảnh và ràng buộc
Hiện trạng ban đầu:
- GitLab cũ: EE 15.6.1 omnibus, chạy trên một EC2 (
i-0old..., private10.10.5.6) trong account Dev-core111111111111, VPC10.10.0.0/16. - Public qua Cloudflare (proxied) rồi Classic ELB (internet-facing) rồi máy cũ port 80/22. TLS terminate ở ELB bằng ACM
*.acme.io. SSH git (git@gitlab.acme.io) đi thẳng ELB:22 (Cloudflare không proxy port 22), nên có sẵn một Route53 private zone nội bộ trỏgitlab.acme.iovề ELB để bypass Cloudflare cho SSH. - Consumer chính: ArgoCD (~570 Application, clone qua SSH), GitLab Runner (docker-autoscaler), và dev vào qua 4 con OpenVPN.
Không có key EE mới. Câu hỏi đầu tiên: cài CE hay EE? Backup của bản EE chỉ restore được lên EE cùng version, nên phải dựng máy mới EE 15.6.1, restore, rồi mới step-upgrade lên 19.2. Không có đường tắt.
2. Kiến trúc: trước và sau
Trước cutover, mọi thứ chui qua Cloudflare và ELB public. Sau cutover, không còn đường ra internet, cross-account đi qua PrivateLink tại một network hub, và Route53 private zone quyết định ai reach bằng đường nào.
Điểm cần nhớ: máy mới không có public IP, nằm trong VPC dev (10.20.0.0/16). Các consumer ở account khác (prod, ArgoCD) không route thẳng vào được. Tất cả đi qua VPC Endpoint Service tại network hub account 333333333333, spoke reach hub qua Transit Gateway. Đây đúng là mô hình mà một service private khác trong tổ chức đã dùng, nên mình nhân bản lại thay vì phát minh cái mới.
Tên tài nguyên (đã đổi):
| Thành phần | Giá trị |
|---|---|
| Máy mới | i-0new..., 10.20.12.34, EE nâng lên 19.2 |
| Internal NLB | gitlab-priv-nlb (443 TLS ACM, 22 TCP) |
| VPC Endpoint Service | vpce-svc-0abc... (allow principal account hub) |
| Interface endpoint (hub) | vpce-0def..., IP 10.40.10.11, .20.22, .30.33 |
| Private zone | acme.io (hub) và override gitlab.acme.io theo từng VPC |
3. Phase 1: backup mà không đụng máy cũ
Máy cũ private, không SSH trực tiếp, điều khiển qua SSM Run Command. Backup đầu tiên chết vì hai lý do.
(a) SSM timeout giết tiến trình. Backup omnibus mặc định gồm artifacts. Repo này có 14GB artifacts. gitlab-backup create chạy 40 phút thì SSM RunShellScript hết timeout-seconds và kill luôn tiến trình, để lại backup dở.
(b) Stale PID lock. Lần chạy sau báo There is a backup and restore task in progress, kèm gợi ý xoá backup_restore.pid. Tiến trình bị SSM kill không kịp dọn PID, mọi backup sau bị chặn.
Cách xử: dọn lock chết, chạy backup detached bằng systemd-run để miễn nhiễm SSM timeout, và skip artifacts (14GB, chấp nhận mất artifacts cũ, thứ cần là code, DB, repo mới).
# 1) xac nhan KHONG con tien trinh that, roi xoa lock chet
ps aux | grep -E "rake gitlab:backup" | grep -v grep # rong
rm -f /opt/gitlab/embedded/service/gitlab-rails/tmp/backup_restore.pid
# 2) viet script backup + upload, chay nhu transient systemd unit
cat >/root/gl-migrate-backup.sh <<'SCRIPT'
#!/bin/bash
BUCKET=acme-gitlab-migrate-transfer
gitlab-backup create STRATEGY=copy SKIP=artifacts CRON=1 || { echo "BACKUP_FAILED rc=$?"; exit 1; }
TAR=$(ls -t /var/opt/gitlab/backups/*_gitlab_backup.tar | head -1)
cp /etc/gitlab/gitlab-secrets.json /etc/gitlab/gitlab.rb /var/opt/gitlab/backups/
aws s3 cp "$TAR" "s3://$BUCKET/migrate/" --only-show-errors && echo UP_TAR_OK
aws s3 cp /var/opt/gitlab/backups/gitlab-secrets.json "s3://$BUCKET/migrate/" && echo UP_SECRETS_OK
aws s3 cp /var/opt/gitlab/backups/gitlab.rb "s3://$BUCKET/migrate/" && echo UP_RB_OK
echo "MIGRATE_BACKUP_DONE $(date -u)"
SCRIPT
chmod +x /root/gl-migrate-backup.sh
systemd-run --unit=gl-migrate-backup --collect /root/gl-migrate-backup.sh
systemd-run tách hẳn tiến trình khỏi cây process của SSM. SSM command trả về ngay, backup chạy tiếp dưới systemd. Theo dõi bằng journalctl -u gl-migrate-backup:
Ba thứ lên S3: tarball 15G, gitlab-secrets.json (bắt buộc để giải mã CI variable và 2FA), và gitlab.rb. Ràng buộc "không đụng máy cũ" vẫn giữ, vì chỉ đọc data và xoá file lock/temp trong thư mục backup, không phải data GitLab, config không sửa.
4. Phase 2: restore và step-upgrade, đây là chỗ ăn thời gian
Máy mới cài EE 15.6.1 khớp version backup (điều kiện sống còn của gitlab-backup restore). Verify khớp trước khi restore, rồi nạp secrets của máy cũ, stop puma và sidekiq, restore.
Hai dòng ERROR: must be owner of extension pg_trgm màu đỏ hù người mới, nhưng lành tính: extension đã tồn tại sẵn, restore chỉ không set được owner. Data vẫn vào đủ. Các dòng skipped restore ... .wiki.bundle doesn't exist là do project chưa từng tạo wiki. Chính vì mấy warning này mà gitlab-backup restore trả exit code 1 dù restore xong, đừng để script tự bail ở đó.
4.1. Các case bug lúc step-upgrade
Restore chỉ mất chục phút. Phần thật sự ngốn thời gian là step-upgrade 15.6.1 lên 19.2, và đây là chỗ dính nhiều bug nhất. Ghi lại từng case:
Case 1: required-stops, không được nhảy cóc. GitLab bắt phải dừng ở đúng các bản trung gian. Đường đi tra ở upgrade-path tool chính thức, cỡ:
15.6.1 -> 15.11.13 -> 16.0.10 -> 16.1.8 -> 16.3.9 -> 16.7.10
-> 16.11.10 -> 17.1.8 -> 17.3.7 -> 17.5.5 -> 17.8.7 -> 17.11.6
-> 18.0.x -> 18.2.x -> ... -> 19.2.0
Nhảy thẳng một phát là reconfigure chết. Mỗi chặng phải cài đúng bản đó, reconfigure, đợi migrations xong, rồi mới đi tiếp.
Case 2: batched background migrations treo, chặn cả chặng sau. Đây là con quỷ số một về thời gian. Sau mỗi upgrade, GitLab đẩy một loạt migration nền chạy dần qua Sidekiq (backfill cột, đổi kiểu dữ liệu, thêm index trên bảng lớn). Với DB cỡ 71k merge request, 113k pipeline, 128k note, mấy migration này bò rất chậm. Nếu bạn upgrade tiếp khi chúng chưa finished, chặng sau báo lỗi và bắt quay lại chờ. Kiểm tra:
gitlab-psql -c "select job_class_name, status from batched_background_migrations
where status not in (3,6);" # 3=finished, 6=finalized
Case 3: nâng PostgreSQL major. GitLab bundle sẵn Postgres. Lên tầm 16.x là phải đẩy PG 13 lên 14, tới 17.x/18.x thì lên PG 16. Chạy gitlab-ctl pg-upgrade. Bẫy: pg-upgrade cần dung lượng đĩa gấp đôi data hiện tại (copy sang cluster mới). Đĩa gần đầy là fail giữa chừng, cực phiền.
Case 4: setting cũ trong gitlab.rb làm reconfigure chết. Qua nhiều major, một số key trong gitlab.rb bị đổi tên hoặc gỡ. Reconfigure gặp key lạ là dừng. Phải soi log reconfigure, gỡ key deprecated, chạy lại.
Case 5: runner registration token bị khai tử (16.0+). Từ 16.0, kiểu đăng ký runner bằng registration_token cũ bị tắt mặc định, chuyển sang runner authentication token mới (glrt-...). Runner cũ mất kết nối cho tới khi tạo token mới. Mình dính đúng case này lúc gắn lại runner (mục 7.7).
Case 6: reconfigure timeout vì migration nặng. Vài migration (thêm index trên ci_builds, backfill cột lớn) chạy chục phút. reconfigure mặc định đợi migration, và nếu chạy trong SSM thì lại đụng SSM timeout, giống hệt lúc backup. Bài học lặp lại: chạy cả chuỗi upgrade detached (systemd-run hoặc tmux), đừng để trong process-tree của SSM.
4.2. Làm gì để tăng tốc quá trình upgrade
Chuỗi upgrade nếu để "tự nhiên" có thể kéo cả ngày, phần lớn là ngồi chờ migration nền. Mấy thứ mình làm để rút ngắn:
1. Ép chạy batched background migrations ngay, đừng chờ Sidekiq nhỏ giọt. Thay vì để chúng bò dần, finalize thủ công cho từng migration đang treo:
# liet ke migration dang active
gitlab-rails runner "Gitlab::Database::BackgroundMigration::BatchedMigration.active \
.each { |m| puts \"#{m.job_class_name} #{m.table_name}.#{m.column_name}\" }"
# finalize dong bo (chay het ngay, thay vi cho troi)
gitlab-rake gitlab:background_migrations:finalize[CopyColumnUsingBackgroundMigrationJob,ci_builds,id,'[["id"],["id_convert_to_bigint"]]']
2. Tạm nâng cấu hình máy trong lúc upgrade. Migration nặng CPU và IO. Đổi instance type to hơn (nhiều vCPU, RAM, EBS gp3 IOPS cao) trong đêm upgrade, xong hạ lại. Thời gian build index rút đáng kể.
3. Dồn tài nguyên cho Sidekiq, bỏ đói Puma. Máy migration không phục vụ traffic, nên tăng concurrency Sidekiq và tắt gần hết Puma để Sidekiq nuốt migration nhanh hơn:
# /etc/gitlab/gitlab.rb (chi trong giai doan upgrade)
sidekiq['max_concurrency'] = 50
puma['worker_processes'] = 0
4. Chỉnh Postgres cho việc bulk. Tạm nâng maintenance_work_mem và max_wal_size để build index nhanh hơn, trả về mặc định sau khi xong.
5. Tải sẵn package cho mọi chặng từ đầu. Không ngồi chờ download giữa các stop:
apt-get download gitlab-ee=15.11.13-ee.0 gitlab-ee=16.0.10-ee.0 gitlab-ee=16.3.9-ee.0 ...
6. Tắt cron job và integration trong lúc upgrade để Sidekiq không phải tranh worker với pipeline schedule, webhook, mirror.
Kết thúc chuỗi:
$ cat /opt/gitlab/embedded/service/gitlab-rails/VERSION
19.2.0-ee
$ gitlab-rails runner 'puts %Q[projects=#{Project.count} users=#{User.count} MR=#{MergeRequest.count}]'
projects=458 users=192 MR=71854
5. Phase 3: đưa về private với NLB và PrivateLink
Đây là phần dễ làm sai. Ý định ban đầu là dựng một internal ALB (HTTP/443) trong VPC dev. Sai, vì hai điều phát hiện khi soi cách ArgoCD thật sự nói chuyện với GitLab:
- ArgoCD clone qua SSH (
git@gitlab.acme.io), không phải HTTPS. ALB là L7 HTTP, không cõng được port 22. - Cross-account phải đi PrivateLink, mà VPC Endpoint Service chỉ đứng trước NLB, không đứng trước ALB.
Nên đổi sang NLB với hai listener: 443 TLS (terminate bằng ACM *.acme.io, forward TCP:80 cho GitLab nginx) và 22 TCP (passthrough tới sshd của máy).
# target group: HTTPS (TLS terminate o NLB -> box:80) va SSH (TCP -> box:22)
aws elbv2 create-target-group --name gitlab-priv-https-tg --protocol TCP --port 80 \
--vpc-id vpc-dev --target-type ip \
--health-check-protocol HTTP --health-check-path /users/sign_in --matcher HttpCode=200
aws elbv2 create-target-group --name gitlab-priv-ssh-tg --protocol TCP --port 22 \
--vpc-id vpc-dev --target-type ip --health-check-protocol TCP
aws elbv2 register-targets --target-group-arn $TGH --targets Id=10.20.12.34,Port=80
aws elbv2 register-targets --target-group-arn $TGS --targets Id=10.20.12.34,Port=22
# NLB internal + listener
aws elbv2 create-load-balancer --name gitlab-priv-nlb --type network --scheme internal \
--subnets subnet-1a subnet-1b subnet-1c
aws elbv2 create-listener --load-balancer-arn $NLB --protocol TLS --port 443 \
--certificates CertificateArn=$ACM --ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
--default-actions Type=forward,TargetGroupArn=$TGH
aws elbv2 create-listener --load-balancer-arn $NLB --protocol TCP --port 22 \
--default-actions Type=forward,TargetGroupArn=$TGS
# VPC Endpoint Service dung truoc NLB, cho account hub tao interface endpoint
SVC=$(aws ec2 create-vpc-endpoint-service-configuration \
--network-load-balancer-arns $NLB --no-acceptance-required \
--query 'ServiceConfiguration.ServiceId' --output text)
aws ec2 modify-vpc-endpoint-service-permissions --service-id $SVC \
--add-allowed-principals arn:aws:iam::333333333333:root
Bên network hub (333333333333), tạo interface endpoint trong shared-vpc (10.40.x), SG cho 22 và 443 từ dải nội bộ. Test từ trong VPC (NLB private nên chỉ test được từ bên trong), đúng SNI và Host gitlab.acme.io: https=200, ssl=0, và banner SSH-2.0-OpenSSH. Cross-account từ prod VPN và từ một pod trong cụm ArgoCD tới hub endpoint cũng OK cả 443 lẫn 22. PrivateLink tunnel nội bộ, nên không cần mở route segmentation của VPC dev.
6. Phase 4: cutover bằng Route53 private zone (split-horizon)
Cutover không đụng Cloudflare (Cloudflare không reach được endpoint private). Điểm flip nằm ở Route53 private zone mà tất cả VPC dùng để resolve gitlab.acme.io.
Vấn đề: dev VPN cùng VPC với NLB (10.20) nhưng không route tới hub (10.40). Nên cần split-horizon:
- Zone dùng chung (hub)
gitlab.acme.iotrỏ về hub endpoint, dành cho prod VPN, ArgoCD, các account khác. - Zone override riêng cho VPC dev
gitlab.acme.iotrỏ thẳng NLB, dành cho dev VPN cùng VPC.
# override zone cho DEV VPC -> NLB (alias chay vi record CUNG account voi NLB)
Z=$(aws route53 create-hosted-zone --name gitlab.acme.io --caller-reference dev-1 \
--hosted-zone-config PrivateZone=true \
--vpc VPCRegion=ap-southeast-1,VPCId=vpc-dev --query 'HostedZone.Id' --output text)
aws route53 change-resource-record-sets --hosted-zone-id $Z --change-batch '{
"Changes":[{"Action":"UPSERT","ResourceRecordSet":{"Name":"gitlab.acme.io.","Type":"A",
"AliasTarget":{"HostedZoneId":"'$NLB_ZONE'","DNSName":"gitlab-priv-nlb-...elb.amazonaws.com.","EvaluateTargetHealth":false}}}]}'
# zone hub: doi CNAME(ELB cu) THANH A alias -> hub endpoint
aws route53 change-resource-record-sets --hosted-zone-id $HUB_ZONE --change-batch '{
"Changes":[
{"Action":"DELETE","ResourceRecordSet":{"Name":"gitlab.acme.io.","Type":"CNAME","TTL":300,
"ResourceRecords":[{"Value":"acme-gitlab-legacy-lb-...elb.amazonaws.com."}]}},
{"Action":"CREATE","ResourceRecordSet":{"Name":"gitlab.acme.io.","Type":"A",
"AliasTarget":{"HostedZoneId":"'$VPCE_ZONE'","DNSName":"vpce-0def-...vpce.amazonaws.com.","EvaluateTargetHealth":false}}}
]}'
Rollback bằng cách trỏ bản ghi zone hub về ELB cũ. Máy cũ và ELB cũ không đụng gì, chạy song song.
7. Bảy con quỷ nhỏ hậu cutover
Phần này vui. Mọi lỗi dưới đây gặp thật, xen giữa 4 giờ sáng.
7.1. SSM timeout và stale PID lock
Đã kể ở Phase 1. Bài học: tác vụ dài thì systemd-run detached, đừng để trong process-tree của SSM. Và luôn SKIP=artifacts nếu object store không dùng.
7.2. SSH host key mismatch, ArgoCD chết đứng
Ngay sau cutover, ArgoCD báo ssh: handshake failed: knownhosts: key mismatch. Máy mới tự sinh host key mới, khác máy cũ, nên known_hosts của ArgoCD (pin key cũ) không khớp. Cách đúng cho migration là giữ nguyên host identity: copy host key của máy cũ sang máy mới, restart sshd. Sau đó ssh-keyscan trả về đúng fingerprint cũ, không phải sửa gì phía client.
7.3. authorized_keys rỗng sau restore, auth publickey fail
Host key khớp rồi, ArgoCD lại báo unable to authenticate, attempted methods [none publickey]. Máy mới dùng file authorized_keys cho SSH auth, mà sau restore file này rỗng 0 byte. gitlab-backup restore khôi phục DB (có 111 SSH key) nhưng không tự sinh lại file. Fix bằng đúng một rake task: gitlab-rake gitlab:shell:setup.
7.4. NLB chưa bật cross-zone, 2/3 endpoint IP không reach port 22
Client resolve gitlab.acme.io ra 3 IP endpoint (round-robin). Test thấy chỉ 1/3 IP mở được port 22. Máy đích chỉ nằm ở một AZ, mà NLB mặc định tắt cross-zone, nên node NLB ở AZ khác không có target và connect fail, lúc trúng lúc trật. Fix một dòng: bật load_balancing.cross_zone.enabled=true.
7.5. Alias Route53 cross-account tới VPC endpoint không resolve
Zone override cho prod VPN (ở account prod) alias tới hub endpoint (ở account hub), dig trả rỗng. Trong khi zone hub (cùng account endpoint) thì alias chạy ngon. Nguyên nhân: alias Route53 tới VPC endpoint chỉ resolve khi bản ghi cùng account với endpoint. Cross-account thì dùng A record IP thật của endpoint thay cho alias.
7.6. DNS negative caching
Đổi record xong mà dig trên máy prod vẫn trả rỗng, vì lần alias-hỏng trước bị cache negative trong systemd-resolved. Query thẳng Amazon resolver 169.254.169.253 để kiểm tra, rồi resolvectl flush-caches. Đổi DNS mà không thấy thì nghi negative cache trước khi nghi Route53.
7.7. VPN thiếu route push, dev báo "connect xong vẫn không vào"
Dev báo nối vpn-core.acme.io xong vẫn không mở được GitLab. Con này là OpenVPN Access Server ở VPC prod. Soi config: nó push DNS đúng (VPC resolver, resolve ra 10.40.x), nhưng danh sách route push có 10.60, 10.10, một đống dải Cloudflare (104.x, 172.64.x, dấu tích thời GitLab còn public), mà thiếu 10.40.0.0/16 (mạng hub endpoint). Client resolve ra 10.40.x nhưng không có route tới đó, traffic đi ra default gateway. Push thêm route rồi cho client reconnect:
sacli --key "vpn.server.routing.private_network.23" --value "10.40.0.0/16" ConfigPut
sacli start # client disconnect + reconnect de nhan route moi
Rà cả 4 con VPN luôn, một con còn đặt DNS primary là 8.8.8.8 (public, không resolve được private zone), sửa về VPC resolver.
8. Mẹo: DaemonSet debug để soi mạng tức thì
Suốt cutover, thao tác lặp nhiều nhất là: "từ cụm X, có resolve được gitlab.acme.io không, có reach :443 và :22 không?". Chạy kubectl run --image=netshoot mỗi lần khá chậm, phải kéo image và chờ schedule. Thay vào đó, đặt sẵn một DaemonSet netshoot trên mọi node, có ngay một pod để exec vào, không chờ.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: netshoot-debug
namespace: infra-debug
spec:
selector:
matchLabels: { app: netshoot-debug }
template:
metadata:
labels: { app: netshoot-debug }
spec:
tolerations:
- operator: Exists # chay ca tren node co taint
containers:
- name: netshoot
image: nicolaka/netshoot:latest
command: ["sleep", "infinity"]
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 100m, memory: 128Mi }
terminationGracePeriodSeconds: 1
Soi tức thì từ bất kỳ node nào:
POD=$(kubectl -n infra-debug get pod -l app=netshoot-debug \
--field-selector spec.nodeName=$NODE -o jsonpath='{.items[0].metadata.name}')
kubectl -n infra-debug exec $POD -- bash -c '
getent ahostsv4 gitlab.acme.io
nc -z -w3 gitlab.acme.io 443 && echo 443_OK
nc -z -w3 gitlab.acme.io 22 && echo 22_OK
ssh-keyscan -t ed25519 gitlab.acme.io 2>/dev/null | ssh-keygen -lf -
'
DaemonSet debug là con dao hai lưỡi, netshoot có đủ đồ nghề mạng. Đặt trong namespace riêng, giới hạn resources, và gỡ sau khi xong (kubectl delete ds netshoot-debug -n infra-debug). Đừng để nó nằm vĩnh viễn trong prod. Trong giai đoạn cutover cũng đáng bật GuardDuty Runtime Monitoring (DaemonSet agent) để có eyes on egress bất thường khi mình đang mở và đóng đường mạng liên tục.
9. Kết quả và bài học
Nghiệm thu: cả ba nhóm consumer đều vào box mới, private.
| Nguồn | Đường | 443 | 22 (SSH git) |
|---|---|---|---|
| Dev VPN | override zone tới NLB (cùng VPC) | 200 | OK |
| Prod VPN | override zone tới hub endpoint | 200 | OK |
| ArgoCD | zone hub tới hub endpoint | 200 | git ls-remote chạy thật |
| Runner v2 | NLB trực tiếp | 200 | online |
Vài thứ đọng lại:
- Đừng nâng máy mới lên version đích quá sớm. Backup cross-version không restore hay merge được. Nếu máy cũ còn phát sinh commit và MR sau backup, bạn không thể sync incremental giữa 15.6.1 và 19.2. Rút ngắn cửa sổ giữa backup cuối và cutover.
- PrivateLink, không phải peering. Cross-account reach một service private nên đi qua VPC Endpoint Service tại hub, không phá segmentation, không cần route thẳng vào VPC đích.
- NLB cho GitLab, không phải ALB, vì cần cả SSH port 22, và vì endpoint service chỉ đứng trước NLB. Nhớ bật cross-zone nếu target ở một AZ.
- Giữ nguyên host identity khi migrate (copy
/etc/ssh/ssh_host_*) để client không dính key-mismatch. Và luôn chạygitlab-rake gitlab:shell:setupsau restore. - Alias Route53 tới VPC endpoint chỉ chạy same-account. Cross-account dùng A record IP.
- DNS đổi mà không thấy thì nghi negative cache (
resolvectl flush-caches) trước khi nghi Route53. - Cutover mạng đi kèm cutover VPN routes. Service đổi từ public (dải Cloudflare) sang private (dải nội bộ mới) thì VPN phải push dải mới, nếu không thì "connect xong vẫn không vào".
Phần ngốn giờ nhất không phải cutover, mà là chuỗi step-upgrade và mớ batched background migration ở giữa. Chuẩn bị máy khoẻ, ép migration chạy ngay thay vì chờ, và làm mọi thứ detached, đó là cách rút một ngày xuống còn một đêm. Máy cũ thì không đụng một dòng config nào tới tận lúc tắt.

