3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-04-28

システム開発におけるセキュリティシフトレフトの実践 ~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
  2. インスタンスタイプは「t3.medium」以上推奨
    スクリーンショット 2025-04-28 10.56.09.png
  3. キーペアはなし(SSM接続のポリシーがIAMユーザーorIAMロールにアタッチされている必要がある)
  4. ネットワーク設定はSonarQubeを稼働させたいVPC,サブネットを指定
  5. セキュリティグループは新規作成
    スクリーンショット 2025-04-28 11.02.19.png
    スクリーンショット 2025-04-28 11.03.13.png
    上記設定でインスタンスを作成

EC2にSSMからアクセスして、SonarQube立ち上げ

  1. EC2のセッションマネージャーからEC2に接続
    スクリーンショット 2025-04-28 11.07.04.png

  2. Dockerインストール

    sudo yum install -y docker
    sudo systemctl start docker
    sudo systemctl enable docker
    
  3. 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 
    
  4. sonarqube用のDockerfile保存ディレクトリ作成

    mkdir ~/sonarqube
    cd ~/sonarqube
    
  5. 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
    
  2. アクセス確認
    http://[EC2パブリックIP]:9000 //パブリックIPは今アクセスしているEC2インスタンスのIPv4パブリックIP
    

SonarQubeでプロジェクト作成

  1. アクセスして初回ログイン(初期ユーザー名、パスワードは両方とも「admin」)
    スクリーンショット 2025-04-28 11.16.38.png
  2. パスワード変更(初回アクセス時のみ)
    スクリーンショット 2025-04-28 11.20.19.png
  3. プロジェクト作成
    1. どのリポジトリに対して静的コード解析を実行するか選択(Manuallyを選択)
      スクリーンショット 2025-04-28 11.21.11.png
    2. プロジェクト名とキーを設定
      スクリーンショット 2025-04-28 11.24.07.png
    3. どの構成管理ツールから静的コード解析を実行するか選択(ローカル実行の場合はLocally)
      スクリーンショット 2025-04-28 11.27.36.png
    4. トークンを生成
      ※ Expire Inはトークンの有効期限なので、切れたら再発行が必要になります(再発行したくない場合は「No expiration」を選択)
      スクリーンショット 2025-04-28 11.30.32.png
    5. トークンは静的コード解析時に使うので、すぐに参照できるところに保存
      スクリーンショット 2025-04-28 11.35.28.png
    6. スキャン実行するプロジェクトの言語と実行環境のOSを選択(今回はReactプロジェクトで実行環境はMacOS)
      スクリーンショット 2025-04-28 11.37.58.png
  4. ローカルでスキャン実行するためのコマンドをインストール
    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
脆弱性や、CodeSmellなどが検知できていることを確認

AWS上で実行(実装はIaC

次に、AWS上に環境を構築し、パイプラインに組み込んだ形で実行してみました。

  1. CodePipelineでCodeCommitnoリポジトリに対してCodeBuildでBuild&Scanを実行
    スクリーンショット 2025-04-28 13.27.13.png

パイプラインに日時で実行(実装はIaC

毎日(8:30)にトリガー起動をして、自動的にSASTが走る仕組みを構築しました。

  1. EventBridghで実現
    スクリーンショット 2025-04-28 13.22.27.png

Slackで結果通知(実装はIaC

さらに、解析結果をSlackに通知し、チーム全体で結果を素早く把握できるようにしました。

  1. AWS SNSでSlcakのチャンネルメールアドレスに通知することで、Slackに通知を実現
    image.png

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)],
     });
    
  2. CodeCommitからコード取得
    AWS CodeCommitから最新ソースを取得

     // CodeCommit リポジトリ(既存のものを参照)
      const repo = codecommit.Repository.fromRepositoryName(this, 'Repo', process.env.REPO_NAME!);
    
     // CodePipeline のアーティファクト
      const sourceOutput = new codepipeline.Artifact();
    
  3. 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 },
          },
        },
      });
    
  4. 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,
            }),
          ],
        },
      ],
    });
    
  5. 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!);
      
    2. 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についても取り組み、実行結果の共有方法まで深堀りしていきたいと思います。

3
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?