この記事では、OCI上で稼働していた次のWeb構成をOracle Kubernetes Engine(OKE)へ移行した手順をまとめます。今回の移行対象は、実際に運営しているパソコン・周辺機器 売れ筋ガイドです。
移行前: OCI DNS → OCI Load Balancer → Private VM(Nginx + Certbot)
移行後: OCI DNS → OCI Native Ingress → OKE(Nginx Pods)
└→ cert-manager
単にコンテナを起動するだけではなく、次の作業まで実施しました。
- 新しいLoad BalancerへのHTTPS設定
- DNS切り替え前のHTTPS事前テスト
-
cert-managerによるLet's Encrypt証明書の自動更新 - HTTPからHTTPSへのリダイレクト
- 旧証明書の失効と削除
- 旧Load Balancer、旧Private VM、ブートボリュームの廃止
- Terraformコードと実環境の整合
環境
- Oracle Kubernetes Engine(OKE)
- OCI Native Ingress Controller
- Kubernetes Ingress API
networking.k8s.io/v1 - cert-manager v1.20.3
- Nginx 1.29 Alpine
- Terraform
- GitHub Actions + OCIR
- 対象ドメイン
letsgopc.netwww.letsgopc.net
記事中のOCID、メールアドレス、レジストリ名などは省略またはプレースホルダーにしています。
1. OKEへWebアプリケーションをデプロイする
Webサイトは静的コンテンツなので、NginxコンテナとしてOKEへデプロイしました。
FROM nginx:1.29-alpine
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY index.html /usr/share/nginx/html/index.html
COPY robots.txt /usr/share/nginx/html/robots.txt
COPY sitemap.xml /usr/share/nginx/html/sitemap.xml
COPY articles/ /usr/share/nginx/html/articles/
EXPOSE 80
Deploymentは2 replicasとし、Service経由で公開します。
apiVersion: apps/v1
kind: Deployment
metadata:
name: letsgopc-web
namespace: letsgopc
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: letsgopc-web
template:
metadata:
labels:
app.kubernetes.io/name: letsgopc-web
spec:
containers:
- name: letsgopc-web
image: <REGION>.ocir.io/<NAMESPACE>/letsgopc-web:<COMMIT_SHA>
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
落とし穴:rollout restartだけでは新しいイメージにならない
GitHub Actionsで新しいコンテナイメージをpushしても、Deploymentのimageが古いコミットSHAを指したままでは、次のコマンドだけでは新しいイメージへ切り替わりません。
kubectl rollout restart deployment/letsgopc-web -n letsgopc
実際にPod内のNginx設定を確認すると、古いイメージのdefault.confが使われていました。
kubectl exec -n letsgopc deployment/letsgopc-web -- \
sed -n '1,120p' /etc/nginx/conf.d/default.conf
Deploymentのイメージタグを新しいコミットSHAへ更新し、改めて適用しました。
kubectl apply -f kubernetes/deployment.yaml
kubectl rollout status deployment/letsgopc-web \
-n letsgopc \
--timeout=180s
2. OCI Native IngressでHTTPとHTTPSを公開する
HTTP用Ingressでは、apexとwwwの両方を同じServiceへルーティングします。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: letsgopc-web
namespace: letsgopc
annotations:
oci-native-ingress.oraclecloud.com/http-listener-port: "80"
spec:
ingressClassName: letsgopc-native-ingress
rules:
- host: letsgopc.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: letsgopc-web
port:
number: 80
- host: www.letsgopc.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: letsgopc-web
port:
number: 80
HTTPS用IngressはKubernetes TLS Secretを参照します。バックエンドのNginxはHTTPで待ち受けるため、バックエンドTLSは無効にしました。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: letsgopc-web-https
namespace: letsgopc
annotations:
oci-native-ingress.oraclecloud.com/https-listener-port: "443"
oci-native-ingress.oraclecloud.com/backend-tls-enabled: "false"
spec:
ingressClassName: letsgopc-native-ingress
tls:
- hosts:
- letsgopc.net
- www.letsgopc.net
secretName: letsgopc-tls
rules:
- host: letsgopc.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: letsgopc-web
port:
number: 80
- host: www.letsgopc.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: letsgopc-web
port:
number: 80
3. DNS切り替え前に新Load Balancerをテストする
DNSを切り替える前に、opensslのSNIとcurl --resolveを使って新Load Balancerを直接確認しました。
echo | timeout 15 openssl s_client \
-connect <NEW_LB_IP>:443 \
-servername letsgopc.net \
2>/dev/null | \
openssl x509 \
-noout \
-subject \
-issuer \
-serial \
-dates \
-ext subjectAltName
curl -sSI \
--resolve letsgopc.net:443:<NEW_LB_IP> \
https://letsgopc.net/
curl -sSI \
--resolve www.letsgopc.net:443:<NEW_LB_IP> \
https://www.letsgopc.net/
さらに、複数回アクセスして全Podが正常に応答することを確認しました。
for i in $(seq 1 10); do
curl -sS \
--connect-timeout 3 \
--max-time 10 \
--resolve letsgopc.net:443:<NEW_LB_IP> \
-o /dev/null \
-w '%{http_code}\n' \
https://letsgopc.net/
done
4. X-Forwarded-Protoを使ってHTTPSへリダイレクトする
TLSはLoad Balancerで終端されるため、バックエンドNginxから見る通信はHTTPです。単純に80番ポートへのアクセスをHTTPSへリダイレクトすると、リダイレクトループになる可能性があります。
そこで、Load Balancerが付与するX-Forwarded-Protoがhttpの場合だけリダイレクトしました。
map $http_x_forwarded_proto $redirect_to_https {
default 0;
http 1;
}
server {
listen 80;
listen [::]:80;
server_name letsgopc.net www.letsgopc.net;
root /usr/share/nginx/html;
index index.html;
if ($redirect_to_https) {
return 301 https://$host$request_uri;
}
location / {
try_files $uri $uri/ =404;
}
}
ローカルのPodmanで、HTTP相当なら301、HTTPS終端後相当なら200になることを確認しました。
curl -sSI \
-H 'Host: letsgopc.net' \
-H 'X-Forwarded-Proto: http' \
http://127.0.0.1:18080/
curl -sSI \
-H 'Host: letsgopc.net' \
-H 'X-Forwarded-Proto: https' \
http://127.0.0.1:18080/
5. DNSを新Load Balancerへ切り替える
事前テストが成功してから、apexとwwwのAレコードを新Load Balancerへ変更しました。
dig @ns1.example-dns.net +short letsgopc.net A
dig @1.1.1.1 +short letsgopc.net A
dig @8.8.8.8 +short www.letsgopc.net A
権威DNS、Cloudflare、Google Public DNSの順で新IPが返ることを確認してから、通常のURLで確認します。
curl -sSI http://letsgopc.net/
curl -sSI https://letsgopc.net/
curl -sSI http://www.letsgopc.net/
curl -sSI https://www.letsgopc.net/
期待結果は、HTTPが301 Moved Permanently、HTTPSが200 OKです。
6. cert-managerでLet's Encryptを自動更新する
最初から本番環境を使わず、Let's Encrypt StagingでHTTP-01が成功することを確認してからProductionへ切り替えました。
Issuer
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: letsencrypt-production
namespace: letsgopc
spec:
acme:
email: <YOUR_EMAIL>
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-production-account-key
solvers:
- http01:
ingress:
name: letsgopc-web
serviceType: NodePort
Certificate
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: letsgopc-tls
namespace: letsgopc
spec:
secretName: letsgopc-tls
issuerRef:
name: letsencrypt-production
kind: Issuer
privateKey:
algorithm: ECDSA
size: 256
rotationPolicy: Always
dnsNames:
- letsgopc.net
- www.letsgopc.net
落とし穴:HTTP-01 challengeが404になる
最初の試行ではChallengeが次の状態になりました。
Waiting for HTTP-01 challenge propagation:
wrong status code '404', expected '200'
solver用Ingressが個別に作成され、OCI Native Ingress ControllerがsolverのServiceポート8089に対応する別listenerやrouting policyを作ろうとしていました。その過程で複数のsolver Ingressが競合し、NoEtagMatchや存在しないbackend setへの参照も発生しました。
この環境では、IssuerのHTTP-01 solverに既存Ingressを明示しました。
solvers:
- http01:
ingress:
name: letsgopc-web
serviceType: NodePort
これにより、ACME challenge用pathが既存の80番Ingressへ追加され、同じHTTP listenerで検証できました。
また、solverのNodePortは動的に割り当てられます。Security Listでは固定NodePortだけでなく、Load BalancerのサブネットCIDRからworker nodeへのNodePort範囲を許可しました。
ingress_security_rules {
protocol = "6"
source = var.public_subnet_cidr_block
source_type = "CIDR_BLOCK"
stateless = false
tcp_options {
min = 30000
max = 32767
}
}
0.0.0.0/0からNodePortを開けるのではなく、Load Balancerが存在するサブネットCIDRに限定することが重要です。
発行・更新状態を確認する
kubectl get certificate,certificaterequest,order,challenge \
-n letsgopc
kubectl get certificate letsgopc-tls \
-n letsgopc \
-o custom-columns='NAME:.metadata.name,READY:.status.conditions[0].status,ISSUER:.spec.issuerRef.name,REVISION:.status.revision,NOT_AFTER:.status.notAfter,RENEWAL_TIME:.status.renewalTime'
Secret内の証明書と、実際にLoad Balancerが配信している証明書のserialが一致することも確認しました。
kubectl get secret letsgopc-tls \
-n letsgopc \
-o jsonpath='{.data.tls\.crt}' | \
base64 --decode | \
openssl x509 -noout -serial -dates
echo | timeout 15 openssl s_client \
-connect letsgopc.net:443 \
-servername letsgopc.net \
2>/dev/null | \
openssl x509 -noout -serial -dates
7. 証明書の秘密鍵を安全に切り替える
Kubernetes Secretの確認時は、kubectl get secret -o yamlなどで秘密鍵のbase64データをターミナルやチャットへ貼り付けないよう注意が必要です。base64は暗号化ではありません。
今回は新しい証明書で鍵をローテーションし、旧環境の証明書を失効させました。
privateKey:
rotationPolicy: Always
新旧証明書の公開鍵が異なることを確認してから、旧VM上でCertbot timerを停止し、旧証明書をrevoke・deleteしました。
8. 旧Load Balancerと旧VMを廃止する
削除順序は次のようにしました。
- DNSが新Load Balancerを返すことを確認
- 新環境のHTTPSが200であることを確認
- 旧VMのNginxとCertbot timerを停止
- 旧証明書を失効・削除
- 旧Load BalancerをTerraformで削除
- 旧Private VMとブートボリュームをTerraformで削除
- 踏み台用Public Instanceを停止
各Terraform planでは、削除対象以外が含まれていないことを必ず確認しました。
旧Load Balancer: Plan: 0 to add, 0 to change, 7 to destroy
旧Private VM: Plan: 0 to add, 0 to change, 1 to destroy
DNSのremote state依存を先に外す
DNSのAレコードが旧Load Balancerのremote stateを参照していたため、そのままLB moduleを削除するとDNS moduleが壊れます。
# Before
rdata = data.terraform_remote_state.load_balancer.outputs.load_balancer_public_ip
# After
rdata = var.web_load_balancer_ip
variable "web_load_balancer_ip" {
description = "The public IP address of the active web load balancer"
type = string
default = "<NEW_LB_IP>"
}
さらに、OCI DNS Zoneを管理しているリージョンとTerraform providerのリージョンが一致していることを確認しました。リージョンが違うと、存在するZoneをTerraformが「削除された」と誤認し、再作成を計画することがあります。
最終的にDNSとComputeの両方がNo changesになることを確認しました。
terraform -chdir=dns plan
terraform -chdir=compute plan
最終結果
http://letsgopc.net/ → 301 → HTTPS
https://letsgopc.net/ → 200 OK
http://www.letsgopc.net/ → 301 → HTTPS
https://www.letsgopc.net/ → 200 OK
- WebサイトはOKE上の2 Podで稼働
- OCI Native IngressでHTTP/HTTPSを公開
- apexと
wwwの両方に対応 - cert-managerがLet's Encrypt証明書を自動更新
- 旧Load Balancer、旧Private VM、旧ブートボリュームは削除
- 踏み台用Public Instanceは停止
- TerraformとGitHubの構成も実環境へ同期
まとめ
今回の移行で特に重要だったのは次の点です。
- DNS切り替え前に
curl --resolveとSNIで新LBを検証する - コンテナイメージはDeploymentが参照するタグまで更新する
- TLS終端後のリダイレクトには
X-Forwarded-Protoを使う - cert-managerのHTTP-01を既存の80番Ingressへ載せる
- ACME solverの動的NodePortを、LBサブネットからだけ許可する
- 旧Terraform stateへの依存を外してから旧リソースを削除する
- 証明書だけでなく秘密鍵もローテーションする
OKEへの移行は、アプリケーションのデプロイだけでは終わりません。HTTPS、証明書更新、DNS、Terraform state、旧リソースの廃止まで一連の流れとして扱うことで、安全に移行できました。
