Skip to content

OCI Autoscale

cloudspells.providers.oci.autoscale

Scalable Workload spell for CloudSpells.

Provides ScalableWorkload, which creates a complete horizontally-scalable OCI compute tier with load balancing and autoscaling:

  • OCI Load Balancer (flexible shape) in the VCN's public subnet with HTTP and optional HTTPS listeners.
  • Instance Configuration as a launch template for pool instances.
  • Instance Pool in the VCN's private subnet, spread across all availability domains.
  • Autoscaling Configuration — metric-based (CPU/memory) or schedule-based (cron expressions).

Supporting configuration dataclasses:

  • LoadBalancerConfig: Customise LB port, health check path, bandwidth, and SSL.
  • MetricScalingPolicy: Scale in/out based on CPU or memory utilisation thresholds.
  • ScheduleScalingPolicy: Scale in/out on a Quartz cron schedule (e.g. business hours).
  • ScheduleEntry: A single cron-schedule scaling action.
  • ScalingMetric: Enum of available metric types.
  • ScalingAction: Enum of available scaling action types.

MetricScalingPolicy dataclass

Metric-based autoscaling policy configuration.

Triggers scale-out when scale_out_threshold is exceeded and scale-in when the metric falls below scale_in_threshold. Maps to OCI threshold autoscaling, AWS target tracking / step scaling, or GCP autoscaling policies.

Attributes:

Name Type Description
scale_out_threshold int

Percentage threshold to trigger scale-out (add instances). Default: 80.

scale_in_threshold int

Percentage threshold to trigger scale-in (remove instances). Default: 20.

scale_out_value int

Number of instances to add when scaling out. Default: 1.

scale_in_value int

Number of instances to remove when scaling in (negative value). Default: -1.

cooldown_in_seconds int

Minimum time between consecutive scaling actions (300-3600 s). Default: 300.

metric ScalingMetric

The metric to monitor. Default: ScalingMetric.CPU_UTILIZATION.

Example
policy = MetricScalingPolicy(
    scale_out_threshold=70,
    scale_in_threshold=25,
    cooldown_in_seconds=600,
)
Source code in packages/cloudspells-core/src/cloudspells/core/abstractions/autoscale.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@dataclass
class MetricScalingPolicy:
    """Metric-based autoscaling policy configuration.

    Triggers scale-out when *scale_out_threshold* is exceeded and scale-in
    when the metric falls below *scale_in_threshold*.  Maps to OCI threshold
    autoscaling, AWS target tracking / step scaling, or GCP autoscaling
    policies.

    Attributes:
        scale_out_threshold: Percentage threshold to trigger scale-out
            (add instances).  Default: `80`.
        scale_in_threshold: Percentage threshold to trigger scale-in
            (remove instances).  Default: `20`.
        scale_out_value: Number of instances to add when scaling out.
            Default: `1`.
        scale_in_value: Number of instances to remove when scaling in
            (negative value).  Default: `-1`.
        cooldown_in_seconds: Minimum time between consecutive scaling
            actions (300-3600 s).  Default: `300`.
        metric: The metric to monitor.  Default:
            `ScalingMetric.CPU_UTILIZATION`.

    Example:
        ```python
        policy = MetricScalingPolicy(
            scale_out_threshold=70,
            scale_in_threshold=25,
            cooldown_in_seconds=600,
        )
        ```
    """

    scale_out_threshold: int = 80
    scale_in_threshold: int = 20
    scale_out_value: int = 1
    scale_in_value: int = -1
    cooldown_in_seconds: int = 300
    metric: ScalingMetric = ScalingMetric.CPU_UTILIZATION

ScalingAction

Bases: Enum

Action types for scheduled scaling policies.

Attributes:

Name Type Description
CHANGE_COUNT_BY

Change the instance count by a relative delta (e.g. +2 or -1).

CHANGE_COUNT_TO

Set the instance count to an absolute target value.

Source code in packages/cloudspells-core/src/cloudspells/core/abstractions/autoscale.py
44
45
46
47
48
49
50
51
52
53
54
class ScalingAction(Enum):
    """Action types for scheduled scaling policies.

    Attributes:
        CHANGE_COUNT_BY: Change the instance count by a relative delta
            (e.g. `+2` or `-1`).
        CHANGE_COUNT_TO: Set the instance count to an absolute target value.
    """

    CHANGE_COUNT_BY = "CHANGE_COUNT_BY"
    CHANGE_COUNT_TO = "CHANGE_COUNT_TO"

ScalingMetric

Bases: Enum

Metrics available for autoscaling policies.

Attributes:

Name Type Description
CPU_UTILIZATION

Scale based on average CPU utilisation (percent).

MEMORY_UTILIZATION

Scale based on average memory utilisation (percent).

Source code in packages/cloudspells-core/src/cloudspells/core/abstractions/autoscale.py
32
33
34
35
36
37
38
39
40
41
class ScalingMetric(Enum):
    """Metrics available for autoscaling policies.

    Attributes:
        CPU_UTILIZATION: Scale based on average CPU utilisation (percent).
        MEMORY_UTILIZATION: Scale based on average memory utilisation (percent).
    """

    CPU_UTILIZATION = "CPU_UTILIZATION"
    MEMORY_UTILIZATION = "MEMORY_UTILIZATION"

ScheduleEntry dataclass

A single scheduled scaling action.

Attributes:

Name Type Description
cron_expression str

Quartz cron format expression in UTC (e.g. "0 0 9 ? * MON-FRI *").

action ScalingAction

Whether to change the count by a delta or to an absolute value.

value int

Magnitude of the scaling action.

display_name str

Human-readable label for this schedule entry.

Example
scale_up = ScheduleEntry(
    cron_expression="0 0 8 ? * MON-FRI *",
    action=ScalingAction.CHANGE_COUNT_TO,
    value=10,
    display_name="Business-hours scale-up",
)
Source code in packages/cloudspells-core/src/cloudspells/core/abstractions/autoscale.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
@dataclass
class ScheduleEntry:
    """A single scheduled scaling action.

    Attributes:
        cron_expression: Quartz cron format expression in UTC
            (e.g. `"0 0 9 ? * MON-FRI *"`).
        action: Whether to change the count by a delta or to an absolute
            value.
        value: Magnitude of the scaling action.
        display_name: Human-readable label for this schedule entry.

    Example:
        ```python
        scale_up = ScheduleEntry(
            cron_expression="0 0 8 ? * MON-FRI *",
            action=ScalingAction.CHANGE_COUNT_TO,
            value=10,
            display_name="Business-hours scale-up",
        )
        ```
    """

    cron_expression: str
    action: ScalingAction
    value: int
    display_name: str

ScheduleScalingPolicy dataclass

Schedule-based autoscaling policy configuration.

Attributes:

Name Type Description
schedules list[ScheduleEntry]

Ordered list of ScheduleEntry objects (maximum 50 per policy on most providers).

Example
policy = ScheduleScalingPolicy(
    schedules=[
        ScheduleEntry("0 0 8 ? * MON-FRI *",
                      ScalingAction.CHANGE_COUNT_TO, 10,
                      "Scale up business hours"),
        ScheduleEntry("0 0 20 ? * MON-FRI *",
                      ScalingAction.CHANGE_COUNT_TO, 2,
                      "Scale down after hours"),
    ]
)
Source code in packages/cloudspells-core/src/cloudspells/core/abstractions/autoscale.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
@dataclass
class ScheduleScalingPolicy:
    """Schedule-based autoscaling policy configuration.

    Attributes:
        schedules: Ordered list of `ScheduleEntry` objects
            (maximum 50 per policy on most providers).

    Example:
        ```python
        policy = ScheduleScalingPolicy(
            schedules=[
                ScheduleEntry("0 0 8 ? * MON-FRI *",
                              ScalingAction.CHANGE_COUNT_TO, 10,
                              "Scale up business hours"),
                ScheduleEntry("0 0 20 ? * MON-FRI *",
                              ScalingAction.CHANGE_COUNT_TO, 2,
                              "Scale down after hours"),
            ]
        )
        ```
    """

    schedules: list[ScheduleEntry] = field(default_factory=list)

