AQ Tech Blog

手を動かして学ぶOWASP DevSecOps Guideline - 第1回:CI/CD基盤構築編

作成者: kohei.nomura|2025年12月19日

本記事はアジアクエスト Advent Calendar 2025の記事です。

はじめに

はじめまして!クラウドインテグレーション部の野村です。

近年、開発と運用の垣根を低くし、スピードと品質を両立する「DevOps」プロセスに、セキュリティを組み込む考え方として「DevSecOps」が注目されています。

しかし、DevSecOpsを実践するうえで「具体的にどのような実装を行えばよいのか」がイメージしづらいと感じる方も多いのではないでしょうか。

そこで本記事から複数回に渡って、OWASPが公開しているOWASP DevSecOps Guidelineを参考に、実際にCI/CDパイプラインを構築しながらその考え方を学んでいきたいと思います。

おことわり

DevSecOpsは本来、要件定義や設計段階でのセキュリティポリシー策定から、運用フェーズでの監査までを含む広い概念です。
その中でもCI/CD パイプラインへのセキュリティ実装に焦点を当て、開発からデプロイまでの自動化プロセスを通じてDevSecOpsを実践的に学ぶことを目的とします。

最終目標と参照ガイドライン

最終的にOWASP DevSecOps Guidelineが提唱する下図のようなセキュアなCI/CDパイプラインを再現することを目指します。

※今回実装する対象は下図の上部にあるアプリケーション側のCI/CDパイプラインとセキュリティチェック及び、Central Vulnerability Management(脆弱性統合管理ツール)とします。

出典:OWASP Foundation, ‘OWASP DevSecOps Guideline - v-0.2’, Licensed under CC BY-SA 4.0

上図では、CI/CDパイプラインの各フェーズで、以下のようなセキュリティチェックが自動で実行されるようにすることで、アプリケーションがデプロイされる前の段階で脆弱性に対処できるようになっています。

  • Secret Scanning : Gitリポジトリやコード内で、APIキー、パスワードなどの機密情報が含まれていないかチェックします。(ツール例:git-secret)

  • SAST(静的コード解析) : アプリケーションのソースコードを実行せずに分析し、セキュリティ上の脆弱性を検出します。(ツール例:SonarQube)

  • SCA(ソフトウェア構成分析) : アプリケーションのソースコードを解析し、使用されているOSSやライブラリを特定します。検出したOSSやライブラリの品質を評価する機能を持つものも存在します。(ツール例:snyk)

  • Container Scanning : コンテナイメージをスキャンし、脆弱性データベースと照らし合わせて対象となっているCVEを特定します。(ツール例:trivy、AWS Inspector)

  • DAST(動的コード解析) : 実行中のアプリケーションに対して疑似攻撃を仕掛け、アプリケーションの実行時でないと発見しずらい問題(SQLインジェクション、クロスサイトスクリプティングなど)を検出します。図では、stg環境にデプロイしたアプリケーションに対して実行しています。(ツール例:OWASP ZAP)

  • Central Vulnerability Management(脆弱性統合管理ツール) : 様々なセキュリティコンポーネントのスキャン結果を一元管理し、システム管理者が脆弱性情報を把握しやすくします。(ツール例:OWASP DefectDojo、AWS Security Hub)

本記事で実施すること

本記事では、今後の連載で各種セキュリティチェックを組み込んでいくためのベースラインとして、以下の2つの要素を構築します。

1. アプリケーションのデプロイ先となるAWSリソースの準備

セキュリティスキャン用の学習アプリを動かす、外部に公開しない安全なインフラを構築します。

  • コンテナ実行環境:

    • Amazon ECS(Fargate)クラスターとサービス(stg / prd の2環境)を構築します。
    • デプロイするアプリケーションには、学習用の脆弱性を多数含むOWASP Juice Shopを使用します。(今後のセキュリティスキャンで脆弱性が検出されるようにするため)
  • イメージリポジトリ:

    • CI/CDパイプラインでビルドしたコンテナイメージを格納するAmazon ECRリポジトリを準備します。
  • セキュアなアクセス経路:

    • アプリケーションを外部インターネットに公開せず、踏み台EC2SSM Session Manager経由でのみアクセス可能な安全な経路を確立します。

