Provision a Cluster via CloudFormation

Provision a Cluster via CloudFormation

CloudFormation

Step 1: Copy the curl Command from the UI

On the Create a new replicaset (Master) screen, click { REST API }. The following example is copied:

curl -v \
  -H "Authorization: Bearer <YOUR_JWT_TOKEN>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "host": {
      "name":            "<HOST_NAME>",
      "ssh_port":        <SSH_PORT>,
      "username":        "<SSH_USERNAME>",
      "password":        "<SSH_PASSWORD>",
      "private_key":     "<OPTIONAL_PRIVATE_KEY_CONTENT>"
    },
    "cluster": {
      "name":            "<CLUSTER_NAME>"
    },
    "ex_params": {
      "replicaset_name": "<REPLICASET_NAME>",
      "repository":      "<BACKUP_REPOSITORY>",
      "ssl":             <USE_SSL>
      "service_name":    "proxmox",
      "proxmox":         true,
      "distributor":     "<OS_DISTRIBUTOR>",
      "release":         "<OS_VERSION>"
    },
    "name":              "<NODE_NAME>",
    "auto_delete":       false,
    "version":           "<DB_VERSION>",
    "auto_delete_days":  1,
    "port":              <DB_PORT>,
    "sizing":            "<SIZING>",
    "dir_123Cluster":    "<DATA_DIRECTORY>",
    "drop_inventories":  false,
    "database_user":     "<DB_ADMIN_USER>",
    "database_pwd":      "<DB_ADMIN_PASSWORD>",
    "rest_api":          true
  }' \
  <API_BASE_URL>

Step 2: Parse the curl Command

  • Authorization header: Extract your JWT token from Bearer <YOUR_JWT_TOKEN>.
  • Content-Type & Accept: Preserve both application/json headers.
  • Payload fields
  • host.name / host.ssh_port / host.username / host.password / host.private_key
  • cluster.name
  • ex_params.replicaset_name / ex_params.repository / ex_params.ssl / ex_params.distributor / ex_params.release
  • name (node identifier)
  • auto_delete / auto_delete_days
  • version / port / sizing / dir_123Cluster / drop_inventories
  • database_user / database_pwd
  • rest_api
  • Endpoint
  • Base URI: <API_BASE_URL>

Step 3: Translate into CloudFormation

Directory Structure

├── params.json

├── run.sh

└── template.yaml

CloudFormation Parameters

Replace fields in corner brackets <> in parameters file with values extracted from the URL.

File params.json:

[
    {
        "ParameterKey": "ApiBaseUrl",
        "ParameterValue": "<API_BASE_URL>"
    },
    {
        "ParameterKey": "JwtToken",
        "ParameterValue": "<YOUR_JWT_TOKEN>"
    },
    {
        "ParameterKey": "HostName",
        "ParameterValue": "<HOST_NAME>"
    },
    {
        "ParameterKey": "SshUser",
        "ParameterValue": "<SSH_USERNAME>"
    },
    {
        "ParameterKey": "SshPassword",
        "ParameterValue": "<SSH_PASSWORD>"
    },
    {
        "ParameterKey": "OsDistributor",
        "ParameterValue": "<OS_DISTRIBUTOR>"
    },
    {
        "ParameterKey": "OSRelease",
        "ParameterValue": "<OS_VERSION>"
    },
    {
        "ParameterKey": "ClusterName",
        "ParameterValue": "<CLUSTER_NAME>"
    },
    {
        "ParameterKey": "ReplicasetName",
        "ParameterValue": "<REPLICASET_NAME>"
    },
    {
        "ParameterKey": "DbVersion",
        "ParameterValue": "<DB_VERSION>"
    },
    {
        "ParameterKey": "Port",
        "ParameterValue": "<DB_PORT>"
    },
    {
        "ParameterKey": "Sizing",
        "ParameterValue": "<SIZING>"
    },
    {
        "ParameterKey": "DataDirectory",
        "ParameterValue": "<DATA_DIRECTORY>"
    },
    {
        "ParameterKey": "DatabaseUser",
        "ParameterValue": "<DB_ADMIN_USER>"
    },
    {
        "ParameterKey": "DatabasePassword",
        "ParameterValue": "<DB_ADMIN_PASSWORD>"
    },
    {
        "ParameterKey": "EnableSsl",
        "ParameterValue": "<USE_SSL>"
    }
]

CloudFormation Template

Copy template contents as is.

    Description: Enable SSL for DB connections
    Default: 'false'
File template.yaml:
AWSTemplateFormatVersion: '2010-09-09'
Description: |
  CloudFormation template for provisioning a DB cluster in 123cluster.
  Uses a Lambda-backed custom resource to interact with the 123cluster REST API.