OciLoadBalancerConfig dataclass

Bases: LoadBalancerConfig

OCI-specific load balancer configuration extending the cloud-neutral base.

Adds OCI flexible-shape bandwidth parameters to the base LoadBalancerConfig. Use this class instead of LoadBalancerConfig when deploying ScalableWorkload on OCI and you need to control the LB's minimum or maximum bandwidth allocation.

Attributes:

Name Type Description
backend_port int

int. Port on backend instances to receive forwarded traffic and health-check probes. Default: 80.

health_check_path str

str. HTTP path used for backend health checks. Default: "/health".

is_public bool

bool. Whether the load balancer is assigned a public IP. Default: True.

min_bandwidth_mbps int

int. Minimum bandwidth allocated to the OCI flexible load-balancer shape in Mbps. OCI will not reduce below this value even when traffic is idle. Default: 10.

max_bandwidth_mbps int

int. Maximum bandwidth the OCI flexible load-balancer shape may burst to in Mbps. Default: 100.

ssl_certificate_name str | None

str | None. Name of a certificate object already uploaded to the load balancer. When set, an HTTPS listener on port 443 is created alongside the HTTP listener on port 80. Default: None (HTTP only).

Example
from cloudspells.providers.oci.autoscale import OciLoadBalancerConfig

lb_cfg = OciLoadBalancerConfig(
    backend_port=8080,
    health_check_path="/api/health",
    min_bandwidth_mbps=100,
    max_bandwidth_mbps=500,
)
Source code in packages/cloudspells-oci/src/cloudspells/providers/oci/autoscale.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@dataclass
class OciLoadBalancerConfig(_BaseLoadBalancerConfig):
    """OCI-specific load balancer configuration extending the cloud-neutral base.

    Adds OCI flexible-shape bandwidth parameters to the base `LoadBalancerConfig`.
    Use this class instead of `LoadBalancerConfig` when deploying `ScalableWorkload`
    on OCI and you need to control the LB's minimum or maximum bandwidth allocation.

    Attributes:
        backend_port: `int`. Port on backend instances to receive forwarded traffic
            and health-check probes.  Default: `80`.
        health_check_path: `str`. HTTP path used for backend health checks.
            Default: `"/health"`.
        is_public: `bool`. Whether the load balancer is assigned a public IP.
            Default: `True`.
        min_bandwidth_mbps: `int`. Minimum bandwidth allocated to the OCI flexible
            load-balancer shape in Mbps.  OCI will not reduce below this value even
            when traffic is idle.  Default: `10`.
        max_bandwidth_mbps: `int`. Maximum bandwidth the OCI flexible load-balancer
            shape may burst to in Mbps.  Default: `100`.
        ssl_certificate_name: `str | None`. Name of a certificate object already
            uploaded to the load balancer.  When set, an HTTPS listener on port 443
            is created alongside the HTTP listener on port 80.
            Default: `None` (HTTP only).

    Example:
        ```python
        from cloudspells.providers.oci.autoscale import OciLoadBalancerConfig

        lb_cfg = OciLoadBalancerConfig(
            backend_port=8080,
            health_check_path="/api/health",
            min_bandwidth_mbps=100,
            max_bandwidth_mbps=500,
        )
        ```
    """

    min_bandwidth_mbps: int = 10
    max_bandwidth_mbps: int = 100

ScalableWorkload

Bases: BaseResource, AbstractScalableWorkload

OCI Scalable Workload with load balancer, instance pool, and autoscaling.

Creates a complete horizontally-scalable compute tier following OCI best practices:

  • OCI Load Balancer (flexible shape) in the VCN public subnet — internet-facing, with HTTP and optional HTTPS listeners.
  • Instance Configuration as the launch template for pool VMs.
  • Instance Pool in the VCN private subnet, spread across all availability domains.
  • Autoscaling Configuration — metric-based (CPU/memory thresholds) or schedule-based (Quartz cron). Pass None to disable autoscaling.

Security rules are added automatically to the VCN via Vcn.add_security_list_rules before Vcn.finalize_network is called.

Attributes:

Name Type Description
vcn Vcn | VcnRef

The Vcn or VcnRef this workload is deployed into.

shape Input[str]

Compute shape for instance pool VMs (e.g. "VM.Standard.E4.Flex").

ocpus Input[float]

Number of OCPUs per instance.

memory_in_gbs Input[float]

RAM in GiB per instance.

ssh_public_key str

OpenSSH public key installed on instances.

ssh_private_key str | None

Corresponding private key, or None when the caller supplied their own public key.

image_id Input[str] | None

OCID of the boot image resolved for the pool instances.

user_data str | None

Base64-encoded cloud-init user data string stored internally, or None. Pass a plain str or bytes to __init__; encoding is performed automatically.

min_instances int

Minimum (floor) number of instances for autoscaling.

max_instances int

Maximum (ceiling) number of instances for autoscaling.

initial_instances int

Instance count when the pool is first created.

load_balancer_config LoadBalancerConfig

LoadBalancerConfig in use.

scaling_policy MetricScalingPolicy | ScheduleScalingPolicy | None

MetricScalingPolicy, ScheduleScalingPolicy, or None.

auto_generated_keys bool

True when SSH keys were auto-generated.

load_balancer LoadBalancer

The oci.loadbalancer.LoadBalancer resource.

backend_set BackendSet

The oci.loadbalancer.BackendSet resource.

listeners list[Listener]

List of oci.loadbalancer.Listener resources (HTTP, and optionally HTTPS).

instance_configuration InstanceConfiguration

The oci.core.InstanceConfiguration resource used as the pool launch template.

instance_pool InstancePool

The oci.core.InstancePool resource.

autoscaling_configuration AutoScalingConfiguration | None

The oci.autoscaling.AutoScalingConfiguration resource, or None when autoscaling is disabled.

id Output[str]

pulumi.Output[str] of the instance pool OCID.

Example
vcn = Vcn(name="app", compartment_id=comp_id, cidr_block="10.0.0.0/16")

pool = ScalableWorkload(
    name="web",
    compartment_id=comp_id,
    vcn=vcn,
    min_instances=2,
    max_instances=10,
    load_balancer_config=LoadBalancerConfig(backend_port=8080),
    scaling_policy=MetricScalingPolicy(scale_out_threshold=70),
)

