API Gatewayの1つであるKong GatewayにはSaaS版があり、Konnectという名前で展開されている。
このKonnectでは証明書を登録し、管理する機能がある。
Konnectはこの証明書の有効期限を監視・通知する機能を'24/9時点では提供していない。
しかし、APIによる期限の取得は可能となっているため、ユーザ自身が作り込むことで監視・通知することが出来るようになっている。
今回はこれを使って証明書の有効期限をLambdaで取得し、CloudWatchにメトリクスとして登録してアラート通知を受け取るようにしてみる。
Konnect APIの基礎
KonnectはAPIによる操作も可能で、操作にはPersonal Access Token(PAT)を使ってアクセスする。公式の説明ページはこちら。
PATはKonnectにログイン後、右上のユーザのアイコンからPersonal Access Tokens
から発行できる。
証明書は各Control Plane(CP)のAPIエンドポイントを参照することで確認できる。
エンドポイントの仕様は以下となる。
仕様に従って試しに叩いてみる。
叩く上でcontrolPlaneID
が必要となるが、これはKonnectでCP選択後にOverviewから確認できる
CPのIDとPATを環境変数に設定する。
CP_ID="baa71427-4848-4d59-bca4-1fb276a2fec2"
KONNECT_TOKEN=kpat_ER1WQ0rnLX8xxxx
curlでAPIのエンドポイントにアクセスする。
curl -H "Authorization: Bearer $KONNECT_TOKEN" https://us.api.konghq.com/v2/control-planes/${CP_ID}/core-entities/certificates | jq .
以下のようなJSONフォーマットの結果が返ってくる。
{
"data": [
{
"cert": "-----BEGIN CERTIFICATE-----\nMIIDk
:(省略)
"metadata": {
"dns_names": [
"anatropia.mydomain",
"bromobenzene.mydomain"
],
"expiry": "1748741716",
"issuer": "CN=rootca",
:(省略)
data
は配列となっていて、証明書がそれぞれ格納されており、expiry
に期限(UNIX時間)が入っている。
$ date -r 1748741716
2025年 6月 1日 日曜日 10時35分16秒 JST
なので expiry
- 今日のUNIX時間 = 有効期限 をメトリクスとして登録すれば、証明書の期限をCloud Watch上で監視できるようになる。
実装
ここでは以下のように実装する
- Lambda(Python)で実装する
- PATはSecrets Managerに登録し、Lambdaから参照する
- CPのIDはイベントとして1つだけ渡す
- 証明書の期限はCP上にあるもの全てをメトリクスとして登録する
- Lambdaの呼び出しはEventBridgeで日次で呼び出す
- 全ての設定はCLIで行う
Secrets ManagerへPATを登録
先ほどPATを設定した環境変数KONNECT_TOKEN
を使ってSecrets Managerにkonnect-token
という名前でPATを登録する。
aws secretsmanager create-secret \
--name konnect-token \
--secret-string "{\"konnect-token\":\"${KONNECT_TOKEN}\"}"
これはLambdaがKonnectとアクセスする際に利用する。
権限の設定
最初にLambdaの実行ロールを作成し、ポリシーをアタッチする。
aws iam create-role --role-name lambda-ex --assume-role-policy-document '{"Version": "2012-10-17","Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}'
aws iam attach-role-policy --role-name lambda-ex --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
今回はPATをSecrets Managerから参照するので、SecretManagerから参照できるようポリシーを追加でアタッチする。
SecretのARNを取得する。
aws secretsmanager describe-secret --secret-id konnect-token
取得したARNに対しポリシーを設定する
aws iam put-role-policy --role-name lambda-ex \
--policy-name SecretsManagerAccess \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:us-east-1:<アカウントID>:secret:konnect-token-<文字列>"
}
]
}'
LambdaからCloudWatchにメトリクスをPushするためのポリシーを付与する。
cat <<EOF > ./cloudwatch-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "cloudwatch:PutMetricData",
"Resource": "*"
}
]
}
EOF
aws iam create-policy \
--policy-name LambdaCloudWatchPutMetricAccess \
--policy-document file://cloudwatch-policy.json
aws iam attach-role-policy \
--role-name lambda-ex \
--policy-arn arn:aws:iam::<アカウントID>:policy/LambdaCloudWatchPutMetricAccess
コードの作成
Lambdaで実行するコードを作成する。
適当なディレクトリを作成する。
mkdir kong_cert_monitor
cd kong_cert_monitor
コードを作成する。
cat <<EOF > ./lambda_function.py
import requests
import json
from datetime import datetime
import boto3
from botocore.exceptions import ClientError
def get_secret():
secret_name = "konnect-token"
region_name = "us-east-1"
session = boto3.session.Session()
client = session.client(
service_name='secretsmanager',
region_name=region_name
)
try:
get_secret_value_response = client.get_secret_value(
SecretId=secret_name
)
except ClientError as e:
raise e
print(f"Secret String: {get_secret_value_response['SecretString']}")
secret_dict = json.loads(get_secret_value_response['SecretString'])
return secret_dict.get(secret_name)
def get_certificates(api_token, cp_id, certificate_type="certificates"):
url = f"https://us.api.konghq.com/v2/control-planes/{cp_id}/core-entities/{certificate_type}"
headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json"
}
print(f"Requesting {certificate_type} from {url}")
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
print(f"{certificate_type.capitalize()} data retrieved: {response.json()}")
except requests.exceptions.RequestException as e:
print(f"Error retrieving {certificate_type}: {e}")
print(f"Bearer {api_token}")
raise e
return response.json()
def check_certificate_expiry(cert_data):
expiry_dates = []
for cert in cert_data['data']:
try:
expiry_timestamp = int(cert['metadata']['expiry'])
expiry_date = datetime.utcfromtimestamp(expiry_timestamp)
certificate_id = cert['id']
print(f"Certificate {certificate_id} expiry date: {expiry_date}")
expiry_dates.append((certificate_id, expiry_date))
except KeyError as e:
print(f"Error extracting expiry date for certificate {cert['id']}: {e}")
continue
return expiry_dates
def put_cloudwatch_metric(expiry_date, certificate_id, metric_name='CertificateExpiry'):
cloudwatch = boto3.client('cloudwatch')
try:
print(f"Sending metric data to CloudWatch for certificate {certificate_id} with metric name {metric_name}")
cloudwatch.put_metric_data(
Namespace='KongCertificates',
MetricData=[
{
'MetricName': metric_name,
'Dimensions': [
{
'Name': 'CertificateId',
'Value': certificate_id
},
],
'Timestamp': datetime.utcnow(),
'Value': (expiry_date - datetime.utcnow()).days,
},
]
)
print(f"Metric data sent to CloudWatch for certificate {certificate_id} with metric name {metric_name}")
except ClientError as e:
print(f"Error sending metric data to CloudWatch for certificate {certificate_id}: {e}")
raise e
def lambda_handler(event, context):
try:
print("Starting Lambda function")
cp_id = event.get('cp_id')
if not cp_id:
print("Error: cp_id is not provided")
return
api_token = get_secret()
# 通常の証明書の取得と処理
cert_data = get_certificates(api_token, cp_id, certificate_type="certificates")
expiry_dates = check_certificate_expiry(cert_data)
for certificate_id, expiry_date in expiry_dates:
put_cloudwatch_metric(expiry_date, certificate_id)
# CA証明書の取得と処理
ca_cert_data = get_certificates(api_token, cp_id, certificate_type="ca_certificates")
ca_expiry_dates = check_certificate_expiry(ca_cert_data)
for certificate_id, expiry_date in ca_expiry_dates:
put_cloudwatch_metric(expiry_date, certificate_id, metric_name='CA CertificateExpiry')
print("Lambda function completed successfully")
except Exception as e:
print(f"Error in lambda_handler: {e}")
raise e
EOF
実行に必要なファイルをインストールする。
pip install requests -t .
なお、自分の環境だと以下のエラーが出た。
[ERROR] Runtime.ImportModuleError: Unable to import module 'lambda_function': urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'OpenSSL 1.0.2k-fips 26 Jan 2017'. See: https://github.com/urllib3/urllib3/issues/2168
Traceback (most recent call last):
なので追加でurllibのバージョンを固定でインストールした。
pip install --upgrade urllib3==1.26.16 -t .
ファイル一式をzipに固めてLambdaにKongCertMonitorという名前で登録する。
zip -r ../kong_cert_monitor.zip .
Lambdaの関数を作成する。
aws lambda create-function \
--function-name KongCertMonitor \
--runtime python3.8 \
--role arn:aws:iam::<アカウントID>:role/lambda-ex \
--handler lambda_function.lambda_handler \
--zip-file fileb://../kong_cert_monitor.zip \
--no-cli-pager
なお、2回目以降は以下で更新する。
aws lambda update-function-code \
--function-name KongCertMonitor \
--zip-file fileb://../kong_cert_monitor.zip \ --no-cli-pager
動作確認
実行に3秒以上掛かることがあるので、デフォルトのタイムアウト値から30秒に変更する。
aws lambda update-function-configuration \
--function-name KongCertMonitor \
--timeout 30 --no-cli-pager
aws lambda invoke
を使ってテストする。
PAYLOAD=$(echo -n '{"cp_id": "'"${CP_ID}"'"}' | base64)
aws lambda invoke \
--function-name KongCertMonitor \
--payload "$PAYLOAD" \
output.txt
問題なければ以下のような出力が得られる。
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
ログも見てみる。
最新のログストリーム名を取得する。
LOG_STREAM=$(aws logs describe-log-streams --log-group-name /aws/lambda/KongCertMonitor --query "logStreams[].[logStreamName]" --output text | tail -n 1)
ログを取得する。
aws logs get-log-events \
--log-group-name /aws/lambda/KongCertMonitor \
--log-stream-name "$LOG_STREAM" \
--query "events[].[message]" \
--output text
証明書が上手く取得できて飛ばせている様子が確認できるはずだ。
Cloud WatchのUIからもメトリクスが確認できる。反映に少し時間が掛かる気がするので、すぐに表示できない場合は10分程度待ってから見てみると確認できる。
日次で実行させる
日次で実行するのにEventBridgeを利用する。
まず日次実行のためのイベントを作成する。
aws events put-rule \
--name DailyKongCertMonitor \
--schedule-expression "cron(0 0 * * ? *)" \
--description "Run Lambda function to monitor Kong certificates every day at midnight"
--schedule-expression "cron(0 0 * * ? *)"
で毎日UTCで0時(日本時間で午前9時)に実行するよう指定している。
次にLambdaに対して先ほど作成したイベントからのアクセスを許可する。
aws lambda add-permission \
--function-name KongCertMonitor \
--statement-id DailyKongCertMonitorPermission \
--action "lambda:InvokeFunction" \
--principal events.amazonaws.com \
--source-arn arn:aws:events:<リージョン>:<アカウントID>:rule/DailyKongCertMonitor
先ほど作成したイベントとLambda関数を紐づける。
ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --no-cli-pager --output text)
cat <<EOF > ../input.json
{
"Rule": "DailyKongCertMonitor",
"Targets": [
{
"Id": "1",
"Arn": "arn:aws:lambda:$AWS_REGION:359304582046:function:KongCertMonitor",
"Input": "{\"cp_id\": \"$CP_ID\"}"
}
]
}
EOF
aws events put-targets --cli-input-json file://../input.json
アラートを設定する
最初にアラートに関するSNSトピックを作成し、自身のメールアドレスを購読対象とする。
MY_MAIL=xxx@xxx.xxx
aws sns create-topic --name KonnectCertAlarm
aws sns subscribe \
--topic-arn arn:aws:sns:${AWS_REGION}:${ACCOUNT_ID}:KonnectCertAlarm \
--protocol email \
--notification-endpoint $MY_MAIL
コマンド実行後、購読するかの確認のメールが届くので、Confirm subscriptionをクリックして購読する。
次にアラームを作成し、作成したSNSトピックと紐づける。
ここではaws cloudwatch put-metric-alarm
を使って証明書ごとにアラームを1つ作成する。
ただ、何度も呼ぶのは面倒なので、アラームを作成するためのスクリプトを用意する。
cd ..
cat <<'EOF' > ./set_cert_alerm.sh
#!/bin/bash
if [ "$#" -lt 2 ]; then
echo "Usage: $0 <MetricName> <CertId1> [<CertId2> ... <CertIdN>]"
exit 1
fi
METRIC_NAME=$1
shift
CERT_IDS=("$@")
AWS_REGION=${AWS_REGION:-us-east-1}
ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --no-cli-pager --output text)
for CERT_ID in "${CERT_IDS[@]}"; do
echo "Creating alarm for Certificate ID: $CERT_ID"
aws cloudwatch put-metric-alarm \
--alarm-name "${METRIC_NAME} Below 300 for $CERT_ID" \
--metric-name "$METRIC_NAME" \
--namespace "KongCertificates" \
--statistic "Minimum" \
--treat-missing-data "notBreaching" \
--threshold 300 \
--comparison-operator "LessThanThreshold" \
--evaluation-periods 1 \
--period 86400 \
--dimensions "Name=CertificateId,Value=$CERT_ID" \
--alarm-actions "arn:aws:sns:${AWS_REGION}:${ACCOUNT_ID}:KonnectCertAlarm"
if [ $? -eq 0 ]; then
echo "Alarm created successfully for Certificate ID: $CERT_ID"
else
echo "Failed to create alarm for Certificate ID: $CERT_ID"
fi
done
EOF
chmod +x ./set_cert_alerm.sh
引数の詳細な説明は省くが、--threshold 300
にしているのはメトリクスで一番小さいものが257(証明書が切れるまで残り257日)となっていて、これを使ってアラートが上がることを確認したいからである。
この値については環境に合わせて修正するとよい。
なおコマンド仕様としては最初の引数でメトリクス名を渡し、2番目以降の引数に証明書のID(Konnectから取得できるID)を渡して設定する。
スクリプトを使ってアラームを作成する。最初に通常のCertificateに対するアラームを作成する。
./set_cert_alerm.sh "CertificateExpiry" 665c146b-5138-482b-8f75-4148c413a191 8042fd85-7ff5-44fb-ae7f-879f0c8f45a1
次にCA Certificateに対するアラームを作成する。
./set_cert_alerm.sh "CA CertificateExpiry" 6f41b3de-ca61-42c1-a63e-68b2249195fc 9eb569ac-81ce-4308-87c0-6aedf0e508d2
なお、ここで動作確認をしたいのだが、一時的にEventBridgeの実行間隔を5分間隔と早めてメトリクスが溜まるようにした。
アラーム作成後、CloudWatchのコンソールを確認するとアラームが確認でき、発火したことも確認できる。
メールでも以下のような通知が届いていることが確認できた。
You are receiving this email because your Amazon CloudWatch Alarm "CertificateExpiry Below 300 for 665c146b-5138-482b-8f75-4148c413a191" in the US East (N. Virginia) region has entered the ALARM state, because "Threshold Crossed: 1 datapoint [257.0 (16/09/24 06:16:00)] was less than the threshold (300.0)." at "Monday 16 September, 2024 06:21:55 UTC".