SonarQubeでの静的解析をパイプラインに乗せて毎日自動実行できるようにしてみた +α(Slack通知)

# システム開発におけるセキュリティシフトレフトの実践 ～SASTパイプラインの作成～

近年、システム開発においてセキュリティの重要性がますます高まっています。特に「開発・運用プロセス全体にセキュリティを組み込む」というDevSecOpsの考え方が普及し、**セキュリティを開発初期（シフトレフト）で実装する**取り組みが重視されています。  
本記事では、その中でもSAST(静的アプリケーションセキュリティテスト)にフォーカスし、実際にパイプラインに組み込んでみた内容について紹介します。

---

## セキュリティテストの種類

セキュリティテストは主に以下の3つに分類されます。

- **SCA**（Software Composition Analysis）  
  → ライブラリやOSSの脆弱性検査

- **SAST**（Static Application Security Testing）  
  → ソースコードの静的解析による脆弱性検査

- **DAST**（Dynamic Application Security Testing）  
  → アプリケーションの実行時に外部から脆弱性を検査

今回はこの中から**SAST**にフォーカスし、SonarQubeを使ったパイプライン連携に挑戦しました。

## SonarQubeとは
SonarQubeはオープンソースの静的コード解析ツールです。コードの品質問題、セキュリティ脆弱性、バグを自動的に検出し、開発者に報告する機能を備えています。無料版（Community Edition）が提供されており、個人開発や小規模プロジェクトでも気軽に導入できるのが大きなメリットのため、このツールを使うことにしました。

## やってみたこと

### EC2でSonarQube環境を構築
構築する環境は、EC2にSonarQubeをインストールして立ち上げ、ソースコードの場所をSonarQubeサーバーに渡すことによって、静的コードチェックを行い、SonarQube内に診断レポートを作成する環境になります。この環境より開発チームは常に最新のコード品質状況を把握できるようになります。

#### EC2インスタンス作成
1. マシンイメージはAmazonLinux2023
![スクリーンショット 2025-04-28 10.54.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/6adf7ee9-5ff3-4c87-8c6e-15c06439c015.png)
1. インスタンスタイプは「t3.medium」以上推奨
![スクリーンショット 2025-04-28 10.56.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/2d48f432-394d-4547-8c9d-5145cdd730d3.png)
1. キーペアはなし(SSM接続のポリシーがIAMユーザーorIAMロールにアタッチされている必要がある)
1. ネットワーク設定はSonarQubeを稼働させたいVPC,サブネットを指定
1. セキュリティグループは新規作成
![スクリーンショット 2025-04-28 11.02.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/62282416-d709-4ada-a549-f0503c87ee72.png)
![スクリーンショット 2025-04-28 11.03.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/fd2a194c-910d-428f-8e4f-c55ea39d0f0e.png)
上記設定でインスタンスを作成
#### EC2にSSMからアクセスして、SonarQube立ち上げ
1. EC2のセッションマネージャーからEC2に接続
![スクリーンショット 2025-04-28 11.07.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/76e1fa51-864b-42c2-82a2-496a99a3d8e0.png)
1. Dockerインストール
    ```
    sudo yum install -y docker
    sudo systemctl start docker
    sudo systemctl enable docker
    ```

1. Docker composeインストール
    ```
    sudo curl -L "https://github.com/docker/compose/releases/download/v2.34.0/docker- compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 
    ```
1. sonarqube用のDockerfile保存ディレクトリ作成
    ```
    mkdir ~/sonarqube
    cd ~/sonarqube
    ```
1. Docker composeファイル作成
    ```
    vi docker-compose.yml
    ```

ファイル内容
```
    services:
      sonarqube:
        image: sonarqube:lts
        container_name: sonarqube
        environment:
          - SONARQUBE_JDBC_URL=jdbc:postgresql://db:5432/*****
          - SONARQUBE_JDBC_USERNAME=*********
          - SONARQUBE_JDBC_PASSWORD=*********
        ports:
          - "9000:9000"
        volumes:
          - sonarqube_data:/opt/sonarqube/data
          - sonarqube_extensions:/opt/sonarqube/extensions
          - sonarqube_logs:/opt/sonarqube/logs
        networks:
          - sonarnetwork
      db:
        image: postgres:13
        container_name: sonar_postgres
        environment:
          - POSTGRES_USER=******
          - POSTGRES_PASSWORD=*********
          - POSTGRES_DB=**********
        volumes:
          - sonar_postgres_data:/var/lib/postgresql/data
        networks:
          - sonarnetwork

    volumes:
          sonarqube_data:
          sonarqube_extensions:
          sonarqube_logs:
          sonar_postgres_data:

    networks:
      sonarnetwork:
```