pulumi.export("lb_ip", pool.get_load_balancer_ip())
Source code in packages/cloudspells-oci/src/cloudspells/providers/oci/autoscale.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
class ScalableWorkload(BaseResource, AbstractScalableWorkload):
    """OCI Scalable Workload with load balancer, instance pool, and autoscaling.

    Creates a complete horizontally-scalable compute tier following OCI best
    practices:

    - OCI Load Balancer (flexible shape) in the VCN public subnet —
      internet-facing, with HTTP and optional HTTPS listeners.
    - Instance Configuration as the launch template for pool VMs.
    - Instance Pool in the VCN private subnet, spread across all
      availability domains.
    - Autoscaling Configuration — metric-based (CPU/memory thresholds) or
      schedule-based (Quartz cron).  Pass `None` to disable autoscaling.

    Security rules are added automatically to the VCN via
    `Vcn.add_security_list_rules` before `Vcn.finalize_network` is called.

    Attributes:
        vcn: The `Vcn` or `VcnRef` this workload is deployed into.
        shape: Compute shape for instance pool VMs
            (e.g. `"VM.Standard.E4.Flex"`).
        ocpus: Number of OCPUs per instance.
        memory_in_gbs: RAM in GiB per instance.
        ssh_public_key: OpenSSH public key installed on instances.
        ssh_private_key: Corresponding private key, or `None` when the caller
            supplied their own public key.
        image_id: OCID of the boot image resolved for the pool instances.
        user_data: Base64-encoded cloud-init user data string stored internally,
            or `None`.  Pass a plain `str` or `bytes` to `__init__`; encoding
            is performed automatically.
        min_instances: Minimum (floor) number of instances for autoscaling.
        max_instances: Maximum (ceiling) number of instances for autoscaling.
        initial_instances: Instance count when the pool is first created.
        load_balancer_config: `LoadBalancerConfig` in use.
        scaling_policy: `MetricScalingPolicy`, `ScheduleScalingPolicy`, or `None`.
        auto_generated_keys: `True` when SSH keys were auto-generated.
        load_balancer: The `oci.loadbalancer.LoadBalancer` resource.
        backend_set: The `oci.loadbalancer.BackendSet` resource.
        listeners: List of `oci.loadbalancer.Listener` resources (HTTP,
            and optionally HTTPS).
        instance_configuration: The `oci.core.InstanceConfiguration`
            resource used as the pool launch template.
        instance_pool: The `oci.core.InstancePool` resource.
        autoscaling_configuration: The
            `oci.autoscaling.AutoScalingConfiguration` resource, or `None`
            when autoscaling is disabled.
        id: `pulumi.Output[str]` of the instance pool OCID.

    Example:
        ```python
        vcn = Vcn(name="app", compartment_id=comp_id, cidr_block="10.0.0.0/16")

        pool = ScalableWorkload(
            name="web",
            compartment_id=comp_id,
            vcn=vcn,
            min_instances=2,
            max_instances=10,
            load_balancer_config=LoadBalancerConfig(backend_port=8080),
            scaling_policy=MetricScalingPolicy(scale_out_threshold=70),
        )

        pulumi.export("lb_ip", pool.get_load_balancer_ip())
        ```
    """

    vcn: Vcn | VcnRef
    shape: pulumi.Input[str]
    ocpus: pulumi.Input[float]
    memory_in_gbs: pulumi.Input[float]
    ssh_public_key: str
    ssh_private_key: str | None
    image_id: pulumi.Input[str] | None
    user_data: str | None
    min_instances: int
    max_instances: int
    initial_instances: int
    load_balancer_config: LoadBalancerConfig
    scaling_policy: MetricScalingPolicy | ScheduleScalingPolicy | None
    auto_generated_keys: bool

    # Resources
    load_balancer: oci.loadbalancer.LoadBalancer
    backend_set: oci.loadbalancer.BackendSet
    listeners: list[oci.loadbalancer.Listener]
    instance_configuration: oci.core.InstanceConfiguration
    instance_pool: oci.core.InstancePool
    autoscaling_configuration: oci.autoscaling.AutoScalingConfiguration | None
    id: pulumi.Output[str]

    def __init__(
        self,
        name: str,
        compartment_id: pulumi.Input[str],
        vcn: Vcn | VcnRef,
        stack_name: str | None = None,
        # Instance configuration
        shape: pulumi.Input[str] = "VM.Standard.E4.Flex",
        ocpus: pulumi.Input[float] = 1,
        memory_in_gbs: pulumi.Input[float] = 16,
        image_id: pulumi.Input[str] | None = None,
        os_name: str = "oracle",
        ssh_public_key: pulumi.Input[str] | None = None,
        user_data: str | bytes | None = None,
        boot_volume_size_in_gbs: pulumi.Input[int] = 50,
        nsg_ids: list[pulumi.Input[str]] | None = None,
        # Pool configuration
        min_instances: int = 1,
        max_instances: int = 5,
        initial_instances: int | None = None,
        # Load balancer configuration
        load_balancer_config: LoadBalancerConfig | None = None,
        # Scaling policy (metric OR schedule, not both); pass None to disable autoscaling
        scaling_policy: MetricScalingPolicy | ScheduleScalingPolicy | None = _UNSET,  # type: ignore[assignment]
        defined_tags: dict[str, Any] | None = None,
        opts: pulumi.ResourceOptions | None = None,
    ) -> None:
        """Create a scalable workload with load balancer, instance pool, and autoscaling.

        Args:
            name: Logical name for the workload (e.g. `"web"`).
            compartment_id: OCID of the OCI compartment to deploy into.
            vcn: `Vcn` or `VcnRef` providing the 4-tier network.  The load
                balancer is placed in the public subnet and the instance pool
                in the private subnet.
            stack_name: Pulumi stack name.  Defaults to
                `pulumi.get_stack()` when `None`.
            shape: OCI compute shape for instance pool VMs
                (default: `"VM.Standard.E4.Flex"`).
            ocpus: Number of OCPUs per instance (default: `1`).
            memory_in_gbs: RAM in GiB per instance (default: `16`).
            image_id: Explicit boot image OCID.  When `None`, the latest
                image compatible with `shape` and `os_name` is resolved
                automatically.
            os_name: Friendly OS name used to auto-discover the latest image
                when `image_id` is `None`.  Supported values: `"oracle"`
                (Oracle Linux 8, default), `"ubuntu"` (Canonical Ubuntu
                22.04), `"windows"` (Windows Server 2022 Standard).
                Ignored when `image_id` is provided.
            ssh_public_key: OpenSSH public key to install on instances.
                When `None` or empty, a key pair is auto-generated and
                exported as Pulumi secrets.
            user_data: Cloud-init script as a plain `str` or `bytes`.
                CloudSpells base64-encodes it before passing to OCI.  When
                `None`, no user data is injected.
            boot_volume_size_in_gbs: Boot volume size in GiB (default:
                `50`).
            nsg_ids: List of Network Security Group OCIDs to attach to each
                pool instance VNIC.  When `None`, no NSGs are attached and
                security is enforced by the subnet security list alone.
                Provide NSG OCIDs (e.g. from `Nsg`) to add a second,
                resource-level security layer on pool VMs.
            min_instances: Minimum number of instances in the pool
                (default: `1`).
            max_instances: Maximum number of instances the autoscaler may
                create (default: `5`).
            initial_instances: Initial instance count when the pool is first
                created.  Defaults to `min_instances`.
            load_balancer_config: `LoadBalancerConfig` dataclass.  Defaults
                to `LoadBalancerConfig()` (port 80, health check `/health`,
                10-100 Mbps, public).
            scaling_policy: Autoscaling policy.  Pass a `MetricScalingPolicy`
                (CPU/memory threshold), a `ScheduleScalingPolicy`
                (cron-based), or `None` to disable autoscaling entirely.
                When omitted, defaults to `MetricScalingPolicy()`
                (80% CPU scale-out).
            defined_tags: OCI defined tags applied to the load balancer,
                instance configuration, instance pool, and autoscaling
                resources, in `{"namespace": {"key": "value"}}` format.
                When `None` no defined tags are applied.
            opts: Pulumi resource options forwarded to the component.
        """
        super().__init__("custom:compute:ScalableWorkload", name, compartment_id, stack_name, opts)

        self.name = name
        self.vcn = vcn
        self.compartment_id = compartment_id
        self.shape = shape
        self.ocpus = ocpus
        self.memory_in_gbs = memory_in_gbs
        self.image_id = image_id
        self.boot_volume_size_in_gbs = boot_volume_size_in_gbs
        self.min_instances = min_instances
        self.max_instances = max_instances
        self.initial_instances = initial_instances if initial_instances is not None else min_instances
        self.load_balancer_config = load_balancer_config or LoadBalancerConfig()
        self.scaling_policy = MetricScalingPolicy() if scaling_policy is _UNSET else scaling_policy
        self.listeners = []
        self.autoscaling_configuration = None
        # [GAP] G2: store defined_tags for propagation to all sub-resources
        self._defined_tags = defined_tags
        # [GAP] G3: store nsg_ids for pool VNIC in InstanceConfiguration
        self._nsg_ids = nsg_ids or []

        # [GAP] G1: base64-encode user_data (parity with ComputeInstance).
        # OCI InstanceConfiguration metadata["user_data"] requires base64,
        # just as oci.core.Instance does.  Accept plain str/bytes and encode
        # here so callers do not need to pre-encode the payload.
        if user_data is not None:
            raw: bytes = user_data.encode() if isinstance(user_data, str) else user_data
            self.user_data = base64.b64encode(raw).decode()
        else:
            self.user_data = None
        # [GAP] G4: store os_name for image resolution fallback (parity with ComputeInstance)
        self._os_name = os_name

        # Handle SSH key - either use provided or auto-generate
        self._setup_ssh_keys(ssh_public_key)

        # Add security rules for load balancer and instance pool
        self._add_scalable_workload_security_rules()

        # Finalize the VCN network
        self.vcn.finalize_network()

        # Verify subnets exist after finalization
        assert self.vcn.public_subnet is not None, "VCN public subnet must exist after finalization"
        assert self.vcn.private_subnet is not None, "VCN private subnet must exist after finalization"

        # [GAP] G4: pass os_name so non-Oracle images can be resolved
        resolved_image_id = str(image_id) if image_id is not None else None
        self.image_id = OciHelper().resolve_image_id(str(compartment_id), str(shape), resolved_image_id, self._os_name)

        # Get availability domains
        ads = oci.identity.get_availability_domains(compartment_id=str(compartment_id))
        self.availability_domains = ads.availability_domains

        # Create resources in order
        self._create_load_balancer()
        self._create_instance_configuration()
        self._create_instance_pool()
        self._create_autoscaling_configuration()

        self.id = self.instance_pool.id

        # Register outputs
        outputs: dict[str, pulumi.Output[str] | str] = {
            "instance_pool_id": self.instance_pool.id,
            "load_balancer_id": self.load_balancer.id,
        }

        outputs.update(self._get_ssh_outputs())

        self.register_outputs(outputs)

    def _add_scalable_workload_security_rules(self) -> None:
        """Add security rules for load balancer and instance pool communication.

        Calls `Vcn.add_security_list_rules` with:

        Public subnet (Load Balancer):

        - Ingress: HTTP (80) and HTTPS (443) from internet (`0.0.0.0/0`).
        - Egress: Backend port (`LoadBalancerConfig.backend_port`) to
          private subnet CIDR.

        Private subnet (Instance Pool):

        - Ingress: Backend port from public subnet CIDR (load balancer
          health checks and forwarded traffic).
        - Egress: HTTPS (443) to OCI service CIDR block (monitoring,
          telemetry, software updates).

        Note:
            SSH access to pool instances is not managed here.  Deploy a
            `Bastion` spell alongside this workload to enable time-limited
            SSH via the OCI Bastion Service.

            Must be called before `Vcn.finalize_network`.
        """
        public_subnet_cidr: pulumi.Input[str] = self.vcn.get_public_subnet_cidr()
        private_subnet_cidr: pulumi.Input[str] = self.vcn.get_private_subnet_cidr()
        svc_cidr: pulumi.Output[str] = self.vcn._svc_cidr_block
        backend_port = self.load_balancer_config.backend_port

        # Public subnet ingress rules (Load Balancer)
        public_ingress_rules: list[oci.core.SecurityListIngressSecurityRuleArgs] = [
            oci.core.SecurityListIngressSecurityRuleArgs(
                description="HTTP traffic from internet to load balancer",
                protocol="6",  # TCP
                source="0.0.0.0/0",
                source_type="CIDR_BLOCK",
                tcp_options=oci.core.SecurityListIngressSecurityRuleTcpOptionsArgs(
                    min=80,
                    max=80,
                ),
            ),
            oci.core.SecurityListIngressSecurityRuleArgs(
                description="HTTPS traffic from internet to load balancer",
                protocol="6",  # TCP
                source="0.0.0.0/0",
                source_type="CIDR_BLOCK",
                tcp_options=oci.core.SecurityListIngressSecurityRuleTcpOptionsArgs(
                    min=443,
                    max=443,
                ),
            ),
        ]

        # Public subnet egress rules (Load Balancer to backend instances)
        public_egress_rules: list[oci.core.SecurityListEgressSecurityRuleArgs] = [
            oci.core.SecurityListEgressSecurityRuleArgs(
                description=f"Load balancer forwards traffic to backend instances on port {backend_port}",
                protocol="6",  # TCP
                destination=private_subnet_cidr,
                destination_type="CIDR_BLOCK",
                tcp_options=oci.core.SecurityListEgressSecurityRuleTcpOptionsArgs(
                    min=backend_port,
                    max=backend_port,
                ),
            ),
        ]

        # Private subnet ingress rules (Instance Pool)
        private_ingress_rules: list[oci.core.SecurityListIngressSecurityRuleArgs] = [
            oci.core.SecurityListIngressSecurityRuleArgs(
                description=f"Traffic from load balancer to application on port {backend_port}",
                protocol="6",  # TCP
                source=public_subnet_cidr,
                source_type="CIDR_BLOCK",
                tcp_options=oci.core.SecurityListIngressSecurityRuleTcpOptionsArgs(
                    min=backend_port,
                    max=backend_port,
                ),
            ),
        ]

        # Private subnet egress rules (Instance Pool to OCI services)
        private_egress_rules: list[oci.core.SecurityListEgressSecurityRuleArgs] = [
            oci.core.SecurityListEgressSecurityRuleArgs(
                description="Instances access OCI services for monitoring, telemetry, and updates",
                protocol="6",  # TCP
                destination=svc_cidr,
                destination_type="SERVICE_CIDR_BLOCK",
                tcp_options=oci.core.SecurityListEgressSecurityRuleTcpOptionsArgs(
                    min=443,
                    max=443,
                ),
            ),
        ]

        # Add rules to VCN security lists
        self.vcn.add_security_list_rules(
            public_ingress=public_ingress_rules,
            public_egress=public_egress_rules,
            private_ingress=private_ingress_rules,
            private_egress=private_egress_rules,
        )

    def _create_load_balancer(self) -> None:
        """Create the OCI Load Balancer, backend set, and HTTP/HTTPS listeners.

        Always creates an HTTP listener on port 80.  When
        `LoadBalancerConfig.ssl_certificate_name` is set, an additional HTTPS
        listener on port 443 is created using that certificate.

        Sets `self.load_balancer`, `self.backend_set`, and `self.listeners`
        on the instance.
        """
        lb_config = self.load_balancer_config

        assert self.vcn.public_subnet is not None

        # Create load balancer
        lb_name = self.create_resource_name("lb")
        self.load_balancer = oci.loadbalancer.LoadBalancer(
            lb_name,
            compartment_id=self.compartment_id,
            display_name=lb_name,
            shape="flexible",
            shape_details=oci.loadbalancer.LoadBalancerShapeDetailsArgs(
                minimum_bandwidth_in_mbps=lb_config.min_bandwidth_mbps,
                maximum_bandwidth_in_mbps=lb_config.max_bandwidth_mbps,
            ),
            subnet_ids=[self.vcn.public_subnet.id],
            is_private=not lb_config.is_public,
            freeform_tags=self.create_freeform_tags(lb_name, "load-balancer"),
            # [GAP] G2: propagate defined_tags to load balancer
            defined_tags=self._defined_tags,
            opts=pulumi.ResourceOptions(parent=self),
        )

        # Create backend set with health check on the backend port
        bs_name = self.create_resource_name("bs")
        self.backend_set = oci.loadbalancer.BackendSet(
            bs_name,
            load_balancer_id=self.load_balancer.id,
            name=bs_name,
            policy="ROUND_ROBIN",
            health_checker=oci.loadbalancer.BackendSetHealthCheckerArgs(
                protocol="HTTP",
                port=lb_config.backend_port,
                url_path=lb_config.health_check_path,
                interval_ms=10000,
                timeout_in_millis=3000,
                retries=3,
            ),
            opts=pulumi.ResourceOptions(parent=self),
        )

        # Always create HTTP listener on port 80
        http_listener_name = self.create_resource_name("listener-http")
        self.listeners.append(
            oci.loadbalancer.Listener(
                http_listener_name,
                load_balancer_id=self.load_balancer.id,
                name=http_listener_name,
                default_backend_set_name=self.backend_set.name,
                port=80,
                protocol="HTTP",
                opts=pulumi.ResourceOptions(parent=self),
            )
        )

        # Optionally create HTTPS listener on port 443
        if lb_config.ssl_certificate_name:
            https_listener_name = self.create_resource_name("listener-https")
            self.listeners.append(
                oci.loadbalancer.Listener(
                    https_listener_name,
                    load_balancer_id=self.load_balancer.id,
                    name=https_listener_name,
                    default_backend_set_name=self.backend_set.name,
                    port=443,
                    protocol="HTTPS",
                    ssl_configuration=oci.loadbalancer.ListenerSslConfigurationArgs(
                        certificate_name=lb_config.ssl_certificate_name,
                        verify_peer_certificate=False,
                    ),
                    opts=pulumi.ResourceOptions(parent=self),
                )
            )

    def _create_instance_configuration(self) -> None:
        """Create the OCI Instance Configuration as a launch template for the pool.

        The configuration captures the shape, image, SSH key, cloud-init user
        data, and private-subnet placement so that the instance pool can
        provision identical VMs automatically.

        Sets `self.instance_configuration` on the instance.
        """
        ic_name = self.create_resource_name("ic")

        # Subnets are guaranteed to exist after finalize_network()
        assert self.vcn.private_subnet is not None

        # Build metadata
        metadata: dict[str, str] = {
            "ssh_authorized_keys": self.ssh_public_key,
        }
        if self.user_data:
            metadata["user_data"] = self.user_data

        self.instance_configuration = oci.core.InstanceConfiguration(
            ic_name,
            compartment_id=self.compartment_id,
            display_name=ic_name,
            instance_details=oci.core.InstanceConfigurationInstanceDetailsArgs(
                instance_type="compute",
                launch_details=oci.core.InstanceConfigurationInstanceDetailsLaunchDetailsArgs(
                    compartment_id=self.compartment_id,
                    shape=self.shape,
                    shape_config=oci.core.InstanceConfigurationInstanceDetailsLaunchDetailsShapeConfigArgs(
                        ocpus=self.ocpus,
                        memory_in_gbs=self.memory_in_gbs,
                    ),
                    source_details=oci.core.InstanceConfigurationInstanceDetailsLaunchDetailsSourceDetailsArgs(
                        source_type="image",
                        image_id=self.image_id,
                        boot_volume_size_in_gbs=str(self.boot_volume_size_in_gbs),
                    ),
                    create_vnic_details=oci.core.InstanceConfigurationInstanceDetailsLaunchDetailsCreateVnicDetailsArgs(
                        assign_public_ip=False,
                        subnet_id=self.vcn.private_subnet.id,
                        # [GAP] G3: wire nsg_ids into pool VNIC so instances
                        # can be placed behind caller-supplied NSGs
                        nsg_ids=self._nsg_ids if self._nsg_ids else None,
                    ),
                    metadata=metadata,
                    # [GAP] G2: propagate defined_tags to instance configuration
                    defined_tags=self._defined_tags,
                ),
            ),
            freeform_tags=self.create_freeform_tags(ic_name, "instance-configuration"),
            opts=pulumi.ResourceOptions(parent=self),
        )

    def _create_instance_pool(self) -> None:
        """Create the OCI Instance Pool and attach it to the load balancer backend set.

        Spreads instances across all availability domains in the region.  The
        pool size starts at `initial_instances` and is managed by the
        autoscaling configuration between `min_instances` and `max_instances`.

        Sets `self.instance_pool` on the instance.
        """
        pool_name = self.create_resource_name("pool")

        # Subnets are guaranteed to exist after finalize_network()
        assert self.vcn.private_subnet is not None

        # Build placement configurations for all ADs
        placement_configs = [
            oci.core.InstancePoolPlacementConfigurationArgs(
                availability_domain=ad.name,
                primary_subnet_id=self.vcn.private_subnet.id,
            )
            for ad in self.availability_domains
        ]

        self.instance_pool = oci.core.InstancePool(
            pool_name,
            compartment_id=self.compartment_id,
            display_name=pool_name,
            instance_configuration_id=self.instance_configuration.id,
            size=self.initial_instances,
            placement_configurations=placement_configs,
            load_balancers=[
                oci.core.InstancePoolLoadBalancerArgs(
                    backend_set_name=self.backend_set.name,
                    load_balancer_id=self.load_balancer.id,
                    port=self.load_balancer_config.backend_port,
                    vnic_selection="PrimaryVnic",
                ),
            ],
            freeform_tags=self.create_freeform_tags(pool_name, "instance-pool"),
            # [GAP] G2: propagate defined_tags to instance pool
            defined_tags=self._defined_tags,
            opts=pulumi.ResourceOptions(parent=self),
        )

    def _create_autoscaling_configuration(self) -> None:
        """Dispatch to the appropriate autoscaling factory based on *scaling_policy*.

        Delegates to `_create_metric_autoscaling` or
        `_create_schedule_autoscaling`.  Does nothing when `scaling_policy`
        is `None`.

        Sets `self.autoscaling_configuration` on the instance (or leaves it
        `None` if autoscaling is disabled).
        """
        if self.scaling_policy is None:
            return

        asc_name = self.create_resource_name("asc")

        if isinstance(self.scaling_policy, MetricScalingPolicy):
            self._create_metric_autoscaling(asc_name)
        elif isinstance(self.scaling_policy, ScheduleScalingPolicy):
            self._create_schedule_autoscaling(asc_name)

    def _create_metric_autoscaling(self, asc_name: str) -> None:
        """Create a threshold-based autoscaling configuration for the instance pool.

        Configures two rules against the metric specified in
        `MetricScalingPolicy.metric`:

        - **Scale out**: fires when the metric exceeds
          `MetricScalingPolicy.scale_out_threshold` (`GT` operator)
          and adds `MetricScalingPolicy.scale_out_value` instances.
        - **Scale in**: fires when the metric falls below
          `MetricScalingPolicy.scale_in_threshold` (`LT` operator)
          and removes `abs(scale_in_value)` instances.

        Args:
            asc_name: Fully-qualified OCI resource name for the autoscaling
                configuration (created by `BaseResource.create_resource_name`).
        """
        policy = self.scaling_policy
        assert isinstance(policy, MetricScalingPolicy)

        self.autoscaling_configuration = oci.autoscaling.AutoScalingConfiguration(
            asc_name,
            compartment_id=self.compartment_id,
            display_name=asc_name,
            auto_scaling_resources=oci.autoscaling.AutoScalingConfigurationAutoScalingResourcesArgs(
                id=self.instance_pool.id,
                type="instancePool",
            ),
            cool_down_in_seconds=policy.cooldown_in_seconds,
            is_enabled=True,
            policies=[
                oci.autoscaling.AutoScalingConfigurationPolicyArgs(
                    display_name=f"{asc_name}-policy",
                    policy_type="threshold",
                    capacity=oci.autoscaling.AutoScalingConfigurationPolicyCapacityArgs(
                        initial=self.initial_instances,
                        max=self.max_instances,
                        min=self.min_instances,
                    ),
                    rules=[
                        # Scale out rule
                        oci.autoscaling.AutoScalingConfigurationPolicyRuleArgs(
                            action=oci.autoscaling.AutoScalingConfigurationPolicyRuleActionArgs(
                                type="CHANGE_COUNT_BY",
                                value=policy.scale_out_value,
                            ),
                            display_name="Scale Out",
                            metric=oci.autoscaling.AutoScalingConfigurationPolicyRuleMetricArgs(
                                metric_type=policy.metric.value,
                                threshold=oci.autoscaling.AutoScalingConfigurationPolicyRuleMetricThresholdArgs(
                                    operator="GT",
                                    value=policy.scale_out_threshold,
                                ),
                            ),
                        ),
                        # Scale in rule
                        oci.autoscaling.AutoScalingConfigurationPolicyRuleArgs(
                            action=oci.autoscaling.AutoScalingConfigurationPolicyRuleActionArgs(
                                type="CHANGE_COUNT_BY",
                                value=policy.scale_in_value,
                            ),
                            display_name="Scale In",
                            metric=oci.autoscaling.AutoScalingConfigurationPolicyRuleMetricArgs(
                                metric_type=policy.metric.value,
                                threshold=oci.autoscaling.AutoScalingConfigurationPolicyRuleMetricThresholdArgs(
                                    operator="LT",
                                    value=policy.scale_in_threshold,
                                ),
                            ),
                        ),
                    ],
                ),
            ],
            freeform_tags=self.create_freeform_tags(asc_name, "autoscaling-configuration"),
            # [GAP] G2: propagate defined_tags to metric autoscaling configuration
            defined_tags=self._defined_tags,
            opts=pulumi.ResourceOptions(parent=self),
        )

    def _create_schedule_autoscaling(self, asc_name: str) -> None:
        """Create a cron-schedule-based autoscaling configuration for the instance pool.

        One OCI autoscaling policy is created per `ScheduleEntry` in
        `ScheduleScalingPolicy.schedules`.  Each policy uses a Quartz cron
        expression in UTC and performs a `ScheduleEntry.action` of either
        `CHANGE_COUNT_BY` or `CHANGE_COUNT_TO`.

        Args:
            asc_name: Fully-qualified OCI resource name for the autoscaling
                configuration (created by `BaseResource.create_resource_name`).
        """
        policy = self.scaling_policy
        assert isinstance(policy, ScheduleScalingPolicy)

        # For schedule-based policies, we create one policy per schedule entry
        policies = []
        for entry in policy.schedules:
            policies.append(
                oci.autoscaling.AutoScalingConfigurationPolicyArgs(
                    display_name=entry.display_name,
                    policy_type="scheduled",
                    capacity=oci.autoscaling.AutoScalingConfigurationPolicyCapacityArgs(
                        initial=self.initial_instances,
                        max=self.max_instances,
                        min=self.min_instances,
                    ),
                    execution_schedule=oci.autoscaling.AutoScalingConfigurationPolicyExecutionScheduleArgs(
                        expression=entry.cron_expression,
                        timezone="UTC",
                        type="cron",
                    ),
                    resource_action=oci.autoscaling.AutoScalingConfigurationPolicyResourceActionArgs(
                        action=entry.action.value,
                        action_type="power",
                    ),
                ),
            )

        self.autoscaling_configuration = oci.autoscaling.AutoScalingConfiguration(
            asc_name,
            compartment_id=self.compartment_id,
            display_name=asc_name,
            auto_scaling_resources=oci.autoscaling.AutoScalingConfigurationAutoScalingResourcesArgs(
                id=self.instance_pool.id,
                type="instancePool",
            ),
            is_enabled=True,
            policies=policies,
            freeform_tags=self.create_freeform_tags(asc_name, "autoscaling-configuration"),
            # [GAP] G2: propagate defined_tags to schedule autoscaling configuration
            defined_tags=self._defined_tags,
            opts=pulumi.ResourceOptions(parent=self),
        )

    def export(self) -> None:
        """Export standard scalable workload stack outputs.

        Publishes load balancer IP, load balancer OCID, and instance pool
        OCID under keys derived from the spell's logical name.  The SSH
        private key is exported as a Pulumi secret only when it was
        auto-generated.

        Example:
            ```python
            pool = ScalableWorkload(name="web-pool", ...)
            pool.export()
            # Exports: web_pool_lb_ip, web_pool_lb_id, web_pool_pool_id,
            #          and conditionally web_pool_ssh_private_key (secret)
            ```
        """
        prefix = self.name.replace("-", "_")
        pulumi.export(f"{prefix}_lb_ip", self.get_load_balancer_ip())
        pulumi.export(f"{prefix}_lb_id", self.get_load_balancer_id())
        pulumi.export(f"{prefix}_pool_id", self.get_instance_pool_id())
        if self.auto_generated_keys and self.ssh_private_key:
            pulumi.export(f"{prefix}_ssh_private_key", pulumi.Output.secret(self.ssh_private_key))

    def get_load_balancer_ip(self) -> pulumi.Output[str]:
        """Return the public IP address of the load balancer.

        Resolves the first entry in the load balancer's `ip_address_details`
        list, which is the public VIP when `LoadBalancerConfig.is_public`
        is `True`.

        Returns:
            `pulumi.Output[str]` resolving to the IP address string, or an
            empty string if the load balancer has no IP details yet.
        """
        return self.load_balancer.ip_address_details.apply(
            lambda details: details[0].ip_address or "" if details else ""
        )

    def get_instance_pool_id(self) -> pulumi.Output[str]:
        """Return the OCID of the instance pool.

        Returns:
            `pulumi.Output[str]` resolving to the instance pool OCID.
        """
        return self.instance_pool.id

    def get_load_balancer_id(self) -> pulumi.Output[str]:
        """Return the OCID of the load balancer.

        Returns:
            `pulumi.Output[str]` resolving to the load balancer OCID.
        """
        return self.load_balancer.id

    def get_ssh_public_key(self) -> str:
        """Return the SSH public key installed on pool instances.

        Returns:
            OpenSSH public key string (auto-generated or caller-supplied).
        """
        return self.ssh_public_key

    def get_ssh_private_key(self) -> str | None:
        """Return the SSH private key if it was auto-generated.

        Returns:
            PEM-encoded private key string when keys were auto-generated,
            or `None` when the caller supplied their own public key.
        """
        return self.ssh_private_key

