Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ECI-201 Add terraform templates to serverless #817

Merged
merged 1 commit into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions oci/oci-orm-metrics/metrics-setup/compartment.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
resource "oci_identity_compartment" "datadog-compartment" {
provider = oci.home
# Required
compartment_id = var.compartment_ocid
description = "Compartment for Terraform resources."
name = var.datadog_compartment
freeform_tags = local.freeform_tags
defined_tags = {}
}
22 changes: 22 additions & 0 deletions oci/oci-orm-metrics/metrics-setup/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Source from https://registry.terraform.io/providers/oracle/oci/latest/docs/data-sources/identity_region_subscriptions

data "oci_identity_region_subscriptions" "subscriptions" {
# Required
provider = oci.home
tenancy_id = var.tenancy_ocid
}

data "oci_objectstorage_namespace" "namespace" {
provider = oci.home
compartment_id = var.tenancy_ocid
}

data "oci_identity_tenancy" "tenancy_metadata" {
tenancy_id = var.tenancy_ocid
}

data "oci_core_subnet" "input_subnet" {
depends_on = [module.vcn]
#Required
subnet_id = var.create_vcn ? module.vcn[0].subnet_id[local.subnet] : var.function_subnet_id
}
54 changes: 54 additions & 0 deletions oci/oci-orm-metrics/metrics-setup/function_setup.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
resource "null_resource" "Login2OCIR" {
count = local.user_image_provided ? 0 : 1
provisioner "local-exec" {
command = "echo '${var.oci_docker_password}' | docker login ${local.oci_docker_repository} --username ${local.ocir_namespace}/${var.oci_docker_username} --password-stdin"
}
}

### Repository in the Container Image Registry for the container images underpinning the function
# resource "oci_artifacts_container_repository" "function_repo" {
# # note: repository = store for all images versions of a specific container image - so it included the function name
# depends_on = [null_resource.Login2OCIR]
# count = local.user_image_provided ? 0 : 1
# compartment_id = oci_identity_compartment.datadog-compartment.id
# display_name = local.ocir_repo_name
# is_public = false
# defined_tags = {}
# freeform_tags = local.freeform_tags
# }

# ### build the function into a container image and push that image to the repository in the OCI Container Image Registry
resource "null_resource" "FnImagePushToOCIR" {
count = local.user_image_provided ? 0 : 1
depends_on = [oci_functions_application.metrics_function_app, null_resource.Login2OCIR]

# remove function image (if it exists) from local container registry
provisioner "local-exec" {
command = "image=$(docker images | grep ${local.function_name} | awk -F ' ' '{print $3}') ; docker rmi -f $image &> /dev/null ; echo $image"
working_dir = "metrics-function"
}

# pull the function image (if it exists) from the container registry
provisioner "local-exec" {
command = "fn build --verbose"
working_dir = "metrics-function"
}

# tag the container image with the proper name - based on the actual name of the function
provisioner "local-exec" {
command = "image=$(docker images | grep ${local.function_name} | awk -F ' ' '{print $3}') ; docker tag $image ${local.docker_image_path}"
working_dir = "metrics-function"
}
# create a container image based on fake-fun (hello world), tagged for the designated function name
provisioner "local-exec" {
command = "docker push ${local.docker_image_path}"
working_dir = "metrics-function"
}

# remove function image (if it exists) from local container registry
provisioner "local-exec" {
command = "image=$(docker images | grep ${local.function_name} | awk -F ' ' '{print $3}') ; docker rmi -f $image &> /dev/null ; echo $image"
working_dir = "metrics-function"
}

}
34 changes: 34 additions & 0 deletions oci/oci-orm-metrics/metrics-setup/functions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
resource "oci_functions_application" "metrics_function_app" {
depends_on = [data.oci_core_subnet.input_subnet]
compartment_id = oci_identity_compartment.datadog-compartment.id
config = {
"DD_API_KEY" = var.datadog_api_key
"DD_COMPRESS" = "true"
"DD_INTAKE_HOST" = var.datadog_environment
"DD_INTAKE_LOGS" = "false"
"DD_MAX_POOL" = "20"
"TENANCY_OCID" = var.tenancy_ocid
}
defined_tags = {}
display_name = "${var.datadog_compartment}-function-app"
freeform_tags = local.freeform_tags
network_security_group_ids = [
]
shape = "GENERIC_ARM"
subnet_ids = [
data.oci_core_subnet.input_subnet.id,
]
}

