Help us understand the problem. What is going on with this article?

AWS-CDKによる複数環境の管理を楽にするツールを作った

概要

aws-cdkをさらに便利にするツールをnpmに公開しました!!
本番環境、検証環境など複数環境を管理するのに便利かと思います。お試しください。

cdk.gif

https://github.com/masahirompp/cdk-env-manager

それでは以下詳細。

aws-cdkの考慮事項

aws-cdk(またはCloudFormation)でAWS環境を管理する場合、以下のようなことを考慮すると思います。

  • A. 複数の環境(production, staging, developmentなど)を管理したい
    • 環境は無限に増える前提でコーディングすべき
    • (ケース・バイ・ケースだが)1つのAWSアカウント上で、複数環境を管理したい
  • B. デプロイ時のパラメータ(URL, InstanceType, RemovalPolicyなど)を環境毎に管理したい
    • パラメータの数が増えるとContextや環境変数で渡すのつらい
    • パラメータを開発者で共有したいけれど、ローカルのJSONファイルなどにハードコーディングはやめたい、Gitの管理下にも置きたくない
  • C. StackのOutputs(ApiGatewayのEndpointUrlなど)を環境毎に管理したい
    • Outputsを開発者で共有したいけれど、ローカルのJSONファイルなどにハードコーディングはやめたい、Gitの管理下にも置きたくない
  • D. デプロイ時の手順を統一したい
    • cdk diffで差分を確認してからデプロイするなど
  • E. Stackを適切に分けたい

これらをうまく解決するには、

  • B, Cの各環境のパラメータやOutputsについては、SSMのパラメータストアで管理することで、ローカルファイルを使わず、開発者間で共有できそうです。
  • Aもパラメータストアのパスを上手く区切ればできそうです。
  • Dは、aws-cdkのラッパーを作ればできそう。

ということで、ツール作りました!!

ツールの使い方

事前準備

aws-cdkなどインストールして、cdk init --language typescript等の初期設定は済ましておいてください。

(本ツールはTypeScriptのみ対応です)

cdk-env-managerをインストール

yarn add -D cdk-env-manager
# npm install -D cdk-env-manager

cdk Stackの作成

各Stackのコードを作成します。

  • cdk-env-managerからCdkStackBaseをimportして継承してください。
  • createResourcesメソッドを実装してください。
// S3Stack.ts
import * as cdk from '@aws-cdk/core'
import * as s3 from '@aws-cdk/aws-s3'
import { CdkStackBase } from 'cdk-env-manager'

type Input = { removalPolicy: cdk.RemovalPolicy }
type Output = { myBucketArn: string }

export class S3Stack extends CdkStackBase<Input, Output> {
  createResources() {
    const myBucket = new s3.Bucket(this, this.name('MyBucket'), {
      removalPolicy: this.props.removalPolicy
    })

    this.createOutputsSsmParameters({ myBucketName: myBucket.bucketName })

    return {
      myBucketArn: myBucket.bucketArn
    }
  }
}

補足として

  • InputはこのStackが受け取るパラメータです。
  • OutputはこのStackが出力するパラメータです。他のStackに渡したい場合に使います。
  • createOutputsSsmParameters(...)は値をSSMのパラメータストアに書き出します。アプリ側から参照したい設定値などを、SSMに書き出してください。(アプリ側からの参照方法は後述)
  • this.name("MyBucket")は、"DevMyBucket"のように環境名のprefixをつけてくれるヘルパーメソッドです。

同様にここではもう1つStackを作成。

// RoleStack.ts
import * as iam from '@aws-cdk/aws-iam'
import { CdkStackBase } from 'cdk-env-manager'

type Input = { bucketArn: string }
type Output = { myRoleArn: string }

export class RoleStack extends CdkStackBase<Input, Output> {
  createResources() {
    const myManagedPolicy = new iam.ManagedPolicy(this, this.name('MyManagedRole'), {
      statements: [
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: ['s3:GetObject'],
          resources: [this.props.bucketArn]
        })
      ]
    })

    const myRole = new iam.Role(this, this.name('MyRole2'), {
      assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com'),
      managedPolicies: [myManagedPolicy]
    })

    return { myRoleArn: myRole.roleArn }
  }
}

cdk appの作成

複数のStackをまとめるcdk appを作成します。

  • cdk-env-managerからCdkAppBaseをimportして継承してください。
  • createStacksメソッドを実装してください。
  • 末尾2行は必須です。
// MyApp.ts
import * as cdk from '@aws-cdk/core'
import { CdkAppBase } from 'cdk-env-manager'
import { RoleStack } from './stacks/RoleStack'
import { S3Stack } from './stacks/S3Stack'

type Parameter = { removalPolicy: cdk.RemovalPolicy }

export class MyApp extends CdkAppBase<Parameter> {
  async createStacks() {
    const s3Stack = new S3Stack(this, {
      cdkEnvKey: this.cdkEnvKey,
      stackName: 'S3Stack',
      removalPolicy: this.deployParameters.removalPolicy
    })

    new RoleStack(this, {
      cdkEnvKey: this.cdkEnvKey,
      stackName: 'RoleBucket',
      bucketArn: s3Stack.exports.myBucketArn
    })
  }
}