__init__(name: str, compartment_id: pulumi.Input[str], vcn: Vcn | VcnRef, stack_name: str | None = None, shape: pulumi.Input[str] = 'VM.Standard.E4.Flex', ocpus: pulumi.Input[float] = 1, memory_in_gbs: pulumi.Input[float] = 16, image_id: pulumi.Input[str] | None = None, os_name: str = 'oracle', ssh_public_key: pulumi.Input[str] | None = None, user_data: str | bytes | None = None, boot_volume_size_in_gbs: pulumi.Input[int] = 50, nsg_ids: list[pulumi.Input[str]] | None = None, min_instances: int = 1, max_instances: int = 5, initial_instances: int | None = None, load_balancer_config: LoadBalancerConfig | None = None, scaling_policy: MetricScalingPolicy | ScheduleScalingPolicy | None = _UNSET, defined_tags: dict[str, Any] | None = None, opts: pulumi.ResourceOptions | None = None) -> None

Create a scalable workload with load balancer, instance pool, and autoscaling.

Parameters:

Name Type Description Default
name str

Logical name for the workload (e.g. "web").

required
compartment_id Input[str]

OCID of the OCI compartment to deploy into.

required
vcn Vcn | VcnRef

Vcn or VcnRef providing the 4-tier network. The load balancer is placed in the public subnet and the instance pool in the private subnet.