1. Docker compose実行
    ```
    sudo docker-compose up -d
    ```
1. アクセス確認
    ```
    http://[EC2パブリックIP]:9000 //パブリックIPは今アクセスしているEC2インスタンスのIPv4パブリックIP
    ```
#### SonarQubeでプロジェクト作成
1. アクセスして初回ログイン(初期ユーザー名、パスワードは両方とも「admin」)
![スクリーンショット 2025-04-28 11.16.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/f1dee5ff-de06-40b6-8b4c-325f593bf892.png)
1. パスワード変更（初回アクセス時のみ）
![スクリーンショット 2025-04-28 11.20.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/1be9d04a-27dd-4cec-8ff1-01ed204d7de4.png)
1. プロジェクト作成
    1. どのリポジトリに対して静的コード解析を実行するか選択（Manuallyを選択）
![スクリーンショット 2025-04-28 11.21.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/10d13bb9-3941-4b5e-8ec0-96a1340d075c.png)
    1. プロジェクト名とキーを設定
![スクリーンショット 2025-04-28 11.24.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/05e676f3-b5da-446b-8d63-8d607d6eb5bf.png)
    1. どの構成管理ツールから静的コード解析を実行するか選択（ローカル実行の場合はLocally）
![スクリーンショット 2025-04-28 11.27.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/1a2e8bab-2c69-46e5-9c4b-3d17325cddcc.png)
    1. トークンを生成
    ※ Expire Inはトークンの有効期限なので、切れたら再発行が必要になります（再発行したくない場合は「No expiration」を選択）
![スクリーンショット 2025-04-28 11.30.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/d3332b73-9d84-403f-9275-918240bb793e.png)
    1. トークンは静的コード解析時に使うので、すぐに参照できるところに保存
![スクリーンショット 2025-04-28 11.35.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/ff98976d-3498-4789-bbce-06fe3b9232b4.png)
    1. スキャン実行するプロジェクトの言語と実行環境のOSを選択（今回はReactプロジェクトで実行環境はMacOS）
![スクリーンショット 2025-04-28 11.37.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/3206902c-c549-45b3-a6c2-9affeb25d7eb.png)
1. ローカルでスキャン実行するためのコマンドをインストール
    1. 下記リンクにアクセスしてSonarScannerコマンドをインストール
    https://docs.sonarsource.com/sonarqube-server/9.9/analyzing-source-code/scanners/sonarscanner/
    ※ 前提としてJavaがインストールされていること（インストールがまだの人はOpenJDKをインストールしてください）
    https://openjdk.org/

### ローカルからスキャン実行
ローカル環境でSonarQubeをセットアップし、セットアップ中に表示されたコマンドで静的コード解析を試しました。
```
sonar-scanner \
  -Dsonar.projectKey=sampleProject \
  -Dsonar.sources=. \
  -Dsonar.host.url=http://xx.xx.xx.xx:9000 \
  -Dsonar.login=sqp_9999999999999999
```
スキャン結果
![スクリーンショット 2025-04-28 12.00.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/480286f7-083c-484b-b68d-c4936cdc7eb2.png)
脆弱性や、CodeSmellなどが検知できていることを確認

### AWS上で実行（実装は[IaC]）
次に、AWS上に環境を構築し、パイプラインに組み込んだ形で実行してみました。
1. CodePipelineでCodeCommitnoリポジトリに対してCodeBuildでBuild&Scanを実行
![スクリーンショット 2025-04-28 13.27.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/60a47b3e-1943-4056-b84e-086b71c320a7.png)


### パイプラインに日時で実行（実装は[IaC]）
毎日（8:30）にトリガー起動をして、自動的にSASTが走る仕組みを構築しました。
1. EventBridghで実現
    ![スクリーンショット 2025-04-28 13.22.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/87530800-2556-434f-9205-fa24ed1adc1c.png)

