Skip to content

Applications

Application Modules provide infrastructure and configuration required to launch and maintain Applications through Nullstone.

These modules enable a suite of features in the Nullstone UI, CLI, and Engine beyond a normal Terraform module. This document provides reference to build your own application module that enables each of these features.

Managed Artifacts

In Nullstone, your application code is deployed as Docker containers, archive files (zip), or directories. Nullstone provides a simple mechanism to deploy new artifacts without the need for special tooling based on the cloud provider or platform (e.g. Lambda, Fargate, Kubernetes).

The Application module is responsible for provisioning the docker registry or storage bucket where the artifacts are stored. Additionally, the application module needs to output the artifact store and authorized user to push an artifact. Refer to the sections below for usage, outputs, and supported applications fore each of the supported packaging formats. (Docker Image, Zip File, Directory)

Docker Image

CLI Usage:

shell
nullstone push --source=<docker-image> --app=my-app --env=dev
nullstone push --source=<docker-image> --app=my-app --env=dev

Contract Outputs:

  • image_repo_url - A Docker repository URL where the images are pushed
  • image_pusher - A user with permission to push docker images to image_repo_url

Supported App Contracts:

Zip File

CLI Usage:

shell
nullstone push --source=<archive>.zip --app=my-app --env=dev
nullstone push --source=<archive>.zip --app=my-app --env=dev

Contract Outputs:

  • artifacts_bucket_name - A bucket name where artifacts are stored.
  • artifacts_key_template - A template string used to name versioned artifacts in the storage bucket.
  • deployer - A user with explicit access to upload artifacts to the storage bucket.

Supported App Contracts:

Directory

CLI Usage:

shell
nullstone push --source=./<directory> --app=my-app --env=dev
nullstone push --source=./<directory> --app=my-app --env=dev

Contract Outputs:

  • bucket_name - A bucket name where artifacts are stored.
  • deployer - A user with explicit access to upload artifacts to the storage bucket.

Supported App Contracts:

Automated Deploys and Rollbacks

In Nullstone, your application code is deployed either as Docker containers, archive files (zip), or directories. Nullstone provides a simple mechanism to deploy new versions of code without the need for special tooling based on the cloud provider or platform (e.g. Lambda, Fargate, Kubernetes). This enables Nullstone to:

  • Provide an auditable list of app versions
  • Track health of app version deployments
  • Enable rolling back to previous versions of the app

Application Version

In Nullstone, you can deploy a new version of your application through the CLI/UI or Terraform. To ensure the deployed application version is synchronized, the Nullstone Terraform provider offers a data source named ns_app_env. The ns_app_env data source exposes a version attribute that refers to the currently-deployed version of the application.

If you use nullstone modules generate, the following app.tf file is generated for you.

hcl
data "ns_app_env" "this" {
  stack_id = data.ns_workspace.this.stack_id
  app_id   = data.ns_workspace.this.block_id
  env_id   = data.ns_workspace.this.env_id
}

locals {
  app_version = data.ns_app_env.this.version
}
data "ns_app_env" "this" {
  stack_id = data.ns_workspace.this.stack_id
  app_id   = data.ns_workspace.this.block_id
  env_id   = data.ns_workspace.this.env_id
}

locals {
  app_version = data.ns_app_env.this.version
}

Environment Variables

Nullstone allows developers to configure applications by injecting environment variables. Nullstone injects these into your application module through the service_env_vars variable. If you are creating an application module, make sure to add it:

hcl
variable "env_vars" {
  type        = map(string)
  default     = {}
  description = <<EOF
The environment variables to inject into the service.
These are typically used to configure a service per environment.
It is dangerous to put sensitive information in this variable because they are not protected and could be unintentionally exposed.
EOF
}
variable "env_vars" {
  type        = map(string)
  default     = {}
  description = <<EOF
The environment variables to inject into the service.
These are typically used to configure a service per environment.
It is dangerous to put sensitive information in this variable because they are not protected and could be unintentionally exposed.
EOF
}