Parameters:
  # API Configuration
  ApiBaseUrl:
    Type: String
    Description: Base URL for 123cluster API

  JwtToken:
    Type: String
    Description: JWT token for API authentication
    NoEcho: true

  # Host Configuration
  HostName:
    Type: String
    Description: Host name address of the DB server

  SshPort:
    Type: Number
    Description: SSH port for initial host setup
    Default: '22'

  SshUser:
    Type: String
    Description: SSH username
    Default: 'root'

  SshPassword:
    Type: String
    Description: SSH password
    NoEcho: true

  PrivateKey:
    Type: String
    Description: Optional SSH private key content
    NoEcho: true
    Default: ''

  OsDistributor:
    Type: String
    Description: OS distributor name

  OSRelease:
    Type: String
    Description: OS release number

  # Cluster Configuration
  ClusterName:
    Type: String
    Description: Name of the cluster

  ReplicasetName:
    Type: String
    Description: Internal replicaset identifier

  Repository:
    Type: String
    Description: Backup repository location
    Default: 'network'

  DbVersion:
    Type: String
    Description: Desired DB version

  Port:
    Type: Number
    Description: Service port

  Sizing:
    Type: String
    Description: Resource sizing for the node
    Default: 'SMALL'

  DataDirectory:
    Type: String
    Description: Data directory for DB files

  # Database Credentials
  DatabaseUser:
    Type: String
    Description: Database administrator username

  DatabasePassword:
    Type: String
    Description: Database administrator password
    NoEcho: true

  EnableSsl:
    Type: String
    AllowedValues:
      - 'true'
      - 'false'

  DropInventories:
    Type: String
    Description: Delete all backups and exports if exist
    Default: 'false'
    AllowedValues:
      - 'true'
      - 'false'

  AutoDelete:
    Type: String
    Description: Delete cluster automatically
    Default: 'false'
    AllowedValues:
      - 'true'
      - 'false'

  AutoDeleteDays:
    Type: Number
    Description: Delete cluster automatically after N days
    Default: '1'

Resources:
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: CloudFormationResponsePolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: '*'

  # Lambda function for custom resource
  ProvisionClusterFunction:
    Type: AWS::Lambda::Function
    Properties:
      Runtime: python3.11
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Timeout: 300
      Code:
        ZipFile: |
          import json
          import json
          import urllib3
          import os
          from urllib.parse import urljoin
          from urllib3.exceptions import InsecureRequestWarning

          urllib3.disable_warnings(InsecureRequestWarning)
          http = urllib3.PoolManager(
              cert_reqs='CERT_NONE',
              assert_hostname=False
          )

          def handler(event, context):
              """
              CloudFormation custom resource handler for 123cluster provisioning.
              Handles Create operations.
              """

              request_type = event['RequestType']
              properties = event['ResourceProperties']

              api_base_url = properties['ApiBaseUrl']
              jwt_token = properties['JwtToken']

              headers = {
                  'Authorization': f'Bearer {jwt_token}',
                  'Content-Type': 'application/json',
                  'Accept': 'application/json'
              }

              payload = {
                  'host': {
                      'name': properties['HostName'],
                      'ssh_port': int(properties['SshPort']),
                      'username': properties['SshUser'],
                      'password': properties['SshPassword'],
                      'private_key': properties.get('PrivateKey', '')
                  },
                  'cluster': {
                      'name': properties['ClusterName']
                  },
                  'ex_params': {
                      'replicaset_name': properties['ReplicasetName'],
                      'repository': properties['Repository'],
                      'ssl': properties['EnableSsl'].lower() == 'true',
                      'service_name': 'proxmox',
                      'proxmox': True,
                      'distributor': properties['OsDistributor'],
                      'release': properties['OSRelease'],
                  },
                  'name': properties['ClusterName'],
                  'auto_delete': properties['AutoDelete'].lower() == 'true',
                  'version': properties['DbVersion'],
                  'auto_delete_days': int(properties['AutoDeleteDays']),
                  'port': int(properties['Port']),
                  'sizing': properties['Sizing'],
                  'dir_123Cluster': properties['DataDirectory'],
                  'drop_inventories': properties['DropInventories'].lower() == 'true',
                  'database_user': properties['DatabaseUser'],
                  'database_pwd': properties['DatabasePassword'],
                  'rest_api': True
              }

              try:
                  if request_type == 'Create':

                      response = http.request(
                          'POST',
                          api_base_url,
                          body=json.dumps(payload),
                          headers=headers
                      )

                      response_data = json.loads(response.data.decode('utf-8'))

                      if response.status >= 200 and response.status < 300:
                          send_response(event, context, 'SUCCESS', {
                              'ClusterId': properties['ClusterName'],
                              'Response': json.dumps(response_data)
                          })
                      else:
                          send_response(event, context, 'FAILED', {
                              'Reason': f'API returned status {response.status}: {response_data}'
                          })

                  elif request_type == 'Update':
                      send_response(event, context, 'SUCCESS', {
                          'Message': 'Update operations not implemented'
                      })

                  elif request_type == 'Delete':
                      send_response(event, context, 'SUCCESS', {
                          'Message': 'Delete operations not implemented'
                      })

              except Exception as e:
                  send_response(event, context, 'FAILED', {
                      'Reason': str(e)
                  })

          def send_response(event, context, status, response_data):
              """Send response to CloudFormation"""
              response_body = {
                  'Status': status,
                  'Reason': response_data.get('Reason', 'See CloudWatch logs'),
                  'PhysicalResourceId': event.get('PhysicalResourceId', 
                                       context.log_stream_name),
                  'StackId': event['StackId'],
                  'RequestId': event['RequestId'],
                  'LogicalResourceId': event['LogicalResourceId'],
                  'Data': response_data
              }

              http.request(
                  'PUT',
                  event['ResponseURL'],
                  body=json.dumps(response_body),
                  headers={'Content-Type': ''}
              )

  # Custom resource for DB cluster provisioning
  DBCluster:
    Type: Custom::DBCluster
    Properties:
      ServiceToken: !GetAtt ProvisionClusterFunction.Arn
      ApiBaseUrl: !Ref ApiBaseUrl
      JwtToken: !Ref JwtToken
      HostName: !Ref HostName
      SshPort: !Ref SshPort
      SshUser: !Ref SshUser
      SshPassword: !Ref SshPassword
      PrivateKey: !Ref PrivateKey
      OsDistributor: !Ref OsDistributor
      OSRelease: !Ref OSRelease
      ClusterName: !Ref ClusterName
      ReplicasetName: !Ref ReplicasetName
      Repository: !Ref Repository
      DbVersion: !Ref DbVersion
      Port: !Ref Port
      Sizing: !Ref Sizing
      DataDirectory: !Ref DataDirectory
      DatabaseUser: !Ref DatabaseUser
      DatabasePassword: !Ref DatabasePassword
      EnableSsl: !Ref EnableSsl
      DropInventories: !Ref DropInventories
      AutoDelete: !Ref AutoDelete
      AutoDeleteDays: !Ref AutoDeleteDays