### Slackで結果通知（実装は[IaC]）
さらに、解析結果をSlackに通知し、チーム全体で結果を素早く把握できるようにしました。
1. AWS SNSでSlcakのチャンネルメールアドレスに通知することで、Slackに通知を実現
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/03dbe06f-cc2f-4f9c-9d48-8fc1cf99910f.png)


[IaC]:#iacによる環境構築

---

## IaCによる環境構築

今回の取り組みでは、**Infrastructure as Code（IaC）**　を活用してSonarQube環境を整備しました。
今回はAWS CDKで実装しました。

- **EC2＋SonarQube構築**  
    EC2上にSonarQubeサーバーを構築。
    VPCとサブネット、セキュリティグループの作成
```
    const vpc = new ec2.Vpc(this, 'VPC_NAME(※ここには任意の名前を入れてください)', {
    // 今回はSonarQube用のEC2しか立てないので、cidrを28にしています（LBとか置きたい場合は変更してください）
    // IPも今回は10.9.0.0を使っていますが、使われていたら変えてください
    cidr: '10.9.0.0/28',
    maxAzs: 1,
    natGateways: 0,
    subnetConfiguration: [
    {
      name: 'VPC_NAME(ここには任意の名前を入れてください)',
      subnetType: ec2.SubnetType.PUBLIC,
      cidrMask: 28,
    },
    ],
    });
    
    // セキュリティグループ作成（port9000許可）
    const securityGroup = new ec2.SecurityGroup(this, 'SG_NAME(ここには任意の名前を入れてください)', {
      vpc,
      description: 'Allow Http port9000 access',
      allowAllOutbound: true,
        });
    // SonarQubeはデフォルトポートが9000なのでport9000を開けています
        securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(9000), 'Allow HTTP access port 9000');
    SonarQubeEC2・SSMポリシー付与IAMロール作成
    // IAMロールの作成（SSMに必要なポリシーを付与）
    const role = new iam.Role(this, 'SSMROLE_NAME()', {
      assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'), // SSM用のポリシー
        iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3FullAccess'), // S3へのフルアクセス
      ],
    });
    // Amazon Linux 2023 AMI
    const ami = ec2.MachineImage.latestAmazonLinux2023();
    
    // EC2インスタンス作成
    const ec2instance = new ec2.Instance(this, 'EC2_NAME(※ここには任意の名前を入れてください)', {
      vpc,
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
      machineImage: ami,
      securityGroup,
      role: role,
    });
    
    // EC2インスタンス起動時にSonarQubeをDockerで起動するためのUserDataを追加
    ec2instance.addUserData(
      'sudo yum update -y',
      'sudo yum install -y docker',
      'sudo systemctl start docker',
      'sudo systemctl enable docker',
      'sudo curl -L "https://github.com/docker/compose/releases/download/v2.34.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose',
      'sudo chmod +x /usr/local/bin/docker-compose',
      `echo "version: '3'
    services:
      sonarqube:
        image: sonarqube:lts
        container_name: sonarqube
        environment:
          - SONARQUBE_JDBC_URL=jdbc:postgresql://db:5432/*****
          - SONARQUBE_JDBC_USERNAME=*********
          - SONARQUBE_JDBC_PASSWORD=*********
        ports:
          - '9000:9000'
        volumes:
          - sonarqube_data:/opt/sonarqube/data
          - sonarqube_extensions:/opt/sonarqube/extensions
          - sonarqube_logs:/opt/sonarqube/logs
        networks:
          - sonarnetwork
      db:
        image: postgres:13
        container_name: sonar_postgres
        environment:
          - POSTGRES_USER=******
          - POSTGRES_PASSWORD=**********
          - POSTGRES_DB=********
        volumes:
          - sonar_postgres_data:/var/lib/postgresql/data
        networks:
          - sonarnetwork
    
    volumes:
      sonarqube_data:
      sonarqube_extensions:
      sonarqube_logs:
      sonar_postgres_data:
    
    networks:
      sonarnetwork:" > /home/ec2-user/docker-compose.yml`,
          'sudo docker-compose -f /home/ec2-user/docker-compose.yml up -d',
        )
