5
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

ACMを利用した独自取得SSL証明書更新をAnsibleでやってみる

はじめに

Webサービスを運営しているとSSL証明書の更新作業が発生します。
AWSでELBを利用している場合、以下の2パターンになると思います。

  • 1.ACM(AWS Certificate Manager)でSSL証明書を発行し、ELBにアタッチ
  • 2.取得済みSSL証明書をELBにアタッチ(IAM)

1.ACMでSSL証明書を発行し、ELBにアタッチ

ACMでSSL証明書を発行してELBにアタッチしている場合、更新作業は不要です。以上。

2.取得済みSSL証明書をELBにアタッチ(IAM)

独自に取得したSSL証明書を利用する場合、

  • 1.IAMに新しいSSL証明書をアップロード
  • 2.ELBにアタッチされているSSL証明書を新しい証明書に切り替え
  • 3.上記を繰り返す

というような作業になります。

ACMが使えるのなら話は早いのですが、何らかの事情によりACMでSSL証明書を発行せず独自に取得したSSL証明書を利用するケースは意外と多いのではないでしょうか。

しかし、その場合でもACMを利用することで作業負荷を減らすことができます。

ACMを利用した取得済みSSL証明書更新

ACMを利用するメリットは、アップロードしたSSL証明書を上書きできることです。
ELBにアタッチしたSSL証明書を切り替える必要がないため、ACMに対する作業だけで済みます。

やり方

  • 1.ACMに取得済みSSL証明書をアップロード
  • 2.ELBに上記ACMのSSL証明書をアタッチ
  • 3.ACMのアップロード済みSSL証明書を更新

ACMを利用した取得済みSSL証明書更新(by Ansible)

上記作業をAnsibleでやってみます。

前提

  • AWS CLIが必要です。
  • credential情報は環境変数かaws configureでセットしてある必要があります。
  • ELBへのSSL証明書アタッチ作業はフォローしません。

機能

AnsibleにはACMを操作するモジュールがありませんので、shellモジュールでAWS CLIを実行します。

SSL証明書新規登録/更新

新規登録

  • ARN未指定の場合
  • work/にあるSSL証明書ファイルをアップロード
  • 登録後、work/にアサインされたARNをテキストファイルで保存
  • SSL証明書ごとのリージョン指定に対応(CloudFrontのため)
    • 未指定の場合、my_vars.aws.common.regionで指定したリージョンとなる

更新

  • ARN指定かつyearタグを変更した時
  • yearタグを変更しない限り、更新をスキップする。同一証明書を更新しても実害はないが、アカウントあたり20回/年の制限に引っかかるため
    • アップロードしすぎるとYou have imported the maximum number of 20 certificates in the last year.のようなエラーが出ます。公式ドキュメントにはないような。。。

タグ更新

  • タグのみの更新はできない。新規登録時またはyearタグ変更時、タグも更新される
  • 複数指定可能
  • タグ削除には未対応

ディレクトリ構成

ディレクトリ構成
site.yml
roles/
|--acm/
|  |--tasks/
|  |  |--main.yml
|  |--templates/
|  |  |--save_arn.j2
hosts/aws                  #inventory
host_vars/
|--localhost.yml
work/                      #SSL証明書ファイルを配置しておく
|--example.com-2017.crt        #サーバ証明書
|--example.com-2017.ca-bundle  #中間証明書
|--example.com-2017.key        #サーバ秘密鍵(パスフレーズなし)

inventory

AWSリソース関連モジュールはすべてlocalhostで実行するので、下記のようなインベントリファイルを用意します。

hosts/aws
[aws]
localhost

vars

こんな感じに変数を定義します。今回はhost_varsで定義しました。

host_vars/localhost.yml
---
my_vars:
  aws:
    common:
      region: ap-northeast-1
    acm:
      ssl_crt:
        example_com:
          name: example.com
          # arn: arn:aws:acm:ap-northeast-1:XXXXXXX:certificate/XXXXXXXXXX #アップロード済みの場合ARNを指定する
          tags:
            - key: year
              value: '2017'
            # - key: tag_key   #他のタグをつける場合
            #   value: tag_value
          files:
            crt:
              org_file: example.com-2017.crt
            chain:
              org_file: example.com-2017.ca-bundle
            key:
              org_file: example.com-2017.key
        example_com_cloudfront:
          name: example.com
          region: us-east-1 #CloudFrontはus-east-1固定
          # arn: arn:aws:acm:us-east-1:XXXXXXX:certificate/XXXXXXXXXX #アップロード済みの場合ARNを指定する
          tags:
            - key: year
              value: '2017'
            # - key: tag_key   #他のタグをつける場合
            #   value: tag_value
          files:
            crt:
              org_file: example.com-2017.crt
            chain:
              org_file: example.com-2017.ca-bundle
            key:
              org_file: example.com-2017.key