Outputs:
  ClusterId:
    Description: ID of the created cluster
    Value: !GetAtt DBCluster.ClusterId

  ApiResponse:
    Description: Full API response from cluster creation
    Value: !GetAtt DBCluster.Response

  LambdaFunctionArn:
    Description: ARN of the Lambda function handling provisioning
    Value: !GetAtt ProvisionClusterFunction.Arn

Run shell script (optional)

Copy shell script as is (for simple run).

File run.sh:

#!/bin/bash
aws cloudformation create-stack \
  --capabilities CAPABILITY_IAM \
  --stack-name my-stack-name \
  --template-body file://template.yaml \
  --parameters file://params.json

Step 4: Deploy the Stack

Use AWS CLI to deploy the stack

# Validate the template

aws cloudformation validate-template \

  --template-body file://template.yaml

# Create the stack

aws cloudformation create-stack \

  --capabilities CAPABILITY_IAM \

  --stack-name my-stack-name \

  --template-body file://template.yaml \

  --parameters file://params.json

# Monitor stack creation

aws cloudformation wait stack-create-complete \

  --stack-name mongodb-replicaset-master

# Retrieve outputs

aws cloudformation describe-stacks \

  --stack-name mongodb-replicaset-master \

  --query 'Stacks[0].Outputs'

Additional Guidance & Best Practices

Parameterization

All user-specific and environment-specific values are exposed as CloudFormation parameters, making the template portable and reusable across multiple clusters, regions, and AWS accounts.

Security

  • Sensitive Parameters: Use NoEcho: true for passwords and tokens to prevent them from appearing in CloudFormation console or API responses.
  • Secrets Management: Consider integrating with AWS Secrets Manager for production deployments:
  • JwtToken:
  •   Type: String
  •   Default: '{{resolve:secretsmanager:123cluster/jwt:SecretString:token}}'
  •   NoEcho: true
  • IAM Permissions: The Lambda execution role follows least-privilege principles with only necessary permissions.

API Versioning

Always verify endpoint and payload requirements with the latest 123cluster API documentation to ensure compatibility. Update the Lambda function code as the API evolves.

Cost Optimization

  • Lambda functions are billed per invocation and execution time; the provisioning function typically completes within seconds.
  • Consider setting appropriate timeouts and memory allocation for the Lambda function.
  • Use CloudWatch Logs retention policies to manage log storage costs.

No items found.