```
#### SonarQubeの設定をコードで管理するためにシェルスクリプトを作成(APIを使った設定)
- **管理者パスワード変更**  
  セキュリティ向上のため、初期パスワードから変更。
    ```
    # [初回のみ]password変更 // SonarQubeのあるEC2パブリックURLを指定
    curl -X POST "$EC2_SONARQUBE_URL/api/users/change_password" \
    -u "admin:admin" \
    -d "login=admin&previousPassword=admin&password=$SONAR_PASSWORD"
    ```
- **プロジェクト作成**  
  解析対象プロジェクトを自動作成。
    ```
    # プロジェクト作成
    curl -X POST -u "admin:$SONAR_PASSWORD" "$EC2_SONARQUBE_URL/api/projects/create" \
    -d "name=$PROJECT_NAME&project=$PROJECT_KEY"
    ```
- **Quality Gate設定**  
    解析結果の合否基準（Quality Gate）も設定し、自動判定できるようにしました。
    重要度がBlocker,Critical,Majorのものが出た時に検知するように設定
    ```
  # Quality Gate 作成
  curl -X POST -u "admin:$SONAR_PASSWORD" "$EC2_SONARQUBE_URL/api/qualitygates/create" \
    -d "name=$QUALITY_GATE_NAME"
  # 条件追加
  curl -X POST -u "admin:$SONAR_PASSWORD" "$EC2_SONARQUBE_URL/api/qualitygates/create_condition" \
    -d "gateName=$QUALITY_GATE_NAME&metric=blocker_violations&op=GT&error=0&type=INT"

  curl -X POST -u "admin:$SONAR_PASSWORD" "$EC2_SONARQUBE_URL/api/qualitygates/create_condition" \
    -d "gateName=$QUALITY_GATE_NAME&metric=critical_violations&op=GT&error=0&type=INT"

  curl -X POST -u "admin:$SONAR_PASSWORD" "$EC2_SONARQUBE_URL/api/qualitygates/create_condition" \
    -d "gateName=$QUALITY_GATE_NAME&metric=major_violations&op=GT&error=0&type=INT"

  # 作成した Quality Gate をデフォルトに設定
  curl -X POST -u "admin:$SONAR_PASSWORD" "$EC2_SONARQUBE_URL/api/qualitygates/set_as_default" \
    -d "name=$QUALITY_GATE_NAME"
    ```
---

## パイプライン構成

パイプラインは次のような流れで構成しました。

1. **EventBridgeで時間起動**  
   定期的な実行(毎日8:30)に対応
    ```
        // EventBridge による日次スケジュール（JST 8:30実行）
     const eventbridge = new events.Rule(this, 'EVENTBRIDGE_NAME(※ここには任意の名前を入れてください)', {
       schedule: events.Schedule.cron({ minute: '30', hour: '23', weekDay: 'MON-FRI'  }), // UTC
       targets: [new targets.CodePipeline(pipline)],
     });
    ```
1. **CodeCommitからコード取得**  
   AWS CodeCommitから最新ソースを取得
   ```
    // CodeCommit リポジトリ（既存のものを参照）
     const repo = codecommit.Repository.fromRepositoryName(this, 'Repo', process.env.REPO_NAME!);

    // CodePipeline のアーティファクト
     const sourceOutput = new codepipeline.Artifact();
   ```

1. **CodeBuildでスキャン実行**  
   SonarQube解析用のジョブを実行
   ```
    // CodeBuild プロジェクト（SonarQube スキャンを実行）
     const project = new codebuild.PipelineProject(this, 'PROJECT_NAME(※ここには任意の名前を入れてください)', {
       buildSpec: codebuild.BuildSpec.fromObject({
         version: '0.2',
         phases: {
           install: {
             'runtime-versions': {
               java: 'corretto11',
             },
             commands: [
               'export SONAR_SCANNER_VERSION=4.7.0.2747',
               'export SONAR_SCANNER_HOME=$HOME/.sonar/sonar-scanner-$SONAR_SCANNER_VERSION-linux',
               'curl --create-dirs -sSLo $HOME/.sonar/sonar-scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_VERSION-linux.zip',
               'unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/',
               'export PATH=$SONAR_SCANNER_HOME/bin:$PATH',
               'export SONAR_SCANNER_OPTS="-server"',
             ],
           },
           build: {
             commands: [
               '# SonarQube スキャンの実行',
               'export PATH=$PATH:$(pwd)/sonar-scanner/bin',
               `sonar-scanner -Dsonar.projectKey=${process.env.PROJECT_KEY} -Dsonar.projectName=${process.env.PROJECT_NAME} -Dsonar.sources=. -Dsonar.host.url=${process.env.EC2_SONARQUBE_URL} -Dsonar.login=$SONAR_TOKEN -Dsonar.exclusions=${process.env.EXCLUSION_PATH} -Dsonar.inclusions=${process.env.INCLUSION_PATH} -Dsonar.log.=DEBUG -Dsonar.verbose=true`,
               ],
           },
         },
       }),
       environment: {
         buildImage: codebuild.LinuxBuildImage.STANDARD_5_0,
         environmentVariables: {
           SONAR_TOKEN: {
             value: process.env.SONAR_TOKEN, 
           },
           SNS_TOPIC_ARN: { value: topic.topicArn },
         },
       },
     });
   ```
1. Pipeline作成
    ```
        // CodePipeline 定義
    const pipline = new codepipeline.Pipeline(this, 'PIPELINE_NAME(※ここには任意の名前を入れてください)', {
      stages: [
        {
          stageName: 'Source',
          actions: [
            new codepipeline_actions.CodeCommitSourceAction({
              actionName: 'CodeCommit',
              repository: repo,
              branch: process.env.BRANCH_NAME!,
              output: sourceOutput,
            }),
          ],
        },
        {
          stageName: 'BuildAndScan',
          actions: [
            new codepipeline_actions.CodeBuildAction({
              actionName: 'SonarScan',
              project,
              input: sourceOutput,
            }),
          ],
        },
      ],
    });
    ```

1. **SNSで結果送信**  
   スキャン結果（合否や問題件数）をSNS経由で通知
   
    1. SNSトピック作成
        ```
            // SNS トピックの作成
         const topic = new sns.Topic(this, 'ALERTTOPIC_NAME(※ここには任意の名前を入れてください)', {
           displayName: 'SonarQube Quality Gate Alerts',
         });
         
         // SNS トピックにメール通知を追加
         topic.addSubscription(
           new sns_subs.EmailSubscription(process.env.EMAIL_ADDRESS!)
         );
        
         // SNSトピックに対する publish 権限をCodeBuildに付与
         topic.grantPublish(project.role!);
        ```
    1. CodeBuildでSNSを起動するコマンドを追記
        ```
        build: {
                commands: [
                  '# SonarQube スキャンの実行',
                  'export PATH=$PATH:$(pwd)/sonar-scanner/bin',
                  `sonar-scanner -Dsonar.projectKey=${process.env.PROJECT_KEY} -Dsonar.projectName=${process.env.PROJECT_NAME} -Dsonar.sources=. -Dsonar.host.url=${process.env.EC2_SONARQUBE_URL} -Dsonar.login=$SONAR_TOKEN -Dsonar.exclusions=${process.env.EXCLUSION_PATH} -Dsonar.inclusions=${process.env.INCLUSION_PATH} -Dsonar.log.=DEBUG -Dsonar.verbose=true`,
                  
                  '# Quality Gate 結果の取得',
                  `RESPONSE=$(curl -s -u "$SONAR_TOKEN:" "${process.env.EC2_SONARQUBE_URL}/api/qualitygates/project_status?projectKey=${process.env.PROJECT_KEY}")`,
                  'STATUS=$(echo "$RESPONSE" | jq -r \'.projectStatus.status\')',
        
                  `SUMMARY=$(echo "$RESPONSE" | jq -r '
                    .projectStatus.conditions[] |
                    "- \\(
                      if .status == "OK" then "✅[OK]" else "❌[NG]" end
                    ) \\(
                      if .metricKey == "new_coverage" then "カバレッジ"
                      elif .metricKey == "new_duplicated_lines_density" then "重複行(%)"
                      elif .metricKey == "new_maintainability_rating" then "保守性"
                      elif .metricKey == "new_reliability_rating" then "信頼性"
                      elif .metricKey == "new_security_rating" then "セキュリティ"
                      elif .metricKey == "new_security_hotspots_reviewed" then "セキュリティホットスポットのレビュー率"
                      elif .metricKey == "blocker_violations" then "ブロッカー違反"
                      elif .metricKey == "critical_violations" then "クリティカル違反"
                      elif .metricKey == "major_violations" then "メジャー違反"
                      else .metricKey
                      end
                    ): 値=\\(.actualValue)"' | paste -sd '\\n' -
                  )`,
        
        
                  // メール本文作成
                  `FAILED_MESSAGE="Project $PROJECT_KEY failed the Quality Gate.\nViolations Summary:\n$SUMMARY\nSonarQube URL:\n${process.env.EC2_SONARQUBE_URL}/dashboard?id=${process.env.PROJECT_KEY}"`,
        
        
                  'echo "Quality Gate Status: $STATUS"',
        
                  '# ステータスが ERROR の場合、SNS に通知',
                  'if [ "$STATUS" = "ERROR" ]; then echo "Sending alert to SNS..."; aws sns publish --topic-arn $SNS_TOPIC_ARN --subject "[❌FAILED] SonarQube Quality Gate Failed" --message "$FAILED_MESSAGE"; else aws sns publish --topic-arn $SNS_TOPIC_ARN --subject "[✅SUCCEED] SonarQube Quality Gate succeed" --message "SonarQube Quality Gate succeed"; fi',
                ],
              },
        ```

- AWS CDKコード全文(EC2)　
```
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as dotenv from 'dotenv';

