28 Automatisierung von Builds und Deployments

28.1 Build-Automatisierung im CI/CD-Kontext

Die Automatisierung von Builds in Continuous Integration Pipelines transformiert manuelle Prozesse in deterministisch reproduzierbare Abläufe. Gradle fungiert als zentrale Build-Engine, die von CI-Servern wie Jenkins, GitLab CI oder GitHub Actions orchestriert wird. Die Trennung zwischen Build-Logik in Gradle und Pipeline-Orchestrierung im CI-System ermöglicht lokale Reproduzierbarkeit bei gleichzeitiger Skalierbarkeit in der Cloud. Jeder Git-Push triggert automatisierte Builds, die Code kompilieren, Tests ausführen und Artefakte generieren.

Die Pipeline-Integration erfolgt über standardisierte Gradle-Commands, die CI-System-agnostisch sind. Der Gradle Wrapper garantiert Version-Konsistenz zwischen lokalen Entwicklungsumgebungen und CI-Servern. Build-Parameter werden über Gradle Properties oder Umgebungsvariablen injiziert, wodurch die gleichen Build-Skripte in verschiedenen Kontexten funktionieren. Diese Portabilität reduziert Vendor-Lock-in und vereinfacht CI-System-Migration.

// CI-optimized Build Configuration
tasks.register("ciBuild") {
    description = "Complete build for CI pipeline"
    group = "ci"
    
    dependsOn("clean", "assemble", "check", "jacocoTestReport")
    
    doFirst {
        // CI-spezifische Konfiguration
        project.extra["ci.build"] = true
        project.extra["build.number"] = System.getenv("BUILD_NUMBER") ?: "LOCAL"
        project.extra["build.timestamp"] = Instant.now().toString()
        
        logger.lifecycle("Starting CI Build #${project.extra["build.number"]}")
    }
    
    doLast {
        // Build-Metadaten für Pipeline
        val metadata = file("${buildDir}/ci-metadata.json")
        metadata.writeText(groovy.json.JsonOutput.toJson(mapOf(
            "project" to project.name,
            "version" to project.version,
            "buildNumber" to project.extra["build.number"],
            "timestamp" to project.extra["build.timestamp"],
            "commitHash" to executeGitCommand("rev-parse HEAD"),
            "branch" to executeGitCommand("rev-parse --abbrev-ref HEAD"),
            "artifacts" to collectBuildArtifacts()
        )))
    }
}

// Pipeline-Stage-Tasks
tasks.register("ciCompile") {
    description = "Compilation stage for CI"
    dependsOn("compileJava", "compileTestJava", "processResources")
}

tasks.register("ciTest") {
    description = "Test execution stage for CI"
    dependsOn("test", "integrationTest")
    
    // Parallele Test-Execution für CI
    tasks.withType<Test>().configureEach {
        if (project.hasProperty("ci.build")) {
            maxParallelForks = Runtime.getRuntime().availableProcessors()
            failFast = false  // Alle Tests ausführen für vollständigen Report
        }
    }
}

tasks.register("ciPackage") {
    description = "Package artifacts for deployment"
    dependsOn("bootJar", "distZip")
    
    doLast {
        // Artifact Signing für Release Builds
        if (project.version.toString().matches(Regex("\\d+\\.\\d+\\.\\d+"))) {
            signArtifacts()
        }
    }
}

Build-Caching beschleunigt CI-Builds erheblich durch Wiederverwendung vorheriger Build-Outputs. Remote Build-Caches teilen Outputs zwischen CI-Agents und Entwickler-Maschinen. Die Cache-Key-Berechnung berücksichtigt alle relevanten Inputs inklusive Source-Code, Dependencies und Build-Configuration. Cache-Hits eliminieren redundante Compilation und Test-Execution, was Build-Zeiten von Minuten auf Sekunden reduzieren kann.

28.2 Deployment-Automatisierung mit Gradle

Deployment-Automatisierung erweitert den Build-Prozess um die Bereitstellung von Artefakten in Zielumgebungen. Gradle Tasks orchestrieren Deployment-Schritte von Artifact-Upload über Service-Konfiguration bis Health-Checks. Die Integration mit Deployment-Plattformen erfolgt über REST APIs, CLI-Tools oder dedizierte Plugins. Diese Build-Tool-zentrierte Deployment-Strategie hält Deployment-Logik versioniert und testbar im Repository.

