監視の必要性
皆さん、SSL/TLS証明書をELBで利用していますか。
期限が切れてしまうと、即サービスに影響しますので、監視しておくことが重要ですよね。
え?証明書なんて自動更新されるでしょ。監視なんて要らないよ。
はい。確かにACMで発行された証明書の場合は、失効前に自動更新されるようになっていますが、どうしてもサードパーティーで発行した証明書を使わなければならない ケースもあります。
その場合に、ACMでは自動更新しません。
忘れないようにと、毎日・毎週手動でチェックしにいくわけにもいかないので、適切なタイミングで通知を受け取りたいです。
CloudWatchの方法
クラスメソッドさんで公開されている情報にありますが、2021年3月より CloudWatchの DaysToExpiry
メトリクスで取得できるようになっています。
このメトリクスでCloudWatchアラームを設定すれば、自分のベストタイミングの日数に通知を受け取ることができます。
CloudWatchの方法で困ること
CloudWatchで設定する場合に、いくつか困ることがあります。
- 新しい証明書をACMにインポートするごとにCloudWatchアラームの設定が必要
- 期限切れ前に証明書を入れ替えたタイミングで古いCloudWatchアラームは削除が必要
- ELBの廃止で使わなくなった証明書のCloudWatchアラームも削除が必要
- ACMの情報だけだと、どのELBが影響するのかがパッと分からない(対象ELBが複数あると特に)
これを解決するためには、Lambda関数によるカスタム監視ですよね。
そっかー、Lambda関数を作るしかないなあー、仕方ないなあー(単に作りたいだけ)
Lambda関数の方法
概要
デフォルトリージョンの全てのELBに対して、証明書を利用しているか、および、そのACMの期限をチェックします。
last_days
変数で指定したしきい値を切っていたら、通知します。
自動更新の証明書なら(RenewalEligibility が ELIGIBLE)、通知の対象外にします。
これにより、メンテナンスフリーで、かつ、必要十分な通知を行うことができます。
さらに通知内容には、ELBのARN、リスナーのARN、ドメイン名を含めることでパッと分かるようにします。
コード
Pythonで記述します。
import boto3
import json
from datetime import datetime, timedelta
elbv2 = boto3.client('elbv2')
# 期限切れまでの残り日数のしきい値
last_days = 30
# ACMリスト
acms = boto3.client('acm').list_certificates()['CertificateSummaryList']
# ELB(elbv2)リスト
elbs = elbv2.describe_load_balancers()['LoadBalancers']
# 期限切れが近いかをチェック
# 自動更新の対象ではなく かつ 期限切れ近いなら引数をそのまま返す
def select_expiration_warning(filtered):
if not filtered:
return None
if filtered[0]['RenewalEligibility'] == 'ELIGIBLE':
return None
if filtered[0]['NotAfter'].replace(tzinfo=None) - datetime.now() > timedelta(days=last_days):
return None
return filtered
def lambda_handler(event, context):
expiration_warnings = []
# ELB, Listener, Certification の組み合わせリスト生成
elbs_listeners_certs = []
for elb_arn in map(lambda x: x['LoadBalancerArn'], elbs):
for listener in elbv2.describe_listeners(LoadBalancerArn=elb_arn)['Listeners']:
for cert in listener.get('Certificates', []):
elbs_listeners_certs.append( (elb_arn, listener, cert ) )
# 組み合わせリストの単位で期限切れが近いかをチェック
for e_l_c in elbs_listeners_certs:
cert_filtered = list(filter(lambda x: x['CertificateArn'] == e_l_c[2]['CertificateArn'], acms))
if select_expiration_warning(cert_filtered):
# warning対象をリストに格納
expiration_warnings.append({
'LoadBalancer': e_l_c[0],
'ListenerArn': e_l_c[1]['ListenerArn'],
'CertificateArn': cert_filtered[0]['CertificateArn'],
'NotAfter': cert_filtered[0]['NotAfter'].isoformat(),
'DomainName': cert_filtered[0]['DomainName']
})
if len(expiration_warnings) > 0:
boto3.client('sns').publish(
TopicArn = 'arn:aws:sns:REGION_NAME:ACCOUNT_ID:TOPIC_NAME',
Message = json.dumps(expiration_warnings, indent=4)
)
return {}
このコードで書き換えが必須の箇所は、
arn:aws:sns:REGION_NAME:ACCOUNT_ID:TOPIC_NAME
の箇所です。
ここは各自のSNSトピックを作成しておき、そのARNを指定します。
Lambda関数の設定
- ランタイム
- Python 3.10
- 実行ロールのIAMポリシー
- AWSLambdaBasicExecutionRole
- AWSCertificateManagerReadOnly
- ElasticLoadBalancingReadOnly
- AmazonSNSFullAccess (※sns.publishが利用できるポリシーに絞ってもOK)
- タイムアウト
- 3秒 → 3 + <リスナー数 × 0.5>秒 程度 (少し余裕を持たせておく)
last_days をチューニングする必要があれば、
import os
last_days = int(os.getenv('last_days'))
としてもいいでしょう。
EventBridgeの設定
EventBridgeスケジューラで、Lambdaを1日1回実行するように設定します。
CloudWatchアラームを使うたびに思う
CloudWatchアラームは、1つ1つのリソースに設定するので、運用する中でリソースが増加・減少するたびに、CloudWatchアラームも(ある程度は自動化できるとしても)メンテナンスが必要だなぁと感じています。
Lambda関数で動的にリソースのリストを取得することで、リソースが増加・減少してもメンテフリーにできる方が楽!というケースがところどころあるんですよね。
適材適所で使っていきましょう。