Redshift Serverlessを立ち上げるためのVPC付きのCloudFormationのテンプレートを作ってみた

2022.09.20

はじめに

好物はインフラとフロントエンドのかじわらゆたかです。 Redshift Serverless熱いですね。

GAされて以後、様々なブログがDevelopers.IOでも出ているのもあり、 気軽に試せるように環境構築用のCloudFormationのテンプレートを書いてみました。

書いてみた

Redshift_Serverless.yml

AWSTemplateFormatVersion: "2010-09-09"
Description: "Redshift Serverless and VPC"
Parameters:
  Env:
    Type: "String"
    Default: "test"
  ProjectName:
    Type: "String"
  CidrBlock:
    Description: Please type the CidrBlock.
    Type: String
    Default: 192.168.0.0/22
  BaseCapacity: 
    Type: Number
    Default: 32
  EnhancedVpcRouting: 
    Type: String
    AllowedValues:
      - true
      - false
    Default: false 
  PubliclyAccessible: 
    Type: String
    AllowedValues:
      - true
      - false
    Default: true
  AdminUsername:
    Type: String
    Default: awsuser
  AdminUserPassword:
    Type: String
    Description: Must be 8-64 characters long. Must contain at least one uppercase letter, one lowercase letter and one number. Can be any printable ASCII character except “/”, ““”, or “@”.
    NoEcho: true
    MinLength: 8
    MaxLength: 64
  KmsKeyId:
    Type: String

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Sub ${CidrBlock}
      EnableDnsSupport: True
      EnableDnsHostnames: True
      InstanceTenancy: default
      Tags:
      - Key: Name
        Value: !Sub ${ProjectName}-redshiftserverless-${Env}-VPC
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: Application
        Value:
          Ref: AWS::StackId
      - Key: Network
        Value: Public
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId:
        Ref: VPC
      InternetGatewayId:
        Ref: InternetGateway
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    DependsOn: AttachGateway
    Properties:
      VpcId:
        Ref: VPC
      Tags:
      - Key: Name
        Value: !Sub |
          ${ProjectName}-redshiftserverless-${Env}-public-rtb
      - Key: Application
        Value:
          Ref: AWS::StackId
  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId:
        Ref: PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: InternetGateway

  Subnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: VPC
      AvailabilityZone: !Select [ 0, !GetAZs ]
      CidrBlock: !Select [ 0, !Cidr [ !GetAtt VPC.CidrBlock, 4, 8 ]]
  Subnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: VPC
      AvailabilityZone: !Select [ 1, !GetAZs ]
      CidrBlock: !Select [ 1, !Cidr [ !GetAtt VPC.CidrBlock, 4, 8 ]]
  Subnet3:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: VPC
      AvailabilityZone: !Select [ 2, !GetAZs ]
      CidrBlock: !Select [ 2, !Cidr [ !GetAtt VPC.CidrBlock, 4, 8 ]]

  Subnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId:
        Ref: Subnet1
      RouteTableId:
        Ref: PublicRouteTable
  Subnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId:
        Ref: Subnet2
      RouteTableId:
        Ref: PublicRouteTable
  Subnet3RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId:
        Ref: Subnet3
      RouteTableId:
        Ref: PublicRouteTable
  RedshiftServerlessSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId:
        Ref: VPC
      GroupDescription: Marker security group for Application server.
      Tags:
      - Key: Name
        Value: !Sub |
          ${ProjectName}-redshiftserverless-${Env}-sg
  RedshiftServerlessRole:
    Type: "AWS::IAM::Role"
    Properties:
      Path: "/"
      RoleName: !Sub "${ProjectName}-${Env}-redshift-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - redshift.amazonaws.com
            Action: sts:AssumeRole
      MaxSessionDuration: 3600
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/AmazonAthenaFullAccess"
        - "arn:aws:iam::aws:policy/AmazonS3FullAccess"
        - "arn:aws:iam::aws:policy/AWSGlueConsoleFullAccess"
        - "arn:aws:iam::aws:policy/AmazonRedshiftAllCommandsFullAccess"
      Description: "Allows Redshift clusters to call AWS services on your behalf."
      Tags:
        - Key: "Name"
          Value: !Sub "${ProjectName}-redshiftserverless-${Env}-redshift-role"
  RedshiftServerlessWorkGroup:
    Type: AWS::RedshiftServerless::Workgroup
    Properties: 
      WorkgroupName: !Sub "${ProjectName}-${Env}-redshift-wg"
      BaseCapacity: !Ref BaseCapacity
      EnhancedVpcRouting: !Ref EnhancedVpcRouting
      NamespaceName: !Ref RedshiftServerlessNamespace
      PubliclyAccessible: !Ref PubliclyAccessible
      SecurityGroupIds: 
        - !Ref RedshiftServerlessSecurityGroup
      SubnetIds: 
        - !Ref Subnet1
        - !Ref Subnet2
        - !Ref Subnet3
  RedshiftServerlessNamespace:
    Type: AWS::RedshiftServerless::Namespace
    Properties: 
      NamespaceName: !Sub "${ProjectName}-${Env}-redshift-ns"
      AdminUsername: !Ref AdminUsername
      AdminUserPassword: !Ref AdminUserPassword
      KmsKeyId: !Ref KmsKeyId
      DbName: !Sub "${ProjectName}-db"
      IamRoles: 
        - !GetAtt RedshiftServerlessRole.Arn

create_Redshift_Serverless.sh

#!/bin/bash 
set -eu
export AWS_PAGER=""
CHANGESET_OPTION="--no-execute-changeset"