dotenv.config();

export class InsurtechLabssSonarqube extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPC作成（パブリックサブネット付き）
    const vpc = new ec2.Vpc(this, 'VPC_NAME(※ここには任意の名前を入れてください)', {
      cidr: '10.9.0.0/28',
      maxAzs: 1,
      natGateways: 0,
      subnetConfiguration: [
        {
          name: 'insurtech-labss-sonarqube-subnet-public',
          subnetType: ec2.SubnetType.PUBLIC,
          cidrMask: 28,
        },
      ],
    });


    // セキュリティグループ作成（port9000許可）
    const securityGroup = new ec2.SecurityGroup(this, 'SG_NAME(※ここには任意の名前を入れてください)', {
      vpc,
      description: 'Allow Http port9000 access',
      allowAllOutbound: true,
    });

    securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(9000), 'Allow HTTP access port 9000');



    // IAMロールの作成（SSMに必要なポリシーを付与）
    const role = new iam.Role(this, 'SSMROLE_NAME(※ここには任意の名前を入れてください)', {
      assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'), // SSM用のポリシー
        iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3FullAccess'), // S3へのフルアクセス
      ],
    });
    

    // Amazon Linux 2023 AMI
    const ami = ec2.MachineImage.latestAmazonLinux2023();

    // EC2インスタンス作成
    const ec2instance = new ec2.Instance(this, 'EC2INSTANCE_NAME(※ここには任意の名前を入れてください)', {
      vpc,
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
      machineImage: ami,
      securityGroup,
      role: role,
    });

    // EC2インスタンス起動時にSonarQubeをDockerで起動するためのUserDataを追加
    ec2instance.addUserData(
      'sudo yum update -y',
      'sudo yum install -y docker',
      'sudo systemctl start docker',
      'sudo systemctl enable docker',
      'sudo curl -L "https://github.com/docker/compose/releases/download/v2.34.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose',
      'sudo chmod +x /usr/local/bin/docker-compose',
      `echo "version: '3'
services:
  sonarqube:
    image: sonarqube:lts
    container_name: sonarqube
    environment:
      - SONARQUBE_JDBC_URL=jdbc:postgresql://db:5432/******
      - SONARQUBE_JDBC_USERNAME=*********
      - SONARQUBE_JDBC_PASSWORD=*********
    ports:
      - '9000:9000'
    volumes:
      - sonarqube_data:/opt/sonarqube/data
      - sonarqube_extensions:/opt/sonarqube/extensions
      - sonarqube_logs:/opt/sonarqube/logs
    networks:
      - sonarnetwork
  db:
    image: postgres:13
    container_name: sonar_postgres
    environment:
      - POSTGRES_USER=*********
      - POSTGRES_PASSWORD=*********
      - POSTGRES_DB=********
    volumes:
      - sonar_postgres_data:/var/lib/postgresql/data
    networks:
      - sonarnetwork

volumes:
  sonarqube_data:
  sonarqube_extensions:
  sonarqube_logs:
  sonar_postgres_data:

networks:
  sonarnetwork:" > /home/ec2-user/docker-compose.yml`,
      'sudo docker-compose -f /home/ec2-user/docker-compose.yml up -d',
    )
  }
}