Die Deployment-Konfiguration separiert Was von Wo und Wie. Artifact-Definitionen spezifizieren, welche Dateien deployed werden. Environment-Konfigurationen definieren Zielumgebungen mit URLs, Credentials und Parameters. Deployment-Strategien bestimmen Rollout-Methoden wie Rolling Updates oder Blue-Green Deployments. Diese Separation ermöglicht flexible Kombinationen ohne Code-Duplikation.

// Deployment Configuration
val deploymentEnvironments = mapOf(
    "dev" to DeploymentEnvironment(
        url = "https://dev.deploy.company.com",
        region = "eu-central-1",
        instances = 2,
        healthCheckPath = "/actuator/health",
        rolloutStrategy = "rolling"
    ),
    "staging" to DeploymentEnvironment(
        url = "https://staging.deploy.company.com",
        region = "eu-central-1",
        instances = 4,
        healthCheckPath = "/actuator/health",
        rolloutStrategy = "blue-green"
    ),
    "production" to DeploymentEnvironment(
        url = "https://prod.deploy.company.com",
        region = "eu-west-1",
        instances = 10,
        healthCheckPath = "/actuator/health",
        rolloutStrategy = "canary"
    )
)

// Generic Deployment Task
open class DeployTask : DefaultTask() {
    @Input
    val environment = project.objects.property<String>()
    
    @InputFile
    val artifact = project.objects.fileProperty()
    
    @Input
    val version = project.objects.property<String>()
    
    @TaskAction
    fun deploy() {
        val env = deploymentEnvironments[environment.get()] 
            ?: error("Unknown environment: ${environment.get()}")
        
        logger.lifecycle("Deploying ${artifact.get().asFile.name} to ${environment.get()}")
        
        // Pre-deployment validation
        validateArtifact(artifact.get().asFile)
        validateEnvironment(env)
        
        // Deployment execution
        when (env.rolloutStrategy) {
            "rolling" -> performRollingDeployment(env)
            "blue-green" -> performBlueGreenDeployment(env)
            "canary" -> performCanaryDeployment(env)
            else -> error("Unknown rollout strategy: ${env.rolloutStrategy}")
        }
        
        // Post-deployment verification
        waitForHealthCheck(env)
        runSmokeTests(env)
        
        logger.lifecycle("Deployment to ${environment.get()} completed successfully")
    }
    
    private fun performRollingDeployment(env: DeploymentEnvironment) {
        val batchSize = maxOf(1, env.instances / 3)
        
        for (batch in 0 until env.instances step batchSize) {
            val instances = (batch until minOf(batch + batchSize, env.instances))
            
            logger.lifecycle("Deploying to instances $instances")
            
            instances.forEach { instance ->
                deployToInstance(env, instance)
                Thread.sleep(5000) // Wait between instances
            }
            
            verifyBatch(env, instances)
        }
    }
}

// Environment-specific deployment tasks
deploymentEnvironments.keys.forEach { env ->
    tasks.register<DeployTask>("deployTo${env.capitalize()}") {
        description = "Deploy application to $env environment"
        group = "deployment"
        
        environment.set(env)
        artifact.set(tasks.named<Jar>("bootJar").flatMap { it.archiveFile })
        version.set(project.version.toString())
        
        // Environment-specific dependencies
        if (env != "dev") {
            dependsOn("check", "qualityGate")
        }
    }
}

Container-basierte Deployments standardisieren Artifact-Packaging und Runtime-Environment. Gradle Tasks bauen Docker-Images mit Jib oder Docker-CLI, pushen zu Container-Registries und triggern Container-Orchestration-Updates. Kubernetes-Deployments erfolgen über kubectl-Integration oder Helm-Charts. Diese Container-Strategie abstrahiert Infrastructure-Details und ermöglicht Cloud-agnostisches Deployment.

28.3 Pipeline-Integration und Stage-Orchestrierung

