本記事はアジアクエスト 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つの要素を構築します。
セキュリティスキャン用の学習アプリを動かす、外部に公開しない安全なインフラを構築します。
コンテナ実行環境:
stg / prd の2環境)を構築します。イメージリポジトリ:
セキュアなアクセス経路:
注意: OWASP Juice Shopには意図的に多数の脆弱性が含まれています。無制限の外部公開は絶対に避けてください。
コードの変更をAWS環境へ反映させるためのCI/CDパイプラインを構築します。
ビルド&プッシュ:
2段階デプロイフロー:
stg-juiceshop) 環境へはmainブランチへのpushをトリガーに自動でデプロイします。prd-juiceshop) 環境へは、GitHub Actionsの手動実行ワークフローを介してデプロイするフローを構築します。本記事で構築する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 |
※バージョンは執筆時点のものです。将来的な変更により、一部画面やコマンドの挙動が異なる可能性があります。
最初にAWS環境に各種リソースを構築します。
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ワークフロー
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"
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"
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を以下のように修正して対応してください。
① 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
次にGitHubのリポジトリを作成し、ECRへのイメージPushとECSデプロイを行うCI/CDパイプラインを構築します。
自身の検証用リポジトリをローカルにCloneします。
git clone https://github.com/<your-account>/<your-repository>
cd <your-repository>
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) |
登録手順:
ビルドとステージング環境へのデプロイを自動実行する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は別のワークフローで実施されている前提
プロジェクトルートに以下の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イメージ定義
ここまでの手順で環境構築が完了しましたので、GitHubにpushしてECSへのデプロイがうまくいくか試します。
以下のコマンドでGitHubにアプリケーションをpushしてください。
git add .
git commit -m "Add CI/CD workflows and Dockerfile"
git push origin main
mainブランチへpushすると、GitHub ActionsでCI/CD Pipeline (stg)が自動的に実行されます。
GitHub リポジトリの画面から確認できます:
| ジョブ名 | 内容 |
|---|---|
| Build & Push Docker Image | Dockerイメージをビルドし、ECRにlatestタグでpush |
| Deploy to Staging | ECSのstg-juiceshopサービスに対してforce-new-deploymentを実行し、新イメージでデプロイ |
stg環境へのデプロイが成功すれば、次は本番環境(prd)への手動デプロイを確認します。
本番環境は誤デプロイを防ぐため、手動実行のみとしています。
mainブランチを選択し、再度Run workflowを押すワークフローが実行されると、以下のステップが自動的に行われます:
prd-juiceshopサービスに対してforce-new-deploymentを実行
最後に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です。
owasp-devsecops-techblog-1/terraform
フォルダ配下に移動し、以下のコマンドを実行してAWSリソースを削除してください。
terraform destroy
今回は、今後セキュリティスキャンを組み込むベースとなる環境を構築しました。
次の記事では、API キー、パスワードなどの機密情報が誤ってリポジトリに含まれることを検出して防ぐ「シークレットスキャン」をパイプラインに組み込んでいきたいと思います。
最後まで読んでいただき、ありがとうございました!