resource "oci_functions_function" "metrics_function" {
depends_on = [null_resource.FnImagePushToOCIR, oci_functions_application.metrics_function_app]
#Required
application_id = oci_functions_application.metrics_function_app.id
display_name = "${oci_functions_application.metrics_function_app.display_name}-metrics-function"
memory_in_mbs = "256"

#Optional
defined_tags = {}
freeform_tags = local.freeform_tags
image = local.user_image_provided ? local.custom_image_path : local.docker_image_path
}
42 changes: 42 additions & 0 deletions oci/oci-orm-metrics/metrics-setup/locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
locals {
# Names for the network infra
vcn_name = "${var.datadog_compartment}-vcn"
nat_gateway = "${local.vcn_name}-natgateway"
service_gateway = "${local.vcn_name}-servicegateway"
subnet = "${local.vcn_name}-private-subnet"
}

locals {
# Names for the service connector
connector_name = "${var.datadog_compartment}-connector"
}

locals {
# Tags for the provisioned resource
freeform_tags = {
datadog-terraform = "true"
}
}

locals {
# Name for tenancy namespace, metadata and regions
ocir_namespace = data.oci_objectstorage_namespace.namespace.namespace
oci_regions = tomap({
for reg in data.oci_identity_region_subscriptions.subscriptions.region_subscriptions :
reg.region_name => reg
})
oci_region_key = lower(local.oci_regions[var.region].region_key)
tenancy_home_region = data.oci_identity_tenancy.tenancy_metadata.home_region_key
}

locals {
# OCI docker repository
oci_docker_repository = "${local.oci_region_key}.ocir.io/${local.ocir_namespace}"
oci_docker_host = "${local.oci_region_key}.ocir.io"
ocir_repo_name = "datadog-functions"
function_name = "datadog-function-metrics"
docker_image_path = "${local.oci_docker_repository}/${local.ocir_repo_name}/${local.function_name}:latest"
# custom_image_path = "${local.oci_region_key}.ocir.io/${var.function_image_path}"
custom_image_path = var.function_image_path
user_image_provided = length(var.function_image_path) > 0 ? true : false
}
18 changes: 18 additions & 0 deletions oci/oci-orm-metrics/metrics-setup/metrics-function/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM fnproject/python:3.11-dev AS build-stage
WORKDIR /function
ADD requirements.txt /function/

RUN pip3 install --target /python/ --no-cache --no-cache-dir -r requirements.txt && \
rm -fr ~/.cache/pip /tmp* requirements.txt func.yaml Dockerfile .venv && \
chmod -R o+r /python && \
groupadd --gid 1000 fn && \
adduser --uid 1000 --gid fn fn
ADD . /function/
RUN rm -fr /function/.pip_cache
FROM fnproject/python:3.11
WORKDIR /function
COPY --from=build-stage /python /python
COPY --from=build-stage /function /function
RUN chmod -R o+r /function
ENV PYTHONPATH=/function:/python
ENTRYPOINT ["/python/bin/fdk", "/function/func.py", "handler"]
150 changes: 150 additions & 0 deletions oci/oci-orm-metrics/metrics-setup/metrics-function/func.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import io
import json
import logging
import os
import gzip

from fdk import context, response
import requests
from requests.adapters import HTTPAdapter
from requests.exceptions import HTTPError


logger = logging.getLogger(__name__)


OUTPUT_MESSAGE_VERSION = "v1.0"

_max_pool = int(os.environ.get("DD_MAX_POOL", 10))
_session = requests.Session()
_session.mount("https://", HTTPAdapter(pool_connections=_max_pool))