Die Integration von Gradle in CI/CD-Pipelines erfolgt durch stage-basierte Task-Execution. Jede Pipeline-Stage mappt zu dedizierten Gradle-Tasks, die spezifische Build-Aspekte abdecken. Compile-Stage führt Compilation-Tasks aus, Test-Stage executiert verschiedene Test-Suites, Package-Stage erstellt Deployment-Artefakte. Diese Granularität ermöglicht parallele Stage-Execution und Fast-Failure bei Problemen.

Pipeline-as-Code definiert Build-Pipelines in versionierten Dateien neben dem Source-Code. Jenkinsfile, .gitlab-ci.yml oder GitHub Actions Workflows orchestrieren Gradle-Tasks. Die Pipeline-Definition bleibt minimal und delegiert Komplexität an Gradle. Diese Separation of Concerns macht Pipelines portabel und Build-Logik wiederverwendbar.

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
  push:
    branches: [main, develop]
  pull_request:

jobs:
  compile:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
      - run: ./gradlew ciCompile
      - uses: actions/upload-artifact@v3
        with:
          name: compiled-classes
          path: build/classes

  test:
    needs: compile
    runs-on: ubuntu-latest
    strategy:
      matrix:
        test-suite: [unit, integration, contract]
    steps:
      - uses: actions/checkout@v3
      - uses: actions/download-artifact@v3
        with:
          name: compiled-classes
          path: build/classes
      - run: ./gradlew test${{ matrix.test-suite }}
      
  deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v3
      - run: ./gradlew deployToProduction
// Pipeline-Support-Tasks
tasks.register("pipelineInfo") {
    description = "Export pipeline information"
    
    doLast {
        val pipelineFile = file("${buildDir}/pipeline.json")
        pipelineFile.writeText(groovy.json.JsonOutput.toJson(mapOf(
            "stages" to listOf("compile", "test", "package", "deploy"),
            "tasks" to tasks.names.filter { it.startsWith("ci") || it.startsWith("deploy") },
            "artifacts" to listOf(
                "build/libs/${project.name}-${project.version}.jar",
                "build/distributions/${project.name}-${project.version}.zip"
            ),
            "testSuites" to listOf("unit", "integration", "contract", "performance"),
            "deploymentEnvironments" to deploymentEnvironments.keys
        )))
    }
}

// Stage-Gates zwischen Pipeline-Phasen
tasks.register("stageGate") {
    description = "Verify stage completion criteria"
    
    val stage = project.findProperty("stage") as String? ?: "compile"
    
    doLast {
        when (stage) {
            "compile" -> {
                require(file("build/classes/java/main").exists()) {
                    "Compilation artifacts missing"
                }
            }
            "test" -> {
                val testReport = file("build/reports/tests/test/index.html")
                require(testReport.exists()) { "Test reports missing" }
                
                val failures = parseTestFailures()
                require(failures == 0) { "$failures tests failed" }
            }
            "package" -> {
                val artifact = file("build/libs/${project.name}-${project.version}.jar")
                require(artifact.exists()) { "Package artifact missing" }
                require(artifact.length() > 0) { "Package artifact empty" }
            }
        }
        
        logger.lifecycle("Stage gate '$stage' passed")
    }
}

Build-Matrix-Strategien testen verschiedene Konfigurationen parallel. Operating-System-Matrizen verifizieren Cross-Platform-Compatibility. JDK-Version-Matrizen garantieren Runtime-Compatibility. Dependency-Version-Matrizen testen gegen verschiedene Library-Versionen. Diese Parallelisierung maximiert Test-Coverage bei akzeptabler Pipeline-Duration.

28.4 Artifact-Management und Versioning

Artifact-Management verwaltet Build-Outputs über deren Lifecycle von Creation bis Deprecation. Gradle publiziert Artefakte zu Repository-Managern wie Nexus, Artifactory oder Cloud-Services. Snapshot-Versionen werden kontinuierlich überschrieben, während Release-Versionen immutable sind. Diese Unterscheidung ermöglicht iterative Development mit stabilen Release-Points.

Semantic Versioning kommuniziert Compatibility und Change-Impact durch Version-Numbers. Major-Versions signalisieren Breaking Changes, Minor-Versions neue Features, Patch-Versions Bug-Fixes. Gradle Tasks validieren Version-Schemes und enforced Versioning-Policies. Automatic Version-Bumping basiert auf Commit-Messages oder PR-Labels.