These user-supplied environment variables should be merged with environment variables from other sources. Other sections in this reference use this base to add additional environment variables from capabilities.

hcl
locals {
  standard_env_vars = tomap({
    NULLSTONE_ENV = data.ns_workspace.this.env_name
  })

  env_vars = [for k, v in merge(local.standard_env_vars, var.env_vars) : { name = k, value = v }]
}
locals {
  standard_env_vars = tomap({
    NULLSTONE_ENV = data.ns_workspace.this.env_name
  })

  env_vars = [for k, v in merge(local.standard_env_vars, var.env_vars) : { name = k, value = v }]
}

Public/Private URLs

Nullstone provides developers with visibility into which URLs you can access an application. In this screenshot, api-gateway is accessible inside the network via http://api-gateway.service:80 and externally via https://api.dev.nullstone.io:443.

Public/Private URLs

In the official Nullstone modules, these URLs are aggregated from:

If you are creating or configuring an application module, make sure to output URLs through public_urls and private_urls outputs. If you use nullstone modules generate, the outputs.tf file contains code to output URLs from capabilities. It is best to add additional URLs to the locals additional_private_urls and additional_public_urls.

SSH Access

Nullstone provides a CLI command ssh that enables securely connecting to apps over SSH. This functionality provides cheap, valuable connectivity for teams that do not have VPN or Zero-Trust Access configured.

CLI Usage:

shell
nullstone ssh --app=my-app --env=dev
nullstone ssh --app=my-app --env=dev

Contract Outputs:

  • region - Region in cloud provider where app is deployed to query app information.
  • cluster_arn - Cluster identifier where app is hosted to query app information.
  • service_name - Name of app service to query app information.
  • main_container_name - Specifies the primary container through which the CLI makes an SSH connection.
  • deployer - A user with explicit access to establish an SSH connection.

Supported App Contracts:

High-level Application Status

Nullstone provides a CLI command status that enables viewing application status. This functionality provides cheap, valuable diagnostics without logging into the AWS console.

CLI Usage:

shell
nullstone status --watch --app=my-app --env=dev
nullstone status --watch --app=my-app --env=dev

Contract Outputs:

  • region - Region in cloud provider where app is deployed to query application status.
  • deployer - A user with explicit access to read service and task status of application.

Supported App Contracts:

Application Log Viewing

Nullstone provides a CLI command logs that enables viewing application logs. This functionality provides cheap, valuable diagnostics using the cloud provider's built-in logging functionality without downloading additional tooling.

CLI Usage:

shell
nullstone logs --tail --start '5m' --app=my-app --env=dev
nullstone logs --tail --start '5m' --app=my-app --env=dev

Contract Outputs:

  • log_provider - The type of log provider.
  • log_group_name - The name of the log group in the log provider that holds logs.
  • log_reader - A user with explicit access to read logs from the log provider.

Support Log Providers:

  • cloudwatch

Supported App Contracts:

Containers

Container applications make use of a docker registry for artifacts.

When deploying a new version of the container, the image is tagged with the version name. Then, the app is redeployed with the new image tag.

If you want to create your own container app module, make sure to:

  • Configure the app in your Terraform module to use the app version.
  • Configure the "main" container name. (The container that is versioned by the app)
  • Output the main_container_name.

Here is example taken from nullstone-modules/aws-fargate-service:

hcl
locals {
  main_container_name = "main"
  
  container_definition = {
    name  = local.main_container_name
    image = "${local.service_image}:${local.app_version}"
  }
}
locals {
  main_container_name = "main"
  
  container_definition = {
    name  = local.main_container_name
    image = "${local.service_image}:${local.app_version}"
  }
}
hcl
output "main_container_name" {
  value       = local.container_definition.name
  description = "string ||| The name of the container definition for the main service container"
}
output "main_container_name" {
  value       = local.container_definition.name
  description = "string ||| The name of the container definition for the main service container"
}