const app = new MyApp()
app.synth()

補足として

  • cdkEnvKeyは環境ごとの名称(Prod, Devなど)が入ります。コード例のようにStackに渡してあげてください。
  • Parameterには環境ごとのパラメータを定義します。詳細は次へ。

パラメータファイルの定義

プロジェクトのルートにcdk.parameters.default.envを作成し、
デフォルトのパラメータをkey=value形式で指定してください。

# cdk.parameters.default.env
REMOVAL_POLICY=retain

環境変数の設定

デプロイ時に必要な以下の環境変数を設定してください。

  • AWS_DEFAULT_REGION
  • AWS_ACCESS_KEY_IDとAWS_SECRET_ACCESS_KEYのペア、またはAWS_PROFILE

ここでは.envファイルに記載している体で進めます。

# .env
AWS_DEFAULT_REGION=ap-northeast-1
AWS_PROFILE=my-profile

デプロイ方法

ここまで来たら準備完了です。デプロイをしてみます。

1. ツールの実行

ここでは、環境変数を.envから読み込んで実行します。

npx dotenv-cli -- cdk-env-manager

2. デプロイする環境の選択

CdkEnvKeyは環境を識別するキーです。CdkEnvKeyを選択してください。
初回実行時は既存の環境はありませんので、「create new Stacks」を選択します。

Screen Shot 2019-12-03 at 2.07.25.png

3. パラメータの確認と設定

デプロイ時のパラメータを、確認するか、変更するか、そのまま続行するか選んでください。

Screen Shot 2019-12-03 at 2.10.34.png

新規作成時や変更を選択した場合は、パラメータの編集ができます。

Screen Shot 2019-12-03 at 2.12.05.png

cdk.parameters.default.envのデフォルトパラメータを増やすと、このプロンプトで編集できるパラメータも増えます。

4. 差分の確認

パラメータが決まったら、ツールが内部的にcdk diffを実行します。
デプロイ時の差分を確認してください。

Screen Shot 2019-12-03 at 2.13.59.png

5. デプロイするStackの選択

diffを確認を受けて、デプロイするStackを選択します。1つでも複数でもOKです。

Screen Shot 2019-12-03 at 2.17.12.png

6. デプロイ実行

5で選択したStackをデプロイします。内部的にはcdk deployを呼び出しているだけです。

Screen Shot 2019-12-03 at 2.18.46.png

cdkのデプロイが終わるのをまって完了!!

アプリ側からStackの出力を参照

SSMに書き出された設定値をアプリ側から読み込みます。ここでは以下の手法をサンプルとします。

  • アプリ側で利用する環境を、環境変数(CDK_ENV_KEY)で指定
  • webpackのビルド時に設定値をSSMから読み込み、DefinePluginでアプリに渡す
import webpack from 'webpack'
import { ENVIRONMENT_VARIABLE_NAME_CDK_ENV_KEY, getStackParameters } from 'cdk-env-manager'

const configFunction: () => Promise<webpack.Configuration> = async () => {
  const cdkEnvKey = process.env[ENVIRONMENT_VARIABLE_NAME_CDK_ENV_KEY]

  // load stack parameters from ssm by cdkEnvKey
  const params = await getStackParameters(cdkEnvKey)

  return {
    entry: 'src/index.ts',
    // ...省略
    plugins: [
      new webpack.DefinePlugin({
        ...Object.keys(params).reduce(
          (payload, key) => ({ ...payload, [key]: JSON.stringify(params[key]) }),
          {}
        )
      })
    ]
  }
}

export default configFunction

SSMからパラメータをまとめて取得するには、getStackParametersを使えばOKです。

最後に

駆け足になってしまってわかりにくいところもあるかもです。
Github側のREADME.mdなどをこれから整理し、わかりやすくしていく予定です。
手伝ってくれる方募集中です!!

(おまけ)Stackを適切にわけたい

Stack間でパラメータを参照する場合、いくつか方法がありますが、
aws-cdkでStack間でパラメータの受け渡しをすると、自動的にクロススタック参照になります。

クロススタック参照は便利ですが、致命傷を避ける必要があります。
致命傷の詳細は、いつもお世話になっているクラスメソッドさんのブログをご参照ください。
https://dev.classmethod.jp/cloud/aws/aws-all-iac/

私が実施している致命傷を避ける方法として、
運用上削除してはいけないユーザデータを含むリソース(RDS Instance, Cognito UserPool, DynamoDB Tableなど)を1つのStackにまとめ(以下UserDataStackと呼びます)

  • UserDataStackを他のStackが参照する(依存する)
  • UserDataStackは他のStackを参照してはならない(依存してはならない)。ただし、UserDataStackのリソースを作るためにどうしても必要なリソースのみ(例えば、RDS Instanceに対するVPCなど)、仕方ないので依存を許可。

というルールで構築しています。

こうしておけば、Stack1〜4をすべて作り直し!!とかできるので、致命傷になりにくいです。

おわり。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away