// Version Management
version = ComputedVersion(
    major = project.findProperty("version.major") as String? ?: "1",
    minor = project.findProperty("version.minor") as String? ?: "0",
    patch = project.findProperty("version.patch") as String? ?: "0",
    qualifier = determineVersionQualifier()
)

fun determineVersionQualifier(): String {
    val branch = executeGitCommand("rev-parse --abbrev-ref HEAD")
    val buildNumber = System.getenv("BUILD_NUMBER") ?: "LOCAL"
    
    return when {
        branch == "main" -> ""  // Release version
        branch == "develop" -> "-SNAPSHOT"
        branch.startsWith("feature/") -> "-${branch.substringAfter("feature/")}-SNAPSHOT"
        branch.startsWith("release/") -> "-RC${buildNumber}"
        else -> "-${branch}-SNAPSHOT"
    }
}

// Artifact Publishing
publishing {
    publications {
        create<MavenPublication>("maven") {
            from(components["java"])
            
            // Version-specific artifact configuration
            artifact(tasks.register<Jar>("sourcesJar") {
                from(sourceSets.main.get().allJava)
                archiveClassifier.set("sources")
            })
            
            if (!version.toString().contains("SNAPSHOT")) {
                artifact(tasks.register<Jar>("javadocJar") {
                    from(tasks.javadoc)
                    archiveClassifier.set("javadoc")
                })
            }
            
            // POM customization
            pom {
                name.set(project.name)
                description.set("${project.name} v${project.version}")
                url.set("https://github.com/company/${project.name}")
                
                scm {
                    connection.set("scm:git:git://github.com/company/${project.name}.git")
                    developerConnection.set("scm:git:ssh://github.com/company/${project.name}.git")
                    url.set("https://github.com/company/${project.name}")
                    tag.set(if (version.toString().contains("SNAPSHOT")) "HEAD" else "v${version}")
                }
            }
        }
    }
    
    repositories {
        maven {
            val releasesRepoUrl = uri("https://nexus.company.com/repository/maven-releases/")
            val snapshotsRepoUrl = uri("https://nexus.company.com/repository/maven-snapshots/")
            url = if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl
            
            credentials {
                username = System.getenv("NEXUS_USERNAME")
                password = System.getenv("NEXUS_PASSWORD")
            }
        }
    }
}

// Artifact Promotion
tasks.register("promoteArtifact") {
    description = "Promote artifact from staging to release"
    
    doLast {
        val stagingRepo = "https://nexus.company.com/repository/maven-staging/"
        val releaseRepo = "https://nexus.company.com/repository/maven-releases/"
        
        val artifact = "${project.group}:${project.name}:${project.version}"
        
        // Verify artifact in staging
        verifyArtifactExists(stagingRepo, artifact)
        
        // Run promotion tests
        runPromotionTests(artifact)
        
        // Copy to release repository
        copyArtifact(stagingRepo, releaseRepo, artifact)
        
        // Tag release in Git
        executeGitCommand("tag -a v${project.version} -m 'Release version ${project.version}'")
        executeGitCommand("push origin v${project.version}")
        
        logger.lifecycle("Artifact $artifact promoted to release")
    }
}

Artifact-Retention-Policies managen Repository-Storage und Compliance-Requirements. Snapshot-Artifacts werden nach definierter Zeit gelöscht. Release-Artifacts werden langfristig archiviert. Security-Scans validieren Artifacts vor Promotion. Diese Policies automatisieren Artifact-Lifecycle-Management und reduzieren manuellen Overhead.

28.5 Rollout-Strategien und Monitoring

Rollout-Strategien kontrollieren, wie neue Versionen in Production deployed werden. Rolling Updates ersetzen Instances sequentiell, wodurch Zero-Downtime-Deployments möglich werden. Blue-Green-Deployments switchen zwischen zwei identischen Environments. Canary-Deployments rollen neue Versionen graduell aus und monitoren Metrics für Anomalien. Gradle Tasks implementieren diese Strategien durch orchestrierte Deployment-Sequenzen.

Health-Checks und Readiness-Probes validieren erfolgreiche Deployments. HTTP-Endpoints signalisieren Application-Readiness. Database-Connectivity-Tests verifizieren Backend-Connections. Smoke-Tests validieren kritische User-Journeys. Diese Validierungen stoppen Rollouts bei Problemen und triggern automatische Rollbacks.