Static Site

Typically, static sites are deployed to a content delivery network (CDN). The content is served close to users around the globe from an object bucket (e.g. S3 in AWS).

When deploying a new version of a static site in Nullstone, the content is placed into an object bucket with the version name as the directory. Then, the CDN is configured to serve assets out of that version's directory.

Nullstone comes with a CDN module (e.g. AWS - nullstone-modules/aws-s3-cdn) that attaches to a static site. However, if you want to create your own CDN module, make sure the CDN is configured to serve out of the correct directory based on the app version. Here is example taken from the Nullstone AWS S3 CDN capability:

hcl
resource "aws_cloudfront_distribution" "this" {
  origin {
    origin_path = trimsuffix("/${local.app_version}", "/")
    // more
  }
  // more
}
resource "aws_cloudfront_distribution" "this" {
  origin {
    origin_path = trimsuffix("/${local.app_version}", "/")
    // more
  }
  // more
}

Capabilities

In the Nullstone UI, you can dynamically add functionality to applications using special Nullstone modules called Capabilities. The Nullstone engine dynamically adds these modules to the application module when applying the application infrastructure changes.

TIP

When creating an application module that supports capabilities, use the nullstone modules generate CLI command. This command creates a capabilities.tf.tmpl file that the Nullstone engine uses to dynamically create Terraform for attached capability modules.

The application module communicates information about the application (e.g. security group, role, etc.) through Terraform variables. The capability module creates cloud resources that grants network access, creates credentials, and more. Finally, the capability module emits outputs (e.g. configuration, secrets, etc.) that the application module aggregates into the final application definition. See Extending > Capabilities to learn more about capabilities.

Automated env variables

In Environment Variables section above, the application was configured to merge built-in environment variables with user-supplied. Additionally, application modules receive env output from capabilities as environment variables. Here is an example snippet from nullstone-modules/aws-fargate-service that merges in env vars from capability outputs.

hcl
container_definition = {
  // ... other configuration
  environment = concat(local.env_vars, try(local.capabilities.env, []))
}
container_definition = {
  // ... other configuration
  environment = concat(local.env_vars, try(local.capabilities.env, []))
}

Format:

hcl
output "env" {
  value = [
    {
      name  = "ENV_VAR_NAME"
      value = "env-value" 
    }
  ]
}
output "env" {
  value = [
    {
      name  = "ENV_VAR_NAME"
      value = "env-value" 
    }
  ]
}

Supported App Contracts:

Automated secrets

Similar to Automated Env Variables, application modules receive secrets output from capabilities as sensitive configuration. The application module is responsible for securing these secrets in a secrets vault and injecting into the application.

Refer to nullstone-modules/aws-fargate-service/secrets.tf for an example of how to consume secrets from capabilities and add to AWS Secrets Manager. Note that the secret ARN is injected into the service. Here is a small snippet from the same module granting the app access to these secrets.

hcl

locals {
  // These are used to generate an IAM policy statement to allow the app to read the secrets
  secret_arns                = [for as in aws_secretsmanager_secret.app_secret : as.arn]
  secret_statement_resources = length(local.secret_arns) > 0 ? [local.secret_arns] : []
}

data "aws_iam_policy_document" "execution" {
  dynamic "statement" {
    for_each = local.secret_statement_resources

    content {
      sid       = "AllowReadSecrets"
      effect    = "Allow"
      resources = statement.value

      actions = [
        "secretsmanager:GetSecretValue",
        "kms:Decrypt"
      ]
    }
  }
}

locals {
  // These are used to generate an IAM policy statement to allow the app to read the secrets
  secret_arns                = [for as in aws_secretsmanager_secret.app_secret : as.arn]
  secret_statement_resources = length(local.secret_arns) > 0 ? [local.secret_arns] : []
}

