Azure DevOps

CI/CDAzure DevOpsMicrosoftYAMLクラウドエンタープライズDevOps統合

CI/CDツール

Azure DevOps

概要

Azure DevOpsはMicrosoftの包括的なDevOpsプラットフォームです。CI/CD、プロジェクト管理、Git、アーティファクト管理を統合し、YAMLパイプラインによる現代的なワークフロー管理を提供します。

詳細

Azure DevOps(アジュール デブオプス)は、Microsoftが提供する包括的なDevOpsプラットフォームとして、2018年にVisual Studio Team Servicesから進化したクラウドサービスです。CI/CD、ソースコード管理、プロジェクト管理、アーティファクト管理、テスト管理を単一のプラットフォームで統合し、開発からデプロイまでの全工程をサポートします。2024-2025年には、マルチステージYAMLパイプラインが主要機能となり、ビルド、テスト、デプロイフェーズを複数ステージに分割する高度なCI/CDワークフローを実現しています。YAMLベースのパイプラインにより、コードファーストでの設定・管理が可能で、バージョン管理、再利用性、複数プロジェクト間での容易な管理を提供します。Azure Kubernetes Service(AKS)やAzure Container Appsとの連携により、マイクロサービスアーキテクチャへの対応を強化し、オンプレミスサーバーからクラウドネイティブプラットフォームまで統一したデプロイメントを実現します。Microsoftは過去数年間、セキュリティ向上に重点を置き、クラシックパイプラインよりもYAMLパイプラインの使用を推奨しています。

メリット・デメリット

メリット

  • オールインワンプラットフォーム: CI/CD、Git、Issues、Artifacts統合
  • 強力なYAMLサポート: コードファーストでの設定・バージョン管理
  • マルチステージパイプライン: 複雑なワークフローの段階的実行
  • Azureとの統合: ネイティブなMicrosoft Cloud連携
  • エンタープライズ機能: セキュリティ、ガバナンス、スケーラビリティ
  • クロスプラットフォーム: Linux、Windows、macOS対応
  • 豊富なMarketplace: サードパーティ拡張機能の充実
  • 無料プラン: 5ユーザーまで無料利用可能

デメリット

  • Microsoft生態系依存: Azure以外での機能制限
  • 学習コスト: 機能豊富な分、習得に時間が必要
  • UI/UXの複雑さ: 多機能により操作が複雑
  • 料金体系: 大規模チームでのライセンスコスト
  • パフォーマンス: 大量データ処理時のレスポンス低下
  • カスタマイズ制限: Jenkins等と比較した柔軟性の制約
  • 競合ツール統合: Microsoft以外のツールとの統合課題

主要リンク

書き方の例

基本的なYAMLパイプライン

# azure-pipelines.yml
trigger:
- main
- develop

pool:
  vmImage: 'ubuntu-latest'

variables:
  buildConfiguration: 'Release'
  dotNetFramework: 'net8.0'

stages:
- stage: Build
  displayName: 'Build and Test'
  jobs:
  - job: BuildJob
    displayName: 'Build Job'
    steps:
    - task: UseDotNet@2
      displayName: 'Install .NET SDK'
      inputs:
        packageType: sdk
        version: '8.x'
    
    - task: DotNetCoreCLI@2
      displayName: 'Restore packages'
      inputs:
        command: 'restore'
        projects: '**/*.csproj'
    
    - task: DotNetCoreCLI@2
      displayName: 'Build solution'
      inputs:
        command: 'build'
        projects: '**/*.csproj'
        arguments: '--configuration $(buildConfiguration)'
    
    - task: DotNetCoreCLI@2
      displayName: 'Run tests'
      inputs:
        command: 'test'
        projects: '**/*Tests.csproj'
        arguments: '--configuration $(buildConfiguration) --collect "Code Coverage"'
    
    - task: PublishTestResults@2
      displayName: 'Publish test results'
      inputs:
        testResultsFormat: 'VSTest'
        testResultsFiles: '**/*.trx'
    
    - task: PublishCodeCoverageResults@1
      displayName: 'Publish code coverage'
      inputs:
        codeCoverageTool: 'Cobertura'
        summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'

マルチステージデプロイメント

# azure-pipelines.yml
trigger:
- main

variables:
  dockerRegistryServiceConnection: 'myDockerRegistry'
  imageRepository: 'myapp'
  containerRegistry: 'myregistry.azurecr.io'
  dockerfilePath: '$(Build.SourcesDirectory)/Dockerfile'
  tag: '$(Build.BuildId)'

stages:
- stage: Build
  displayName: 'Build and Push'
  jobs:
  - job: Build
    displayName: 'Build job'
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - task: Docker@2
      displayName: 'Build and push image'
      inputs:
        command: buildAndPush
        repository: $(imageRepository)
        dockerfile: $(dockerfilePath)
        containerRegistry: $(dockerRegistryServiceConnection)
        tags: |
          $(tag)
          latest

- stage: DeployStaging
  displayName: 'Deploy to Staging'
  dependsOn: Build
  condition: succeeded()
  jobs:
  - deployment: Deploy
    displayName: 'Deploy to Staging'
    pool:
      vmImage: 'ubuntu-latest'
    environment: 'staging'
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureWebAppContainer@1
            displayName: 'Deploy to Azure Web App'
            inputs:
              azureSubscription: 'myAzureSubscription'
              appName: 'myapp-staging'
              containers: '$(containerRegistry)/$(imageRepository):$(tag)'