注意: OWASP Juice Shopには意図的に多数の脆弱性が含まれています。無制限の外部公開は絶対に避けてください。


2. アプリケーションをAWSリソースにデプロイするGitHub Actionsパイプラインの構築

コードの変更をAWS環境へ反映させるためのCI/CDパイプラインを構築します。

  • ビルド&プッシュ:

    • OWASP Juice Shopのコンテナイメージを取得・ビルドし、ECRリポジトリにプッシュするワークフローを実装します。
  • 2段階デプロイフロー:

    • Staging(stg-juiceshop) 環境へはmainブランチへのpushをトリガーに自動でデプロイします。
    • Production(prd-juiceshop) 環境へは、GitHub Actionsの手動実行ワークフローを介してデプロイするフローを構築します。

構成


CI/CDパイプラインの流れ

本記事で構築するGitHub Actionsのワークフローは、以下のように動作します。

  1. mainブランチへのpushをトリガーにパイプラインが起動
  2. DockerイメージをビルドしてECRにpush
  3. stg環境(stg-juiceshop)に自動デプロイ
  4. GitHub Actionsの手動実行ワークフローで本番にデプロイする

コスト

作成する検証環境のおおよその月額コストは以下の通りです。(2025/12時点)

サービス 料金
ALB 約$23
EC2 約$11
ECS 約$22
合計 約$56

前提条件

記事の内容を実践するにあたって、以下の環境が準備されていることを前提とします。

  • AWS CLIがインストール済みで、認証情報が設定済みであること

  • AWS CLIのSession Managerプラグインがインストール済みであること

  • Terraformがインストール済みであること

  • Dockerがインストール済みであること

  • Gitがインストール済みであること

  • GitHub アカウントが作成済みで、検証用のリポジトリが作成されていること

今回使用したソフトウェア/ミドルウェアのバージョンは以下の通りです。

名称 バージョン
aws-cli 2.31.22
Terraform 1.13.5
Docker 28.2.2
Git 2.43.0

※バージョンは執筆時点のものです。将来的な変更により、一部画面やコマンドの挙動が異なる可能性があります。

手順

1. AWSリソースの構築

最初にAWS環境に各種リソースを構築します。

1.1 Terraformコードの取得

Terraformコードを以下のリポジトリからクローン

git clone https://github.com/koheinomura-aq/owasp-devsecops-techblog-1

ディレクトリ構成は以下のようになっています。

owasp-devsecops-techblog-1/       # プロジェクトルート
├─ README.md
├─ .gitignore
├─ terraform/
│ ├─ provider.tf # provider設定・Terraformバージョン
│ ├─ network.tf # VPC / サブネット / RT / VPCエンドポイント
│ ├─ bastion.tf # 踏み台EC2・SSM用IAMロール・SG・AMI
│ ├─ ecs_ecr_alb.tf # ECR / ECS / ALB / タスク定義 / SG Service
│ ├─ github-actions.tf # GitHub OIDC / Actions用IAMロール
| ├─ variables.tf # 変数ファイル
│ ├─ outputs.tf # リソースの出力値
| └─ terraform.tfvars.example # 環境変数ファイルのテンプレート

└─ .github/
└─ workflows/
├─ deploy-stg.yml # ステージングデプロイ用GitHub Actionsワークフロー
└─ deploy-prd.yml # 本番デプロイ用GitHub Actionsワークフロー


1.2 環境変数の設定

GitHub ActionsのOIDC制御に利用するため、 実行するGitHubリポジトリ情報をterraform.tfvarsに設定します。

cd owasp-devsecops-techblog-1/terraform
cp terraform.tfvars.example terraform.tfvars

terraform.tfvarsを編集し、以下を自分のリポジトリに合わせて設定します。

github_repo_owner = "your-github-username"
github_repo_name = "your-repository-name"


1.3 AWSリソースのデプロイ