// Canary Deployment Implementation
tasks.register("deployCanary") {
    description = "Perform canary deployment with gradual rollout"
    
    doLast {
        val stages = listOf(
            CanaryStage(percentage = 5, duration = Duration.ofMinutes(10)),
            CanaryStage(percentage = 25, duration = Duration.ofMinutes(20)),
            CanaryStage(percentage = 50, duration = Duration.ofMinutes(30)),
            CanaryStage(percentage = 100, duration = null)
        )
        
        val metrics = MetricsCollector()
        
        stages.forEach { stage ->
            logger.lifecycle("Canary stage: ${stage.percentage}% traffic")
            
            // Update load balancer configuration
            updateTrafficDistribution(stage.percentage)
            
            // Deploy to canary instances
            deployToCanaryInstances(calculateInstanceCount(stage.percentage))
            
            // Wait and monitor
            if (stage.duration != null) {
                val startTime = Instant.now()
                
                while (Duration.between(startTime, Instant.now()) < stage.duration) {
                    val currentMetrics = metrics.collect()
                    
                    // Check for anomalies
                    if (detectAnomalies(currentMetrics)) {
                        logger.error("Anomalies detected in canary deployment")
                        rollbackCanary()
                        throw GradleException("Canary deployment failed")
                    }
                    
                    Thread.sleep(30000) // Check every 30 seconds
                }
                
                logger.lifecycle("Canary stage ${stage.percentage}% completed successfully")
            }
        }
        
        logger.lifecycle("Canary deployment completed successfully")
    }
}

// Monitoring Integration
tasks.register("deployWithMonitoring") {
    description = "Deploy with integrated monitoring and alerting"
    
    dependsOn("deployToProduction")
    
    doLast {
        // Configure monitoring
        val monitoringConfig = MonitoringConfiguration(
            metricsEndpoint = "https://metrics.company.com",
            alertingWebhook = "https://alerts.company.com/webhook",
            thresholds = mapOf(
                "error_rate" to 0.01,
                "response_time_p99" to 1000,
                "cpu_usage" to 0.8
            )
        )
        
        // Start monitoring
        val monitor = startMonitoring(monitoringConfig)
        
        // Wait for stabilization
        val stabilizationPeriod = Duration.ofMinutes(15)
        val endTime = Instant.now().plus(stabilizationPeriod)
        
        while (Instant.now().isBefore(endTime)) {
            val metrics = monitor.getCurrentMetrics()
            
            monitoringConfig.thresholds.forEach { (metric, threshold) ->
                val value = metrics[metric] as Double
                if (value > threshold) {
                    sendAlert("Metric $metric exceeded threshold: $value > $threshold")
                    
                    if (shouldRollback(metric, value)) {
                        executeRollback()
                        throw GradleException("Deployment rolled back due to $metric violation")
                    }
                }
            }
            
            Thread.sleep(60000) // Check every minute
        }
        
        logger.lifecycle("Deployment monitoring completed successfully")
    }
}

// Rollback Task
tasks.register("rollback") {
    description = "Rollback to previous deployment"
    
    doLast {
        val currentVersion = getCurrentDeployedVersion()
        val previousVersion = getPreviousVersion(currentVersion)
        
        logger.lifecycle("Rolling back from $currentVersion to $previousVersion")
        
        // Retrieve previous artifact
        val previousArtifact = downloadArtifact(previousVersion)
        
        // Deploy previous version
        deployArtifact(previousArtifact)
        
        // Verify rollback
        waitForHealthCheck()
        runSmokeTests()
        
        // Log rollback event
        logRollbackEvent(currentVersion, previousVersion, "Manual rollback initiated")
        
        logger.lifecycle("Rollback to $previousVersion completed")
    }
}

Deployment-Monitoring tracked Key Performance Indicators während und nach Deployments. Error Rates, Response Times und Resource Utilization werden gegen Baselines verglichen. Anomaly Detection identifiziert unerwartete Behavior-Changes. Automated Rollback triggert bei Threshold-Violations. Diese Monitoring-Integration macht Deployments self-healing und reduziert Mean Time To Recovery.