はじめに
AWS AppConfigは、AWSの設定情報を安全にデプロイするためのマネージドサービスだ。
アプリケーションの設定変更をカナリアリリースすることができ、デプロイ中の異常に対してワンクリックでの迅速なロールバックを提供することもできる。
今回は、簡単なAWS AppConfigをTerraformで自動構築できるようにし、カナリアリリースにおける運用を考察する。
前提となる知識は、以下があれば良い。
- カタログレベルでのAWS AppConfigの知識
- Terraformの基礎知識
AWS AppConfigの構成要素
AWS AppConfigは、以下の構成となっている。
構成要素 | 説明 |
---|---|
アプリケーション | 設定プロファイルを束ねる一番大きな単位。実際に設定を利用するアプリケーションの単位と考えると分かりやすい |
設定プロファイル | 複数のホストされた設定バージョンを束ねたもの。たとえば、「予約値」「予約適用日」のように関連した設定を一括りにまとめるようなイメージ |
ホストされた設定バージョン | 設定プロファイルの実際の値を記述する部分 |
環境 | 設定プロファイルを適用する先の環境 |
デプロイ戦略 | どういう設定でデプロイをするか(LINEARなのかEXPONENTIALなのか/何分かけてデプロイするか/ベーキングの時間をどれくらい取るかの設定) |
デプロイ | どの環境にどのバージョンを何のデプロイ戦略でデプロイするかといったデプロイメントの設定情報 |
分かりにくいので、AWSのマネージメントコンソール上でのまとまり方を図で整理すると以下のようになる。
デプロイバージョン1の時にAWS AppConfigから値を取得すると{ 'version': 'stable' }
が取得され、デプロイバージョン2の時に取得すると{ 'version': 'new' }
となる。デプロイバージョン2のデプロイ中は、参照のタイミングによって値が混在して返されることになる。
Terraformで書いてみる
それぞれの構成情報をTerraformで書くと以下のようになる。
アプリケーション
アプリケーションはaws_appconfig_application
のリソースで表現する。
description
は適宜分かりやすい説明を入れておこう。
resource "aws_appconfig_application" "example" {
name = local.appconfig_application_name
description = "Example AppConfig Application"
}
設定プロファイル
設定プロファイルはaws_appconfig_configuration_profile
で表現する。
location_uri
を設定して、この後ホストされた設定バージョンとの紐づけを行う。
description
は適宜分かりやすい説明を(以下同文)
resource "aws_appconfig_configuration_profile" "example" {
name = local.appconfig_configuration_profile_name
description = "Example AppConfig Configuration Profile"
application_id = aws_appconfig_application.example.id
location_uri = "hosted"
}
ホストされた設定バージョン
ホストされた設定バージョンはaws_appconfig_hosted_configuration_version
で表現する。
content
、実際にアプリケーションに返す設定情報を記述しよう。
description
は適宜分かりやすい(以下同文)
resource "aws_appconfig_hosted_configuration_version" "example" {
description = "Example AppConfig Hosted Configuration Version"
application_id = aws_appconfig_application.example.id
configuration_profile_id = aws_appconfig_configuration_profile.example.configuration_profile_id
content_type = "application/json"
content = jsonencode({
version = "stable",
})
}
環境
環境はaws_appconfig_environment
で表現する。
description
は適宜(以下同文)
resource "aws_appconfig_environment" "example" {
name = local.appconfig_environment_name
description = "Example AppConfig Environment"
application_id = aws_appconfig_application.example.id
}
デプロイ戦略
デプロイ戦略はaws_appconfig_deployment_strategy
で表現する。
description
は(以下同文)
final_bake_time_in_minutes
は、AWSマネージメントコンソールで「ベーキング」と表現される部分で、デプロイが100%新機能への切り替わりを迎えた後に、ロールバックのために何分間その状態で残しておくかという設定だ。この期間であれば、マネージメントコンソールのロールバックボタンからワンタッチでのロールバックが行える。
resource "aws_appconfig_deployment_strategy" "example" {
name = local.appconfig_deployment_strategy_name
description = "Example AppConfig Deployment Strategy"
deployment_duration_in_minutes = 10
final_bake_time_in_minutes = 1
growth_factor = 10
growth_type = "EXPONENTIAL"
replicate_to = "NONE"
}
デプロイメント
デプロイメントはaws_appconfig_deployment
で表現する。
description
(以下同文)
ここまで作ってきたもろもろのリソースを指定すれば、新しいデプロイバージョンが払い出されて、そのバージョンの応答として設定値が返却される。
resource "aws_appconfig_deployment" "example" {
description = "My example deployment"
application_id = aws_appconfig_application.example.id
configuration_profile_id = aws_appconfig_configuration_profile.example.configuration_profile_id
environment_id = aws_appconfig_environment.example.environment_id
deployment_strategy_id = aws_appconfig_deployment_strategy.example.id
configuration_version = aws_appconfig_hosted_configuration_version.example.version_number
}
実際にAWS AppConfigの設定を呼び出してみる
以下のようなAWS Lambda関数からAWS AppConfigを呼び出してみよう。
なお、AWS LambdaからAWS AppConfigを呼び出す場合、ユーザーガイドに従い、既にAWSが準備してくれているLambda Layerを呼び出す。Lambda Layerがプロキシ・キャッシュサーバとなって良い感じにAWS AppConfigとの通信を肩代わりしてくれる。
"""Lambda Function Example for AWS AppConfig"""
from urllib import request
import ast
import json
import os
APPCONFIG_APPLICATION_NAME = os.environ['APPCONFIG_APPLICATION_NAME']
APPCONFIG_ENVIRONMENT_NAME = os.environ['APPCONFIG_ENVIRONMENT_NAME']
APPCONFIG_CONFIGURATION_PROFILE_NAME = os.environ['APPCONFIG_CONFIGURATION_PROFILE_NAME']
def lambda_handler(event, context):
"""Lambda Handler"""
url = f'http://localhost:2772/applications/{APPCONFIG_APPLICATION_NAME}/environments/{APPCONFIG_ENVIRONMENT_NAME}/configurations/{APPCONFIG_CONFIGURATION_PROFILE_NAME}'
config = {}
with request.urlopen(url) as response:
try:
config = ast.literal_eval(response.read().decode())
except Exception as e:
print(e)
return {
'statusCode': '200',
'body': json.dumps({
'message': 'OK.',
'version': config['version'],
}),
}
おためしのスクリプトは以上で、このスクリプトを以下のようにAWS Lambdaから呼び出せるようにする。
data "archive_file" "lambda_example" {
type = "zip"
output_path = "../output/lambda_example.zip"
source {
filename = "lambda_example.py"
content = file("../script/lambda_example.py")
}
}
resource "aws_lambda_function" "example" {
depends_on = [
aws_cloudwatch_log_group.lambda,
]
function_name = local.lambda_function_name
filename = data.archive_file.lambda_example.output_path
role = aws_iam_role.lambda.arn
handler = "lambda_example.lambda_handler"
source_code_hash = data.archive_file.lambda_example.output_base64sha256
runtime = "python3.10"
memory_size = 128
timeout = 60
layers = ["arn:aws:lambda:ap-northeast-1:980059726660:layer:AWS-AppConfig-Extension:125"]
environment {
variables = {
APPCONFIG_APPLICATION_NAME = aws_appconfig_application.example.name
APPCONFIG_ENVIRONMENT_NAME = aws_appconfig_environment.example.name
APPCONFIG_CONFIGURATION_PROFILE_NAME = aws_appconfig_configuration_profile.example.name
}
}
}
resource "aws_lambda_function_url" "example" {
function_name = aws_lambda_function.example.function_name
authorization_type = "NONE"
}
また、AWS Lambdaには以下のようにAWS AppConfigへのアクセス権を設定しておこう。
実際に必要な権限は、appconfig:GetLatestConfiguration
、appconfig:StartConfigurationSession
の2つだ。
resource "aws_iam_role" "lambda" {
name = local.iam_lambda_role_name
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
}
data "aws_iam_policy_document" "lambda_assume" {
statement {
effect = "Allow"
actions = [
"sts:AssumeRole",
]
principals {
type = "Service"
identifiers = [
"lambda.amazonaws.com",
]
}
}
}
resource "aws_iam_role_policy" "lambda" {
name = local.iam_lambda_policy_name
role = aws_iam_role.lambda.id
policy = data.aws_iam_policy_document.lambda_custom.json
}
data "aws_iam_policy_document" "lambda_custom" {
statement {
effect = "Allow"
actions = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
]
resources = [
aws_cloudwatch_log_group.lambda.arn,
"${aws_cloudwatch_log_group.lambda.arn}:log-stream:*",
]
}
statement {
effect = "Allow"
actions = [
"appconfig:GetLatestConfiguration",
"appconfig:StartConfigurationSession",
]
resources = [
"${aws_appconfig_environment.example.arn}/configuration/${aws_appconfig_configuration_profile.example.configuration_profile_id}",
]
}
}
これを、terraform apply
してからcurlでLambda Function URLを呼び出すと、{"message": "OK.", "version": "stable"}
という出力が得られるはずだ。
これで、初回のデプロイは完了した。続いて、カナリアのデプロイを実行してみる。
2度目のデプロイをカナリアリリースで行う
2度目のリリースチャレンジ(1回目)
2度目のデプロイをするために、以下の変更を行おう。
aws_appconfig_hosted_configuration_version
の新しいバージョンを作成し、aws_appconfig_deployment
から新しいリソースに向けた状態でterraform apply
する。
+ resource "aws_appconfig_hosted_configuration_version" "example_new" {
+ description = "Example AppConfig Hosted Configuration New Version"
+
+ application_id = aws_appconfig_application.example.id
+ configuration_profile_id = aws_appconfig_configuration_profile.example.configuration_profile_id
+
+ content_type = "application/json"
+ content = jsonencode({
+ version = "new",
+ })
+ }
resource "aws_appconfig_deployment" "example" {
description = "My example deployment"
application_id = aws_appconfig_application.example.id
configuration_profile_id = aws_appconfig_configuration_profile.example.configuration_profile_id
environment_id = aws_appconfig_environment.example.environment_id
deployment_strategy_id = aws_appconfig_deployment_strategy.example.id
- configuration_version = aws_appconfig_hosted_configuration_version.example.version_number
+ configuration_version = aws_appconfig_hosted_configuration_version.example_new.version_number
}
すると、先ほどのLambda Function URLのレスポンスが{"message": "OK.", "version": "new"}
に変わる。
この状態でマネージメントコンソールを見てみると、デプロイ中の状態となっている。
しばらく待っていると、ステータスがベーキングとなり、
さらに設定した時間(今回は1分)経過すると、完了となってロールバックが非活性化されて戻せない状態になる。
これで、無事、デプロイは完了したと言える!
……と思いきや、アプリの動作を検証してみると、一度new
が返ってくると、二度とstable
が返ってくることがなくなる。
これでは、カナリアリリースにならないではないか!
どういうことなのか?
こういうときは、AWS AppConfigのAPI仕様書を確認してみよう。
VersionLabel
The user-defined label for the AWS AppConfig hosted configuration version. This attribute doesn't apply if the configuration is not from an AWS AppConfig hosted configuration version. If the client already has the latest version of the configuration data, this value is empty.
クライアントがすでに最新バージョンの構成データを持っている場合、この値は空です。
というわけで、最新値の場合は何も情報が入ってこないようだ。
AWS AppConfigのカナリアリリースにおける設定値の推移のイメージは以下のカミナシ社の資料が分かりやすい。
AWS AppConfigは、最初にセッションを作ってそれを持ち回っている都合上、セッション単位での切り替え状態を管理している、というのが正しい理解のようだ。
では、セッションを新しく張り直した場合はどうだろうか?
以下のようなスクリプトを作ってみよう。
今度のスクリプトでは、GitHubで公開されているAWS AppConfig Helperを使用する。
from appconfig_helper import AppConfigHelper
import pprint
appconfig = AppConfigHelper(
"appconfig-terraform-example-application",
"appconfig-terraform-example-environment",
"appconfig-terraform-example-configuration-profile",
45 # minimum interval between update checks
)
appconfig.update_config()
pprint.pprint(appconfig.config)
このスクリプトを上記のデプロイ中に動かして、値を集計すると、以下のようになる。
流量が少ないせいか、最初の2,3分はnew
がほとんど発生していないが、徐々にstable
からnew
に切り替わる動作となっているのが分かる。
ということで、新規のセッションであればちゃんとカナリアリリースの動作となることが分かった。
セッションが多くあるときも、結果的にキャッシュがexpireした後に参照する都度、セッション単位でのカナリア判定が行われることで徐々に切り替わっていく動作になるだろう。
ただし、キャッシュの時間を短くしたりするモデルでは、想定より早く切り替わってしまうのではないか、というのは気になった(ここは詳細な検証を行っていないので、実際にキャッシュの時間を短くする場合は検証をしてみることを推奨する)。
いずれにしろ、これで、AWS AppConfigを用いた安全なリリースを行えるようになった、と言えるだろう!
なお、一般的なクラウドネイティブのアプリケーションの作りにしていると、コンテナアプリケーションやAWS Lambda関数はステートレスな作りになっているだろう。
せっかくセッション単位での新旧の値が新バージョンになった際は、その値が戻ることがないようエージェントが作られていても、ユーザトラフィック単位で見た場合に新旧の動作が混在になることは避けられない(一部、ALBのスティッキーセッションの設定を使用している場合を除く)。
AWS AppConfigを使っているから安全、と過信せず、過渡期に新旧両バージョンが動作してもアプリケーションの整合性が保たれることは、都度設計を行って確認していくようにしよう。
AWS AppConfigのSLAについて
ユーザーガイドに書いてある通り、AWS AppConfigはAWS Systems Managerの一機能として提供されている。
AWS Systems Managerは、以下のSLAのサイトで謳われているとおり、99.9%のSLAとなっている。
ただし、前述した通り、AWS AppConfigのクライアントはエージェント型になっていて、
- 取得した情報をアプリケーション内にキャッシュするモデルである
- キャッシュ後に取得失敗したとしてもエラーにならず、前の値を継続して使用する
という特性から、安定動作しているアプリケーション上では決して低いSLAではないと言える。
プロダクション利用する際は、この辺りのSLAの情報とアプリケーションの作りを正しく理解し、AWSのSLAだけにとらわれないサービスレベルの定義を行おう。