owasp-devsecops-techblog-1/terraformフォルダ配下で、以下のコマンドを実行します。

Terraformの初期化

terraform init

実行計画の確認

terraform plan

リソースの作成

terraform apply

実行後、以下のようなOutputsが表示されます。

Apply complete! Resources: 38 added, 0 changed, 0 destroyed.

Outputs:

alb_dns_name = "internal-xxxxx-internal-alb-xxxxxxxxxx.ap-northeast-1.elb.amazonaws.com"
bastion_instance_id = "i-xxxxxxxxxxxxxxxxx"
ecr_repository_url = "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/devsecops-juiceshop"
ecs_cluster_name = "devsecops-juiceshop-cluster"
github_actions_role_arn = "arn:aws:iam::xxxxxxxxxxxx:role/devsecops-github-actions-role"
prd_service_name = "prd-juiceshop"
stg_service_name = "stg-juiceshop"
subnets = {
"private_a" = "subnet-xxxxxxxxxxxxxxxxx"
"private_c" = "subnet-xxxxxxxxxxxxxxxxx"
}
vpc_endpoints_dns = {
"ec2messages" = "vpce-xxxxxxxxxxxxxxxxx.ec2messages.ap-northeast-1.vpce.amazonaws.com"
"ecr_api" = "vpce-xxxxxxxxxxxxxxxxx.api.ecr.ap-northeast-1.vpce.amazonaws.com"
"ecr_dkr" = "vpce-xxxxxxxxxxxxxxxxx.dkr.ecr.ap-northeast-1.vpce.amazonaws.com"
"logs" = "vpce-xxxxxxxxxxxxxxxxx.logs.ap-northeast-1.vpce.amazonaws.com"
"ssm" = "vpce-xxxxxxxxxxxxxxxxx.ssm.ap-northeast-1.vpce.amazonaws.com"
"ssmmessages" = "vpce-xxxxxxxxxxxxxxxxx.ssmmessages.ap-northeast-1.vpce.amazonaws.com"
}
vpc_id = "vpc-xxxxxxxxxxxxxxxxx"

このうち、GitHub Actionsの設定で使用する値は以下の5つです。

ecr_repository_url = "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/devsecops-juiceshop"
ecs_cluster_name = "devsecops-juiceshop-cluster"
github_actions_role_arn = "arn:aws:iam::xxxxxxxxxxxx:role/devsecops-github-actions-role"
prd_service_name = "prd-juiceshop"
stg_service_name = "stg-juiceshop"


GitHub OIDC Provider作成時のエラーが出る場合

terraform applyの実行時に、環境によっては以下のエラーが出る可能性があります。

Error: creating IAM OIDC Provider: operation error IAM: CreateOpenIDConnectProvider,
StatusCode: 409, EntityAlreadyExists:
Provider with url https://token.actions.githubusercontent.com already exists.

これは GitHub OIDCプロバイダーが既にAWSアカウントに存在しているため発生する正常なエラーです。

その場合は、Terraformを以下のように修正して対応してください。

修正手順(github-actions.tf)

① OIDCプロバイダー作成リソース(10〜21行目)をコメントアウト

# resource "aws_iam_openid_connect_provider" "github" {
# url = "https://token.actions.githubusercontent.com"
#
# client_id_list = [
# "sts.amazonaws.com"
# ]
#
# thumbprint_list = [
# "9e99a48a9960b14926bb7f3b02e22da0ecd4e50f"
# ]
# }

② 既存OIDCを参照するdataブロック(28〜30行目)のコメントアウトを解除

data "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
}

③ IAMロールのPrincipal.Federatedの参照先をresourceからdataに変更

# Federated = aws_iam_openid_connect_provider.github.arn
Federated = data.aws_iam_openid_connect_provider.github.arn

修正後、再度以下を実行してください。

terraform apply

2.GitHub Actionsの設定

次にGitHubのリポジトリを作成し、ECRへのイメージPushとECSデプロイを行うCI/CDパイプラインを構築します。

2.1 ローカルにリポジトリをClone

自身の検証用リポジトリをローカルにCloneします。

