diff --git a/.gitignore b/.gitignore index 223ebaa2..332f2d45 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,35 @@ terraform.rc # Go workspace file go.work + +# Byte-compiled / cache +__pycache__/ +*.py[cod] +*$py.class + +# Virtual environments +.venv/ +venv/ +ENV/ + +# Distribution / packaging +build/ +dist/ +*.egg-info/ +.eggs/ +pulumi/sdks + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# IDEs +.vscode/ +.idea/ + +# Jupyter +.ipynb_checkpoints/ + +# Type checking +.mypy_cache/ diff --git a/README.md b/README.md index dcd94069..6fe0522e 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # Sample Infrastructure as code (IaC) solutions + A collection of sample Infrastructure as code (IaC) solutions for deploying infrastructure to IBM Cloud. ## Contents +- [Pulumi](pulumi/README.md) - [Terragrunt](terragrunt/README.md) diff --git a/pulumi/Pulumi.yaml b/pulumi/Pulumi.yaml new file mode 100644 index 00000000..e58b8523 --- /dev/null +++ b/pulumi/Pulumi.yaml @@ -0,0 +1,45 @@ +name: pulumi-demo +description: Provision Terraform IBM Modules using Pulumi. +runtime: + name: python + options: + toolchain: pip + virtualenv: venv +config: + pulumi:tags: + value: + pulumi:template: python +packages: + ibm: + source: terraform-provider + version: 0.12.0 + parameters: + - ibm-cloud/ibm + ibm_rg_module: + source: terraform-module + version: 0.1.8 + parameters: + - terraform-ibm-modules/resource-group/ibm + - 1.4.6 + - ibm_rg_module + ibm_cos_module: + source: terraform-module + version: 0.1.8 + parameters: + - terraform-ibm-modules/cos/ibm + - 10.7.2 + - ibm_cos_module + wx_discovery: + source: terraform-module + version: 0.1.8 + parameters: + - terraform-ibm-modules/watsonx-discovery/ibm + - 1.12.0 + - wx_discovery + ibm_kms_module: + source: terraform-module + version: 0.1.8 + parameters: + - terraform-ibm-modules/kms-all-inclusive/ibm + - 5.5.5 + - ibm_kms_module diff --git a/pulumi/README.md b/pulumi/README.md new file mode 100644 index 00000000..e74011e2 --- /dev/null +++ b/pulumi/README.md @@ -0,0 +1,104 @@ +# Description + +This Pulumi example will demonstrate how to provision Resource Group, Key Protect, Object Storage and Watson Discovery instances using Terraform IBM Modules. + +## Prerequisites + +Before you begin, ensure you have: + +a) [Pulumi CLI](https://www.pulumi.com/docs/get-started/download-install/) installed + +b) [IBM Cloud CLI](https://cloud.ibm.com/docs/cli) installed and configured (optional) + +c) An [IBM Cloud API key](https://cloud.ibm.com/docs/account?topic=account-userapikey) + +d) [Git](https://git-scm.com/install/) installed + +## Steps + +**1. Clone the repository** + +```sh +git clone https://github.com/terraform-ibm-modules/sample-iac-solutions.git +cd sample-iac-solutions/pulumi-egs/pulumi +``` + +**2. Log in to Pulumi (either you can create an account in Pulumi or use local-only mode).** + +```sh +pulumi login --local +``` + +**3. Create new Pulumi Stack** + +Provide the Stack details and Passphrase to be used + +```sh +pulumi stack init dev # dev is name of the new stack that will be created. +``` + +![initialize-stack](./images/initialize-stack.png) + +**4. Install dependencies** + +Add pulumi IBM provider package and Terraform modules to the project : + +```sh +pulumi package add terraform-provider ibm-cloud/ibm +pulumi package add terraform-module terraform-ibm-modules/cos/ibm 10.7.2 ibm_cos_module +pulumi package add terraform-module terraform-ibm-modules/kms-all-inclusive/ibm 5.5.5 ibm_kms_module +pulumi package add terraform-module terraform-ibm-modules/resource-group/ibm 1.4.6 ibm_rg_module +pulumi package add terraform-module terraform-ibm-modules/watsonx-discovery/ibm 1.11.1 wx_discovery +``` + +![Add Packages](./images/add_packages.png) + +**5. Configure IBM Cloud credentials** + +Export your API key: + +```sh +export IBMCLOUD_API_KEY= # pragma: allowlist secret +``` + +**6. Set Pulumi stack config** + +If required to change the default values, update the configuration file for region, prefix and access group: + +```sh +pulumi config set region us-south # Provide region +pulumi config set prefix pulumi-demo # Provide the prefix +pulumi config set access_group "Public Access" # Replace with Access group of your choice. Make sure the provided Access Group exists in IBMCloud account. +``` + +![Add configuration](./images/config.png) + +**7. Preview the changes** + +```sh +pulumi preview +``` + +![pulumi preview](./images/preview.png) + +**8. Apply the changes** + +```sh +pulumi up +``` + +**9. Destroy the resources** + +```sh +pulumi down +``` + +## Validation + +After deployment, verify that resources were provisioned successfully: + +- Pulumi Console Output – Confirm resource creation summary. +- Login to IBM Cloud Console and verify – + - COS instance and bucket under Object Storage + - Watson Discovery instance in the dashboard + - Resource Group and Key Protect instance under Resource List diff --git a/pulumi/__init__.py b/pulumi/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pulumi/__main__.py b/pulumi/__main__.py new file mode 100644 index 00000000..99a78f68 --- /dev/null +++ b/pulumi/__main__.py @@ -0,0 +1,57 @@ +# ---------------------------------------------------------------------------------------------------- +# Pulumi Program: Provision IBM Cloud Object Storage (COS) instance & buckets +# and Watson Discovery instance using terraform-ibm-modules. +# +# Required Pulumi packages: +# - IBM provider package +# pulumi package add terraform-provider ibm-cloud/ibm +# ---------------------------------------------------------------------------------------------------- + + +from constants import COS_ENDPOINT +from terraform_ibm_modules.object_storage import ( + configure_bucket_website, + configure_public_access, + create_cos_instance, + upload_static_files, +) +from terraform_ibm_modules.resource_group import create_resource_group +from terraform_ibm_modules.watson_discovery import create_watson_discovery + +import pulumi + + +def main(): + """Main method to provision resources.""" + + # Resource Group + rg = create_resource_group() + + # Cloud Object Storage + cos = create_cos_instance(rg) + configure_public_access(cos) + upload_static_files(cos) + configure_bucket_website(cos) + + # Uncomment below to use Key Protect and COS with Key protect enabled + # kms_instance = create_kms_instance(rg) + # cos = create_cos_instance(rg, kms_instance) + + # Watson Discovery + watson_discovery = create_watson_discovery(rg) + + # Export outputs + pulumi.export("resource_group_name", rg.resource_group_name) + # pulumi.export("kms_instance_crn", kms_instance.key_protect_crn) # Uncomment if provisioning Key Protect. + pulumi.export("bucket_name", cos.bucket_name) + pulumi.export("cos_instance_name", cos.cos_instance_name) + pulumi.export( + "website_endpoint", + cos.bucket_crn.apply(lambda crn: f"https://{cos.bucket_name}.{COS_ENDPOINT}"), + ) + pulumi.export("watson_discovery_id", watson_discovery.id) + pulumi.export("watson_discovery_dashboard", watson_discovery.dashboard_url) + + +if __name__ == "__main__": + main() diff --git a/pulumi/config.py b/pulumi/config.py new file mode 100644 index 00000000..95c12915 --- /dev/null +++ b/pulumi/config.py @@ -0,0 +1,22 @@ +from constants import KMS_KEY_NAME, KMS_KEY_RING_NAME + +import pulumi + +config = pulumi.Config() +REGION = config.get("region") or "us-south" +PREFIX = config.get("prefix") or "pul-eg" +ACCESS_GROUP = config.get("access_group") or "Public Access" + +KMS_KEY_CONFIGURATION = { + "key_name": KMS_KEY_NAME, + "standard_key": True, + "rotation_interval_month": 4, + "dual_auth_delete_enabled": True, + "force_delete": True, +} + +KMS_KEYS = { + "key_ring_name": KMS_KEY_RING_NAME, + "existing_key_ring": False, + "keys": [KMS_KEY_CONFIGURATION], +} diff --git a/pulumi/constants.py b/pulumi/constants.py new file mode 100644 index 00000000..09b7aff1 --- /dev/null +++ b/pulumi/constants.py @@ -0,0 +1,21 @@ +EXISTING_RESOURCE_GROUP = ( + "Default" # Change to None if want to create a new Resource Group. +) +NEW_RG_NAME = "new-rg" # If want to create a new Resource Group EXISTING_RESOURCE_GROUP should be changed to None + +# KMS Configuration +KP_NAME = "key-protect" +KMS_KEY_NAME = "pulumi-key-for-cos" +KMS_KEY_RING_NAME = "pulumi-key-ring" + +COS_SERVICE_NAME = "cloud-object-storage" +COS_INSTANCE_NAME = "pulumi-cos" + +BUCKET_STORAGE_CLASS = "standard" +BUCKET_NAME = "pulumi-bucket" +BUCKET_TYPE = "bucket" +COS_ENDPOINT = "s3.us-south.cloud-object-storage.appdomain.cloud" + +STATIC_DIR_NAME = "static" + +WATSON_DISCOVERY_NAME = "watson-discovery" diff --git a/pulumi/images/add_packages.png b/pulumi/images/add_packages.png new file mode 100644 index 00000000..61afc8a2 Binary files /dev/null and b/pulumi/images/add_packages.png differ diff --git a/pulumi/images/config.png b/pulumi/images/config.png new file mode 100644 index 00000000..ab0f737e Binary files /dev/null and b/pulumi/images/config.png differ diff --git a/pulumi/images/initialize-stack.png b/pulumi/images/initialize-stack.png new file mode 100644 index 00000000..f2046e4a Binary files /dev/null and b/pulumi/images/initialize-stack.png differ diff --git a/pulumi/images/preview.png b/pulumi/images/preview.png new file mode 100644 index 00000000..a8d76a9f Binary files /dev/null and b/pulumi/images/preview.png differ diff --git a/pulumi/requirements.txt b/pulumi/requirements.txt new file mode 100644 index 00000000..bc4e4308 --- /dev/null +++ b/pulumi/requirements.txt @@ -0,0 +1 @@ +pulumi>=3.0.0,<4.0.0 diff --git a/pulumi/terraform_ibm_modules/__init__.py b/pulumi/terraform_ibm_modules/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pulumi/terraform_ibm_modules/key_protect.py b/pulumi/terraform_ibm_modules/key_protect.py new file mode 100644 index 00000000..33b2e849 --- /dev/null +++ b/pulumi/terraform_ibm_modules/key_protect.py @@ -0,0 +1,20 @@ +# ---------------------------------------------------------------------------------------------------- +# Required Pulumi packages: +# - IBM Cloud KMS All Inclusive module: +# pulumi package add terraform-module terraform-ibm-modules/kms-all-inclusive/ibm 5.5.5 ibm_kms_module +# ---------------------------------------------------------------------------------------------------- + +import pulumi_ibm_kms_module as ibm_kms_module +from config import KMS_KEYS, PREFIX, REGION +from constants import KP_NAME + + +def create_kms_instance(rg): + """Create Key Protect Instance which can be used for Encrypting COS buckets.""" + return ibm_kms_module.Module( + "pulumi-key-protect", + resource_group_id=rg.resource_group_id, + key_protect_instance_name=f"{PREFIX}-{KP_NAME}", + region=REGION, + keys=[KMS_KEYS], + ) diff --git a/pulumi/terraform_ibm_modules/object_storage.py b/pulumi/terraform_ibm_modules/object_storage.py new file mode 100644 index 00000000..992dfd4e --- /dev/null +++ b/pulumi/terraform_ibm_modules/object_storage.py @@ -0,0 +1,118 @@ +# ---------------------------------------------------------------------------------------------------- +# Required Pulumi packages: +# - IBM Cloud Object Storage module: +# pulumi package add terraform-module terraform-ibm-modules/cos/ibm 10.7.2 ibm_cos_module +# ---------------------------------------------------------------------------------------------------- + +import glob +import os + +import pulumi_ibm as ibm +import pulumi_ibm_cos_module as cosmod +from config import PREFIX, REGION +from constants import ( + BUCKET_NAME, + BUCKET_STORAGE_CLASS, + BUCKET_TYPE, + COS_INSTANCE_NAME, + COS_SERVICE_NAME, + KMS_KEY_NAME, + KMS_KEY_RING_NAME, + STATIC_DIR_NAME, +) +from utils import first_or_self, generate_suffix + +import pulumi + +STATIC_DIR = os.path.join(os.path.dirname(__file__), STATIC_DIR_NAME) + + +def create_cos_instance(rg): + """Provision COS instance and bucket.""" + cos = cosmod.Module( + "cos_instance_bucket", + resource_group_id=rg.resource_group_id, + region=REGION, + cos_instance_name=f"{PREFIX}-{COS_INSTANCE_NAME}", + bucket_name=f"{PREFIX}-{BUCKET_NAME}-{generate_suffix()}", + create_cos_instance=True, + create_cos_bucket=True, + bucket_storage_class=BUCKET_STORAGE_CLASS, + kms_encryption_enabled=False, + ) + + return cos + + +def create_cos_instance_with_key_protect_enabled(rg, kms_instance): + """Provision COS instance and bucket.""" + cos = cosmod.Module( + "cos_instance_bucket", + resource_group_id=rg.resource_group_id, + region=REGION, + cos_instance_name=f"{PREFIX}-{COS_INSTANCE_NAME}", + bucket_name=f"{PREFIX}-{BUCKET_NAME}-{generate_suffix()}", + create_cos_instance=True, + create_cos_bucket=True, + bucket_storage_class=BUCKET_STORAGE_CLASS, + existing_kms_instance_guid=kms_instance.kms_guid, + kms_key_crn=kms_instance.keys.apply( + lambda keys: keys[f"{KMS_KEY_RING_NAME}.{KMS_KEY_NAME}"]["crn"] + ), + ) + + return cos + + +def upload_static_files(cos): + """Upload files from the static directory to the COS bucket.""" + + if not os.path.isdir(STATIC_DIR): + pulumi.log.warn(f"Static directory not found: {STATIC_DIR}") + return + + for fname in glob.glob(os.path.join(STATIC_DIR, "*")): + if os.path.isfile(fname): + ibm.cos_bucket_object.CosBucketObject( + f"file-{os.path.basename(fname)}", + bucket_crn=cos.bucket_crn.apply(first_or_self), + bucket_location=REGION, + content_file=fname, + key=os.path.basename(fname), + ) + + +def configure_bucket_website(cos): + """Enable static website hosting on the COS bucket.""" + ibm.CosBucketWebsiteConfiguration( + "website-config", + bucket_crn=cos.bucket_crn.apply(first_or_self), + bucket_location=REGION, + website_configuration={ + "index_document": {"suffix": "index.html"}, + "error_document": {"key": "error.html"}, + }, + ) + + +def configure_public_access(cos): + """Grant public read access to the COS bucket.""" + + # NOTE: If you want to use a different IAM access group, either change in pulumi config or override here. + ACCESS_GROUP = "geretain-public-access" + + # Lookup the default 'Public Access' IAM access group + public_access_group = ibm.get_iam_access_group(access_group_name=ACCESS_GROUP) + + # Grant public read access to the bucket + ibm.IamAccessGroupPolicy( + "cos-public-access-policy", + access_group_id=public_access_group.groups[0].id, + roles=["Object Reader"], + resources={ + "service": COS_SERVICE_NAME, + "resource_type": BUCKET_TYPE, + "resource_instance_id": cos.cos_instance_guid.apply(first_or_self), + "resource": cos.bucket_name.apply(first_or_self), + }, + ) diff --git a/pulumi/terraform_ibm_modules/resource_group.py b/pulumi/terraform_ibm_modules/resource_group.py new file mode 100644 index 00000000..e401cd5f --- /dev/null +++ b/pulumi/terraform_ibm_modules/resource_group.py @@ -0,0 +1,25 @@ +# ---------------------------------------------------------------------------------------------------- +# Required Pulumi packages: +# - IBM Cloud Resource Group module: +# pulumi package add terraform-module terraform-ibm-modules/resource-group/ibm 1.4.6 ibm_rg_module +# ---------------------------------------------------------------------------------------------------- + +import pulumi_ibm_rg_module as rgmod +from config import PREFIX +from constants import EXISTING_RESOURCE_GROUP, NEW_RG_NAME + + +def create_resource_group(): + """ + Create or reference an IBM Cloud resource group. + The default value of Existing Resource Group is Default. If want to create a new resource group, set the EXISTING_RESOURCE_GROUP to None. + The NEW_RG_NAME will be then used to create the new resource group. + """ + if EXISTING_RESOURCE_GROUP: + return rgmod.Module( + "resource_group", existing_resource_group_name=EXISTING_RESOURCE_GROUP + ) + else: + return rgmod.Module( + "resource_group", resource_group_name=f"{PREFIX}-{NEW_RG_NAME}" + ) diff --git a/pulumi/terraform_ibm_modules/static/sample-app.log b/pulumi/terraform_ibm_modules/static/sample-app.log new file mode 100644 index 00000000..d809f8cf --- /dev/null +++ b/pulumi/terraform_ibm_modules/static/sample-app.log @@ -0,0 +1,6 @@ +2025-12-19 23:30:12 INFO [AuthService] User login successful: user_id=12345 +2025-12-19 23:30:15 WARN [PaymentService] Payment gateway response delayed for transaction_id=98765 +2025-12-19 23:30:18 ERROR [OrderService] Failed to create order: order_id=54321, reason=Inventory shortage +2025-12-19 23:30:22 INFO [NotificationService] Email sent to customer: email=prateek.sharma@example.com +2025-12-19 23:30:25 DEBUG [CacheManager] Cache miss for key=session_abc123 +2025-12-19 23:30:30 INFO [SystemMonitor] CPU usage at 72%, memory usage at 65% diff --git a/pulumi/terraform_ibm_modules/watson_discovery.py b/pulumi/terraform_ibm_modules/watson_discovery.py new file mode 100644 index 00000000..8a7430f9 --- /dev/null +++ b/pulumi/terraform_ibm_modules/watson_discovery.py @@ -0,0 +1,18 @@ +# ---------------------------------------------------------------------------------------------------- +# Required Pulumi packages: +# - IBM Cloud Watson Discovery module: +# pulumi package add terraform-module terraform-ibm-modules/watsonx-discovery/ibm 1.12.0 wx_discovery +# ---------------------------------------------------------------------------------------------------- + +import pulumi_wx_discovery as wxd_mod +from config import PREFIX +from constants import WATSON_DISCOVERY_NAME + + +def create_watson_discovery(rg): + """Provision a Watson Discovery instance.""" + return wxd_mod.Module( + WATSON_DISCOVERY_NAME, + resource_group_id=rg.resource_group_id, + watson_discovery_name=f"{PREFIX}-{WATSON_DISCOVERY_NAME}", + ) diff --git a/pulumi/utils.py b/pulumi/utils.py new file mode 100644 index 00000000..73d560d0 --- /dev/null +++ b/pulumi/utils.py @@ -0,0 +1,14 @@ +# Helper Methods + +import random +import string + + +def generate_suffix(length: int = 4) -> str: + """Generate a random alphanumeric suffix for resource names.""" + return "".join(random.choices(string.ascii_lowercase + string.digits, k=length)) + + +def first_or_self(x): + """Return first element if list, else return the value itself.""" + return x[0] if isinstance(x, list) else x