Welcome, builders and creators! Let's talk about a classic developer headache. You’re building a cool new application. You need a database, maybe a message queue, and some other cloud goodies. You also need to run your application on Kubernetes. So you use one tool, maybe Terraform or a cloud console, to create your cloud infrastructure. Then, you switch hats, write a bunch of YAML files, and use kubectl to deploy your application.

The problem? The gap between those two worlds. How do you get your database password from the cloud infrastructure into your Kubernetes cluster securely? Often, it involves a clunky, manual, and error prone process. You might copy and paste credentials, run a script, or use a separate tool to bridge the divide. It feels like you’ve hired two separate construction crews to build a restaurant. One crew builds the building, and the other installs the kitchen. But nobody told the kitchen crew where the power outlets and water lines are!

What if you could be the master architect and builder for the entire project? What if you could define your cloud resources AND your Kubernetes deployments in one place, with one language, and manage them together as a single, cohesive unit?

That's not a dream. That’s the magic of using a true full stack Infrastructure as Code approach. In this article, we’ll explore how to use one Pulumi program to provision cloud infrastructure and deploy a Kubernetes application that uses it. No more glue code, no more manual steps. Just pure, unified deployment joy.

Let's build something!

Provisioning the Foundation: Building Our Playground

First things first, every great application needs a solid foundation. For our example, we'll deploy a stateful application that needs two key pieces of infrastructure:

  1. A managed Kubernetes Cluster to run our application containers.
  2. A managed Database to store our application's data.

We’ll use Python to write our Pulumi program. Let's start by defining our Kubernetes cluster, using Amazon EKS as an example, and a PostgreSQL database using AWS RDS.

Imagine you are simply describing what you want in a Python file.

import pulumi
import pulumi_aws as aws
import pulumi_awsx as awsx
import pulumi_kubernetes as k8s

# Part 1: Provision the foundational cloud infrastructure.

# Create a new VPC for our cluster and database.
# A VPC is like your own private section of the AWS cloud.
vpc = awsx.ec2.Vpc("custom_vpc")

# Create a new EKS cluster.
# This is our managed Kubernetes environment.
eks_cluster = awsx.EksCluster("my_eks_cluster",
    vpc_id=vpc.vpc_id,
    private_subnet_ids=vpc.private_subnet_ids,
    public_subnet_ids=vpc.public_subnet_ids,
    instance_type="t3.medium",
    desired_capacity=2,
)

# Create a new RDS database instance.
# This will be our application's database.
db_password = "yourSuperSecurePassword123" # In a real project, use Pulumi secrets.
db_instance = aws.rds.Instance("my_db_instance",
    engine="postgres",
    instance_class="db.t3.micro",
    allocated_storage=20,
    db_name="mydatabase",
    username="myadmin",
    password=db_password,
    vpc_security_group_ids=[eks_cluster.node_security_group.id], # Allow EKS nodes to connect.
    db_subnet_group_name=aws.rds.SubnetGroup("my_db_subnet_group",
        subnet_ids=vpc.private_subnet_ids
    ).name,
    skip_final_snapshot=True,
)

# We need the cluster's kubeconfig to talk to it.
# Pulumi generates this for us automatically.
kubeconfig = eks_cluster.kubeconfig

Look at that! In just a few lines of Python, we've described a VPC, an EKS cluster, and an RDS database. The best part is how they relate. We told the database to live in the same network as the cluster and to allow connections from the cluster’s nodes. Pulumi understands these dependencies all on its own.

Connecting the Layers: The Magic Bridge

Now for the crucial part. Our application running in Kubernetes needs to know the database address, username, and password. How do we get this information from our newly created RDS instance into our EKS cluster?

Manually? No way! We're going to build a magic bridge.

We can use the outputs from the resources we just created as inputs for our Kubernetes resources, all within the same program. Pulumi handles the flow of information securely and automatically. We'll create a Kubernetes Secret directly, populating it with the live data from our database.

# Part 2: Connect the layers by creating a Kubernetes secret.

# Create a Kubernetes provider instance using the kubeconfig from our EKS cluster.
# This tells Pulumi how to communicate with our new cluster.
k8s_provider = k8s.Provider("k8s_provider", kubeconfig=kubeconfig)

# Now, create the Kubernetes Secret.
# We will populate it with the database connection details.
# Notice how we use the outputs from the 'db_instance' resource directly!
db_secret = k8s.core.v1.Secret("db_secret",
    metadata=k8s.meta.v1.ObjectMetaArgs(name="db_credentials"),
    # The 'apply' function lets us transform the output data.
    string_data={
        "host": db_instance.address,
        "username": db_instance.username,
        "password": db_instance.password,
        "db_name": db_instance.db_name,
    },
    opts=pulumi.ResourceOptions(provider=k8s_provider) # Ensure this uses our new cluster.
)

Voilà! Can you see what happened? We didn't copy anything. We created a k8s.core.v1.Secret and for its data, we pointed directly to db_instance.address, db_instance.username, and so on.

When you run pulumi up, Pulumi first creates the database. Once the database is ready and has an address, Pulumi takes that value and securely pipes it into the definition for the Kubernetes Secret before creating it in your cluster. This is the magic bridge in action. It’s secure, automatic, and fully declarative.

