メインコンテンツへスキップ

AWS AppConfigをつかってみた

· loading · loading ·
kiitosu
著者
kiitosu
aws community builder. 画像処理やデバイスドライバ、データ基盤構築からWebバックエンドまで、多様な領域に携わってきました。地図解析や地図アプリケーションの仕組みにも経験があり、幅広い技術を活かした開発に取り組んでいます。休日は草野球とランニングを楽しんでいます。
目次

はじめに
#

現在のプロジェクトでは、アプリケーションの設定管理を以下のように行っている。

  • 実装コード内にconfigファイルを配置
  • 環境変数で環境(dev/stg/prod等)を切り替え、対応するconfigを読み込む

この方式で感じている不便さ:

  • 設定を変えるたびにデプロイが必要
  • 設定変更のレビュー・マージ・デプロイのリードタイムが長い
  • Feature Flagのような動的な切り替えがやりづらい
  • 環境ごとの設定差分の管理が煩雑

そこで、AWS AppConfigを使えばこのあたりが改善できるのではないかと考え、調べて試してみることにした。

今回作成したコードは以下のリポジトリに置いている。 https://github.com/kiitosu/appconfig-trial

AWS AppConfigとは
#

  • AWS Systems Managerの一機能
  • アプリケーションの設定を外部化し、デプロイなしで動的に変更できるサービス
  • バリデーション、段階的デプロイ、ロールバックの仕組みが組み込まれている

Feature Flag / 動的設定管理の業界動向
#

AppConfigは「Feature Flag / 動的設定管理」という領域に属するサービス。この領域には以下のようなプレイヤーがいる。

サービス 特徴
LaunchDarkly 業界最大手。高機能(A/Bテスト、ユーザーターゲティング等)。高価
Split.io (Harness) 実験・分析に強い
Flagsmith, ConfigCat等 軽量で安価な代替
AWS AppConfig AWSネイティブ。安価。機能はシンプル

AppConfigの立ち位置は「AWSに寄せている人向けの安価でシンプルな選択肢」。

また、OpenFeatureというベンダー中立な標準仕様も出てきており、特定サービスへのロックインを避ける動きもある。

既存方式との比較
#

観点 configファイル + 環境変数 AWS AppConfig
設定変更時 コード変更→デプロイが必要 デプロイ不要で即反映可能
バリデーション 自前で実装 JSON Schemaやバリデータで検証可能
ロールバック 再デプロイ ワンクリックでロールバック
段階的反映 自前で実装 デプロイ戦略で段階的に反映
Feature Flag 自前で実装 組み込み機能として提供
コスト なし 呼び出し回数に応じた従量課金

IaCとの付き合い方
#

AppConfigのインフラ(アプリケーション・環境・設定プロファイルの定義)はIaCで管理できる。しかし設定値そのものはConsole / CLI / APIで動的に変更するのが前提の設計。

「コード = 実態」というIaCの思想と矛盾するように見えるが、これはAppConfig固有の問題ではなく、Feature Flag / 動的設定管理という領域全体の設計思想。LaunchDarklyなど他のサービスも、設定値はダッシュボードから変えるのが前提になっている。

何を どうやって 頻度
インフラ定義(箱) IaC(CDK等)でデプロイ 初回 + たまに
設定値の変更 Console / CLI / API 随時、デプロイ不要

やってみる
#

設定の取得方法
#

ECSからAppConfigの設定を取得する方法は主に2つある(Lambda ExtensionもあるがこれはLambda専用)。

観点 AppConfig Agent(サイドカー) AWS SDK直接呼び出し
アプリ側の実装 localhost:2772 にHTTP GETするだけ セッション管理・ポーリング・キャッシュを自前実装
コンテナ構成 サイドカーコンテナが増える アプリコンテナだけ
設定の更新 Agentが自動ポーリング 自前でポーリング
AWS依存 アプリコードはHTTPだけなのでAWS非依存 AWS SDKに依存

Agentのほうが圧倒的に楽で、AWSも推奨している。SDK直接を選ぶ理由があるとすれば「サイドカーを増やしたくない」くらい。

AppConfig Agentの仕組みとしては、Agent自体は事前に設定を知っているわけではない。アプリからリクエストが来たときに初めてAppConfigのAPIを呼んで設定を取得し、キャッシュする。以降はキャッシュから返しつつ、バックグラウンドでポーリングして最新の設定を取得する。つまりAgentはAppConfig APIとアプリの間に入るキャッシュ付きプロキシとして動作する。

構成
#

今回はECS (Fargate) + AppConfig Agent(サイドカー)構成で試す。

  • ECSタスク内にAppConfig Agentをサイドカーコンテナとして配置
  • アプリケーションコンテナからlocalhost経由でAgentに設定を取得しにいく
  • Agentが設定のキャッシュ・ポーリングを担当するため、アプリ側の実装はシンプルになる

