🕌

AWS CDKの環境構築しすぎて自動化したくなったので自動化してみた

2025/02/17に公開

株式会社Specteeセキュリティチームの都築です。

導入と背景

AWS CDKは、インフラをコードで管理できる便利なツールですが、プロジェクトごとの環境構築や初期設定に手間がかかることがあります。
毎回同じようなコマンドを打ち、構成ファイルを用意するのが面倒に感じたため、環境構築を自動化して効率化を図ることにしました。

本記事では、AWS CDKの環境構築作業を自動化する方法を紹介し、セットアップの手間を大幅に減らす手法を解説します。特に、複数プロジェクトを管理する人にとって役立つ内容です。

方法

今回のディレクトリ構造

ディレクトリツリー
-+- Dockerfile
 +- docker-compose.yml
 +- init.sh
 +- init.py
  1. Dockerコンテナの定義
    1. Dockerfileの作成
    2. docekr-compose.ymlの作成
    3. ユーザーによる設定項目の定義
  2. Dockerビルド時の挙動の定義
    1. CDK初期設定を自動で実行するスクリプトの作成
    2. ユーザーの設定項目に応じたファイルの生成を行うスクリプトの作成
    3. すでにCDKの初期設定が行われている場合の挙動を作成
    4. スクリプトの配置とDockerfile変更
  3. Dockerコンテナで使うコマンド
    1. Dockerのビルドコマンドの実行
    2. Dockerコンテナ内に入るコマンド
    3. AWS Tokenの設定を行うコマンド

Dockerコンテナの定義

  1. Dockerfileの作成

    このDockerコンテナは、AWS CDKの環境構築を簡単に行うためのイメージです。

    Node.js、AWS CLI、CDK CLI、docker.ioなど、CDKの開発に必要なツールをあらかじめセットアップしており、どの環境でもすぐにCDKの操作が可能になります。

    コンテナを使用することで、ローカル環境の依存関係を排除し、プロジェクトごとのセットアップ時間を短縮。チームメンバー間での統一された環境を簡単に共有できます。

    Dockerfile
    FROM ubuntu:22.04
    
    RUN apt update -y && \
        apt upgrade -y && \
        apt-get update -y && \
        apt-get upgrade -y
    
    # install Python3.11
    # install pip
    RUN apt install -y python3.11 \
                       python3-pip
    
    # install Python Modules
    RUN pip install --upgrade pip setuptools
    RUN pip install boto3
    
    # install jq
    RUN apt -y install jq
    
    # Node.js
    RUN apt install -y ca-certificates curl gnupg
    RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
    RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
    RUN apt update
    RUN apt install -y nodejs \
                       gcc \
                       g++ \
                       make
    RUN curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarnkey.gpg >/dev/null
    RUN echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian stable main" | tee /etc/apt/sources.list.d/yarn.list
    RUN apt update && apt-get install yarn -y
    
    # AWS CDK
    RUN npm install -g aws-cdk
    
    # Typescript
    RUN npm install -g typescript@latest
    
    # AWS CLI
    WORKDIR /tmp
    RUN apt install unzip
    RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip"
    RUN unzip awscliv2.zip
    RUN ./aws/install
    WORKDIR /
    
    # Docekr
    RUN apt install -y docker.io
    
    # ディレクトリの作成
    RUN mkdir project
    WORKDIR /project
    
  2. docekr-compose.ymlの作成

    このdocker-compose.ymlは、AWS CDKの開発環境を簡単に管理するための設定ファイルです。
    コンテナのビルドから実行までを自動化し、Node.js、AWS CLI、CDK CLIなどのツールをすぐに利用できる状態で起動します。

    これにより、複数プロジェクトの環境構築を統一し、手動セットアップの手間を省いて効率的な開発フローを実現します。

    docker-compose.yml
    services:
        <サービス名>:
            build:
                context: .
            image: <コンテナのイメージ名>
            container_name: <コンテナ名>
            tty: true
            privileged: true # DockerコンテナからDockerコンテナをビルドできるようにする
            volumes:
                - <ホストのプロジェクトディレクトリ>:/project/
                - ~/.aws/:/root/.aws/ # 必要であれば(オプション)
                - /var/run/docker.sock:/var/run/docker.sock # Dockerのソケットを共有
    
    
  3. ユーザーによる設定項目の定義

    Dockerfileとdocker-compose.ymlを以下のように変更して、ホスト→docker-compose.yml→Dockerfileの順で環境変数を受け取れるようにする

    Dockerfile
    FROM ubuntu:22.04
    
    # 環境変数を設定
    ARG PROJECT_NAME # ここを追加
    ENV PROJECT_NAME=${PROJECT_NAME} # ここを追加
    
    RUN apt update -y && \
        apt upgrade -y && \
        apt-get update -y && \
        apt-get upgrade -y
    
    docker-compose.yml
    build:
    	context: .
    	args:
    		- PROJECT_NAME=<プロジェクト名>
    image: <コンテナのイメージ名>
    

