🐧

[CDK]CloudFormationスタック分けとSSMパラメータストア出力/取得

2023/12/21に公開

CloudFormationのスタックを分けるためのCDK

はじめに

初めまして。アイディオットDX開発部、インフラ担当の小野です。
CDKで構築をする際に、CloudFormationのスタックを分けないと後々の調整が面倒だったり
ドリフトが発生すると全てが動かせなくなったりと非常に不便だったのでスタックを分けて構築してみます。
ポイントは各スタックの必要な情報をSSMのパラメータストアに格納し、別のスタックで取得することです。

前提

  • CDKが使える環境を準備していること
  • typescriptで記述している
  • CLIのユーザを作成して使えること

今回行うこと

  • cdk.Stackの拡張
  • VPCのスタック作成
  • SGのスタック作成
  • スタック呼び出しスクリプトの作成

CDKディレクトリ構成(一部抜粋)

bin
  L standard_stack.ts(スタック呼び出しスクリプト)
lib
  L abstract
    L stack.ts(cdk.Stackを拡張)
  L vpc-stack.ts(VPCを作成し、パラメータストアに出力するスタック)
  L sg-stack.ts(パラメータストアからVPC情報を取得し、SGを作成するスタック)

stack.ts(cdk.Stackを拡張)

リソース名とパラメータストア格納時の名前を設定するメソッド作成をしています。

import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';


export abstract class Stack extends cdk.Stack {

  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
  }

  //リソース名を設定するためのメソッド
  //今回のSSMパラメータストアに出力する目的には直接関係ないのであまり気にしないで下さい。(※Nameタグ付けなどで使用)
  protected createResourceName(
    scope: Construct,
    env: string,
    originalName: string
  ): string {
    const project = scope.node.tryGetContext("project");
    return `${project}-${env}-${originalName}`;
  }

  //SSMパラメータストアに格納する際の名前設定のためのメソッド
  protected createParameterName(
    scope: Construct,
    env: string,
    serviceName: string,
    key: string,
  ): string {
    const project = scope.node.tryGetContext("project");
    return `/${project}/${env}/${serviceName}/${key}`;
  }
}

vpc-stack.ts(VPCを作成し、パラメータストアに出力するスタック)

  • scope.node.tryGetContext("xx");は、 cdk.jsonに値を準備しておくこと
    • 例) "project": "stack-sparetion-test"
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ssm from "aws-cdk-lib/aws-ssm";
import {StackProps, Tags } from 'aws-cdk-lib'
import { Stack } from "./abstract/stack";
import { Construct } from 'constructs';


export class VpcStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

     // VPCを取得
     const vpc = new ec2.Vpc(this, "Vpc", {
      cidr: scope.node.tryGetContext("vpcCidr"),
      //今回はNATを0にしている
      natGateways: 0,
      maxAzs: 3,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: "Public",
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: "PrivateApp",
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
        {
          cidrMask: 24,
          name: "PrivateData",
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        },
      ],
      enableDnsSupport: true,
      enableDnsHostnames: true,
    });

    //SSMパラメータストアに出力
    new ssm.StringParameter(this, "VpcId", {
      parameterName: this.createParameterName(scope, "common", "vpc", "id"),
      stringValue: vpc.vpcId,
    });

    //Nameタグ付け
    Tags.of(vpc).add("Name", this.createResourceName(scope, "common", "vpc"));

    //コスト管理タグ
    Tags.of(vpc).add("Cost", scope.node.tryGetContext("project"));
  }
}

sg-stack.ts(パラメータストアからVPC情報を取得し、SGを作成するスタック)

  • process.env.IP_RANGES_STG_EC2; は、.envにSGのインバウンドに許可したいIPを設定しておくこと
    • 例) IP_RANGES_STG_EC2=192.168.xx.xx/32,172.31.xx.xx/32
