Introduction
Pulumi brings infrastructure as code to Java developers, allowing you to define cloud resources using familiar Java constructs instead of YAML or domain-specific languages. With Pulumi's Java SDK, you can leverage your existing Java skills to provision and manage infrastructure across AWS, Azure, Google Cloud, and Kubernetes with type safety, IDE support, and full testing capabilities.
Article: Mastering Cloud Infrastructure with Pulumi Java SDK
Pulumi's Java SDK enables Java developers to define cloud infrastructure using real Java code, bringing compile-time safety, code reuse, and modern software engineering practices to infrastructure management. Let's explore how to build, test, and deploy infrastructure using Pulumi with Java.
1. Setting Up Pulumi with Java
Prerequisites:
- Java 8 or higher
- Apache Maven or Gradle
- Pulumi CLI
- Cloud provider CLI (AWS, Azure, or GCP)
Installation:
# Install Pulumi CLI curl -fsSL https://get.pulumi.com | sh # Verify installation pulumi version
Maven Configuration:
<!-- pom.xml -->
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany</groupId>
<artifactId>pulumi-infrastructure</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<pulumi.version>3.96.2</pulumi.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.pulumi</groupId>
<artifactId>pulumi</artifactId>
<version>${pulumi.version}</version>
</dependency>
<dependency>
<groupId>com.pulumi</groupId>
<artifactId>aws</artifactId>
<version>${pulumi.version}</version>
</dependency>
<dependency>
<groupId>com.pulumi</groupId>
<artifactId>awsx</artifactId>
<version>${pulumi.version}</version>
</dependency>
<dependency>
<groupId>com.pulumi</groupId>
<artifactId>kubernetes</artifactId>
<version>${pulumi.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.pulumi</groupId>
<artifactId>pulumi-maven-plugin</artifactId>
<version>${pulumi.version}</version>
<configuration>
<mainClass>com.mycompany.infra.Infrastructure</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
2. Basic Pulumi Java Structure
Main Infrastructure Class:
package com.mycompany.infra;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.s3.Bucket;
import com.pulumi.aws.s3.BucketArgs;
public class Infrastructure {
public static void main(String[] args) {
Pulumi.run(Infrastructure::stack);
}
private static void stack(Context ctx) {
// Infrastructure definition goes here
var bucket = new Bucket("my-bucket", BucketArgs.builder()
.bucket("my-unique-bucket-name-12345")
.build());
// Export values for easy access
ctx.export("bucketName", bucket.bucket());
ctx.export("bucketArn", bucket.arn());
}
}
3. AWS Infrastructure Examples
S3 Bucket with CloudFront Distribution:
package com.mycompany.infra.aws;
import com.pulumi.Context;
import com.pulumi.aws.s3.Bucket;
import com.pulumi.aws.s3.BucketArgs;
import com.pulumi.aws.s3.BucketWebsiteArgs;
import com.pulumi.aws.cloudfront.Distribution;
import com.pulumi.aws.cloudfront.DistributionArgs;
import com.pulumi.aws.cloudfront.inputs.DistributionOriginArgs;
import com.pulumi.aws.cloudfront.inputs.DistributionDefaultCacheBehaviorArgs;
import com.pulumi.aws.cloudfront.inputs.DistributionViewerCertificateArgs;
import com.pulumi.aws.acm.Certificate;
import com.pulumi.aws.acm.CertificateArgs;
import com.pulumi.aws.route53.Record;
import com.pulumi.aws.route53.RecordArgs;
import com.pulumi.aws.route53.inputs.RecordAliasArgs;
public class StaticWebsite {
public static void createStaticWebsite(Context ctx) {
// S3 Bucket for static website
var bucket = new Bucket("website-bucket", BucketArgs.builder()
.bucket("my-static-website-12345")
.website(BucketWebsiteArgs.builder()
.indexDocument("index.html")
.errorDocument("error.html")
.build())
.forceDestroy(true)
.build());
// SSL Certificate
var certificate = new Certificate("website-cert", CertificateArgs.builder()
.domainName("example.com")
.validationMethod("DNS")
.build());
// CloudFront Distribution
var distribution = new Distribution("website-distribution", DistributionArgs.builder()
.enabled(true)
.aliases("example.com", "www.example.com")
.origins(DistributionOriginArgs.builder()
.domainName(bucket.websiteEndpoint())
.originId(bucket.arn())
.customOriginConfig(DistributionOriginArgs.CustomOriginConfigArgs.builder()
.httpPort(80)
.httpsPort(443)
.originProtocolPolicy("http-only")
.build())
.build())
.defaultCacheBehavior(DistributionDefaultCacheBehaviorArgs.builder()
.targetOriginId(bucket.arn())
.viewerProtocolPolicy("redirect-to-https")
.allowedMethods("GET", "HEAD", "OPTIONS")
.cachedMethods("GET", "HEAD", "OPTIONS")
.forwardedValues(DistributionDefaultCacheBehaviorArgs.ForwardedValuesArgs.builder()
.queryString(false)
.cookies(DistributionDefaultCacheBehaviorArgs.ForwardedValuesArgs.CookiesArgs.builder()
.forward("none")
.build())
.build())
.minTtl(0)
.defaultTtl(3600)
.maxTtl(86400)
.build())
.viewerCertificate(DistributionViewerCertificateArgs.builder()
.acmCertificateArn(certificate.arn())
.sslSupportMethod("sni-only")
.build())
.build());
ctx.export("websiteUrl", Output.format("https://%s", distribution.domainName()));
ctx.export("bucketName", bucket.bucket());
}
}
VPC and ECS Fargate Service:
package com.mycompany.infra.aws;
import com.pulumi.Context;
import com.pulumi.awsx.ec2.Vpc;
import com.pulumi.awsx.ec2.VpcArgs;
import com.pulumi.awsx.ecs.FargateService;
import com.pulumi.awsx.ecs.FargateServiceArgs;
import com.pulumi.awsx.ecs.inputs.FargateServiceTaskDefinitionArgs;
import com.pulumi.awsx.lb.ApplicationLoadBalancer;
import com.pulumi.awsx.lb.ApplicationLoadBalancerArgs;
import com.pulumi.aws.lb.Listener;
import com.pulumi.aws.lb.ListenerArgs;
import com.pulumi.aws.ecs.Cluster;
import com.pulumi.aws.ecs.ClusterArgs;
public class EcsFargateStack {
public static void createFargateService(Context ctx) {
// Create VPC
var vpc = new Vpc("app-vpc", VpcArgs.builder()
.enableDnsHostnames(true)
.enableDnsSupport(true)
.build());
// Create ECS Cluster
var cluster = new Cluster("app-cluster", ClusterArgs.builder()
.build());
// Create Load Balancer
var alb = new ApplicationLoadBalancer("app-alb", ApplicationLoadBalancerArgs.builder()
.vpcId(vpc.vpcId())
.subnetIds(vpc.publicSubnetIds())
.build());
// Fargate Service
var service = new FargateService("app-service", FargateServiceArgs.builder()
.cluster(cluster.arn())
.assignPublicIp(true)
.taskDefinitionArgs(FargateServiceTaskDefinitionArgs.builder()
.container(FargateServiceTaskDefinitionArgs.ContainerArgs.builder()
.name("app-container")
.image("my-registry/my-java-app:latest")
.cpu(512)
.memory(1024)
.portMappings(FargateServiceTaskDefinitionArgs.PortMappingArgs.builder()
.containerPort(8080)
.targetGroup(alb.defaultTargetGroup())
.build())
.environment(List.of(
FargateServiceTaskDefinitionArgs.KeyValuePairArgs.builder()
.name("SPRING_PROFILES_ACTIVE")
.value("production")
.build(),
FargateServiceTaskDefinitionArgs.KeyValuePairArgs.builder()
.name("JAVA_OPTS")
.value("-Xmx768m -Xms256m")
.build()))
.build())
.build())
.build());
// Export important values
ctx.export("vpcId", vpc.vpcId());
ctx.export("loadBalancerUrl", alb.loadBalancer().apply(lb -> lb.dnsName()));
ctx.export("clusterName", cluster.name());
}
}
4. Kubernetes Infrastructure
Java Application Deployment:
package com.mycompany.infra.k8s;
import com.pulumi.Context;
import com.pulumi.kubernetes.apps.v1.Deployment;
import com.pulumi.kubernetes.apps.v1.DeploymentArgs;
import com.pulumi.kubernetes.core.v1.Service;
import com.pulumi.kubernetes.core.v1.ServiceArgs;
import com.pulumi.kubernetes.core.v1.inputs.ServicePortArgs;
import com.pulumi.kubernetes.core.v1.inputs.ContainerArgs;
import com.pulumi.kubernetes.core.v1.inputs.PodSpecArgs;
import com.pulumi.kubernetes.core.v1.inputs.PodTemplateSpecArgs;
import com.pulumi.kubernetes.meta.v1.inputs.ObjectMetaArgs;
import com.pulumi.kubernetes.meta.v1.inputs.LabelSelectorArgs;
import com.pulumi.kubernetes.apps.v1.inputs.DeploymentSpecArgs;
public class KubernetesStack {
public static void createJavaAppDeployment(Context ctx) {
var appName = "order-service";
var appLabels = Map.of("app", appName);
// Deployment
var deployment = new Deployment(appName, DeploymentArgs.builder()
.metadata(ObjectMetaArgs.builder()
.name(appName)
.labels(appLabels)
.build())
.spec(DeploymentSpecArgs.builder()
.replicas(3)
.selector(LabelSelectorArgs.builder()
.matchLabels(appLabels)
.build())
.template(PodTemplateSpecArgs.builder()
.metadata(ObjectMetaArgs.builder()
.labels(appLabels)
.build())
.spec(PodSpecArgs.builder()
.containers(ContainerArgs.builder()
.name(appName)
.image("my-registry/order-service:1.0.0")
.ports(ContainerArgs.PortArgs.builder()
.containerPort(8080)
.name("http")
.build())
.env(
ContainerArgs.EnvVarArgs.builder()
.name("JAVA_OPTS")
.value("-XX:+UseContainerSupport -Xmx512m -Xms256m")
.build(),
ContainerArgs.EnvVarArgs.builder()
.name("SPRING_PROFILES_ACTIVE")
.value("kubernetes")
.build()
)
.resources(ContainerArgs.ResourceRequirementsArgs.builder()
.requests(Map.of(
"cpu", "250m",
"memory", "512Mi"
))
.limits(Map.of(
"cpu", "500m",
"memory", "768Mi"
))
.build())
.livenessProbe(ContainerArgs.LivenessProbeArgs.builder()
.httpGet(ContainerArgs.HttpGetArgs.builder()
.path("/actuator/health")
.port("http")
.build())
.initialDelaySeconds(45)
.periodSeconds(10)
.build())
.readinessProbe(ContainerArgs.ReadinessProbeArgs.builder()
.httpGet(ContainerArgs.HttpGetArgs.builder()
.path("/actuator/health/readiness")
.port("http")
.build())
.initialDelaySeconds(30)
.periodSeconds(5)
.build())
.build())
.build())
.build())
.build())
.build());
// Service
var service = new Service(appName, ServiceArgs.builder()
.metadata(ObjectMetaArgs.builder()
.name(appName)
.build())
.spec(com.pulumi.kubernetes.core.v1.inputs.ServiceSpecArgs.builder()
.selector(appLabels)
.ports(ServicePortArgs.builder()
.port(80)
.targetPort("http")
.protocol("TCP")
.build())
.type("ClusterIP")
.build())
.build());
ctx.export("deploymentName", deployment.metadata().apply(m -> m.name()));
ctx.export("serviceName", service.metadata().apply(m -> m.name()));
}
}
5. Advanced Patterns and Best Practices
Configuration Management:
package com.mycompany.infra.config;
import com.pulumi.Config;
import com.pulumi.Context;
public class AppConfig {
private final String environment;
private final String appVersion;
private final int replicaCount;
private final DatabaseConfig databaseConfig;
public AppConfig(Context ctx) {
Config config = ctx.config();
this.environment = config.get("environment").orElse("dev");
this.appVersion = config.get("appVersion").orElse("latest");
this.replicaCount = config.getInteger("replicaCount").orElse(2);
this.databaseConfig = new DatabaseConfig(config);
}
public static class DatabaseConfig {
private final String username;
private final String password;
private final String instanceClass;
public DatabaseConfig(Config config) {
this.username = config.get("dbUsername").orElse("appuser");
this.password = config.requireSecret("dbPassword");
this.instanceClass = config.get("dbInstanceClass").orElse("db.t3.small");
}
}
// Getters
public String getEnvironment() { return environment; }
public String getAppVersion() { return appVersion; }
public int getReplicaCount() { return replicaCount; }
public DatabaseConfig getDatabaseConfig() { return databaseConfig; }
}
Modular Infrastructure Components:
package com.mycompany.infra.components;
import com.pulumi.aws.rds.Instance;
import com.pulumi.aws.rds.InstanceArgs;
import com.pulumi.aws.rds.inputs.InstanceS3ImportArgs;
import com.pulumi.core.Output;
public class DatabaseComponent {
private final Instance databaseInstance;
public DatabaseComponent(String name, DatabaseArgs args) {
this.databaseInstance = new Instance(name, InstanceArgs.builder()
.engine("mysql")
.engineVersion("8.0")
.instanceClass(args.getInstanceClass())
.allocatedStorage(20)
.dbName(args.getDbName())
.username(args.getUsername())
.password(args.getPassword())
.skipFinalSnapshot(true)
.backupRetentionPeriod(7)
.backupWindow("03:00-04:00")
.maintenanceWindow("sun:04:00-sun:05:00")
.build());
}
public Output<String> getEndpoint() {
return databaseInstance.endpoint();
}
public Output<String> getConnectionString() {
return Output.format("jdbc:mysql://%s/%s",
databaseInstance.endpoint(),
databaseInstance.dbName());
}
public static class DatabaseArgs {
private final String instanceClass;
private final String dbName;
private final String username;
private final String password;
public DatabaseArgs(String instanceClass, String dbName, String username, String password) {
this.instanceClass = instanceClass;
this.dbName = dbName;
this.username = username;
this.password = password;
}
// Getters
public String getInstanceClass() { return instanceClass; }
public String getDbName() { return dbName; }
public String getUsername() { return username; }
public String getPassword() { return password; }
}
}
Using Components:
package com.mycompany.infra;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.mycompany.infra.components.DatabaseComponent;
import com.mycompany.infra.config.AppConfig;
public class MainStack {
public static void main(String[] args) {
Pulumi.run(MainStack::stack);
}
private static void stack(Context ctx) {
var config = new AppConfig(ctx);
// Create database using component
var database = new DatabaseComponent("app-db",
new DatabaseComponent.DatabaseArgs(
config.getDatabaseConfig().getInstanceClass(),
"myapp",
config.getDatabaseConfig().getUsername(),
config.getDatabaseConfig().getPassword()
));
ctx.export("databaseEndpoint", database.getEndpoint());
ctx.export("databaseConnectionString", database.getConnectionString());
}
}
6. Testing Infrastructure Code
Unit Testing with Pulumi Mocks:
package com.mycompany.infra.test;
import com.pulumi.test.Mocks;
import com.pulumi.test.TestOptions;
import com.pulumi.test.internal.PulumiTestInternal;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class InfrastructureTest {
@Test
void testS3BucketCreation() {
var result = PulumiTestInternal.builder()
.withOptions(TestOptions.builder()
.preview(true)
.build())
.withMocks(new MyMocks())
.testAsync(Infrastructure::stack)
.join();
var bucketName = result.outputs().get("bucketName").getValue();
assertThat(bucketName).isEqualTo("my-unique-bucket-name-12345");
}
static class MyMocks implements Mocks {
@Override
public Result newResource(ResourceArgs args) {
switch (args.type) {
case "aws:s3/bucket:Bucket":
return Result.of(args.name + "-id", Map.of(
"bucket", args.name,
"arn", "arn:aws:s3:::" + args.name
));
default:
return Result.empty();
}
}
@Override
public Result call(CallArgs args) {
return Result.of(Map.of());
}
}
}
7. Deployment and CI/CD
Pulumi.yaml:
name: my-infrastructure runtime: java description: Infrastructure as Code with Java template: config: aws:region: description: AWS region to deploy to default: us-west-2 environment: description: Deployment environment default: dev
GitHub Actions Workflow:
name: Deploy Infrastructure
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Java
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: 'maven'
- name: Set up Pulumi
uses: pulumi/setup-pulumi@v3
- name: Build project
run: mvn compile
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2
- name: Deploy infrastructure
run: pulumi up --stack dev --yes
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
8. Best Practices for Pulumi Java
1. Use Strong Typing:
// Good - type safe
new Bucket("my-bucket", BucketArgs.builder()
.bucket("my-bucket")
.versioning(BucketVersioningArgs.builder()
.enabled(true)
.build())
.build());
// Avoid - untyped
new Bucket("my-bucket");
2. Organize with Packages:
src/main/java/com/mycompany/infra/ ├── components/ # Reusable infrastructure components ├── stacks/ # Main stack definitions ├── config/ # Configuration classes └── utils/ # Utility classes
3. Use Outputs Properly:
// Chain outputs correctly
var bucket = new Bucket("my-bucket", BucketArgs.builder()
.bucket("my-bucket")
.build());
var object = new BucketObject("index.html", BucketObjectArgs.builder()
.bucket(bucket.bucket()) // Use output directly
.key("index.html")
.build());
// Export computed values
ctx.export("websiteUrl",
Output.format("https://%s.s3.amazonaws.com/index.html", bucket.bucket()));
4. Handle Secrets Securely:
var dbPassword = new RandomPassword("db-password", RandomPasswordArgs.builder()
.length(16)
.special(true)
.build());
var db = new Instance("database", InstanceArgs.builder()
.password(dbPassword.result()) // Automatically treated as secret
.build());
Benefits of Pulumi Java SDK
- Type Safety - Compile-time checking of infrastructure definitions
- IDE Support - Full code completion, refactoring, and debugging
- Code Reuse - Leverage Java packages, classes, and inheritance
- Testing - Comprehensive unit and integration testing capabilities
- Familiar Tooling - Use Maven/Gradle, JUnit, and existing CI/CD pipelines
- Multi-Cloud - Consistent experience across AWS, Azure, GCP, and Kubernetes
Conclusion
The Pulumi Java SDK brings infrastructure as code into the Java ecosystem with full type safety, IDE support, and modern software engineering practices. By using Java for infrastructure definition, teams can leverage their existing skills, tools, and patterns to manage cloud resources effectively.
Whether you're deploying simple S3 buckets or complex microservices architectures, Pulumi Java provides a robust, maintainable approach to infrastructure management that integrates seamlessly with your Java development workflow.
Call to Action: Start by converting a simple cloud resource (like an S3 bucket) to Pulumi Java. Experience the benefits of type safety and IDE support firsthand, then gradually expand to more complex infrastructure components. Your infrastructure code will become more maintainable, testable, and reliable.