0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

# Terraformで構築したOCI VM+Load BalancerをOKEへ移行し、cert-managerでLet's Encryptを自動更新する

0
Last updated at Posted at 2026-08-04

この記事では、OCI上で稼働していた次のWeb構成をOracle Kubernetes Engine(OKE)へ移行した手順をまとめます。今回の移行対象は、実際に運営しているパソコン・周辺機器 売れ筋ガイドです。

letsgopc-oke-migration-infographic-anonymized.png

移行前: 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.net
    • www.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を廃止する

削除順序は次のようにしました。

  1. DNSが新Load Balancerを返すことを確認
  2. 新環境のHTTPSが200であることを確認
  3. 旧VMのNginxとCertbot timerを停止
  4. 旧証明書を失効・削除
  5. 旧Load BalancerをTerraformで削除
  6. 旧Private VMとブートボリュームをTerraformで削除
  7. 踏み台用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の構成も実環境へ同期

まとめ

今回の移行で特に重要だったのは次の点です。

  1. DNS切り替え前にcurl --resolveとSNIで新LBを検証する
  2. コンテナイメージはDeploymentが参照するタグまで更新する
  3. TLS終端後のリダイレクトにはX-Forwarded-Protoを使う
  4. cert-managerのHTTP-01を既存の80番Ingressへ載せる
  5. ACME solverの動的NodePortを、LBサブネットからだけ許可する
  6. 旧Terraform stateへの依存を外してから旧リソースを削除する
  7. 証明書だけでなく秘密鍵もローテーションする

OKEへの移行は、アプリケーションのデプロイだけでは終わりません。HTTPS、証明書更新、DNS、Terraform state、旧リソースの廃止まで一連の流れとして扱うことで、安全に移行できました。

参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?