1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS AppConfig+Terraformでアプリケーションの設定情報を安全にデプロイする

Last updated at Posted at 2024-11-24

はじめに

AWS AppConfigは、AWSの設定情報を安全にデプロイするためのマネージドサービスだ。

アプリケーションの設定変更をカナリアリリースすることができ、デプロイ中の異常に対してワンクリックでの迅速なロールバックを提供することもできる。

今回は、簡単なAWS AppConfigをTerraformで自動構築できるようにし、カナリアリリースにおける運用を考察する。

前提となる知識は、以下があれば良い。

  • カタログレベルでのAWS AppConfigの知識
  • Terraformの基礎知識

AWS AppConfigの構成要素

AWS AppConfigは、以下の構成となっている。

構成要素 説明
アプリケーション 設定プロファイルを束ねる一番大きな単位。実際に設定を利用するアプリケーションの単位と考えると分かりやすい
設定プロファイル 複数のホストされた設定バージョンを束ねたもの。たとえば、「予約値」「予約適用日」のように関連した設定を一括りにまとめるようなイメージ
ホストされた設定バージョン 設定プロファイルの実際の値を記述する部分
環境 設定プロファイルを適用する先の環境
デプロイ戦略 どういう設定でデプロイをするか(LINEARなのかEXPONENTIALなのか/何分かけてデプロイするか/ベーキングの時間をどれくらい取るかの設定)
デプロイ どの環境にどのバージョンを何のデプロイ戦略でデプロイするかといったデプロイメントの設定情報

分かりにくいので、AWSのマネージメントコンソール上でのまとまり方を図で整理すると以下のようになる。

構成要素まとめ.png

デプロイバージョン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との通信を肩代わりしてくれる。

script/lambda_example.py
"""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:GetLatestConfigurationappconfig: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.png

しばらく待っていると、ステータスがベーキングとなり、

キャプチャ2.png

さらに設定した時間(今回は1分)経過すると、完了となってロールバックが非活性化されて戻せない状態になる。

キャプチャ3.png

これで、無事、デプロイは完了したと言える!

……と思いきや、アプリの動作を検証してみると、一度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)

このスクリプトを上記のデプロイ中に動かして、値を集計すると、以下のようになる。

image.png

流量が少ないせいか、最初の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だけにとらわれないサービスレベルの定義を行おう。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?