証明書の有効期間は1年であることが多く、年に1度の更新作業は忘れやすい。サーバーが10台・20台と増えると、どのサーバーの証明書がいつ期限を迎えるかをスプレッドシートで管理するのは現実的でなくなる。
Ansibleのcommunity.cryptoコレクションを使えば、秘密鍵の生成・CSR作成・証明書の配布・期限確認を一つのroleで管理できる。「証明書の更新を人が覚えておく」という属人的な運用から抜け出せる。
この記事では、community.cryptoの証明書関連モジュールを活用した設計パターンを解説する。自己署名証明書の生成roleから、既存証明書の期限確認と自動更新、Let's Encryptとの連携まで、設計の考え方を中心にカバーする。Ansible VaultやRoleの基礎から体系的に習得したい方にはAnsibleの実践ハンズオン講座もぜひ参照してほしい。
この記事のポイント
・community.cryptoコレクションで秘密鍵・CSR・証明書の全工程をAnsible管理できる
・x509_certificate_infoで期限残日数を取得し、条件分岐で自動更新を設計できる
・Ansible Vaultと組み合わせて秘密鍵をリポジトリに安全に格納できる
・Let's Encrypt連携もacme_certificateモジュールで一つのPlaybookに統合できる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
SSL証明書を手動管理することのリスク
証明書管理の典型的な失敗パターンが2つある。一つ目は「更新を忘れて期限切れ」。証明書の更新作業がカレンダーの予定として個人の頭の中にしかなく、担当者の異動や休暇で見逃されるケースだ。Let's Encryptが無料で証明書を発行するようになってから「証明書のコストは0円」になったが、「更新の手間が0」にはなっていない。certbotのcronを設定しているだけで自動更新できるはずが、cronの環境変数が不足していて静かに失敗していた——という現場は珍しくない。
二つ目は「どのサーバーに何の証明書が入っているかわからない」。10台を超えると、各サーバーに個別にSSHして
openssl x509 -in /etc/ssl/certs/server.crt -noout -dates を叩いて確認するのは非現実的になる。Ansibleでこれを解決するアプローチは2つを組み合わせる。「証明書をAnsibleが生成・配布・管理する」構成と、「Ansibleが既存証明書の期限を一元監視する」構成だ。
community.cryptoで使える証明書関連モジュールの全体像
証明書管理に使うモジュールはすべてcommunity.cryptoコレクションに含まれる。まずコレクションのインストールを確認する。# community.cryptoコレクションのインストール $ ansible-galaxy collection install community.crypto Starting galaxy collection install process Process install dependency map Starting collection install process Downloading https://galaxy.ansible.com/download/community-crypto-2.21.1.tar.gz Installing 'community.crypto:2.21.1' to '~/.ansible/collections/ansible_collections/community/crypto' community.crypto:2.21.1 was installed successfully
・openssl_privatekey:秘密鍵の生成(RSA/EC両対応)
・openssl_csr:CSR(Certificate Signing Request)の生成
・x509_certificate:証明書の生成。自己署名・CAサインなど複数の provider を選べる
・x509_certificate_info:証明書のメタデータ(期限・SAN・発行者など)をJSON形式で取得
・acme_certificate:ACME v2プロトコル経由でLet's Encryptなどから証明書を取得
Ansible 2.10以降はすべてFQCN(完全修飾コレクション名)で呼び出すのが正式な書き方だ。短名(openssl_certificate)は引き続き動作するが、将来的に廃止予定のため新規Playbookでは
community.crypto.x509_certificate と書くことを推奨する。
秘密鍵とCSRを生成するroleの基本設計
自己署名証明書を生成して複数のWebサーバーに配布するroleの構成から始める。ディレクトリ構造は以下のとおりだ。roles/ssl_cert/ ├── defaults/ │ └── main.yml # 証明書の有効期間・鍵長などのデフォルト値 ├── handlers/ │ └── main.yml # Webサーバーのreloadハンドラー ├── tasks/ │ ├── main.yml # タスクのエントリーポイント │ ├── generate.yml # 秘密鍵・CSR・証明書の生成 │ └── deploy.yml # 配布とサービス再起動 └── files/ └── (暗号化済みの秘密鍵ファイルをVaultで格納する場合はここに置く)
# roles/ssl_cert/defaults/main.yml ssl_cert_key_size: 4096 ssl_cert_key_type: "RSA" ssl_cert_country: "JP" ssl_cert_org: "Example Corp" ssl_cert_validity_days: 365 ssl_cert_tmp_dir: "/tmp/ansible_ssl" ssl_cert_key_dir: "/etc/ssl/private" ssl_cert_crt_dir: "/etc/ssl/certs" ssl_cert_service_name: "nginx"
delegate_to: localhost でコントローラーノード上で証明書を生成し、各サーバーに配布する設計にすることで、管理対象ノードにOpenSSLのツールチェーンがインストールされていなくても動作する。
# roles/ssl_cert/tasks/generate.yml --- - name: 作業ディレクトリを確保する ansible.builtin.file: path: "{{ ssl_cert_tmp_dir }}" state: directory mode: "0700" delegate_to: localhost - name: 秘密鍵を生成する community.crypto.openssl_privatekey: path: "{{ ssl_cert_tmp_dir }}/{{ inventory_hostname }}.key" size: "{{ ssl_cert_key_size }}" type: "{{ ssl_cert_key_type }}" mode: "0600" delegate_to: localhost - name: CSRを生成する community.crypto.openssl_csr: path: "{{ ssl_cert_tmp_dir }}/{{ inventory_hostname }}.csr" privatekey_path: "{{ ssl_cert_tmp_dir }}/{{ inventory_hostname }}.key" common_name: "{{ ansible_fqdn }}" country_name: "{{ ssl_cert_country }}" organization_name: "{{ ssl_cert_org }}" subject_alt_name: - "DNS:{{ inventory_hostname }}" - "DNS:{{ ansible_fqdn }}" delegate_to: localhost - name: 自己署名証明書を生成する community.crypto.x509_certificate: path: "{{ ssl_cert_tmp_dir }}/{{ inventory_hostname }}.crt" privatekey_path: "{{ ssl_cert_tmp_dir }}/{{ inventory_hostname }}.key" csr_path: "{{ ssl_cert_tmp_dir }}/{{ inventory_hostname }}.csr" provider: selfsigned selfsigned_not_after: "+{{ ssl_cert_validity_days }}d" mode: "0644" delegate_to: localhost
# roles/ssl_cert/tasks/deploy.yml --- - name: 秘密鍵を配布する ansible.builtin.copy: src: "{{ ssl_cert_tmp_dir }}/{{ inventory_hostname }}.key" dest: "{{ ssl_cert_key_dir }}/{{ inventory_hostname }}.key" owner: root group: root mode: "0600" notify: reload webserver - name: 証明書を配布する ansible.builtin.copy: src: "{{ ssl_cert_tmp_dir }}/{{ inventory_hostname }}.crt" dest: "{{ ssl_cert_crt_dir }}/{{ inventory_hostname }}.crt" owner: root group: root mode: "0644" notify: reload webserver
# roles/ssl_cert/handlers/main.yml --- - name: reload webserver ansible.builtin.service: name: "{{ ssl_cert_service_name }}" state: reloaded
証明書の期限確認を自動化するタスク設計
証明書を配布した後は「期限の残り日数を定期チェックする」仕組みが必要だ。x509_certificate_info モジュールで証明書のメタデータを取得し、残り日数が閾値を下回ったら更新フローを起動する設計にする。
# tasks/check_expiry.yml --- - name: 証明書の有効期限情報を取得する community.crypto.x509_certificate_info: path: "{{ ssl_cert_crt_dir }}/{{ inventory_hostname }}.crt" register: cert_info - name: 有効期限の残り日数を計算する ansible.builtin.set_fact: cert_days_remaining: >- {{ ((cert_info.not_after | to_datetime('%Y%m%d%H%M%SZ')) - (ansible_date_time.iso8601 | to_datetime('%Y-%m-%dT%H:%M:%SZ'))).days }} - name: 期限確認の結果を表示する ansible.builtin.debug: msg: "{{ inventory_hostname }}: 証明書残り {{ cert_days_remaining }} 日(期限 {{ cert_info.not_after }})" - name: 期限が近い場合に警告を出す(30日未満) ansible.builtin.debug: msg: "【警告】{{ inventory_hostname }} の証明書は {{ cert_days_remaining }} 日で期限切れ" when: cert_days_remaining | int < 30 - name: 期限が30日未満なら自動更新する ansible.builtin.include_tasks: generate.yml when: cert_days_remaining | int < 30
TASK [証明書の有効期限情報を取得する] **** ok: [web01.example.com] ok: [web02.example.com] ok: [web03.example.com] TASK [期限確認の結果を表示する] **** ok: [web01.example.com] => { "msg": "web01.example.com: 証明書残り 342 日(期限 20271015103022Z)" } ok: [web02.example.com] => { "msg": "web02.example.com: 証明書残り 18 日(期限 20261103143011Z)" } ok: [web03.example.com] => { "msg": "web03.example.com: 証明書残り 289 日(期限 20270822080341Z)" } TASK [期限が近い場合に警告を出す(30日未満)] **** skipping: [web01.example.com] ok: [web02.example.com] => { "msg": "【警告】web02.example.com の証明書は 18 日で期限切れ" } skipping: [web03.example.com]
Let's Encryptと組み合わせたacme_certificateの設計
自己署名証明書ではなく、Let's Encryptの無料証明書を使うケースも多い。community.cryptoのacme_certificateモジュールで、ACME認証(Let's EncryptのDV証明書取得)をPlaybookに組み込める。
HTTP-01チャレンジを使うPlaybookの骨格を示す。
# tasks/letsencrypt.yml --- - name: ACME用アカウント秘密鍵を生成する(初回のみ) community.crypto.openssl_privatekey: path: "{{ ssl_cert_tmp_dir }}/acme_account.key" size: 4096 delegate_to: localhost run_once: true - name: ドメイン用の秘密鍵を生成する community.crypto.openssl_privatekey: path: "{{ ssl_cert_tmp_dir }}/{{ item }}.key" size: 2048 loop: "{{ ssl_domains }}" delegate_to: localhost - name: CSRを生成する community.crypto.openssl_csr: path: "{{ ssl_cert_tmp_dir }}/{{ item }}.csr" privatekey_path: "{{ ssl_cert_tmp_dir }}/{{ item }}.key" common_name: "{{ item }}" loop: "{{ ssl_domains }}" delegate_to: localhost - name: ACME Challenge(HTTP-01)を開始する community.crypto.acme_certificate: account_key_src: "{{ ssl_cert_tmp_dir }}/acme_account.key" csr: "{{ ssl_cert_tmp_dir }}/{{ item }}.csr" dest: "{{ ssl_cert_tmp_dir }}/{{ item }}.crt" fullchain_dest: "{{ ssl_cert_tmp_dir }}/{{ item }}_fullchain.crt" acme_directory: "https://acme-v02.api.letsencrypt.org/directory" acme_version: 2 challenge: "http-01" remaining_days: 30 loop: "{{ ssl_domains }}" delegate_to: localhost register: acme_challenge
.well-known/acme-challenge/ 配下にHTTPで到達できることを確認する。チャレンジファイルをWebサーバーの公開ディレクトリに設置し、応答が返ったら証明書が発行される流れだ。DNS-01チャレンジを使う場合は、DNSへのTXTレコード設定が必要になる。AWSのRoute53を使っているなら
community.aws.route53 と組み合わせてDNSレコードの自動設置もPlaybookに含められる。ワイルドカード証明書(*.example.com)はHTTP-01では取得できないためDNS-01を選ぶ。
Ansible Vaultで秘密鍵をリポジトリに安全に保管する設計
証明書管理のroleをGitリポジトリで管理する場合、秘密鍵をそのままfiles/ ディレクトリに置くと秘密鍵がリポジトリに残ってしまう。Ansible Vaultを使って秘密鍵ファイル自体を暗号化してリポジトリに保存するアプローチが一般的だ。
# 秘密鍵を生成した後、Vaultで暗号化する $ ansible-vault encrypt /tmp/ansible_ssl/web01.example.com.key --output roles/ssl_cert/files/web01.example.com.key.vault Encryption successful # 暗号化されたことを確認する $ head -1 roles/ssl_cert/files/web01.example.com.key.vault $ANSIBLE_VAULT;1.1;AES256
ansible.builtin.copy の src にvaultで暗号化したファイルを指定するだけでよい。ansible-playbook実行時に --vault-password-file または --ask-vault-pass を指定すれば、Ansibleが自動的に復号して配布する。
- name: 暗号化された秘密鍵を配布する(Vaultが自動復号) ansible.builtin.copy: src: "web01.example.com.key.vault" dest: "/etc/ssl/private/web01.example.com.key" owner: root group: root mode: "0600" notify: reload webserver
・コントローラーローカルに置く設計:リポジトリには含めず、コントローラーノードのローカルファイルシステムに保持する。シンプルだが、コントローラーノードが単一障害点になるリスクがある
・Vaultで暗号化してリポジトリに含める設計:Gitで履歴管理でき、複数チームメンバーで共有できる。ただしVaultのパスワード管理が別途必要になる
チームの規模とセキュリティポリシーに合わせて選択する。CI/CD環境(GitHub ActionsやAWX)で自動実行する場合は、VaultのパスワードをCI側のSecretsに格納してPlaybook実行時に渡す構成が多い。
トラブルシュート|証明書関連のよくあるエラー
1. community.cryptoコレクションが見つからない
エラーメッセージ:couldn't resolve module/action 'community.crypto.x509_certificate'requirements.ymlにコレクションが記載されていないか、
ansible-galaxy collection install が実行されていないケースだ。
# requirements.yml にコレクションを記載する $ cat requirements.yml --- collections: - name: community.crypto version: ">=2.0.0" # インストールする $ ansible-galaxy collection install -r requirements.yml
2. Pythonのcryptographyパッケージが不足している
エラーメッセージ:You need to have the Python cryptography library (>= 1.6) installed on the remote hostcommunity.cryptoモジュールの多くはPythonのcryptographyパッケージを必要とする。
# RHEL系(RHEL 9.4で確認) $ sudo dnf install python3-cryptography # Ubuntu系(Ubuntu 24.04 LTSで確認) $ sudo apt install python3-cryptography
3. delegate_to: localhostでPermission denied
エラーメッセージ:Permission denied: '/tmp/web01.key'秘密鍵の生成先
/tmp/ がスティッキービット(chmod 1777)になっている場合、rootで生成したファイルを別ユーザーが読めないことがある。defaults/main.ymlの ssl_cert_tmp_dir をansibleユーザーがアクセスできる専用ディレクトリに変更する。
# defaults/main.yml で生成先を変更する ssl_cert_tmp_dir: "{{ lookup('env', 'HOME') }}/.ansible/ssl"
4. x509_certificate_infoの日付計算でずれが出る
証明書のnot_after フィールドはUTCで返ってくる。サーバーのタイムゾーンがJSTに設定されている場合、ansible_date_timeとの比較で9時間のズレが生じることがある。比較には必ずUTC基準で行う。
# ansible_date_time.iso8601_microを使いUTC比較を確実にする cert_days_remaining: >- {{ ((cert_info.not_after | to_datetime('%Y%m%d%H%M%SZ')) - (ansible_date_time.iso8601_micro | to_datetime('%Y-%m-%dT%H:%M:%S.%fZ'))).days }}
本記事のまとめ
| やりたいこと | 使うモジュール・設計アプローチ |
|---|---|
| RSA秘密鍵を生成する | community.crypto.openssl_privatekey |
| CSRを生成する | community.crypto.openssl_csr |
| 自己署名証明書を生成する | community.crypto.x509_certificate(provider: selfsigned) |
| 証明書の期限残日数を確認する | community.crypto.x509_certificate_info + set_fact |
| 30日未満で自動更新する | when条件 + include_tasks |
| Let's Encryptの証明書を取得する | community.crypto.acme_certificate(acme_version: 2) |
| 秘密鍵をリポジトリに安全に保存する | Ansible Vault(ansible-vault encrypt) |
| 生成処理をコントローラーに集約する | delegate_to: localhost |
community.cryptoの他のモジュール(PKCS#12生成・証明書失効リスト管理など)も活用すれば、証明書ライフサイクル全体をAnsibleで管理できる。
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:Ansibleで設定ドリフトを自動検出する設計|定期checkモードとSlack通知で構成ずれを継続監視する方法
- この記事の属するカテゴリ:Ansibleへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます