X

Best Practices from Oracle Development's A‑Team

Using Terraform to manage an Oracle Functions application

Introduction

Managing a complete functions application is potentially quite complex, with many moving parts.

This terraform sketch is meant to demonstrate and provide a sketch for managing the complexities of the deployment of a complete functions application to Oracle Cloud Infrastructure.

The attached file is a complete functional sketch designed to be modified and adapted to fit the needs of a Functions application through the use of some simple configuration data, and extension through standard terraform extension mechanisms.

We're going to deploy this sketch, and then review some of the possible configuration options for use in your own functions applications. This sketch will be seen in future blog posts from me on functions as well.

Terraform

Terraform versions from around 0.13 through 1.0 are verified to work with this sketch, as such, simply obtain terraform from the usual download page at terraform.io and follow it's installation instructions.

Do note that this sketch is tested to work on Linux and Unix type systems. I cannot vouch for it's compatibility with a Windows system.

How to use the complete sketch

The sketch is a complete working example, that will deploy two copies of the python 'hello world' function as a single application to OCI. Here is how to set it up.

If you have previously followed the client setup directions, then you should have a working functions context. Verify by running 'fn i context' on your command line. It should output something similar to the following:


Current context: [a context name]

api-url: https://functions.[an OCI region].oci.oraclecloud.com
oracle.compartment-id: [a compartment OCID]
oracle.profile: [the name of your .oci/config profile]
provider: oracle
registry: [an OCI short region code].ocir.io/[path to your object storage registry]

Note that the url and registry might have different host region names, depending on your configured OCI region.

A configured OCI CLI is also a pre-requisite for this sketch, as such you should be able to run the OCI CLI against your target OCI tenancy successfully. (The Fn setup guide above goes through this).

Where is this sketch going to put the functions application? It'll be using the oracle.compartment-id specified from the fn context above, unless you specify an alternative compartment through the terraform variable 'compartment'.

Once you have extracted the sketch to an empty directory, you should be able to run 'terraform -chdir=tf init' to initialize the local terraform metadata. You should see output similar to this:


    Initializing modules...
    - functions in modules/fn
    
    Initializing the backend...
    
    Initializing provider plugins...
    - Finding hashicorp/oci versions matching "~> 4.5.0"...
    - Finding hashicorp/null versions matching "~> 3.0.0"...
    - Finding latest version of hashicorp/external...
    - Installing hashicorp/oci v4.5.0...
    - Installed hashicorp/oci v4.5.0 (signed by HashiCorp)
    - Installing hashicorp/null v3.0.0...
    - Installed hashicorp/null v3.0.0 (signed by HashiCorp)
    - Installing hashicorp/external v2.1.0...
    - Installed hashicorp/external v2.1.0 (signed by HashiCorp)
    
    Terraform has created a lock file .terraform.lock.hcl to record the provider
    selections it made above. Include this file in your version control repository
    so that Terraform can guarantee to make the same selections by default when
    you run "terraform init" in the future.
    
    Terraform has been successfully initialized!
    
    You may now begin working with Terraform. Try running "terraform plan" to see
    any changes that are required for your infrastructure. All Terraform commands
    should now work.
    
    If you ever set or change modules or backend configuration for Terraform,
    rerun this command to reinitialize your working directory. If you forget, other
    commands will detect it and remind you to do so if necessary.

Note that the versions may vary slightly, as things change fairly dynamically in terraform providers.