data "aws_iam_policy_document" "execution" {
  dynamic "statement" {
    for_each = local.secret_statement_resources

    content {
      sid       = "AllowReadSecrets"
      effect    = "Allow"
      resources = statement.value

      actions = [
        "secretsmanager:GetSecretValue",
        "kms:Decrypt"
      ]
    }
  }
}

Supported App Contracts:

Automated datastore access

When creating an application module to support datastore capabilities, make sure to configure the following:

  • Handle env outputs from capability modules. (see Automated env variables)
  • Handle secrets outputs from capability modules. (see Automated secrets)
  • Inject a network identity for the capability to grant network access.
  • Inject an IAM role for the capability to grant any provider-specific privileges.

The following snippet taken from nullstone-modules/aws-fargate-service demonstrates injecting the network identity (security_group_id) and the IAM role (role_name) into capabilities. local.app_metadata is passed into each capability module as a variable in capabilities.tf.tmpl.

hcl
locals {
  app_metadata = tomap({
    security_group_id = aws_security_group.this.id
    role_name         = aws_iam_role.task.name
  })
}
locals {
  app_metadata = tomap({
    security_group_id = aws_security_group.this.id
    role_name         = aws_iam_role.task.name
  })
}

Automated public access

By default, applications are deployed into private subnets which are inaccessible to consumers outside the network. To expose an application to the public, you attach a capability to the application that creates a public entry device (e.g. Load Balancer, CDN, API Gateway). Usually, you attach a subdomain in the Nullstone UI and Nullstone automatically selects a capability module that matches the application module.

Adding public access to an application module is specific to the application platform and the type of public entry device. Depending on your app platform and public entry target, make sure to configure the following:

  • Handle public URLs. (see Public/Private URLs).
  • [Optional] Inject network identity or app metadata.
  • [Optional] Register app with public entry targets.

Refer to the official Nullstone modules for examples on how to connect.

Third-party Logs/Metrics

Frequently, software teams use third-party logs and metrics providers. By attaching a telemetry capability to your app, you can change the where your logs and metrics go.

When creating application modules to support these log providers, it's important to read your specific log provider instructions. Most log capabilities stream logs from a cloudwatch log group created by the application to the third party. To support these capabilities, make sure to inject the log group name through app_metadata capability variable. Here is an example app.tf from nullstone-modules/aws-fargate-service.

hcl
locals {
  app_metadata = tomap({
    // Inject app metadata into capabilities here (e.g. security_group_name, role_name)
    security_group_id  = aws_security_group.this.id
    role_name          = aws_iam_role.task.name
    main_container     = local.main_container_name
    service_port       = var.port
    log_group_name     = module.logs.name
    internal_subdomain = var.port == 0 ? "" : "${local.block_name}.${local.service_domain}"
  })
}
locals {
  app_metadata = tomap({
    // Inject app metadata into capabilities here (e.g. security_group_name, role_name)
    security_group_id  = aws_security_group.this.id
    role_name          = aws_iam_role.task.name
    main_container     = local.main_container_name
    service_port       = var.port
    log_group_name     = module.logs.name
    internal_subdomain = var.port == 0 ? "" : "${local.block_name}.${local.service_domain}"
  })
}

Sidecars

A sidecar is another container that runs alongside your primary application container. Sidecars are used to add application monitoring, service mesh attachment, request proxying, and more. This is possible because sidecars operate on the same network interface as the primary application container; the sidecar can communicate with the app container over local networking.

Here are two examples of sidecars:

Adding support for sidecars in your application module relies heavily on your platform (e.g. Kubernetes, Fargate, etc.). To build support in your application module, make sure to:

  • Receive sidecars output from capabilities and merge into container/pod definitions.
  • Configure each attached sidecar with your log configuration.
  • Redirect any public entry appliances to any proxy sidecars. (Usually done by reading sidecars[].owns_service_port = true from capabilities)