AWS CDKの環境構築しすぎて自動化したくなったので自動化してみた
株式会社Specteeセキュリティチームの都築です。
導入と背景
AWS CDKは、インフラをコードで管理できる便利なツールですが、プロジェクトごとの環境構築や初期設定に手間がかかることがあります。
毎回同じようなコマンドを打ち、構成ファイルを用意するのが面倒に感じたため、環境構築を自動化して効率化を図ることにしました。
本記事では、AWS CDKの環境構築作業を自動化する方法を紹介し、セットアップの手間を大幅に減らす手法を解説します。特に、複数プロジェクトを管理する人にとって役立つ内容です。
方法
今回のディレクトリ構造
-+- Dockerfile
+- docker-compose.yml
+- init.sh
+- init.py
- Dockerコンテナの定義
- Dockerfileの作成
- docekr-compose.ymlの作成
- ユーザーによる設定項目の定義
- Dockerビルド時の挙動の定義
- CDK初期設定を自動で実行するスクリプトの作成
- ユーザーの設定項目に応じたファイルの生成を行うスクリプトの作成
- すでにCDKの初期設定が行われている場合の挙動を作成
- スクリプトの配置とDockerfile変更
- Dockerコンテナで使うコマンド
- Dockerのビルドコマンドの実行
- Dockerコンテナ内に入るコマンド
- AWS Tokenの設定を行うコマンド
Dockerコンテナの定義
-
Dockerfileの作成
このDockerコンテナは、AWS CDKの環境構築を簡単に行うためのイメージです。
Node.js、AWS CLI、CDK CLI、docker.ioなど、CDKの開発に必要なツールをあらかじめセットアップしており、どの環境でもすぐにCDKの操作が可能になります。
コンテナを使用することで、ローカル環境の依存関係を排除し、プロジェクトごとのセットアップ時間を短縮。チームメンバー間での統一された環境を簡単に共有できます。
DockerfileFROM 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
-
docekr-compose.ymlの作成
このdocker-compose.ymlは、AWS CDKの開発環境を簡単に管理するための設定ファイルです。
コンテナのビルドから実行までを自動化し、Node.js、AWS CLI、CDK CLIなどのツールをすぐに利用できる状態で起動します。これにより、複数プロジェクトの環境構築を統一し、手動セットアップの手間を省いて効率的な開発フローを実現します。
docker-compose.ymlservices: <サービス名>: 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のソケットを共有
-
ユーザーによる設定項目の定義
Dockerfileとdocker-compose.ymlを以下のように変更して、ホスト→docker-compose.yml→Dockerfileの順で環境変数を受け取れるようにする
DockerfileFROM 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.ymlbuild: context: . args: - PROJECT_NAME=<プロジェクト名> image: <コンテナのイメージ名>
Dockerビルド時の挙動の定義
-
CDK初期設定を自動で実行するスクリプトの作成
このスクリプトは、AWS CDKプロジェクトの初期設定を自動化するために作成しました。
プロジェクトのディレクトリ作成、CDKの初期化、必要な依存パッケージのインストールを一括で実行し、手動作業を省きます。これにより、毎回の環境セットアップが数秒で完了し、開発の効率化を図れます。
※今回はinit.shというファイル名で保存します
init.shdirectory=/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
-
ユーザーの設定項目に応じたファイルの生成を行うスクリプトの作成
この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()
-
すでにCDKの初期設定が行われている場合の挙動を作成
このスクリプトは、CDKの初期設定がすでに行われているかをチェックし、設定済みの場合は何も実行しません。
未設定の場合のみ、CDKの初期化処理を自動実行し、不要な重複作業を防ぎます。これにより、効率的かつ安全な環境セットアップが可能です。※ init.shを変更します。
init.shdirectory=/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.shrm $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.ymlprivileged: true command: sh /init.sh # ここを追加 volumes: - <ホストのプロジェクトディレクトリ>:/project/
-
スクリプトの配置とDockerfile変更とdocker-compose.ymlの変更
1-3の修正をまとめると以下のようになります。
-
スクリプト(init.sh)
init.shdirectory=/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
-
スクリプト(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()
-
Dockerfile
DockerfileFROM 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
-
docker-compose.yml
docker-compose.ymlservices: <サービス名>: 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コンテナで使うコマンド
-
Dockerを実行するコマンド
Dockerのコンテナをビルドするコマンド
COMMAND$ docker compose build
Dockerのコンテナを起動するコマンド
COMMAND$ docker compose up -d
Dockerの状態を確認するコマンド
COMMAND$ docker compose ps
-
Dockerコンテナ内に入るコマンド
コンテナに入るコマンド
COMMAND$ docker compose exec <コンテナ名> bash
-
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のプロジェクトが自動作成されるようになりました。
Discussion