Skip to content

Commit 37cec41

Browse files
authored
Create Model Deploy with Private Networking (#624)
1 parent f91c57d commit 37cec41

File tree

1 file changed

+352
-0
lines changed

1 file changed

+352
-0
lines changed
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
import argparse
2+
import json
3+
import logging
4+
import sys
5+
import os
6+
import time
7+
import oci
8+
import oci.data_science as data_science
9+
from oci.data_science.models import (
10+
CreateDataSciencePrivateEndpointDetails,
11+
CreateModelDeploymentDetails,
12+
ModelConfigurationDetails,
13+
InstanceConfiguration,
14+
FixedSizeScalingPolicy,
15+
CategoryLogDetails,
16+
LogDetails,
17+
SingleModelDeploymentConfigurationDetails
18+
)
19+
from oci.signer import Signer
20+
21+
logging.basicConfig(stream=sys.stdout, level=logging.INFO,
22+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
23+
logger = logging.getLogger("Model Deployment with Private Endpoint")
24+
25+
class ModelDeploymentError(Exception):
26+
"""Custom exception for model deployment errors."""
27+
pass
28+
29+
class ModelDeployment:
30+
def __init__(self, oci_config_file: str = "~/.oci/config", config_json_path: str = "model_deployment_config.json"):
31+
self.model_deployment_id = None
32+
self.private_endpoint_id = None
33+
self.config_file = oci_config_file
34+
self.config_json_path = config_json_path
35+
self.active = False
36+
37+
# Load configuration and initialize clients
38+
self._load_config()
39+
self._validate_config()
40+
self._initialize_clients()
41+
42+
def _load_config(self):
43+
"""Load configuration from JSON file."""
44+
try:
45+
if not os.path.exists(self.config_json_path):
46+
raise ModelDeploymentError(f"Configuration file {self.config_json_path} not found")
47+
48+
with open(self.config_json_path, 'r') as fp:
49+
data = json.load(fp)
50+
51+
# Model Deployment - Required parameters
52+
self.project_id = data.get('project_id')
53+
self.compartment_id = data.get('compartment_id')
54+
self.deployment_name = data.get('deployment_name')
55+
self.model_id = data.get('model_id')
56+
self.instance_shape_name = data.get('instance_shape_name')
57+
self.subnet_id = data.get('subnet_id')
58+
59+
# Model Deployment - Optional parameters with defaults
60+
self.instance_count = int(data.get('instance_count', 1))
61+
self.bandwidth_mbps = int(data.get('bandwidth_mbps', 10))
62+
63+
# Private Endpoint Configuration
64+
self.pe_compartment_id = data.get('private_endpoint_compartment_id', self.compartment_id)
65+
self.pe_display_name = data.get('private_endpoint_display_name', f"{self.deployment_name}_private_endpoint")
66+
self.pe_description = data.get('private_endpoint_description', 'Data Science Private Endpoint')
67+
self.pe_subnet_id = data.get('private_endpoint_subnet_id', self.subnet_id)
68+
self.pe_nsg_ids = data.get('private_endpoint_nsg_ids', [])
69+
70+
# Use existing private endpoint if specified
71+
self.existing_private_endpoint_id = data.get('existing_private_endpoint_id')
72+
73+
# Logging configuration
74+
self.access_log_id = data.get('access_log_id')
75+
self.predict_log_id = data.get('predict_log_id')
76+
self.log_group_id = data.get('log_group_id')
77+
78+
logger.info(f"Configuration loaded for deployment: {self.deployment_name}")
79+
80+
except json.JSONDecodeError as e:
81+
logger.error(f"Invalid JSON in configuration file: {e}")
82+
raise ModelDeploymentError(f"Invalid JSON in configuration file: {e}")
83+
except Exception as e:
84+
logger.error(f"Failed to load configuration: {e}")
85+
raise ModelDeploymentError(f"Failed to load configuration: {e}")
86+
87+
def _validate_config(self):
88+
"""Validate required configuration parameters."""
89+
if not (self.project_id and self.compartment_id and self.model_id):
90+
logger.error("Model deployment failed. Missing required parameters: project_id, compartment_id, or model_id")
91+
raise ModelDeploymentError("Missing required parameters: project_id, compartment_id, or model_id")
92+
93+
if not self.subnet_id and not self.pe_subnet_id:
94+
logger.error("Either subnet_id or private_endpoint_subnet_id is required")
95+
raise ModelDeploymentError("Either subnet_id or private_endpoint_subnet_id is required")
96+
97+
logger.info("Configuration validation completed successfully")
98+
99+
def _initialize_clients(self):
100+
"""Initialize OCI clients and authentication."""
101+
try:
102+
config_path = os.path.expanduser(self.config_file)
103+
if not os.path.exists(config_path):
104+
raise ModelDeploymentError(f"OCI config file not found: {config_path}")
105+
106+
# Initialize OCI configuration and clients
107+
self.oci_config = oci.config.from_file(config_path, "DEFAULT")
108+
self.data_science_client = data_science.DataScienceClient(config=self.oci_config)
109+
self.data_science_composite_client = data_science.DataScienceClientCompositeOperations(
110+
self.data_science_client
111+
)
112+
113+
# Initialize signer for authentication
114+
self.auth = Signer(
115+
tenancy=self.oci_config['tenancy'],
116+
user=self.oci_config['user'],
117+
fingerprint=self.oci_config['fingerprint'],
118+
private_key_file_location=self.oci_config['key_file'],
119+
pass_phrase=self.oci_config.get('pass_phrase')
120+
)
121+
122+
logger.info("OCI clients initialized successfully")
123+
124+
except Exception as e:
125+
logger.error(f"Failed to initialize OCI clients: {e}")
126+
raise ModelDeploymentError(f"Failed to initialize OCI clients: {e}")
127+
128+
def create_private_endpoint(self):
129+
"""Create a Data Science private endpoint."""
130+
if self.existing_private_endpoint_id:
131+
logger.info(f"Using existing private endpoint: {self.existing_private_endpoint_id}")
132+
self.private_endpoint_id = self.existing_private_endpoint_id
133+
return self.private_endpoint_id
134+
135+
logger.info("Starting to create Data Science private endpoint")
136+
137+
try:
138+
# Create private endpoint configuration
139+
create_pe_details = CreateDataSciencePrivateEndpointDetails(
140+
compartment_id=self.pe_compartment_id,
141+
display_name=self.pe_display_name,
142+
description=self.pe_description,
143+
subnet_id=self.pe_subnet_id,
144+
nsg_ids=self.pe_nsg_ids if self.pe_nsg_ids else None
145+
)
146+
147+
# Create private endpoint and wait for completion
148+
start_time = time.time()
149+
create_pe_response = self.data_science_composite_client.create_data_science_private_endpoint_and_wait_for_state(
150+
create_data_science_private_endpoint_details=create_pe_details,
151+
wait_for_states=["ACTIVE", "FAILED"]
152+
)
153+
154+
elapsed_time = time.time() - start_time
155+
156+
if create_pe_response.data.lifecycle_state == "ACTIVE":
157+
self.private_endpoint_id = create_pe_response.data.id
158+
logger.info(f"Private endpoint created successfully in {elapsed_time:.2f} seconds")
159+
logger.info(f"Private Endpoint ID: {self.private_endpoint_id}")
160+
return self.private_endpoint_id
161+
else:
162+
logger.error(f"Private endpoint creation failed with state: {create_pe_response.data.lifecycle_state}")
163+
raise ModelDeploymentError(f"Private endpoint creation failed with state: {create_pe_response.data.lifecycle_state}")
164+
165+
except Exception as e:
166+
logger.error(f"Failed to create private endpoint: {e}")
167+
raise ModelDeploymentError(f"Private endpoint creation failed: {e}")
168+
169+
def create_model_deployment(self):
170+
"""Create a model deployment with private endpoint."""
171+
if not self.private_endpoint_id:
172+
raise ModelDeploymentError("Private endpoint ID not available")
173+
174+
logger.info(f"Starting to create model deployment with private endpoint: {self.private_endpoint_id}")
175+
176+
try:
177+
# Configure instance with private endpoint
178+
self.instance_configuration = InstanceConfiguration()
179+
self.instance_configuration.instance_shape_name = self.instance_shape_name
180+
self.instance_configuration.subnet_id = self.subnet_id
181+
self.instance_configuration.private_endpoint_id = self.private_endpoint_id
182+
183+
# Configure scaling policy
184+
self.scaling_policy = FixedSizeScalingPolicy()
185+
self.scaling_policy.instance_count = self.instance_count
186+
187+
# Create model configuration
188+
model_config_details = ModelConfigurationDetails(
189+
model_id=self.model_id,
190+
bandwidth_mbps=self.bandwidth_mbps,
191+
instance_configuration=self.instance_configuration,
192+
scaling_policy=self.scaling_policy
193+
)
194+
195+
# Create single model deployment configuration
196+
single_model_deployment_config_details = SingleModelDeploymentConfigurationDetails(
197+
deployment_type="SINGLE_MODEL",
198+
model_configuration_details=model_config_details
199+
)
200+
201+
# Create model deployment details
202+
create_model_deployment_details = CreateModelDeploymentDetails(
203+
display_name=self.deployment_name,
204+
model_deployment_configuration_details=single_model_deployment_config_details,
205+
compartment_id=self.compartment_id,
206+
project_id=self.project_id
207+
)
208+
209+
# Add logging configuration if provided
210+
if self.log_group_id and (self.predict_log_id or self.access_log_id):
211+
create_model_deployment_details.category_log_details = self.create_logging(
212+
self.log_group_id, self.access_log_id, self.predict_log_id
213+
)
214+
215+
# Create model deployment and wait for completion
216+
start_time = time.time()
217+
create_model_deployment_response = self.data_science_composite_client.create_model_deployment_and_wait_for_state(
218+
create_model_deployment_details=create_model_deployment_details,
219+
wait_for_states=["SUCCEEDED", "FAILED"]
220+
)
221+
222+
elapsed_time = time.time() - start_time
223+
work_request_resources = create_model_deployment_response.data.resources
224+
self.model_deployment_id = work_request_resources[0].identifier
225+
226+
if create_model_deployment_response.data.status == "SUCCEEDED":
227+
logger.info(f"Model deployment created successfully in {elapsed_time:.2f} seconds")
228+
logger.info(f"Model Deployment ID: {self.model_deployment_id}")
229+
self.active = True
230+
else:
231+
logger.error("Failed to create model deployment")
232+
raise ModelDeploymentError("Model deployment creation failed")
233+
234+
except Exception as e:
235+
logger.error(f"Failed to create model deployment: {e}")
236+
raise ModelDeploymentError(f"Model deployment creation failed: {e}")
237+
238+
def write_model_deployment_info(self):
239+
"""Write model deployment and private endpoint information to configuration file."""
240+
try:
241+
if not self.model_deployment_id:
242+
raise ModelDeploymentError("No model deployment ID available")
243+
244+
model_deployment = self.data_science_client.get_model_deployment(
245+
model_deployment_id=self.model_deployment_id
246+
)
247+
248+
# Get private endpoint details if available
249+
private_endpoint_details = None
250+
if self.private_endpoint_id:
251+
try:
252+
pe_response = self.data_science_client.get_data_science_private_endpoint(
253+
data_science_private_endpoint_id=self.private_endpoint_id
254+
)
255+
private_endpoint_details = {
256+
"id": pe_response.data.id,
257+
"display_name": pe_response.data.display_name,
258+
"state": pe_response.data.lifecycle_state,
259+
"subnet_id": pe_response.data.subnet_id,
260+
"fqdn": getattr(pe_response.data, 'fqdn', None)
261+
}
262+
except Exception as e:
263+
logger.warning(f"Could not retrieve private endpoint details: {e}")
264+
265+
# Create backup of original config
266+
if os.path.exists(self.config_json_path):
267+
backup_path = f"{self.config_json_path}.backup"
268+
with open(self.config_json_path, 'r') as src, open(backup_path, 'w') as dst:
269+
dst.write(src.read())
270+
logger.info(f"Configuration backup created: {backup_path}")
271+
272+
# Update configuration file
273+
with open(self.config_json_path, 'r+') as f:
274+
data = json.load(f)
275+
data['model_deployment_url'] = getattr(model_deployment.data, 'model_deployment_url', None)
276+
data['model_deployment_id'] = model_deployment.data.id
277+
data['created_private_endpoint_id'] = self.private_endpoint_id
278+
279+
if private_endpoint_details:
280+
data['private_endpoint_details'] = private_endpoint_details
281+
282+
f.seek(0)
283+
json.dump(data, f, indent=4)
284+
f.truncate()
285+
286+
logger.info(f"Deployment info written to {self.config_json_path}")
287+
logger.info(f"Model Deployment ID: {model_deployment.data.id}")
288+
logger.info(f"Private Endpoint ID: {self.private_endpoint_id}")
289+
290+
if hasattr(model_deployment.data, 'model_deployment_url'):
291+
logger.info(f"Model Deployment URL: {model_deployment.data.model_deployment_url}")
292+
293+
except Exception as e:
294+
logger.error(f"Failed to write deployment info: {e}")
295+
raise ModelDeploymentError(f"Failed to write deployment info: {e}")
296+
297+
def create_logging(self, log_group_id: str, access_log_id: str, predict_log_id: str) -> CategoryLogDetails:
298+
"""Create logging configuration for model deployment."""
299+
try:
300+
category_log_details = CategoryLogDetails()
301+
302+
if access_log_id:
303+
access_log_details = LogDetails()
304+
access_log_details.log_id = access_log_id
305+
access_log_details.log_group_id = log_group_id
306+
category_log_details.access = access_log_details
307+
308+
if predict_log_id:
309+
predict_log_details = LogDetails()
310+
predict_log_details.log_id = predict_log_id
311+
predict_log_details.log_group_id = log_group_id
312+
category_log_details.predict = predict_log_details
313+
314+
return category_log_details
315+
316+
except Exception as e:
317+
logger.error(f"Failed to create logging configuration: {e}")
318+
raise ModelDeploymentError(f"Failed to create logging configuration: {e}")
319+
320+
def main():
321+
parser = argparse.ArgumentParser(description="Deploy A Model with Private Endpoint")
322+
parser.add_argument("--oci_config_file", nargs="?",
323+
help="Config file containing OCIDs",
324+
default="~/.oci/config")
325+
parser.add_argument("--config_json", nargs="?",
326+
help="JSON configuration file path",
327+
default="model_deployment_config.json")
328+
parser.add_argument("--pe_only", action="store_true",
329+
help="Create only private endpoint")
330+
parser.add_argument("--md_only", action="store_true",
331+
help="Create only model deployment (requires existing private endpoint)")
332+
333+
args = parser.parse_args()
334+
335+
try:
336+
# Initialize model deployment
337+
md = ModelDeployment(args.oci_config_file, args.config_json)
338+
# Create both private endpoint and model deployment
339+
md.create_private_endpoint()
340+
md.create_model_deployment()
341+
md.write_model_deployment_info()
342+
logger.info("Private endpoint and model deployment completed successfully!")
343+
344+
except ModelDeploymentError as e:
345+
logger.error(f"Deployment error: {e}")
346+
sys.exit(1)
347+
except Exception as e:
348+
logger.error(f"Unexpected error: {e}")
349+
sys.exit(1)
350+
351+
if __name__ == "__main__":
352+
main()

0 commit comments

Comments
 (0)