required
stack_name str | None

Pulumi stack name. Defaults to pulumi.get_stack() when None.

None
shape Input[str]

OCI compute shape for instance pool VMs (default: "VM.Standard.E4.Flex").

'VM.Standard.E4.Flex'
ocpus Input[float]

Number of OCPUs per instance (default: 1).

1
memory_in_gbs Input[float]

RAM in GiB per instance (default: 16).

16
image_id Input[str] | None

Explicit boot image OCID. When None, the latest image compatible with shape and os_name is resolved automatically.

None
os_name str

Friendly OS name used to auto-discover the latest image when image_id is None. Supported values: "oracle" (Oracle Linux 8, default), "ubuntu" (Canonical Ubuntu 22.04), "windows" (Windows Server 2022 Standard). Ignored when image_id is provided.

'oracle'
ssh_public_key Input[str] | None

OpenSSH public key to install on instances. When None or empty, a key pair is auto-generated and exported as Pulumi secrets.

None
user_data str | bytes | None

Cloud-init script as a plain str or bytes. CloudSpells base64-encodes it before passing to OCI. When None, no user data is injected.

None
boot_volume_size_in_gbs Input[int]

Boot volume size in GiB (default: 50).

50
nsg_ids list[Input[str]] | None

List of Network Security Group OCIDs to attach to each pool instance VNIC. When None, no NSGs are attached and security is enforced by the subnet security list alone. Provide NSG OCIDs (e.g. from Nsg) to add a second, resource-level security layer on pool VMs.