# 実行時に指定された引数の数、つまり変数 $# の値が 4 でなければエラー終了。
if [ $# -ne 4 ]; then
  echo "指定された引数は$#個です。" 1>&2
  echo "実行するには3個の引数が必要です。" 1>&2
  echo "$0 [deploy or check] [ProjectName] [AdminUserPassword Parameter Store Key ][AWS CLI Profile]"
  exit 1
fi

# 第1引数がdeployの場合、Change setの確認は行わず、Deployを行います。
if [ $1 = "deploy" ]; then
  echo "deploy mode"
  CHANGESET_OPTION=""
else
  echo "check mode"
fi

# 第2引数にはプロジェクト名を指定します。
ProjectName=$2
# 第3引数にはRedshift Serverlessのパスワードが格納されているパラメータストアのキーを指定します。
AdminUserPasswordKey=$3
# 第4引数にはAWS CLIのProfileを指定します。
profile=$4

# パラメータストアから格納されているパスワードの値を取得します。
AdminUserPassword=`aws ssm get-parameter --name ${AdminUserPasswordKey}  --profile $profile --with-decryption --query Parameter.Value --output text`

# RedshiftのAWS マネージドキーのIDを取得します。別の鍵を使う場合は個々の値を書き換えてください。
KmsKeyID=$(aws kms list-aliases --output text --profile $profile --query 'Aliases[?AliasName == `alias/aws/redshift`].TargetKeyId')

CFN_TEMPLATE=./Redshift_Serverless.yml
CFN_STACK_NAME=${ProjectName}-redshiftserverless

# テンプレートの実行
aws cloudformation deploy --stack-name ${CFN_STACK_NAME} --template-file ${CFN_TEMPLATE} ${CHANGESET_OPTION} \
  --profile $profile \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides \
  ProjectName=$ProjectName \
  AdminUserPassword=$AdminUserPassword \
  KmsKeyId=$KmsKeyID

CloudFormationとシェルで分割している理由

CloudFormationをAWS CLIから動かすのが便利だということをハマコーの記事から学んだので、それを使いたかったのと、 それ以外にも以下の理由があります。

KMSのIDはアカウントごとに固有だがCloudFormationから取得する方法がない

Redshift ServerlessはKMSによる暗号化が必須で、構築時には用いるKMSのIDを入力する必要があります。
この値は動かすAWSアカウントごとに固有になるためCloudFormationのテンプレートに記載するのではなく、CloudFormation単体で取得したいのですが取得することができません。 そのため、シェルの中でAWS マネージドキーのID取得してCloudFormationのパラメーターとして渡しています。

Redshift ServerlessのAdminUserPasswordはSecureString未対応

Cloudformationではパスワード等の文字列をパラメータストアのSecureStringから取得すると言った方法があります。 ServerlessではないRedshiftの場合SecureStringで格納した値をMasterUserPasswordに格納すると言った事が可能なのですが、 RedshiftServerlessは対応しておらずパラメータストアから取得しようとするためにはSecureStringではなく、普通のStringとしてパラメータストアに格納しそれを参照すると言った方法を取る必要があります。(2022/09現在)

動的な参照を使用してテンプレート値を指定する - AWS CloudFormation

パスワードという機密データをテンプレートに埋め込むのはもちろん論外で、パラメータストアに只の文字列として格納するのも微妙です。 そのためSecureStringとしてパラメータストアに格納し、その値の取得をシェルから行いCloudFormationにわたすと言った方法をとっています。
もちろん値はNoEcho: true と設定しているので、Cloudformationのマネージメントコンソール上からの確認はできません。

VPC構築を少しスマートに書いてみました。

Redshift Serverlessを構築する場合、Availability Zoneは3つにまたいだSubnetが必要です。 また、Redshift Serverlessに割り当てるBaseUnitCapacityの数によっては十分なIPも必要です。

VPCを新しく作成した際にSubnetに割り当てるCIDRの計算やAZの文字列を組み合わせたりしたくないなって思っていたのですが、そこらへんはもうしなくてよさそうです。

RegionごとのAvailability Zoneを動的に取得する

AZの取得は以下のようになります。

  Subnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: VPC
      AvailabilityZone: !Select [ 0, !GetAZs ]
      CidrBlock: !Select [ 0, !Cidr [ !GetAtt VPC.CidrBlock, 4, 8 ]]
  Subnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: VPC
      AvailabilityZone: !Select [ 1, !GetAZs ]
      CidrBlock: !Select [ 1, !Cidr [ !GetAtt VPC.CidrBlock, 4, 8 ]]
  Subnet3:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: VPC
      AvailabilityZone: !Select [ 2, !GetAZs ]
      CidrBlock: !Select [ 2, !Cidr [ !GetAtt VPC.CidrBlock, 4, 8 ]]

強調表記したところになるのですが、CloudFormationの組み込み関数である !GetAZsはリージョンのAZのリストを返す関数になります。 それを、!Selectで指定して取得することで、AZの情報を取得することが可能です。

Subnetごとに割り当てるCidrを計算させる。

これも組み込み関数の!Cidrを用いることでVPCに割り当てたCIDRから計算させる事が可能です。 これについては以下のブログで非常にわかり易く解説されております。

構築してみた

以下のような形で構築が可能です。

$ sh ./create_Redshift_Serverless.sh check deviosample "/cm-kajiwara/redshift-serverless-admin-user-password" YourAWSCLIProfile
Waiting for changeset to be created..
Changeset created successfully. Run the following command to review changes:
aws cloudformation describe-change-set --change-set-name arn:aws:cloudformation:ap-northeast-1:123456789012:changeSet/awscli-cloudformation-package-deploy-1663432179/4f875331-1cd9-4a9b-86db-f98871abfca8

まとめ

Redshift Serverlessを構築するためのCloudFormationのテンプレートとシェルを公開してみました。 単一のCloudformationになっていますので、不要になったときの片付けも容易かと思います。 検証時には是非お使いください。