はじめに
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で実行するので、下記のようなインベントリファイルを用意します。
[aws]
localhost
vars
こんな感じに変数を定義します。今回はhost_varsで定義しました。
---
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
モジュールでファイルに出力します。
---
- 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
{{ item.stdout }}
site.yml
---
- name: acm
hosts: localhost
connection: local
roles:
- role: acm
実行
$ 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を実行する場合の参考になれば。