import * as dotenv from 'dotenv';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ssm from "aws-cdk-lib/aws-ssm";
import { Stack } from "./abstract/stack";
import { Construct } from 'constructs';
import { StackProps, Tags, Fn } from 'aws-cdk-lib'
import { Vpc } from 'aws-cdk-lib/aws-ec2'

dotenv.config();

export class SgStack extends Stack {
    constructor(scope: Construct, id: string, props?: StackProps) {
        super(scope, id, props);


        // VPCのIDをSSMパラメータストアから取得
        const vpcId = ssm.StringParameter.valueFromLookup(
            this,
            this.createParameterName(scope, "common", "vpc", "id")
        );

        const vpc = Vpc.fromLookup(this, 'Vpc', { vpcId: vpcId });

        // EC2のSG作成
        const sgEc2 = new ec2.SecurityGroup(this, "SgEc2", {
            vpc: vpc,
            allowAllOutbound: true,
            securityGroupName: this.createResourceName(scope, "stg", "ec2-sg"),
        });

        //インバウンドに許可するIPを設定
        const ipRangesEC2Stg = process.env.IP_RANGES_STG_EC2;

        if (!ipRangesEC2Stg) {
            throw new Error('環境変数 IP_RANGES_STG_EC2 が設定されていません。');
        }

        const ipRangesEC2 = ipRangesEC2Stg.split(',');

        ipRangesEC2.forEach((ipRange: string, index: number) => {
            sgEc2.addIngressRule(
                ec2.Peer.ipv4(ipRange),
                ec2.Port.tcp(22),
                `allow SSH access from IP address ${index + 1}`,
            );
        });

        //Nameタグ付け
        Tags.of(sgEc2).add("Name", this.createResourceName(scope, "stg", "sg-ec2"));

        //SSMパラメータストアに出力
        new ssm.StringParameter(this, "ParamSgEc2", {
            parameterName: this.createParameterName(scope, "stg", "sg-ec2", "id"),
            stringValue: sgEc2.securityGroupId,
        });

    }
}

補足

  • vpc-stack.tssg-stack.ts で以下の値を揃えていることに注目。
    こうすることで出力したパラメータの中から特定のものを指定して取得できる
this.createParameterName(scope, "common", "vpc", "id")

standard_stack.ts(スタック呼び出しスクリプト)

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { VpcStack } from '../lib/vpc-stack';
import { SgStack } from '../lib/sg-stack';


const app = new cdk.App();

new VpcStack(app, "VpcStack", {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION
  },
});

new SgStack(app, "SgStack", {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION,
  },
});

デプロイ時

1.VPCスタックのデプロイ
cdk deploy --profile xxx-user VpcStack

2.SGスタックのデプロイ
cdk deploy --profile xxx-user SgStack

コンソール画面確認

コンソールでスタックが分けられてること・パラメータストアに格納されてることを確認してみましょう

  • CloudFormationのスタック
    logo

  • SSMのパラメータストア
    logo

まとめ

VPCやSG以外にもこのやり方でスタックを分けて作成してみて下さい。
また、もっとこうした方がいいよ〜などありましたら教えていただけると嬉しいです。

あとがき

AI・データ利活用をリードし、世界にインパクトを与えるプロダクトを開発しませんか?

アイディオットでは、今後の事業拡大及びプロダクト開発を担っていただけるエンジニアチームの強化を行っております。
さらに会社の成長を加速させるため、フロントエンドエンジニア、バックエンドエンジニア、インフラエンジニアのメンバーを募集しております!
日本を代表する企業様へ自社プロダクトを活用した、新規事業コンサルティング、開発にご興味のある方はお気軽にご連絡ください。

【リクルートページ】
https://aidiot.jp/recruit/
【募集ポジション一覧】
https://open.talentio.com/r/1/c/aidiot/homes/3925
【採用についてのお問合せ先】
株式会社アイディオット 採用担当:大島
メールアドレス:recruit@aidiot.jp

Discussion