Next, we are going to run 'terraform -chdir=tf plan' to see a plan for deployment of the example sketch to OCI. Again, the output will look something like the following:


    Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
    + create
   <= read (data resources)
  
  Terraform will perform the following actions:
  
    # data.oci_apigateway_gateway.api_gateway will be read during apply
    # (config refers to values not yet known)
   <= data "oci_apigateway_gateway" "api_gateway"  {
        + certificate_id    = (known after apply)
        + compartment_id    = (known after apply)
        + defined_tags      = (known after apply)
        + display_name      = (known after apply)
        + endpoint_type     = (known after apply)
        + freeform_tags     = (known after apply)
        + gateway_id        = (known after apply)
        + hostname          = (known after apply)
        + id                = (known after apply)
        + ip_addresses      = (known after apply)
        + lifecycle_details = (known after apply)
        + state             = (known after apply)
        + subnet_id         = (known after apply)
        + time_created      = (known after apply)
        + time_updated      = (known after apply)
      }
  
    # oci_apigateway_deployment.api_gateway_deployment will be created
    + resource "oci_apigateway_deployment" "api_gateway_deployment" {
        + compartment_id    = ""
        + defined_tags      = (known after apply)
        + display_name      = (known after apply)
        + endpoint          = (known after apply)
        + freeform_tags     = (known after apply)
        + gateway_id        = (known after apply)
        + id                = (known after apply)
        + lifecycle_details = (known after apply)
        + path_prefix       = "/demotffn"
        + state             = (known after apply)
        + time_created      = (known after apply)
        + time_updated      = (known after apply)
  
        + specification {
            + logging_policies {
                + access_log {
                    + is_enabled = true
                  }
  
                + execution_log {
                    + is_enabled = true
                    + log_level  = (known after apply)
                  }
              }
  
            + request_policies {
                + authentication {
                    + audiences                   = (known after apply)
                    + function_id                 = (known after apply)
                    + is_anonymous_access_allowed = (known after apply)
                    + issuers                     = (known after apply)
                    + max_clock_skew_in_seconds   = (known after apply)
                    + token_auth_scheme           = (known after apply)
                    + token_header                = (known after apply)
                    + token_query_param           = (known after apply)
                    + type                        = (known after apply)
  
                    + public_keys {
                        + is_ssl_verify_disabled      = (known after apply)
                        + max_cache_duration_in_hours = (known after apply)
                        + type                        = (known after apply)
                        + uri                         = (known after apply)
  
                        + keys {
                            + alg     = (known after apply)
                            + e       = (known after apply)
                            + format  = (known after apply)
                            + key     = (known after apply)
                            + key_ops = (known after apply)
                            + kid     = (known after apply)
                            + kty     = (known after apply)
                            + n       = (known after apply)
                            + use     = (known after apply)
                          }
                      }
  
                    + verify_claims {
                        + is_required = (known after apply)
                        + key         = (known after apply)
                        + values      = (known after apply)
                      }
                  }
  
                + cors {
                    + allowed_headers              = (known after apply)
                    + allowed_methods              = (known after apply)
                    + allowed_origins              = (known after apply)
                    + exposed_headers              = (known after apply)
                    + is_allow_credentials_enabled = (known after apply)
                    + max_age_in_seconds           = (known after apply)
                  }
  
                + rate_limiting {
                    + rate_in_requests_per_second = (known after apply)
                    + rate_key                    = (known after apply)
                  }
              }
  
            + routes {
                + methods = [
                    + "GET",
                  ]
                + path    = "/hello"
  
                + backend {
                    + body                       = (known after apply)
                    + connect_timeout_in_seconds = (known after apply)
                    + function_id                = (known after apply)
                    + is_ssl_verify_disabled     = (known after apply)
                    + read_timeout_in_seconds    = (known after apply)
                    + send_timeout_in_seconds    = (known after apply)
                    + status                     = (known after apply)
                    + type                       = "ORACLE_FUNCTIONS_BACKEND"
                    + url                        = (known after apply)
  
                    + headers {
                        + name  = (known after apply)
                        + value = (known after apply)
                      }
                  }
  
                + logging_policies {
                    + access_log {
                        + is_enabled = (known after apply)
                      }
  
                    + execution_log {
                        + is_enabled = (known after apply)
                        + log_level  = (known after apply)
                      }
                  }
  
                + request_policies {
                    + authorization {
                        + allowed_scope = (known after apply)
                        + type          = (known after apply)
                      }
  
                    + cors {
                        + allowed_headers              = (known after apply)
                        + allowed_methods              = (known after apply)
                        + allowed_origins              = (known after apply)
                        + exposed_headers              = (known after apply)
                        + is_allow_credentials_enabled = (known after apply)
                        + max_age_in_seconds           = (known after apply)
                      }
  
                    + header_transformations {
                        + filter_headers {
                            + type = (known after apply)
  
                            + items {
                                + name = (known after apply)
                              }
                          }
  
                        + rename_headers {
                            + items {
                                + from = (known after apply)
                                + to   = (known after apply)
                              }
                          }
  
                        + set_headers {
                            + items {
                                + if_exists = (known after apply)
                                + name      = (known after apply)
                                + values    = (known after apply)
                              }
                          }
                      }
  
                    + query_parameter_transformations {
                        + filter_query_parameters {
                            + type = (known after apply)
  
                            + items {
                                + name = (known after apply)
                              }
                          }
  
                        + rename_query_parameters {
                            + items {
                                + from = (known after apply)
                                + to   = (known after apply)
                              }
                          }
  
                        + set_query_parameters {
                            + items {
                                + if_exists = (known after apply)
                                + name      = (known after apply)
                                + values    = (known after apply)
                              }
                          }
                      }
                  }
  
                + response_policies {
                    + header_transformations {
                        + filter_headers {
                            + type = (known after apply)
  
                            + items {
                                + name = (known after apply)
                              }
                          }
  
                        + rename_headers {
                            + items {
                                + from = (known after apply)
                                + to   = (known after apply)
                              }
                          }
  
                        + set_headers {
                            + items {
                                + if_exists = (known after apply)
                                + name      = (known after apply)
                                + values    = (known after apply)
                              }
                          }
                      }
                  }
              }
            + routes {
                + methods = [
                    + "GET",
                  ]
                + path    = "/hello2"
  
                + backend {
                    + body                       = (known after apply)
                    + connect_timeout_in_seconds = (known after apply)
                    + function_id                = (known after apply)
                    + is_ssl_verify_disabled     = (known after apply)
                    + read_timeout_in_seconds    = (known after apply)
                    + send_timeout_in_seconds    = (known after apply)
                    + status                     = (known after apply)
                    + type                       = "ORACLE_FUNCTIONS_BACKEND"
                    + url                        = (known after apply)
  
                    + headers {
                        + name  = (known after apply)
                        + value = (known after apply)
                      }
                  }
  
                + logging_policies {
                    + access_log {
                        + is_enabled = (known after apply)
                      }
  
                    + execution_log {
                        + is_enabled = (known after apply)
                        + log_level  = (known after apply)
                      }
                  }
  
                + request_policies {
                    + authorization {
                        + allowed_scope = (known after apply)
                        + type          = (known after apply)
                      }
  
                    + cors {
                        + allowed_headers              = (known after apply)
                        + allowed_methods              = (known after apply)
                        + allowed_origins              = (known after apply)
                        + exposed_headers              = (known after apply)
                        + is_allow_credentials_enabled = (known after apply)
                        + max_age_in_seconds           = (known after apply)
                      }
  
                    + header_transformations {
                        + filter_headers {
                            + type = (known after apply)
  
                            + items {
                                + name = (known after apply)
                              }
                          }
  
                        + rename_headers {
                            + items {
                                + from = (known after apply)
                                + to   = (known after apply)
                              }
                          }
  
                        + set_headers {
                            + items {
                                + if_exists = (known after apply)
                                + name      = (known after apply)
                                + values    = (known after apply)
                              }
                          }
                      }
  
                    + query_parameter_transformations {
                        + filter_query_parameters {
                            + type = (known after apply)
  
                            + items {
                                + name = (known after apply)
                              }
                          }
  
                        + rename_query_parameters {
                            + items {
                                + from = (known after apply)
                                + to   = (known after apply)
                              }
                          }
  
                        + set_query_parameters {
                            + items {
                                + if_exists = (known after apply)
                                + name      = (known after apply)
                                + values    = (known after apply)
                              }
                          }
                      }
                  }
  
                + response_policies {
                    + header_transformations {
                        + filter_headers {
                            + type = (known after apply)
  
                            + items {
                                + name = (known after apply)
                              }
                          }
  
                        + rename_headers {
                            + items {
                                + from = (known after apply)
                                + to   = (known after apply)
                              }
                          }
  
                        + set_headers {
                            + items {
                                + if_exists = (known after apply)
                                + name      = (known after apply)
                                + values    = (known after apply)
                              }
                          }
                      }
                  }
              }
          }
      }
  
    # oci_apigateway_gateway.managed_api_gateway[0] will be created
    + resource "oci_apigateway_gateway" "managed_api_gateway" {
        + certificate_id    = (known after apply)
        + compartment_id    = ""
        + defined_tags      = (known after apply)
        + display_name      = (known after apply)
        + endpoint_type     = "PUBLIC"
        + freeform_tags     = (known after apply)
        + hostname          = (known after apply)
        + id                = (known after apply)
        + ip_addresses      = (known after apply)
        + lifecycle_details = (known after apply)
        + state             = (known after apply)
        + subnet_id         = (known after apply)
        + time_created      = (known after apply)
        + time_updated      = (known after apply)
      }
  
    # oci_core_default_route_table.route_table[0] will be created
    + resource "oci_core_default_route_table" "route_table" {
        + defined_tags               = (known after apply)
        + display_name               = (known after apply)
        + freeform_tags              = (known after apply)
        + id                         = (known after apply)
        + manage_default_resource_id = (known after apply)
        + state                      = (known after apply)
        + time_created               = (known after apply)
  
        + route_rules {
            + cidr_block        = (known after apply)
            + description       = (known after apply)
            + destination       = "0.0.0.0/0"
            + destination_type  = (known after apply)
            + network_entity_id = (known after apply)
          }
      }
  
    # oci_core_default_security_list.security_list[0] will be created
    + resource "oci_core_default_security_list" "security_list" {
        + defined_tags               = (known after apply)
        + display_name               = (known after apply)
        + freeform_tags              = (known after apply)
        + id                         = (known after apply)
        + manage_default_resource_id = (known after apply)
        + state                      = (known after apply)
        + time_created               = (known after apply)
  
        + egress_security_rules {
            + description      = (known after apply)
            + destination      = "0.0.0.0/0"
            + destination_type = (known after apply)
            + protocol         = "all"
            + stateless        = false
          }
  
        + ingress_security_rules {
            + description = (known after apply)
            + protocol    = "1"
            + source      = "10.10.10.0/24"
            + source_type = (known after apply)
            + stateless   = false
  
            + icmp_options {
                + code = -1
                + type = 3
              }
          }
        + ingress_security_rules {
            + description = (known after apply)
            + protocol    = "6"
            + source      = "0.0.0.0/0"
            + source_type = (known after apply)
            + stateless   = false
  
            + tcp_options {
                + max = 443
                + min = 443
              }
          }
      }
  
    # oci_core_internet_gateway.internet_gateway[0] will be created
    + resource "oci_core_internet_gateway" "internet_gateway" {
        + compartment_id = ""
        + defined_tags   = (known after apply)
        + display_name   = (known after apply)
        + enabled        = true
        + freeform_tags  = (known after apply)
        + id             = (known after apply)
        + state          = (known after apply)
        + time_created   = (known after apply)
        + vcn_id         = (known after apply)
      }
  
    # oci_core_subnet.subnet[0] will be created
    + resource "oci_core_subnet" "subnet" {
        + availability_domain        = (known after apply)
        + cidr_block                 = "10.10.10.0/24"
        + compartment_id             = ""
        + defined_tags               = (known after apply)
        + dhcp_options_id            = (known after apply)
        + display_name               = (known after apply)
        + dns_label                  = (known after apply)
        + freeform_tags              = (known after apply)
        + id                         = (known after apply)
        + ipv6cidr_block             = (known after apply)
        + ipv6public_cidr_block      = (known after apply)
        + ipv6virtual_router_ip      = (known after apply)
        + prohibit_public_ip_on_vnic = (known after apply)
        + route_table_id             = (known after apply)
        + security_list_ids          = (known after apply)
        + state                      = (known after apply)
        + subnet_domain_name         = (known after apply)
        + time_created               = (known after apply)
        + vcn_id                     = (known after apply)
        + virtual_router_ip          = (known after apply)
        + virtual_router_mac         = (known after apply)
      }
  
    # oci_core_vcn.virtual_network[0] will be created
    + resource "oci_core_vcn" "virtual_network" {
        + cidr_block               = "10.10.10.0/24"
        + cidr_blocks              = (known after apply)
        + compartment_id           = ""
        + default_dhcp_options_id  = (known after apply)
        + default_route_table_id   = (known after apply)
        + default_security_list_id = (known after apply)
        + defined_tags             = (known after apply)
        + display_name             = (known after apply)
        + dns_label                = (known after apply)
        + freeform_tags            = (known after apply)
        + id                       = (known after apply)
        + ipv6cidr_block           = (known after apply)
        + ipv6public_cidr_block    = (known after apply)
        + is_ipv6enabled           = (known after apply)
        + state                    = (known after apply)
        + time_created             = (known after apply)
        + vcn_domain_name          = (known after apply)
      }
  
    # oci_functions_application.fn_application will be created
    + resource "oci_functions_application" "fn_application" {
        + compartment_id = ""
        + config         = (known after apply)
        + defined_tags   = (known after apply)
        + display_name   = "demoterraformfunctionapp"
        + freeform_tags  = (known after apply)
        + id             = (known after apply)
        + state          = (known after apply)
        + subnet_ids     = (known after apply)
        + syslog_url     = (known after apply)
        + time_created   = (known after apply)
        + time_updated   = (known after apply)
      }
  
    # oci_identity_policy.api_gateway_fnpolicy will be created
    + resource "oci_identity_policy" "api_gateway_fnpolicy" {
        + ETag           = (known after apply)
        + compartment_id = ""
        + defined_tags   = (known after apply)
        + description    = "APIGW policy for compartment to access FN"
        + freeform_tags  = (known after apply)
        + id             = (known after apply)
        + inactive_state = (known after apply)
        + lastUpdateETag = (known after apply)
        + name           = "apigateway_fn_policies"
        + policyHash     = (known after apply)
        + state          = (known after apply)
        + statements     = [
            + "ALLOW any-user to use functions-family in compartment id  where ALL {request.principal.type= 'ApiGateway', request.resource.compartment.id = ''}",
          ]
        + time_created   = (known after apply)
        + version_date   = (known after apply)
      }
  
    # module.functions["helloworld"].null_resource.deploy_function will be created
    + resource "null_resource" "deploy_function" {
        + id       = (known after apply)
        + triggers = {
            + "fnversion" = "0.0.2"
          }
      }
  
    # module.functions["helloworld"].oci_functions_function.function will be created
    + resource "oci_functions_function" "function" {
        + application_id     = (known after apply)
        + compartment_id     = (known after apply)
        + config             = (known after apply)
        + defined_tags       = (known after apply)
        + display_name       = "hello"
        + freeform_tags      = {
            + "Branch" = "main"
            + "Commit" = "e8fee12e8d09face7e6d94d5b3ba556bd948e2fc"
          }
        + id                 = (known after apply)
        + image              = "iad.ocir.io//hello:0.0.2"
        + image_digest       = (known after apply)
        + invoke_endpoint    = (known after apply)
        + memory_in_mbs      = "256"
        + state              = (known after apply)
        + time_created       = (known after apply)
        + time_updated       = (known after apply)
        + timeout_in_seconds = 30
      }
  
    # module.functions["helloworld2"].null_resource.deploy_function will be created
    + resource "null_resource" "deploy_function" {
        + id       = (known after apply)
        + triggers = {
            + "fnversion" = "0.0.1"
          }
      }
  
    # module.functions["helloworld2"].oci_functions_function.function will be created
    + resource "oci_functions_function" "function" {
        + application_id     = (known after apply)
        + compartment_id     = (known after apply)
        + config             = (known after apply)
        + defined_tags       = (known after apply)
        + display_name       = "hello2"
        + freeform_tags      = {
            + "Branch" = "main"
            + "Commit" = "e8fee12e8d09face7e6d94d5b3ba556bd948e2fc"
          }
        + id                 = (known after apply)
        + image              = "iad.ocir.io//hello2:0.0.1"
        + image_digest       = (known after apply)
        + invoke_endpoint    = (known after apply)
        + memory_in_mbs      = "256"
        + state              = (known after apply)
        + time_created       = (known after apply)
        + time_updated       = (known after apply)
        + timeout_in_seconds = 30
      }
  
  Plan: 13 to add, 0 to change, 0 to destroy.
  
  Changes to Outputs:
    + api_gateway        = {
        + hostname = (known after apply)
        + ips      = (known after apply)
      }
    + function_endpoints = {
        + helloworld  = (known after apply)
        + helloworld2 = (known after apply)
      }
  
  ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  
  Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.  

As you can see, we get a lot of output here. It's basically summarizing that it needs to create a whole lot of resources in your compartment (13 in total). This is terraform's plan of what it intends to do to create the entire application in your OCI compartment.

If this plan looks acceptable, you can run 'terraform -chdir=tf apply' to get this sketch into OCI. (Terraform will prompt if you really want to do this, answer yes to proceed). At the end, you should see an output telling you where to find your API gateway endpoint so you can call the two demo functions.

Now that terraform is managing this functions application, it will continue to keep it in sync, by detecting local changes and redeploying them to OCI, using a minimal change approach and low-to-zero downtime. To update the code for a function, we can use the fn tool to run 'fn bump' in a function's directory. If you do that in the 'helloworld' directory, then rerun 'terraform -chdir=tf plan', you should see that terraform has identified the code change (caused by the fn bump) and is planning a solution to get your function change into the cloud. Again, applying that change will actually do the work.

Applying the sketch to your own functions applications

This sketch is designed to be relatively easy to use for any reasonably organized functions application. The 'fn' directory contains all of the operational terraform code as well as the terraform configuration data for finding your functions and the overall structure of your function application. Let's walk through the primary components and what they do, and how you can configure them.

  • 'terraform.tfvars': this is the primary configuration data for the terraform. Within, you will find the configuration for the example sketch, deploying the two copies of helloworld. There are two primary variables defined in here: 'functions_app' and 'functions'.
    • 'functions_app': This configures the overall application. You can name the application, define the subnet CIDR, an optional existing API gateway OCID, a syslog URL target, and a config template.
    • 'functions': This is a list of functions. Each element consists of three members: 'fnpath' is the path on disk to the actual function, 'path' is an optional URL element within the API gateway for accessing the function via gateway, and methods is a list of HTTP methods to configure at the API gateway for this function.
  • 'config_template': This is a terraform template file used to populate the functions application context data from terraform data. As such, we provide a very simple example that loads the data from a JSON template file. Much more complex examples are possible, if additional resources are provisioned in the terraform, such as buckets and the like.
  • 'compartment': This is a terraform variable, defaulting to the compartment id from the functions context, that can be used to target alternative compartments.
  • fn_context': This is a terraform variable, defaulting to the "current context" in the fn config.yaml file, for the fn context to use to read OCI information.
  • 'fn_config_dir': This is a terraform variable, defaulting to ~/.fn/, to read fn configuration from.

Possible extension ideas

This sketch is intended to be very easy to extend with new capabilities. For example, Angelo, in the blog post on serverless data loading, is using an early draft of this sketch to manage a functions application. Here we see terraform also provisioning the buckets being used in the example, and injecting their location into the functions application using a context template.

We can also provide an existing API gateway, rather than having to deploy a new gateway for each application, for better integration into a larger production environment.

Using an automated build system

This sketch is obviously a strong candidate for deployment as part of a Continuous integration solution, such as Oracle Developer Cloud Service. A forthcoming blog post will look at this problem space in more detail.

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.Captcha