None
min_instances int

Minimum number of instances in the pool (default: 1).

1
max_instances int

Maximum number of instances the autoscaler may create (default: 5).

5
initial_instances int | None

Initial instance count when the pool is first created. Defaults to min_instances.

None
load_balancer_config LoadBalancerConfig | None

LoadBalancerConfig dataclass. Defaults to LoadBalancerConfig() (port 80, health check /health, 10-100 Mbps, public).

None
scaling_policy MetricScalingPolicy | ScheduleScalingPolicy | None

Autoscaling policy. Pass a MetricScalingPolicy (CPU/memory threshold), a ScheduleScalingPolicy (cron-based), or None to disable autoscaling entirely. When omitted, defaults to MetricScalingPolicy() (80% CPU scale-out).

_UNSET
defined_tags dict[str, Any] | None

OCI defined tags applied to the load balancer, instance configuration, instance pool, and autoscaling resources, in {"namespace": {"key": "value"}} format. When None no defined tags are applied.

None
opts ResourceOptions | None

Pulumi resource options forwarded to the component.

None
Source code in packages/cloudspells-oci/src/cloudspells/providers/oci/autoscale.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
def __init__(
    self,
    name: str,
    compartment_id: pulumi.Input[str],
    vcn: Vcn | VcnRef,
    stack_name: str | None = None,
    # Instance configuration
    shape: pulumi.Input[str] = "VM.Standard.E4.Flex",
    ocpus: pulumi.Input[float] = 1,
    memory_in_gbs: pulumi.Input[float] = 16,
    image_id: pulumi.Input[str] | None = None,
    os_name: str = "oracle",
    ssh_public_key: pulumi.Input[str] | None = None,
    user_data: str | bytes | None = None,
    boot_volume_size_in_gbs: pulumi.Input[int] = 50,
    nsg_ids: list[pulumi.Input[str]] | None = None,
    # Pool configuration
    min_instances: int = 1,
    max_instances: int = 5,
    initial_instances: int | None = None,
    # Load balancer configuration
    load_balancer_config: LoadBalancerConfig | None = None,
    # Scaling policy (metric OR schedule, not both); pass None to disable autoscaling
    scaling_policy: MetricScalingPolicy | ScheduleScalingPolicy | None = _UNSET,  # type: ignore[assignment]
    defined_tags: dict[str, Any] | None = None,
    opts: pulumi.ResourceOptions | None = None,
) -> None:
    """Create a scalable workload with load balancer, instance pool, and autoscaling.

    Args:
        name: Logical name for the workload (e.g. `"web"`).
        compartment_id: OCID of the OCI compartment to deploy into.
        vcn: `Vcn` or `VcnRef` providing the 4-tier network.  The load
            balancer is placed in the public subnet and the instance pool
            in the private subnet.
        stack_name: Pulumi stack name.  Defaults to
            `pulumi.get_stack()` when `None`.
        shape: OCI compute shape for instance pool VMs
            (default: `"VM.Standard.E4.Flex"`).
        ocpus: Number of OCPUs per instance (default: `1`).
        memory_in_gbs: RAM in GiB per instance (default: `16`).
        image_id: Explicit boot image OCID.  When `None`, the latest
            image compatible with `shape` and `os_name` is resolved
            automatically.
        os_name: Friendly OS name used to auto-discover the latest image
            when `image_id` is `None`.  Supported values: `"oracle"`
            (Oracle Linux 8, default), `"ubuntu"` (Canonical Ubuntu
            22.04), `"windows"` (Windows Server 2022 Standard).
            Ignored when `image_id` is provided.
        ssh_public_key: OpenSSH public key to install on instances.
            When `None` or empty, a key pair is auto-generated and
            exported as Pulumi secrets.
        user_data: Cloud-init script as a plain `str` or `bytes`.
            CloudSpells base64-encodes it before passing to OCI.  When
            `None`, no user data is injected.
        boot_volume_size_in_gbs: Boot volume size in GiB (default:
            `50`).
        nsg_ids: List of Network Security Group OCIDs to attach to each
            pool instance VNIC.  When `None`, no NSGs are attached and
            security is enforced by the subnet security list alone.
            Provide NSG OCIDs (e.g. from `Nsg`) to add a second,
            resource-level security layer on pool VMs.
        min_instances: Minimum number of instances in the pool
            (default: `1`).
        max_instances: Maximum number of instances the autoscaler may
            create (default: `5`).
        initial_instances: Initial instance count when the pool is first
            created.  Defaults to `min_instances`.
        load_balancer_config: `LoadBalancerConfig` dataclass.  Defaults
            to `LoadBalancerConfig()` (port 80, health check `/health`,
            10-100 Mbps, public).
        scaling_policy: Autoscaling policy.  Pass a `MetricScalingPolicy`
            (CPU/memory threshold), a `ScheduleScalingPolicy`
            (cron-based), or `None` to disable autoscaling entirely.
            When omitted, defaults to `MetricScalingPolicy()`
            (80% CPU scale-out).
        defined_tags: OCI defined tags applied to the load balancer,
            instance configuration, instance pool, and autoscaling
            resources, in `{"namespace": {"key": "value"}}` format.
            When `None` no defined tags are applied.
        opts: Pulumi resource options forwarded to the component.
    """
    super().__init__("custom:compute:ScalableWorkload", name, compartment_id, stack_name, opts)

    self.name = name
    self.vcn = vcn
    self.compartment_id = compartment_id
    self.shape = shape
    self.ocpus = ocpus
    self.memory_in_gbs = memory_in_gbs
    self.image_id = image_id
    self.boot_volume_size_in_gbs = boot_volume_size_in_gbs
    self.min_instances = min_instances
    self.max_instances = max_instances
    self.initial_instances = initial_instances if initial_instances is not None else min_instances
    self.load_balancer_config = load_balancer_config or LoadBalancerConfig()
    self.scaling_policy = MetricScalingPolicy() if scaling_policy is _UNSET else scaling_policy
    self.listeners = []
    self.autoscaling_configuration = None
    # [GAP] G2: store defined_tags for propagation to all sub-resources
    self._defined_tags = defined_tags
    # [GAP] G3: store nsg_ids for pool VNIC in InstanceConfiguration
    self._nsg_ids = nsg_ids or []

    # [GAP] G1: base64-encode user_data (parity with ComputeInstance).
    # OCI InstanceConfiguration metadata["user_data"] requires base64,
    # just as oci.core.Instance does.  Accept plain str/bytes and encode
    # here so callers do not need to pre-encode the payload.
    if user_data is not None:
        raw: bytes = user_data.encode() if isinstance(user_data, str) else user_data
        self.user_data = base64.b64encode(raw).decode()
    else:
        self.user_data = None
    # [GAP] G4: store os_name for image resolution fallback (parity with ComputeInstance)
    self._os_name = os_name

    # Handle SSH key - either use provided or auto-generate
    self._setup_ssh_keys(ssh_public_key)

    # Add security rules for load balancer and instance pool
    self._add_scalable_workload_security_rules()

    # Finalize the VCN network
    self.vcn.finalize_network()

    # Verify subnets exist after finalization
    assert self.vcn.public_subnet is not None, "VCN public subnet must exist after finalization"
    assert self.vcn.private_subnet is not None, "VCN private subnet must exist after finalization"

    # [GAP] G4: pass os_name so non-Oracle images can be resolved
    resolved_image_id = str(image_id) if image_id is not None else None
    self.image_id = OciHelper().resolve_image_id(str(compartment_id), str(shape), resolved_image_id, self._os_name)

    # Get availability domains
    ads = oci.identity.get_availability_domains(compartment_id=str(compartment_id))
    self.availability_domains = ads.availability_domains

    # Create resources in order
    self._create_load_balancer()
    self._create_instance_configuration()
    self._create_instance_pool()
    self._create_autoscaling_configuration()

    self.id = self.instance_pool.id

    # Register outputs
    outputs: dict[str, pulumi.Output[str] | str] = {
        "instance_pool_id": self.instance_pool.id,
        "load_balancer_id": self.load_balancer.id,
    }

    outputs.update(self._get_ssh_outputs())

    self.register_outputs(outputs)