git clone https://github.com/<your-account>/<your-repository>
cd <your-repository>


2.2 GitHub Secretsの登録

Terraformのoutputで得られる値をGitHub Secretsに登録します。
ここで登録されたSecretsは、GitHub Actionsで、AWSリソースへのデプロイを行う際に使用されます。

Secret 名 内容
AWS_ROLE_TO_ASSUME Terraformで作成したGitHub Actions用IAMロールARN
AWS_REGION ap-northeast-1
ECR_REPOSITORY ECRリポジトリ URL
ECS_CLUSTER ECSクラスター名(devsecops-juiceshop-cluster)
ECS_SERVICE_STG ECSのstgサービス名(stg-juiceshop)
ECS_SERVICE_PRD ECSのprdサービス名(prd-juiceshop)

登録手順:

  1. GitHub → リポジトリ → Settings
  2. Secrets and variables → Actions
  3. New repository secret

作成画面 

シークレットの一覧 

2.3 GitHub Actionsのワークフロー作成

ビルドとステージング環境へのデプロイを自動実行するGitHub Actionsワークフロー、及び手動実行で本番環境へのデプロイを行うワークフローを.github/workflows/配下へ追加します。

手順1.1でクローンしたowasp-devsecops-techblog-1/ディレクトリ配下の.githubディレクトリをそのまま自身の検証用リポジトリのプロジェクトルート直下へコピーしてください。

.github/workflows/deploy-stg.yml

name: CI/CD Pipeline (stg)

on:
push:
branches:
- main # mainブランチにpushされたときにパイプラインが動作

permissions:
id-token: write # OIDCを使ったロール引き受けに必要
contents: read

jobs:
build-and-push:
name: Build & Push Docker Image
runs-on: ubuntu-latest # GitHub Actionsの実行環境

steps:
- name: Checkout
uses: actions/checkout@v4
# リポジトリのソースコードを取得

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: $ # OIDCでAssumeするIAMロール
aws-region: $ # AWSリージョン
# ここでGitHubがOIDCを使ってAWS IAMロールを引き受ける

- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
# ECRにログインしてdocker pushを行えるようにする

- name: Build Docker image
run: |
docker build -t juice:latest .
# Dockerfileをもとにイメージをビルド

- name: Tag image
run: |
docker tag juice:latest $:latest
# ビルドしたイメージにECRリポジトリのタグ(latest)を付与

- name: Push image to ECR
run: |
docker push $:latest
# ECRにDockerイメージをpush(latest タグ)

deploy-stg:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: build-and-push #build & pushが成功したら実行

steps:
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: $
aws-region: $
# 再度AWSロールを引き受け(ジョブごとに必要)

- name: Deploy to ECS (stg)
run: |
aws ecs update-service \
--cluster $ \
--service $ \
--force-new-deployment
# ECSのServiceに対してForce New Deploymentを実行
# → 最新のECRイメージを使って再デプロイが走る

.github/workflows/deploy-prd.yml

name: Deploy to Production

on:
workflow_dispatch: # ← 手動実行のみ(mainへのpushでは動かない)
# 本番デプロイは人間がトリガーするようにし、誤デプロイを防ぐ

permissions:
id-token: write # AWS IAMロールの引き受け(OIDC)に必要
contents: read

jobs:
deploy-prd:
name: Deploy to Production
runs-on: ubuntu-latest # GitHub Actionsの実行環境

steps:
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: $ # OIDCで引き受けるIAMロール
aws-region: $ # AWSリージョン
# GitHub ActionsがAWSにアクセスできるように認証・権限設定を行う

- name: Deploy to ECS (prd)
run: |
aws ecs update-service \
--cluster $ \
--service $ \
--force-new-deployment
# ECSサービスに対してForce New Deploymentを実行
# → 最新タグのコンテナイメージ(latest など)を使用して再デプロイ
# ※イメージのbuild & pushは別のワークフローで実施されている前提

2.4 Dockerfile を追加

プロジェクトルートに以下のDockerfileを追加します。