Dockerビルド時の挙動の定義

  1. CDK初期設定を自動で実行するスクリプトの作成

    このスクリプトは、AWS CDKプロジェクトの初期設定を自動化するために作成しました。
    プロジェクトのディレクトリ作成、CDKの初期化、必要な依存パッケージのインストールを一括で実行し、手動作業を省きます。

    これにより、毎回の環境セットアップが数秒で完了し、開発の効率化を図れます。

    ※今回はinit.shというファイル名で保存します

    init.sh
    directory=/project/
    export PROJECT_DIR=$directory
    
    # CDK Projectの作成
    cd $directory
    cdk init app --language typescript
    
    # プロジェクトファイルの削除
    rm $PROJECT_DIR/lib/*.ts
    rm $PROJECT_DIR/bin/*.ts
    
    # Gitの初期化
    rm -rf $PROJECT_DIR/.git
    rm $PROJECT_DIR/.gitignore
    
    # Sourceディレクトリの作成
    mkdir src
    
    return 0
    
  2. ユーザーの設定項目に応じたファイルの生成を行うスクリプトの作成

    このPythonスクリプトは、ユーザーが指定した設定項目に基づいて必要なファイルを自動生成します。
    入力された設定値を元に、テンプレートファイルの作成やカスタマイズを行い、手動でのファイル準備作業を省き、プロジェクトの初期構築を効率化します。

    ※今回はinit.pyというファイル名で保存します

    init.py
    # -*- coding: utf-8 -*-
    
    import os
    import json
    
    project_name = os.environ.get('PROJECT_NAME')
    project_directory = os.environ.get('PROJECT_DIR')
    
    def create_lib_file():
        stack_name = ""      # Stack名
        file_name = ""      # ファイル名
    
        # Stack名を作成
        # ファイル名を作成
        for pn in project_name.split("-"):
            stack_name += f"{pn.upper()[0]}{pn[1:]}"
            file_name += f"{pn.upper()[0]}{pn[1:]}"
        stack_name += "Stack"
    
        # ファイルの作成
        part_of_file1 = '''
        import * as cdk from "aws-cdk-lib";
        import { Construct } from "constructs";
    
        interface '''
    
        part_of_file2 = '''Props extends cdk.StackProps {}
    
        export class '''
    
        part_of_file3 = ''' extends cdk.Stack {
            constructor(
                scope: Construct,
                id: string,
                props: '''
    
        part_of_file4 = '''Props
            ) {
                super(scope, id, props);
            }
        }'''
    
        file = f'''
            {part_of_file1
            }{stack_name
            }{part_of_file2
            }{stack_name
            }{part_of_file3
            }{stack_name
            }{part_of_file4}
        '''
    
        # ファイルの書き込み
        with open(f"{project_directory}lib/{project_name}-stack.ts", mode='w') as f:
            f.write(file)
    
    def create_bin_file():
        stack_name = ""      # Stack名
        file_name = ""      # ファイル名
    
        # Stack名を作成
        # ファイル名を作成
        for pn in project_name.split("-"):
            stack_name += f"{pn.upper()[0]}{pn[1:]}"
            file_name += f"{pn.upper()[0]}{pn[1:]}"
        stack_name += "Stack"
        file_name = f"{file_name.lower()[0]}{file_name[1:]}"
    
        part_of_file1 = '''
        import "source-map-support/register";
        import * as cdk from "aws-cdk-lib";
        import { '''
    
        part_of_file2 = ''' } from "../lib/'''
    
        part_of_file3 = '''";
    
        const app = new cdk.App();
        new '''
    
        part_of_file4 = '''(app, "'''
    
        part_of_file5 = '''", {
            env: {
                account: process.env.CDK_DEFAULT_ACCOUNT,
                region: process.env.CDK_DEFAULT_REGION,
            },
        });
        '''
    
        file = f'''
            {part_of_file1
            }{stack_name
            }{part_of_file2
            }{project_name
            }-stack{part_of_file3
            }{stack_name
            }{part_of_file4
            }{project_name
            }-stack{part_of_file5}
        '''
    
        # ファイルの書き込み
        with open(f"{project_directory}bin/{file_name}.ts", mode='w') as f:
            f.write(file)
    
    def update_cdk_json():
        fo = open(f"{project_directory}cdk.json", mode="r")      # ファイルポインタ
        cdk_json = json.load(fo)
        fo.close()    # ファイルポインタを閉じる
    
        # ファイル名を作成
        file_name = ""      # ファイル名
        for pn in project_name.split("-"):
            file_name += f"{pn.upper()[0]}{pn[1:]}"
        file_name = f"{file_name.lower()[0]}{file_name[1:]}"
    
        # ファイルを変更
        cdk_json['app'] = f"npx ts-node --prefer-ts-exts bin/{file_name}.ts"
    
        # ファイルを書き込み
        with open(f"{project_directory}cdk.json", mode="w") as fo:
            json.dump(cdk_json, fo, indent=4)
    
    if __name__ == '__main__':
        create_lib_file()
    
        create_bin_file()
    
        update_cdk_json()
    
    
  3. すでにCDKの初期設定が行われている場合の挙動を作成

    このスクリプトは、CDKの初期設定がすでに行われているかをチェックし、設定済みの場合は何も実行しません。
    未設定の場合のみ、CDKの初期化処理を自動実行し、不要な重複作業を防ぎます。これにより、効率的かつ安全な環境セットアップが可能です。

    ※ init.shを変更します。

    init.sh
    directory=/project/
    export PROJECT_DIR=$directory
    
    # プロジェクトの有無を確認
    # 以下のIF文を追加
    if [ -n "$(ls $directory)" ]; then
        echo "Welcome back ${PROJECT_NAME} Project!"
        /bin/sh -c "while :; do sleep 10; done"
        return 0
    fi
    
    # CDK Projectの作成
    cd $directory
    cdk init app --language typescript
    
    init.sh
    rm $PROJECT_DIR/bin/*.ts
    
    # プロジェクトファイルの作成
    python3 /init.py # ここを追加
    
    # Gitの初期化
    rm -rf $PROJECT_DIR/.git
    

    ※ Dockerfileを変更します。

    Docekrfile
    # Docekr
    RUN apt install -y docker.io
    
    # プロジェクトinitファイルの追加
    RUN mkdir project_d # ここを追加
    ADD ./init.sh /init.sh # ここを追加
    ADD ./init.py /init.py # ここを追加
    
    # ディレクトリの作成
    RUN mkdir project
    

    ※ docker-compose.ymlを変更します。

    docker-compose.yml
    privileged: true
    command: sh /init.sh # ここを追加
    volumes:
        - <ホストのプロジェクトディレクトリ>:/project/
    
  4. スクリプトの配置とDockerfile変更とdocker-compose.ymlの変更

    1-3の修正をまとめると以下のようになります。

    1. スクリプト(init.sh)

      init.sh
      directory=/project/
      export PROJECT_DIR=$directory
      
      # プロジェクトの有無を確認
      if [ -n "$(ls $directory)" ]; then
          echo "Welcome back ${PROJECT_NAME} Project!"
          /bin/sh -c "while :; do sleep 10; done"
          return 0
      fi
      
      # CDK Projectの作成
      cd $directory
      cdk init app --language typescript
      
      # プロジェクトファイルの削除
      rm $PROJECT_DIR/lib/*.ts
      rm $PROJECT_DIR/bin/*.ts
      
      # プロジェクトファイルの作成
      python3 /init.py
      
      # Gitの初期化
      rm -rf $PROJECT_DIR/.git
      rm $PROJECT_DIR/.gitignore
      
      # Sourceディレクトリの作成
      mkdir src
      
      return 0
      
    2. スクリプト(init.py)

      init.py
      # -*- coding: utf-8 -*-
      
      import os
      import json
      
      project_name = os.environ.get('PROJECT_NAME')
      project_directory = os.environ.get('PROJECT_DIR')
      
      def create_lib_file():
          stack_name = ""      # Stack名
          file_name = ""      # ファイル名
      
          # Stack名を作成
          # ファイル名を作成
          for pn in project_name.split("-"):
              stack_name += f"{pn.upper()[0]}{pn[1:]}"
              file_name += f"{pn.upper()[0]}{pn[1:]}"
          stack_name += "Stack"
      
          # ファイルの作成
          part_of_file1 = '''
          import * as cdk from "aws-cdk-lib";
          import { Construct } from "constructs";
      
          interface '''
      
          part_of_file2 = '''Props extends cdk.StackProps {}
      
          export class '''
      
          part_of_file3 = ''' extends cdk.Stack {
              constructor(
                  scope: Construct,
                  id: string,
                  props: '''
      
          part_of_file4 = '''Props
              ) {
                  super(scope, id, props);
              }
          }'''
      
          file = f'''
              {part_of_file1
              }{stack_name
              }{part_of_file2
              }{stack_name
              }{part_of_file3
              }{stack_name
              }{part_of_file4}
          '''
      
          # ファイルの書き込み
          with open(f"{project_directory}lib/{project_name}-stack.ts", mode='w') as f:
              f.write(file)
      
      def create_bin_file():
          stack_name = ""      # Stack名
          file_name = ""      # ファイル名
      
          # Stack名を作成
          # ファイル名を作成
          for pn in project_name.split("-"):
              stack_name += f"{pn.upper()[0]}{pn[1:]}"
              file_name += f"{pn.upper()[0]}{pn[1:]}"
          stack_name += "Stack"
          file_name = f"{file_name.lower()[0]}{file_name[1:]}"
      
          part_of_file1 = '''
          import "source-map-support/register";
          import * as cdk from "aws-cdk-lib";
          import { '''
      
          part_of_file2 = ''' } from "../lib/'''
      
          part_of_file3 = '''";
      
          const app = new cdk.App();
          new '''
      
          part_of_file4 = '''(app, "'''
      
          part_of_file5 = '''", {
              env: {
                  account: process.env.CDK_DEFAULT_ACCOUNT,
                  region: process.env.CDK_DEFAULT_REGION,
              },
          });
          '''
      
          file = f'''
              {part_of_file1
              }{stack_name
              }{part_of_file2
              }{project_name
              }-stack{part_of_file3
              }{stack_name
              }{part_of_file4
              }{project_name
              }-stack{part_of_file5}
          '''
      
          # ファイルの書き込み
          with open(f"{project_directory}bin/{file_name}.ts", mode='w') as f:
              f.write(file)
      
      def update_cdk_json():
          fo = open(f"{project_directory}cdk.json", mode="r")      # ファイルポインタ
          cdk_json = json.load(fo)
          fo.close()    # ファイルポインタを閉じる
      
          # ファイル名を作成
          file_name = ""      # ファイル名
          for pn in project_name.split("-"):
              file_name += f"{pn.upper()[0]}{pn[1:]}"
          file_name = f"{file_name.lower()[0]}{file_name[1:]}"
      
          # ファイルを変更
          cdk_json['app'] = f"npx ts-node --prefer-ts-exts bin/{file_name}.ts"
      
          # ファイルを書き込み
          with open(f"{project_directory}cdk.json", mode="w") as fo:
              json.dump(cdk_json, fo, indent=4)
      
      if __name__ == '__main__':
          create_lib_file()
      
          create_bin_file()
      
          update_cdk_json()
      
      
    3. Dockerfile

      Dockerfile
      FROM ubuntu:22.04
      
      # 環境変数を設定
      ARG PROJECT_NAME
      ENV PROJECT_NAME=${PROJECT_NAME}
      
      RUN apt update -y && \
          apt upgrade -y && \
          apt-get update -y && \
          apt-get upgrade -y
      
      # install Python3.11
      # install pip
      RUN apt install -y python3.11 \
                         python3-pip
      
      # install Python Modules
      RUN pip install --upgrade pip setuptools
      RUN pip install boto3
      
      # install jq
      RUN apt -y install jq
      
      # Node.js
      RUN apt install -y ca-certificates curl gnupg
      RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
      RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
      RUN apt update
      RUN apt install -y nodejs \
                         gcc \
                         g++ \
                         make
      RUN curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarnkey.gpg >/dev/null
      RUN echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian stable main" | tee /etc/apt/sources.list.d/yarn.list
      RUN apt update && apt-get install yarn -y
      
      # AWS CDK
      RUN npm install -g aws-cdk
      
      # Typescript
      RUN npm install -g typescript@latest
      
      # AWS CLI
      WORKDIR /tmp
      RUN apt install unzip
      RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip"
      RUN unzip awscliv2.zip
      RUN ./aws/install
      WORKDIR /
      
      # Docekr
      RUN apt install -y docker.io
      
      # プロジェクトinitファイルの追加
      RUN mkdir project_d
      ADD ./init.sh /init.sh
      ADD ./init.py /init.py
      
      # ディレクトリの作成
      RUN mkdir project
      WORKDIR /project
      
    4. docker-compose.yml

      docker-compose.yml
      services:
          <サービス名>:
              build:
                  context: .
                  args:
                      - PROJECT_NAME=<プロジェクト名>
              image: <コンテナのイメージ名>
              container_name: <コンテナ名>
              tty: true
              privileged: true
              command: sh /init.sh
              volumes:
                  - <ホストのプロジェクトディレクトリ>:/project/
                  - ~/.aws/:/root/.aws/
                  - /var/run/docker.sock:/var/run/docker.sock
      
      

Dockerコンテナで使うコマンド

  1. Dockerを実行するコマンド

    Dockerのコンテナをビルドするコマンド

    COMMAND
    $ docker compose build
    

    Dockerのコンテナを起動するコマンド

    COMMAND
    $ docker compose up -d
    

    Dockerの状態を確認するコマンド

    COMMAND
    $ docker compose ps
    
  2. Dockerコンテナ内に入るコマンド

    コンテナに入るコマンド

    COMMAND
    $ docker compose exec <コンテナ名> bash
    
  3. AWS Tokenの設定を行うコマンド

    COMMAND
    $ export AWS_ACCESS_KEY_ID="<YOUR_AWS_ACCESS_KEY_ID>"
    $ export AWS_SECRET_ACCESS_KEY="<YOUR_AWS_SECRET_ACCESS_KEY>"
    $ export AWS_SESSION_TOKEN="<YOUR_AWS_SESSION_TOKEN>"
    

結果

DockerをビルドするだけでCDKのプロジェクトが自動作成されるようになりました。

Spectee Developers Blog

Discussion