export() -> None

Export standard scalable workload stack outputs.

Publishes load balancer IP, load balancer OCID, and instance pool OCID under keys derived from the spell's logical name. The SSH private key is exported as a Pulumi secret only when it was auto-generated.

Example
pool = ScalableWorkload(name="web-pool", ...)
pool.export()
# Exports: web_pool_lb_ip, web_pool_lb_id, web_pool_pool_id,
#          and conditionally web_pool_ssh_private_key (secret)
Source code in packages/cloudspells-oci/src/cloudspells/providers/oci/autoscale.py
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
def export(self) -> None:
    """Export standard scalable workload stack outputs.

    Publishes load balancer IP, load balancer OCID, and instance pool
    OCID under keys derived from the spell's logical name.  The SSH
    private key is exported as a Pulumi secret only when it was
    auto-generated.

    Example:
        ```python
        pool = ScalableWorkload(name="web-pool", ...)
        pool.export()
        # Exports: web_pool_lb_ip, web_pool_lb_id, web_pool_pool_id,
        #          and conditionally web_pool_ssh_private_key (secret)
        ```
    """
    prefix = self.name.replace("-", "_")
    pulumi.export(f"{prefix}_lb_ip", self.get_load_balancer_ip())
    pulumi.export(f"{prefix}_lb_id", self.get_load_balancer_id())
    pulumi.export(f"{prefix}_pool_id", self.get_instance_pool_id())
    if self.auto_generated_keys and self.ssh_private_key:
        pulumi.export(f"{prefix}_ssh_private_key", pulumi.Output.secret(self.ssh_private_key))