Role

  • 現在のyearタグの値を取得し(aws acm list-tags-for-certificate)、変数に辞書形式で格納します。yearタグがなければ「N/A」をセットします。
  • 与えられたyearタグの値と比較し、異なっていたら証明書をアップロード(aws acm import-certificate)し、タグをセット(aws acm add-tags-to-certificate)します。
  • aws acm import-certificateの戻り値はARNなので、これをtemplateモジュールでファイルに出力します。
roles/acm/tasks/main.yml
---
- name: tag(year)確認
  shell: >-
    {% if item.value.region is defined %}
    export AWS_DEFAULT_REGION='{{ item.value.region }}' && \
    {% else %}
    export AWS_DEFAULT_REGION='{{ my_vars.aws.common.region }}' && \
    {% endif %}
    aws acm list-tags-for-certificate \
     --certificate-arn {{ item.value.arn }} \
     --query 'Tags[?Key == `year`].Value' \
     --output text
  with_dict: "{{ my_vars.aws.acm.ssl_crt }}"
  when: item.value.arn is defined
  check_mode: no
  changed_when: no
  register: acm_tags

- name: tag(year) dict作成
  set_fact:
    year_tag_dict: >-
      {%- set tmpdict = {} -%}
      {%- for i in range(acm_tags.results|length) -%}
      {%-   if acm_tags.results[i].stdout is undefined -%}
      {%-     set _ = tmpdict.update({acm_tags.results[i].item.value.name: "N/A"}) -%}
      {%-   else -%}
      {%-     set _ = tmpdict.update({acm_tags.results[i].item.value.name: acm_tags.results[i].stdout}) -%}
      {%-   endif -%}
      {%- endfor -%}
      {{ tmpdict }}
  check_mode: no
  changed_when: no

- name: SSL証明書アップロード
  shell: >-
    {% if item.value.region is defined %}
    export AWS_DEFAULT_REGION='{{ item.value.region }}' && \
    {% else %}
    export AWS_DEFAULT_REGION='{{ my_vars.aws.common.region }}' && \
    {% endif %}
    CRT_ARN=$( \
    aws acm import-certificate \
     {% if item.value.arn is defined %}
     --certificate-arn {{ item.value.arn }} \
     {% endif %}
     --certificate file://{{ inventory_dir | dirname }}/work/{{ item.value.files.crt.org_file }} \
     --certificate-chain file://{{ inventory_dir | dirname }}/work/{{ item.value.files.chain.org_file }} \
     --private-key file://{{ inventory_dir | dirname }}/work/{{ item.value.files.key.org_file }} \
     --output text \
     ) && \
    aws acm add-tags-to-certificate \
     --certificate-arn ${CRT_ARN} \
     --tags Key=Name,Value={{ item.value.name }} \
      {% set list = [] %}
      {%- for i in range(item.value.tags|length) -%}
      {%-   set _ = list.append("Key="+item.value.tags[i]['key']+",Value="+item.value.tags[i]['value']) -%}
      {%- endfor -%}
      {{ list | join(' ')}} && \
    echo ${CRT_ARN}
  with_dict: "{{ my_vars.aws.acm.ssl_crt }}"
  when: >-
    not ansible_check_mode
    and year_tag_dict["{{ item.value.name }}"] != "{{ item.value.tags[0].value }}"
  register: acm_crt

- name: save ARN
  template:
    src: save_arn.j2
    dest: "{{ inventory_dir | dirname }}/work/{{ item.item.value.name }}_arn.txt"
    mode: 0644
    group: wheel
  delegate_to: localhost
  changed_when: no
  become: yes
  with_items: "{{ acm_crt.results | default([]) }}"
  when: item.stdout is defined and not ansible_check_mode

template

roles/acm/templates/save_arn.j2
{{ item.stdout }}

site.yml

site.yml
---
- name: acm
  hosts: localhost
  connection: local
  roles:
    - role: acm

実行

Command
$ ansible-playbook -i hosts/aws -l localhost site.yml

まとめ

ACMは、独自取得SSL証明書をELBやCloudFrontにアタッチする際にも利用できます。
特にワイルドカード証明書を複数のELBで利用している場合、SSL証明書の更新作業が1回で済むので便利です。

AnsibleにはACM用モジュールがないため、Ansible内でAWS CLIを強引に実行していますが、ループ処理やテンプレートなどAnsibleの機能が使えるため、シェルスクリプトで全て実装するよりは簡単かと思います。

AnsibleでAWS CLIを実行する場合の参考になれば。

参考

AnsibleでAWSリソースを管理するシリーズ

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
5
Help us understand the problem. What are the problem?