- stage: DeployProduction
  displayName: 'Deploy to Production'
  dependsOn: DeployStaging
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
  jobs:
  - deployment: Deploy
    displayName: 'Deploy to Production'
    pool:
      vmImage: 'ubuntu-latest'
    environment: 'production'
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureRmWebAppDeployment@4
            displayName: 'Deploy to Production'
            inputs:
              ConnectionType: 'AzureRM'
              azureSubscription: 'myAzureSubscription'
              appType: 'webAppContainer'
              WebAppName: 'myapp-production'
              DockerNamespace: '$(containerRegistry)'
              DockerRepository: '$(imageRepository)'
              DockerImageTag: '$(tag)'

Kubernetes デプロイメント

# azure-pipelines.yml
resources:
- repo: self

variables:
  kubernetesServiceConnection: 'myK8sConnection'
  imageRepository: 'myapp'
  containerRegistry: 'myregistry.azurecr.io'
  tag: '$(Build.BuildId)'
  imagePullSecret: 'myregistrykey'

stages:
- stage: Build
  displayName: 'Build stage'
  jobs:
  - job: Build
    displayName: 'Build job'
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - task: Docker@2
      displayName: 'Build and push image to container registry'
      inputs:
        command: 'buildAndPush'
        repository: '$(imageRepository)'
        dockerfile: '**/Dockerfile'
        containerRegistry: 'myDockerRegistry'
        tags: |
          $(tag)

- stage: Deploy
  displayName: 'Deploy stage'
  dependsOn: Build
  jobs:
  - deployment: Deploy
    displayName: 'Deploy job'
    pool:
      vmImage: 'ubuntu-latest'
    environment: 'production.default'
    strategy:
      runOnce:
        deploy:
          steps:
          - task: KubernetesManifest@0
            displayName: 'Create imagePullSecret'
            inputs:
              action: 'createSecret'
              secretName: '$(imagePullSecret)'
              dockerRegistryEndpoint: 'myDockerRegistry'
              kubernetesServiceConnection: '$(kubernetesServiceConnection)'
              
          - task: KubernetesManifest@0
            displayName: 'Deploy to Kubernetes cluster'
            inputs:
              action: 'deploy'
              kubernetesServiceConnection: '$(kubernetesServiceConnection)'
              manifests: |
                $(Pipeline.Workspace)/manifests/deployment.yml
                $(Pipeline.Workspace)/manifests/service.yml
              imagePullSecrets: '$(imagePullSecret)'
              containers: '$(containerRegistry)/$(imageRepository):$(tag)'

複雑なワークフローとテンプレート

# azure-pipelines.yml
trigger:
- main
- feature/*

resources:
  repositories:
  - repository: templates
    type: git
    name: MyProject/pipeline-templates

variables:
- template: variables/common.yml@templates

stages:
- stage: Validate
  displayName: 'Validation Stage'
  jobs:
  - template: jobs/code-quality.yml@templates
    parameters:
      solution: '**/*.sln'
      
- stage: Build
  displayName: 'Build Stage'
  dependsOn: Validate
  jobs:
  - template: jobs/build.yml@templates
    parameters:
      buildConfiguration: 'Release'
      
- stage: Test
  displayName: 'Test Stage'
  dependsOn: Build
  jobs:
  - job: UnitTests
    displayName: 'Unit Tests'
    pool:
      vmImage: 'windows-latest'
    steps:
    - template: steps/dotnet-test.yml@templates
      parameters:
        testProjects: '**/*UnitTests.csproj'
        
  - job: IntegrationTests
    displayName: 'Integration Tests'
    pool:
      vmImage: 'ubuntu-latest'
    services:
      postgres: postgres
    steps:
    - template: steps/dotnet-test.yml@templates
      parameters:
        testProjects: '**/*IntegrationTests.csproj'

- stage: Deploy
  displayName: 'Deploy Stage'
  dependsOn: 
  - Build
  - Test
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
  jobs:
  - template: jobs/deploy.yml@templates
    parameters:
      environment: 'production'
      serviceConnection: 'myAzureConnection'

セキュリティとガバナンス

# azure-pipelines.yml
trigger: none # 手動実行のみ

pool:
  vmImage: 'ubuntu-latest'

variables:
- group: production-secrets
- name: isMain
  value: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')]

stages:
- stage: SecurityScan
  displayName: 'Security Scanning'
  jobs:
  - job: CodeScan
    displayName: 'Code Security Scan'
    steps:
    - task: SonarCloudPrepare@1
      inputs:
        SonarCloud: 'SonarCloud'
        organization: 'myorg'
        scannerMode: 'MSBuild'
        projectKey: 'myproject'
        
    - task: DotNetCoreCLI@2
      inputs:
        command: 'build'
        
    - task: SonarCloudAnalyze@1
    
    - task: SonarCloudPublish@1
    
    - task: WhiteSource@21
      displayName: 'WhiteSource Vulnerability Scan'
      inputs:
        cwd: '$(System.DefaultWorkingDirectory)'

- stage: Deploy
  displayName: 'Production Deploy'
  dependsOn: SecurityScan
  condition: and(succeeded(), eq(variables.isMain, true))
  jobs:
  - deployment: DeployProduction
    displayName: 'Deploy to Production'
    environment: 'production'
    pool:
      vmImage: 'ubuntu-latest'
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureKeyVault@2
            inputs:
              azureSubscription: 'mySubscription'
              KeyVaultName: 'mykeyvault'
              SecretsFilter: '*'
              RunAsPreJob: true
              
          - task: AzureCLI@2
            inputs:
              azureSubscription: 'mySubscription'
              scriptType: 'bash'
              scriptLocation: 'inlineScript'
              inlineScript: |
                az webapp config appsettings set \
                  --resource-group myResourceGroup \
                  --name myapp \
                  --settings DATABASE_URL="$(database-url)"