get_load_balancer_ip() -> pulumi.Output[str]

Return the public IP address of the load balancer.

Resolves the first entry in the load balancer's ip_address_details list, which is the public VIP when LoadBalancerConfig.is_public is True.

Returns:

Type Description
Output[str]

pulumi.Output[str] resolving to the IP address string, or an

Output[str]

empty string if the load balancer has no IP details yet.

Source code in packages/cloudspells-oci/src/cloudspells/providers/oci/autoscale.py
808
809
810
811
812
813
814
815
816
817
818
819
820
821
def get_load_balancer_ip(self) -> pulumi.Output[str]:
    """Return the public IP address of the load balancer.

    Resolves the first entry in the load balancer's `ip_address_details`
    list, which is the public VIP when `LoadBalancerConfig.is_public`
    is `True`.

    Returns:
        `pulumi.Output[str]` resolving to the IP address string, or an
        empty string if the load balancer has no IP details yet.
    """
    return self.load_balancer.ip_address_details.apply(
        lambda details: details[0].ip_address or "" if details else ""
    )

get_instance_pool_id() -> pulumi.Output[str]

Return the OCID of the instance pool.

Returns:

Type Description
Output[str]

pulumi.Output[str] resolving to the instance pool OCID.

Source code in packages/cloudspells-oci/src/cloudspells/providers/oci/autoscale.py
823
824
825
826
827
828
829
def get_instance_pool_id(self) -> pulumi.Output[str]:
    """Return the OCID of the instance pool.

    Returns:
        `pulumi.Output[str]` resolving to the instance pool OCID.
    """
    return self.instance_pool.id

get_load_balancer_id() -> pulumi.Output[str]

Return the OCID of the load balancer.

Returns:

Type Description
Output[str]

pulumi.Output[str] resolving to the load balancer OCID.

Source code in packages/cloudspells-oci/src/cloudspells/providers/oci/autoscale.py
831
832
833
834
835
836
837
def get_load_balancer_id(self) -> pulumi.Output[str]:
    """Return the OCID of the load balancer.

    Returns:
        `pulumi.Output[str]` resolving to the load balancer OCID.
    """
    return self.load_balancer.id

get_ssh_public_key() -> str

Return the SSH public key installed on pool instances.

Returns:

Type Description
str

OpenSSH public key string (auto-generated or caller-supplied).

Source code in packages/cloudspells-oci/src/cloudspells/providers/oci/autoscale.py
839
840
841
842
843
844
845
def get_ssh_public_key(self) -> str:
    """Return the SSH public key installed on pool instances.

    Returns:
        OpenSSH public key string (auto-generated or caller-supplied).
    """
    return self.ssh_public_key

get_ssh_private_key() -> str | None

Return the SSH private key if it was auto-generated.

Returns:

Type Description
str | None

PEM-encoded private key string when keys were auto-generated,

str | None

or None when the caller supplied their own public key.

Source code in packages/cloudspells-oci/src/cloudspells/providers/oci/autoscale.py
847
848
849
850
851
852
853
854
def get_ssh_private_key(self) -> str | None:
    """Return the SSH private key if it was auto-generated.

    Returns:
        PEM-encoded private key string when keys were auto-generated,
        or `None` when the caller supplied their own public key.
    """
    return self.ssh_private_key