def _get_serialized_metric_data(raw_metrics: io.BytesIO) -> str:
return raw_metrics.getvalue().decode("utf-8")


def _generate_metrics_msg(
ctx: context.InvokeContext,
serialized_metric_data: str,
) -> str:
tenancy_ocid = os.environ.get("TENANCY_OCID")

if not tenancy_ocid:
raise ValueError("Missing environment variable: TENANCY_OCID")

# Bump OUTPUT_MESSAGE_VERSION any time this
# structure gets updated
message_dict = {
"version": OUTPUT_MESSAGE_VERSION,
"payload": {
"headers": {
"tenancy_ocid": tenancy_ocid,
"source_fn_app_ocid": ctx.AppID(),
"source_fn_app_name": ctx.AppName(),
"source_fn_ocid": ctx.FnID(),
"source_fn_name": ctx.FnName(),
"source_fn_call_id": ctx.CallID(),
},
"body": serialized_metric_data,
},
}

return json.dumps(message_dict)


def _should_compress_payload() -> bool:
return os.environ.get("DD_COMPRESS", "false").lower() == "true"


def _send_metrics_msg_to_datadog(metrics_message: str) -> str:
endpoint = os.environ.get("DD_INTAKE_HOST")
api_key = os.environ.get("DD_API_KEY")

if not endpoint or not api_key:
raise ValueError(
"Missing one of the following environment variables: DD_INTAKE_HOST, DD_API_KEY"
)

url = f"https://{endpoint}/api/v2/ocimetrics"
api_headers = {"content-type": "application/json", "dd-api-key": api_key}

if _should_compress_payload():
serialized = gzip.compress(metrics_message.encode())
api_headers["content-encoding"] = "gzip"
else:
serialized = metrics_message

http_response = _session.post(url, data=serialized, headers=api_headers)
http_response.raise_for_status()

logger.info(
f"Sent payload size={len(metrics_message)} encoding={api_headers.get('content-encoding', None)}"
)
return http_response.text


def handler(ctx: context.InvokeContext, data: io.BytesIO = None) -> response.Response:
"""
Submits incoming metrics data to Datadog.

Wraps incoming metrics data in a message payload and forwards this
payload to a Datadog endpoint.

Args:
ctx:
An fdk InvokeContext.
data:
A BytesIO stream containing a JSON representation of metrics.
Each metric has the form:

{
"namespace": "<Example Namespace>",
"resourceGroup": "<Example Resource Group>",
"compartmentId": "<Example Compartment ID>",
"name": "<Example Metric Name>",
"dimensions": {
"<Example Dimension Key>": "<Example Dimension Value>",
},
"metadata": {
"<Example Metadata Key>": "<Example Metadata Value>",
},
"datapoints": [
{
"timestamp": "<Example Timestamp in ms since Unix Epoch>",
"value": "<Example Value>",
"count": "<Example count>",
},
]
}

Returns:
An fdk Response in which the body contains any error
messages encountered during processing. At present, HTTP 200
responses will always be returned.
"""

try:
serialized_metric_data = _get_serialized_metric_data(
data,
)

metrics_message = _generate_metrics_msg(
ctx,
serialized_metric_data,
)

result = _send_metrics_msg_to_datadog(metrics_message)
except HTTPError as e:
logger.exception(f"Error sending metrics to Datadog")
result = e.response.text
except Exception as e:
logger.exception("Unexpected error while processing input data")
result = str(e)

return response.Response(
ctx,
response_data=json.dumps({"result": result}),
headers={"Content-Type": "application/json"},
)

8 changes: 8 additions & 0 deletions oci/oci-orm-metrics/metrics-setup/metrics-function/func.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
schema_version: 20180708
name: datadog-function-metrics
version: 0.0.1
runtime: python
build_image: fnproject/python:3.11-dev
run_image: fnproject/python:3.11
entrypoint: /python/bin/fdk /function/func.py handler
memory: 256
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
fdk
requests
Loading
Loading