AppConfigのリソース構成は以下の通り:

Application: my-app
├── Environment: api-service     → APIサーバー用の設定
│   └── ConfigurationProfile: api-config
└── Environment: batch-service   → バッチ処理用の設定
    └── ConfigurationProfile: batch-config

Environmentは同一アプリケーション内で設定の配布先を分ける単位。今回は2つのECSサービス(api-service、batch-service)に対してそれぞれ異なる設定を配布し、独立して動的に変更できることを検証する。

CDKによるインフラ定義
#

CDK(TypeScript)で構築した。AppConfigのL2 Constructがaws-cdk-lib/aws-appconfigにstableとして含まれているため、シンプルに書ける。

AppConfigリソース:

// アプリケーション
const application = new appconfig.Application(this, "MyApp", {
  applicationName: "my-app",
});

// 設定の配布先を分ける
const apiServiceEnv = new appconfig.Environment(this, "ApiServiceEnv", {
  application,
  environmentName: "api-service",
});

const batchServiceEnv = new appconfig.Environment(this, "BatchServiceEnv", {
  application,
  environmentName: "batch-service",
});

// デプロイ戦略(即時反映)
const deploymentStrategy = new appconfig.DeploymentStrategy(this, "Immediate", {
  rolloutStrategy: appconfig.RolloutStrategy.linear({
    growthFactor: 100,
    deploymentDuration: Duration.minutes(0),
    finalBakeTime: Duration.minutes(0),
  }),
});

// 設定データ(初期値。デプロイ後はConsoleから動的に変更できる)
new appconfig.HostedConfiguration(this, "ApiServiceConfig", {
  application,
  name: "api-config",  // 同一Application内で一意
  deployTo: [apiServiceEnv],
  content: appconfig.ConfigurationContent.fromInlineJson(
    JSON.stringify({
      apiEndpoint: "https://api.example.com",
      timeout: 30,
      featureX: false,
    })
  ),
  deploymentStrategy,
});

HostedConfigurationではname(設定プロファイル名)を明示的に指定する必要がある。指定しないと自動生成された名前になり、アプリ側の取得URLと一致しなくなる。また、同一Application内でプロファイル名は一意でなければならない。

ECSタスク定義(api-service):

const apiTaskDef = new ecs.FargateTaskDefinition(this, "ApiTaskDef", {
  memoryLimitMiB: 512,
  cpu: 256,
  runtimePlatform: {
    cpuArchitecture: ecs.CpuArchitecture.ARM64,
    operatingSystemFamily: ecs.OperatingSystemFamily.LINUX,
  },
});

// AppConfigへのアクセス権限
apiTaskDef.taskRole.addToPrincipalPolicy(
  new iam.PolicyStatement({
    actions: [
      "appconfig:StartConfigurationSession",
      "appconfig:GetLatestConfiguration",
    ],
    resources: ["*"],
  })
);

// アプリコンテナ
apiTaskDef.addContainer("app", {
  image: ecs.ContainerImage.fromAsset(path.join(__dirname, "../app")),
  portMappings: [{ containerPort: 3000 }],
  // AppConfig Agentへの問い合わせURLを環境変数で指定
  environment: {
    APPCONFIG_APP: "my-app",
    APPCONFIG_ENV: "api-service",
    APPCONFIG_CONFIG: "api-config",
  },
});

// AppConfig Agent(サイドカー)
apiTaskDef.addContainer("appconfig-agent", {
  image: ecs.ContainerImage.fromRegistry(
    "public.ecr.aws/aws-appconfig/aws-appconfig-agent:2.x"
  ),
  portMappings: [{ containerPort: 2772 }],
});

Apple SiliconのMacでビルドする場合、runtimePlatformでARM64を指定しないとFargate上でexec format errorになるので注意。

アプリケーション側の実装
#

アプリはExpressで、環境変数をもとにAppConfig Agentへの取得URLを組み立てる。

import express from "express";

const appName = process.env.APPCONFIG_APP ?? "my-app";
const envName = process.env.APPCONFIG_ENV ?? "dev";
const configName = process.env.APPCONFIG_CONFIG ?? "my-config";

const app = express();

app.get("/", async (_req, res) => {
  const response = await fetch(
    `http://localhost:2772/applications/${appName}/environments/${envName}/configurations/${configName}`
  );
  const config = await response.json();
  res.json({ message: "Config from AppConfig", config });
});

app.listen(3000, () => console.log("Listening on port 3000"));

取得URLのフォーマットは:

http://localhost:2772/applications/{アプリ名}/environments/{環境名}/configurations/{プロファイル名}

今回はリクエストのたびにAgentに問い合わせる実装にしたが、本番ではアプリ起動時や一定間隔で取得してメモリに保持するのが一般的。

