はじめに
プロジェクトのドキュメント管理、皆さんはどうしていますか?
私たちのプロジェクトでは、技術仕様書やAPI仕様書などのドキュメントをMarkdown形式で管理していました。しかし、ドキュメントの更新のたびに手動でHTMLに変換してS3にアップロードする作業が発生し、運用負荷が課題となっていました。
そこで、AWS CodePipelineを活用して、GitHubへのpush時に自動的にMarkdownをHTMLに変換し、S3静的サイトホスティングで公開する仕組みを構築しました。本記事では、その実装方法と工夫したポイントを紹介します。
ちなみに、筆者はMarkdown Preview Enhancedを愛用していました。
おそらく手動でなくてもMarkdown→html変換はできると思いますが、今回はローカルでhtmlを一切持たない構成と合わせて環境構築してみました。
CodePipelineとは?
AWS CodePipelineは、継続的インテグレーション/継続的デリバリー(CI/CD)を実現するフルマネージドサービスです。
主な特徴:
- ソース管理との連携: GitHub、CodeCommit、S3などと連携可能
- ビルド・テスト: CodeBuildを使用した柔軟なビルド処理
- デプロイ自動化: S3、ECS、Lambda、EC2など様々なデプロイ先に対応
- 視覚的なパイプライン管理: AWSコンソールで進行状況を確認可能
今回は、GitHub → CodeBuild → S3という流れでドキュメントの自動デプロイを実現しました。
GitHub ActionsなどのGit側提供のコネクタを使えば、簡単に環境実装できると思います。
今回はGitHub Actionsが使えない制約下のため、CodePipelineでの連携としました。
課題:ドキュメント管理の煩雑さ
以前の運用方法
プロジェクト開始当初、ドキュメントは以下のように管理していました:
- 別リポジトリでの管理: ドキュメント専用のリポジトリを用意
- 手動変換: Markdownを手動でHTMLに変換
- 手動デプロイ: 変換したHTMLをS3に手動アップロード
発生していた問題
- 運用負荷: 更新のたびに変換・デプロイ作業が必要
- 同期の遅れ: コードとドキュメントが別リポジトリのため、更新が遅れがち
- バージョン管理の複雑さ: コードとドキュメントのバージョン対応が不明確
- レビューの困難さ: ドキュメント変更のレビューがコードレビューと分離
これらの課題を解決するため、コードとドキュメントを同一リポジトリで管理し、自動デプロイする仕組みを構築することにしました。
解決方法:CodePipelineによる自動化
アーキテクチャ概要
GitHub (sandbox branch)
↓ (push trigger)
CodePipeline
↓
CodeBuild (Markdown → HTML変換)
↓
S3 (静的サイトホスティング)
1. GitHubとCodePipelineの連携
CodeStar Connectionsの設定
GitHubとAWSを連携するため、CodeConnectionsを使用しました。
# CDKでの実装例
source_action = codepipeline_actions.CodeStarConnectionsSourceAction(
action_name="GitHub_Source",
owner="your-organization",
repo="your-repository",
branch="sandbox",
output=source_output,
connection_arn="arn:aws:codestar-connections:ap-northeast-1:111122223333:connection/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
)
ポイント:
- 初回はAWSコンソールでCodeConnectionsを手動作成
- GitHubアプリの承認が必要
- 接続後はARNをCDKで参照
以前は「AWS CodeStar Connections」という名称でしたが、現在は「AWS CodeConnections」という名称に変更されています。
CDKではCodeStarConnectionsSourceActionのまま残っているようです。
2. CodeBuildでのMarkdown→HTML変換
buildspec.ymlの設計
CodeBuildの処理を定義するbuildspec.ymlをリポジトリルートに配置しました。
version: 0.2
phases:
install:
runtime-versions:
nodejs: 18
commands:
- echo "Installing dependencies..."
- (cd docs && npm install)
build:
commands:
- echo "Building documentation..."
- (cd docs && npm run build)
- echo "Building API references..."
- (cd docs && npm run build:api)
post_build:
commands:
- echo "Build completed on `date`"
artifacts:
files:
- '**/*'
exclude-paths:
- 'node_modules/**/*'
- 'package.json'
- 'package-lock.json'
- 'build.js'
- 'buildspec.yml'
- '**/*.md'
- '.gitignore'
- 'cdk/**/*'
base-directory: 'docs'
discard-paths: no
cache:
paths:
- 'docs/node_modules/**/*'
工夫したポイント:
- サブシェル
(cd docs && command)を使用してディレクトリ移動の影響を局所化 -
base-directoryで成果物のルートディレクトリを指定 - 不要なファイルを
exclude-pathsで除外 -
node_modulesをキャッシュしてビルド時間を短縮
3. カスタムビルドスクリプト(build.js)
Markdownの変換には、Node.jsのカスタムスクリプトを作成しました。
主な機能
① @import構文のサポート
ドキュメント内でJSONファイルなどを参照できるようにしました。
## データ定義ファイル
@import "./device_type_master.json"
Markdown Preview Enhancedでは、@importで外部ファイルを読み込める便利な機能があります。
今回はこの機能を実装しています。
他にもMarkdown Preview EnhancedはpuntUMLを表示するなどの様々なサポートがあるので、本機能がモジュール的に利用できるなら、それが最も早いと思います(実現性は未確認です)。
実装:
function processImports(markdown, baseDir) {
const importRegex = /@import\s+(?:"([^"]+)"|'([^']+)'|(\S+))/g;
return markdown.replace(importRegex, (match, quoted1, quoted2, unquoted) => {
const importPath = quoted1 || quoted2 || unquoted;
const fullPath = path.join(baseDir, importPath);
if (fs.existsSync(fullPath)) {
const content = fs.readFileSync(fullPath, 'utf-8');
const ext = path.extname(fullPath).toLowerCase();
if (ext === '.json') {
return '```json\n' + content + '\n```';
}
return '```\n' + content + '\n```';
}
return `<!-- Import file not found: ${importPath} -->`;
});
}
上記コードを含めて、全体をKiroに実装してもらいました。
そのため、@importの読込み部分を正規表現でやっており、力業感がぬぐえません。
KiroなどのAIコードエージェントはモジュールを利用せずに、関数を独自に実装したり、正規表現での検索を多く利用する傾向にあるので、よりスマートな実装をされたい方は人間でコーディングすることをお勧めします。
② カスタムスタイルの適用
style_deploy.cssから独自のスタイルを読み込み、統一されたデザインを実現しました。
function extractStyleFromCss() {
const cssPath = path.join(__dirname, 'style_deploy.css');
if (fs.existsSync(cssPath)) {
let cssContent = fs.readFileSync(cssPath, 'utf-8');
return cssContent.trim();
}
return null;
}
カスタムスタイルでは、全体のイメージをCSSで記述しておけます。
私は Markdown Preview Enhancedで使っていたイメージをそのまま持ってきました。
4. S3静的サイトホスティングの設定
CDKでのS3バケット作成
docs_bucket = s3.Bucket(
self,
"DocsBucket",
bucket_name=f"myproject-{stage}-docs",
website_index_document="index.html",
public_read_access=False,
# ここではBlock Public Accessが無効化していますが、安易に設定しないでください
block_public_access=s3.BlockPublicAccess(
block_public_acls=False,
block_public_policy=False,
ignore_public_acls=False,
restrict_public_buckets=False
),
removal_policy=RemovalPolicy.RETAIN,
)
上記設定では簡易にS3静的サイトホスティングを実装するために、Block Public Accessが無効化しています。
よりセキュアな構成では、Block Public Accessで全てブロックし、CloudFrontディストリビューション経由でのhttpsアクセスが推奨されます。
IPアドレス制限
社内ネットワークからのみアクセス可能にしました。
docs_bucket.add_to_resource_policy(
iam.PolicyStatement(
sid="AllowPublicReadFromCorporateIP",
effect=iam.Effect.ALLOW,
principals=[iam.AnyPrincipal()],
actions=["s3:GetObject"],
resources=[f"{docs_bucket.bucket_arn}/*"],
conditions={
"IpAddress": {
"aws:SourceIp": [
"203.0.113.0/24", # 社内IPアドレス(例)
"198.51.100.0/24",
# ... その他の社内IPアドレス
]
}
}
)
)
httpとBlock Public Accessが無効化で公開する代わりに社内IPを制限していますが、CloudFrontやWAFでのIP制限を設定しましょう。
5. CodePipelineの構築(CDK)
全体をCDKで定義し、Infrastructure as Codeを実現しました。
# CodeBuildプロジェクト
build_project = codebuild.PipelineProject(
self,
"DocsBuildProject",
project_name=f"myproject-{stage}-docs-build",
build_spec=codebuild.BuildSpec.from_source_filename("buildspec.yml"),
environment=codebuild.BuildEnvironment(
build_image=codebuild.LinuxBuildImage.STANDARD_7_0,
compute_type=codebuild.ComputeType.SMALL,
),
)
# S3への書き込み権限を付与
docs_bucket.grant_read_write(build_project)
# CodePipeline
pipeline = codepipeline.Pipeline(
self,
"DocsPipeline",
pipeline_name=f"myproject-{stage}-docs-pipeline",
)
# ソースステージ
pipeline.add_stage(
stage_name="Source",
actions=[source_action],
)
# ビルドステージ
pipeline.add_stage(
stage_name="Build",
actions=[build_action],
)
# デプロイステージ
deploy_action = codepipeline_actions.S3DeployAction(
action_name="Deploy",
bucket=docs_bucket,
input=build_output,
extract=True,
)
pipeline.add_stage(
stage_name="Deploy",
actions=[deploy_action],
)
導入効果
個人的には以下導入効果を実感しています。
①ソースコードとドキュメントの一元管理
今まではドキュメント用のリポジトリを作って、mainブランチマージで単にS3バケットにデプロイする簡単なパイプラインを作っていました。
これだとパイプラインは簡単に設定できますが、コード修正時にドキュメントを直した場合はリポジトリを行き来する必要があるため、非常に煩雑でした・・・
今回の修正で、単一リポジトリにコードとドキュメントが格納されているため、リポジトリ間の行き来が無くなりました!
仕様書とコードをどう管理するかはまだまだ議論の余地があると思います。
ただし、Kiroによるスペック駆動開発を利用する場合は、仕様書とコードはできるだけ近い距離にあるのが好ましいと考えられます。
私は仕様書を配信したいので、コードを置くscriptフォルダと仕様書を置くdocsを分けましたが、機能ごとのフォルダで仕様書とコードが並ぶようにすると、コードエージェントによる探索負荷が小さくなるかもしれません。
②html変換作業と作業者環境によるデザイン崩れ
今まではMarkdown Preview Enhancedを使って、Markdown作成後にhtmlへ変換していたため、html変換漏れが発生していました。
また作業者環境により適用されるCSSに差が出たりするため、デザインが変わったり、Git上で無駄に差分が出てしまったりすることがありました。
今回はCodeBuildにてMarkdown->html変換処理が自動化されたので、変換漏れによるドキュメント漏れが無くなりました。
また、適用するCSSも定義済みのため、デザインが崩れることもありません。
まとめ
CodePipelineを活用することで、Markdownドキュメントの自動デプロイを実現し、運用負荷を大幅に削減できました。
実装のポイント
- CodeConnectionsでシームレスなGitHub連携
- buildspec.ymlでの柔軟なビルド処理
- カスタムスクリプトによる高度な変換処理
- CDKによるInfrastructure as Code
参考リンク
この仕組みにより、開発者はドキュメントの更新に集中でき、デプロイ作業から解放されました。同様の課題を抱えている方の参考になれば幸いです。