※ Docker Hubにある公式Juice Shopの最新版イメージをベースにして、それをそのまま使ったイメージを作成する内容

※ 本記事では手順の簡易化のためDocker Hubからのビルド済みイメージ取得としていますが、後続記事でSASTなどを試す際にはソースコードからビルドする形に変更予定です。

FROM bkimminich/juice-shop:latest

ここまでの手順でディレクトリ構成は以下の通りになります。

<your-repository>/
├── .github/
│ └── workflows/
│ ├── deploy-stg.yml # ステージング用GitHub Actionsワークフロー
│ └── deploy-prd.yml # 本番用GitHub Actionsワークフロー
└── Dockerfile # Juice ShopベースのDockerイメージ定義

3.デプロイ

ここまでの手順で環境構築が完了しましたので、GitHubにpushしてECSへのデプロイがうまくいくか試します。

3.1 コードのpush

以下のコマンドでGitHubにアプリケーションをpushしてください。

git add .
git commit -m "Add CI/CD workflows and Dockerfile"
git push origin main


3.2 CI/CD Pipeline (stg)の動作確認

mainブランチへpushすると、GitHub ActionsでCI/CD Pipeline (stg)が自動的に実行されます。

GitHub リポジトリの画面から確認できます:

  1. GitHub → Actionsを開く
  2. 最新のワークフロー(CI/CD Pipeline (stg))がrunningになっていることを確認
  3. 正常に動作すれば以下の2ジョブが成功します
    • Build & Push Docker Image
    • Deploy to Staging

各ジョブで行われること

ジョブ名 内容
Build & Push Docker Image Dockerイメージをビルドし、ECRにlatestタグでpush
Deploy to Staging ECSのstg-juiceshopサービスに対してforce-new-deploymentを実行し、新イメージでデプロイ


3.3 Deploy to Production実行

stg環境へのデプロイが成功すれば、次は本番環境(prd)への手動デプロイを確認します。
本番環境は誤デプロイを防ぐため、手動実行のみとしています。

実行手順

  1. GitHub → Actions を開く
  2. 左側メニューからDeploy to Productionを選択
  3. 右上のRun workflowボタンをクリック
  4. mainブランチを選択し、再度Run workflowを押す

ワークフローが実行されると、以下のステップが自動的に行われます:

  • GitHub ActionsがOIDC を使用して IAM ロールを引き受ける
  • prd-juiceshopサービスに対してforce-new-deploymentを実行
  • 最新のコンテナイメージでprd環境が更新される

 

4.動作確認

最後にOWASP Juice Shopへアクセス可能か確認します。

以下のAWS CLIコマンドを実行し、Session Managerでポートフォワードを開きます。

・ステージング接続用コマンド

aws ssm start-session   --target <BastionサーバのインスタンスID>   --document-name AWS-StartPortForwardingSessionToRemoteHost   --parameters '{"host":["<ALBのDNS名>"],"portNumber":["8080"],"localPortNumber":["<任意のローカルポート番号>"]}'

・本番接続用コマンド

aws ssm start-session   --target <BastionサーバのインスタンスID>   --document-name AWS-StartPortForwardingSessionToRemoteHost   --parameters '{"host":["<ALBのDNS名>"],"portNumber":["80"],"localPortNumber":["<任意のローカルポート番号>"]}'

ブラウザから以下のURLにアクセスします。

・ステージング

http://127.0.0.1:<localPortNumber>/

・本番

http://127.0.0.1:<localPortNumber>/

OWASP Juice Shopのトップ画面がそれぞれ表示されればOKです。

5.リソースの削除

owasp-devsecops-techblog-1/terraform

フォルダ配下に移動し、以下のコマンドを実行してAWSリソースを削除してください。

terraform destroy

まとめ

今回は、今後セキュリティスキャンを組み込むベースとなる環境を構築しました。

次の記事では、API キー、パスワードなどの機密情報が誤ってリポジトリに含まれることを検出して防ぐ「シークレットスキャン」をパイプラインに組み込んでいきたいと思います。

最後まで読んでいただき、ありがとうございました!