Deploying the Application: Let's Go Live

With our foundation and our bridge in place, it’s time for the main event: deploying our application to Kubernetes.

We'll define a standard set of Kubernetes resources: a Deployment to manage our application's pods, a PersistentVolumeClaim for stateful storage, and a Service to expose our application to the world. The most important part is how our Deployment will consume the database credentials from the Secret we just created.

# Part 3: Deploy our application to Kubernetes.

# Define the application's label for easy resource selection.
app_labels = {"app": "my_stateful_app"}

# Create a PersistentVolumeClaim for our application.
pvc = k8s.core.v1.PersistentVolumeClaim("app_pvc",
    metadata=k8s.meta.v1.ObjectMetaArgs(name="app_data"),
    spec=k8s.core.v1.PersistentVolumeClaimSpecArgs(
        access_modes=["ReadWriteOnce"],
        resources=k8s.core.v1.ResourceRequirementsArgs(
            requests={"storage": "1Gi"}
        ),
    ),
    opts=pulumi.ResourceOptions(provider=k8s_provider)
)

# Create a Deployment for our application.
app_deployment = k8s.apps.v1.Deployment("app_deployment",
    metadata=k8s.meta.v1.ObjectMetaArgs(name="my_app"),
    spec=k8s.apps.v1.DeploymentSpecArgs(
        replicas=2,
        selector=k8s.meta.v1.LabelSelectorArgs(match_labels=app_labels),
        template=k8s.core.v1.PodTemplateSpecArgs(
            metadata=k8s.meta.v1.ObjectMetaArgs(labels=app_labels),
            spec=k8s.core.v1.PodSpecArgs(
                containers=[k8s.core.v1.ContainerArgs(
                    name="app_container",
                    image="my_app_image:1.0.0", # Your application's container image.
                    # Here we mount the database credentials from our secret as environment variables.
                    env_from=[k8s.core.v1.EnvFromSourceArgs(
                        secret_ref=k8s.core.v1.SecretEnvSourceArgs(
                            name=db_secret.metadata.name,
                        )
                    )],
                    volume_mounts=[k8s.core.v1.VolumeMountArgs(
                        name="app_storage",
                        mount_path="/data",
                    )],
                )],
                volumes=[k8s.core.v1.VolumeArgs(
                    name="app_storage",
                    persistent_volume_claim=k8s.core.v1.PersistentVolumeClaimVolumeSourceArgs(
                        claim_name=pvc.metadata.name,
                    ),
                )],
            ),
        ),
    ),
    opts=pulumi.ResourceOptions(provider=k8s_provider)
)

# Create a Service to expose the application.
app_service = k8s.core.v1.Service("app_service",
    metadata=k8s.meta.v1.ObjectMetaArgs(name="my_app_service"),
    spec=k8s.core.v1.ServiceSpecArgs(
        selector=app_labels,
        ports=[k8s.core.v1.ServicePortArgs(port=80, target_port=8080)],
        type="LoadBalancer",
    ),
    opts=pulumi.ResourceOptions(provider=k8s_provider, depends_on=[app_deployment])
)

And there you have it. Our application's Deployment configuration references the db_secret by name. Kubernetes will then automatically take the data from that secret and inject it as environment variables into our application container. Our code can now connect to the database without ever having a hardcoded credential.

When you run pulumi up with this complete program, Pulumi builds the entire dependency graph. It understands it must create the VPC first, then the cluster and database, then the secret, and finally the application deployment. It orchestrates everything for you in the correct order.

The Day 2 Advantage: Evolving With Confidence

So, you’ve deployed your stack. Awesome! But we all know the job isn't done on Day 1. What happens when you need to make changes?

Maybe your database needs more power, or you have a new version of your application container. With a fragmented approach, this would mean updating resources in different places, potentially causing downtime or misconfiguration.

With our unified Pulumi program, making changes is trivial and safe.

Let's say we need to scale up our database from a t3.micro to a t3.small and update our application image to version 1.1.0. We simply change those two lines in our program:

# In our database definition...
db_instance = aws.rds.Instance("my_db_instance",
    # ... other properties
    instance_class="db.t3.small", # <-- We changed this!
    # ...
)

# In our deployment definition...
app_deployment = k8s.apps.v1.Deployment("app_deployment",
    spec=k8s.apps.v1.DeploymentSpecArgs(
        # ... other properties
        template=k8s.core.v1.PodTemplateSpecArgs(
            spec=k8s.core.v1.PodSpecArgs(
                containers=[k8s.core.v1.ContainerArgs(
                    name="app_container",
                    image="my_app_image:1.1.0", # <-- And we changed this!
                    # ...
                )],
            ),
        ),
    ),
    # ...
)

Now, you run one single command: pulumi up.

Pulumi will show you a preview of the changes: it will modify the AWS RDS instance and update the Kubernetes Deployment. You see exactly what will happen before it happens. If it looks correct, you confirm, and Pulumi executes the changes atomically. It updates the cloud resource and the Kubernetes resource in one transactional operation.

This is the true power of a full stack IaC approach. Your entire application stack, from the virtual network up to the application code, becomes one manageable, versionable, and auditable unit. It simplifies operations, reduces errors, and makes evolving your platform a joy instead of a chore. ✨