設定を変更してみる
#

  1. npx cdk deploy でインフラと初期設定をデプロイ
  2. ECS Execでコンテナに入り、設定値が取得できることを確認
  3. AWSコンソールから設定値を変更してデプロイ
  4. 再度コンテナ内からアクセスし、アプリをデプロイし直さずに設定が変わっていることを確認

以下、コンソールでの操作手順をスクリーンショットで紹介する。

AppConfigダッシュボード
#

AppConfigダッシュボード

CDKでデプロイしたアプリケーションmy-appが表示されている。

アプリケーションを選択
#

アプリケーションを選択する

my-appの中にbatch-configapi-configの2つのConfiguration Profileがある。今回はapi-configの設定を変更する。

現在の設定を確認し、新しいバージョンを作成
#

新しいバージョン作成に進む

Version 1の設定(timeout: 30)を確認し、「Create version」から新しいバージョンを作成する。

設定値を変更
#

設定を変更

JSON形式で設定を編集できる。ここではtimeout3060に変更した。

デプロイを開始
#

デプロイを開始

デプロイ先の環境(api-service)、ホストされた設定バージョン(2)、デプロイ戦略を選択して「デプロイを開始」。

変更後の設定を確認
#

新しい設定

Version 2としてtimeout: 60の設定がデプロイされた。

ECS Execで動作確認
#

まず設定変更前の値を確認する。

$ aws ecs list-tasks --cluster appconfig-trial --service-name api-service
{
    "taskArns": [
        "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:task/appconfig-trial/4e468c5cc027..."
    ]
}

$ aws ecs execute-command --cluster appconfig-trial --task <タスクARN> --container app --interactive --command "/bin/sh"

Starting session with SessionId: ecs-execute-command-xxxxx
# curl http://localhost:3000/
/bin/sh: 1: curl: not found
# node -e "fetch('http://localhost:3000/').then(r=>r.json()).then(console.log)"
{
  message: 'Config from AppConfig',
  config: {
    apiEndpoint: 'https://api.example.com',
    timeout: 30,
    featureX: false
  }
}

node:22-slimにはcurlが入っていないので、node -eで代用した。timeout: 30が返ってきており、CDKで設定した初期値が取得できている。

この後、上記のコンソール操作でtimeout60に変更してデプロイし、再度同じコマンドを実行すると、アプリのデプロイなしでtimeout60に変わっていることを確認できた。

これでAppConfigによる動的設定変更が実際に動くことを確認できた。

ハマったポイント
#

  • ConfigurationProfile名の衝突: 同一Application内で2つのHostedConfigurationに同じnameを付けるとデプロイが失敗する
  • exec format error: Apple SiliconでビルドしたDockerイメージをFargateで動かすにはruntimePlatformでARM64を指定する必要がある
  • ECS Exec: enableExecuteCommand: trueをサービスに追加し、タスクを再起動しないと有効にならない
  • curlが入っていない: node:22-slimにはcurlがないので、node -e "fetch(...)" で代用した

自分たちのユースケースに合うか
#

実際に調べて試してみて気づいたのは、自分たちの設定は環境ごとのURL接続先など静的な設定が中心だということ。

AppConfigの主要なメリットを現状のユースケースに照らすと:

AppConfigのメリット 現状のユースケースでは
デプロイなしで設定変更 設定をそもそも頻繁に変えない
バリデーション 形式チェック止まりで、設定値の意味的な正しさ(どの環境向けか等)は守れない
段階的ロールアウト 静的設定に段階的反映は不要
Feature Flag 現状使っていない

AppConfigの価値が出るのは:

  • 運用中に頻繁に変える設定がある(Feature Flag、レート制限値、メンテナンスモード切替など)
  • 変更のたびにデプロイを回すのがつらい

逆に静的な設定であれば、現在の方式(configファイル + 環境変数)やParameter Storeでも十分対応できる。

最後に
#

AppConfigは動的な設定管理のためのサービスであり、「設定を頻繁に変える」「デプロイなしで即座に反映したい」というユースケースで真価を発揮する。

一方、自分たちのように静的な設定が中心の場合は、現状のconfigファイル + 環境変数の方式で十分対応できる。導入の手間やIaCとの整合性を考えると、今すぐ移行するメリットは薄い。

導入を検討すべきタイミング:

  • Feature Flagの需要が出てきたとき
  • 設定変更のたびにデプロイを回すのがボトルネックになったとき
  • メンテナンスモード切替など、運用中に即座に切り替えたい設定が増えたとき

「何でもAppConfigにする」のではなく、設定の性質(静的か動的か、変更頻度はどうか)に応じて使い分けることが大事だと感じた。

Reply by Email

関連記事

Lambda Durable Functionsを使ってみる — Step Functionsとの比較からハンズオンまで
· loading · loading
Obsidian CLIとClaude Codeでタスク管理を改善した話
Pages Router / App Router / Hono を1つのNext.jsプロジェクトで比較する