```
- AWS CDKコード全文(Pipeline) 
```
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as codecommit from 'aws-cdk-lib/aws-codecommit';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as sns_subs from 'aws-cdk-lib/aws-sns-subscriptions';
import * as dotenv from 'dotenv';
dotenv.config();

export class SonarqubePipelineStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // CodeCommit リポジトリ（既存のものを参照）
    const repo = codecommit.Repository.fromRepositoryName(this, 'Repo', process.env.REPO_NAME!);

    // SNS トピックの作成
    const topic = new sns.Topic(this, 'ALERTTOPIC_NAME(※ここには任意の名前を入れてください)', {
      displayName: 'SonarQube Quality Gate Alerts',
    });

    // CodePipeline のアーティファクト
    const sourceOutput = new codepipeline.Artifact();

    // CodeBuild プロジェクト（SonarQube スキャンを実行）
    const project = new codebuild.PipelineProject(this, 'PROJECT_NAME(※ここには任意の名前を入れてください)', {
      buildSpec: codebuild.BuildSpec.fromObject({
        version: '0.2',
        phases: {
          install: {
            'runtime-versions': {
              java: 'corretto11',
            },
            commands: [
              'export SONAR_SCANNER_VERSION=4.7.0.2747',
              'export SONAR_SCANNER_HOME=$HOME/.sonar/sonar-scanner-$SONAR_SCANNER_VERSION-linux',
              'curl --create-dirs -sSLo $HOME/.sonar/sonar-scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_VERSION-linux.zip',
              'unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/',
              'export PATH=$SONAR_SCANNER_HOME/bin:$PATH',
              'export SONAR_SCANNER_OPTS="-server"',
            ],
          },
          build: {
            commands: [
              '# SonarQube スキャンの実行',
              'export PATH=$PATH:$(pwd)/sonar-scanner/bin',
              `sonar-scanner -Dsonar.projectKey=${process.env.PROJECT_KEY} -Dsonar.projectName=${process.env.PROJECT_NAME} -Dsonar.sources=. -Dsonar.host.url=${process.env.EC2_SONARQUBE_URL} -Dsonar.login=$SONAR_TOKEN -Dsonar.exclusions=${process.env.EXCLUSION_PATH} -Dsonar.inclusions=${process.env.INCLUSION_PATH} -Dsonar.log.=DEBUG -Dsonar.verbose=true`,
              
              '# Quality Gate 結果の取得',
              `RESPONSE=$(curl -s -u "$SONAR_TOKEN:" "${process.env.EC2_SONARQUBE_URL}/api/qualitygates/project_status?projectKey=${process.env.PROJECT_KEY}")`,
              'STATUS=$(echo "$RESPONSE" | jq -r \'.projectStatus.status\')',

              `SUMMARY=$(echo "$RESPONSE" | jq -r '
                .projectStatus.conditions[] |
                "- \\(
                  if .status == "OK" then "✅[OK]" else "❌[NG]" end
                ) \\(
                  if .metricKey == "new_coverage" then "カバレッジ"
                  elif .metricKey == "new_duplicated_lines_density" then "重複行(%)"
                  elif .metricKey == "new_maintainability_rating" then "保守性"
                  elif .metricKey == "new_reliability_rating" then "信頼性"
                  elif .metricKey == "new_security_rating" then "セキュリティ"
                  elif .metricKey == "new_security_hotspots_reviewed" then "セキュリティホットスポットのレビュー率"
                  elif .metricKey == "blocker_violations" then "ブロッカー違反"
                  elif .metricKey == "critical_violations" then "クリティカル違反"
                  elif .metricKey == "major_violations" then "メジャー違反"
                  else .metricKey
                  end
                ): 値=\\(.actualValue)"' | paste -sd '\\n' -
              )`,


              // メール本文作成
              `FAILED_MESSAGE="Project $PROJECT_KEY failed the Quality Gate.\nViolations Summary:\n$SUMMARY\nSonarQube URL:\n${process.env.EC2_SONARQUBE_URL}/dashboard?id=${process.env.PROJECT_KEY}"`,


              'echo "Quality Gate Status: $STATUS"',
    
              '# ステータスが ERROR の場合、SNS に通知',
              'if [ "$STATUS" = "ERROR" ]; then echo "Sending alert to SNS..."; aws sns publish --topic-arn $SNS_TOPIC_ARN --subject "[❌FAILED] SonarQube Quality Gate Failed" --message "$FAILED_MESSAGE"; else aws sns publish --topic-arn $SNS_TOPIC_ARN --subject "[✅SUCCEED] SonarQube Quality Gate succeed" --message "SonarQube Quality Gate succeed"; fi',
            ],
          },
        },
      }),
      environment: {
        buildImage: codebuild.LinuxBuildImage.STANDARD_5_0,
        environmentVariables: {
          SONAR_TOKEN: {
            value: process.env.SONAR_TOKEN, 
          },
          SNS_TOPIC_ARN: { value: topic.topicArn },
        },
      },
    });

    // CodePipeline 定義
    const pipline = new codepipeline.Pipeline(this, 'PIPELINE_NAME(※ここには任意の名前を入れてください)', {
      stages: [
        {
          stageName: 'Source',
          actions: [
            new codepipeline_actions.CodeCommitSourceAction({
              actionName: 'CodeCommit',
              repository: repo,
              branch: process.env.BRANCH_NAME!,
              output: sourceOutput,
            }),
          ],
        },
        {
          stageName: 'BuildAndScan',
          actions: [
            new codepipeline_actions.CodeBuildAction({
              actionName: 'SonarScan',
              project,
              input: sourceOutput,
            }),
          ],
        },
      ],
    });
    // EventBridge による日次スケジュール（JST 8:30実行）
    const eventbridge = new events.Rule(this, 'EVENTBRIDGE_NAME(※ここには任意の名前を入れてください)', {
      schedule: events.Schedule.cron({ minute: '30', hour: '23', weekDay: 'MON-FRI'  }), // UTC
      targets: [new targets.CodePipeline(pipline)],
    });


    // SNS トピックにメール通知を追加
    topic.addSubscription(
      new sns_subs.EmailSubscription(process.env.EMAIL_ADDRESS!)
    );

    // SNSトピックに対する publish 権限をCodeBuildに付与
    topic.grantPublish(project.role!);


      // 生成リソースにタグを付与
      cdk.Tags.of(project).add('Dept', 'InsurTech');
      cdk.Tags.of(project).add('SystemName', process.env.SYSTEM_NAME!);
      cdk.Tags.of(pipline).add('Dept', 'InsurTech');
      cdk.Tags.of(pipline).add('SystemName', process.env.SYSTEM_NAME!);
      cdk.Tags.of(eventbridge).add('Dept', 'InsurTech');
      cdk.Tags.of(eventbridge).add('SystemName', process.env.SYSTEM_NAME!);
      cdk.Tags.of(topic).add('Dept', 'InsurTech');
      cdk.Tags.of(topic).add('SystemName', process.env.SYSTEM_NAME!);
  }
}
```

---

## 結果

- **毎回確実にチェックできる仕組みが完成！**  
  定期実行により、自動でコード品質を確認できるようになりました。

- **Slack通知による確認の簡略化**
    AWS環境やSonarQubeにアクセスせずにざっくりとしたエラーがどうかを確認できるようになりました。
---

## まとめ

今回の取り組みにより、**パイプライン上での静的解析が自動化**され、セキュリティリスクを早期発見できるようになりました。  
特に、定期実行に対応したことで、継続的にコード品質を保つ体制が整いました。

次回は、さらに範囲を広げて**DAST**についても取り組み、実行結果の共有方法まで深堀りしていきたいと思います。