Promoting Content

This section explains how to use the Connect Server APIs to download code from content in one location and deploy to another. This workflow can help take application changes from a staging environment and deploy them into production.

The Connect Server API Reference contains documentation for each of the endpoints used in these recipes.

The Promoting Content recipe uses bash snippets and relies on curl to perform HTTP requests. These recipes use the CONNECT_SERVER and CONNECT_API_KEY environment variables introduced in the Getting Started section of this cookbook.

Scenario

Here is a scenario that can take advantage of this Promoting Content recipe. Your organization may have similar separation of permissions and environments.

Your data scientists write applications and reports. They rapidly iterate, experiment, and share updates. Content updates are deployed to a Posit Connect staging environment. This environment lets the team share in-progress work without altering your business critical production content.

The Connect production environment hosts content that is visible to your customers and stakeholders. Deployment into production is done by a deployment engineer - not the data scientist.

The data science team develops updates to an application, which are then peer reviewed, tested, and approved for production use. The application developers do not have permissions to update content in the production environment. They hand-off to the deployment engineer, who has production privileges. The deployment engineer downloads the exact bundle archive from the staging environment and deploys into production.

Before starting

You need the following pieces of information about your staging and production environments before attempting to download a bundle archive:

Staging environment

  • STAGING_SERVER - The base URL for your Connect staging environment, such as https://connect-staging.company.com/.
  • STAGING_CONTENT_GUID - The source content GUID within your staging environment.
  • STAGING_API_KEY - A Connect API Key within your staging environment. The user associated with this API Key must be a collaborator for your source staging content.

Production environment

  • PROD_SERVER - The base URL for your Posit Connect production environment, such as https://connect.company.com/.
  • PROD_CONTENT_GUID - The target content GUID within your production environment. This workflow assumes the target content already exists. Use the Creating Content recipe to create a new content item.
  • PROD_API_KEY - A Posit Connect API Key within your production environment. The user associated with this API Key must be a collaborator (or owner) of your target production content.

Additional information

  • The STAGING_SERVER and PROD_SERVER values appear elsewhere as CONNECT_SERVER.
  • The STAGING_API_KEY and PROD_API_KEY are CONNECT_API_KEY elsewhere.
  • The STAGING_CONTENT_GUID and PROD_CONTENT_GUID values are CONTENT_GUID in other recipes.
Important

Do not accidentally mix-and-match your staging/production configuration. API Keys for staging will not be recognized in your production environment.

Workflow

The content promotion workflow includes three steps:

  1. Download the archive file for a source bundle from the staging environment.
  2. Upload the archive file into the production environment.
  3. Deploy the new production bundle.

Bundle download (staging)

The GET /v1/content/{guid} endpoint returns information about a single content item and indicates the active bundle with its bundle_id field.

curl --silent --show-error -L --max-redirs 0 --fail \
    -H "Authorization: Key ${STAGING_API_KEY}" \
    "${STAGING_SERVER}__api__/v1/content/${STAGING_CONTENT_GUID}"
# => {
# =>   "guid": "b99b9b77-a8ae-4ecd-93aa-3c23baf9cefe",
# =>   "title": "staging content",
# =>   ...
# =>   "bundle_id": "584",
# =>   ...
# => }

Extract the bundle ID from the JSON response and assign it to a STAGING_BUNDLE_ID environment variable:

export STAGING_BUNDLE_ID="584"

Use this bundle ID to download its archive file using the GET /v1/content/{guid}/bundles/{id}/download bundle download endpoint.

curl --silent --show-error -L --max-redirs 0 --fail -J -O \
    -H "Authorization: Key ${STAGING_API_KEY}" \
    "${STAGING_SERVER}__api__/v1/content/${STAGING_CONTENT_GUID}/bundles/${STAGING_BUNDLE_ID}/download"

Connect suggests the filename bundle-${STAGING_BUNDLE_ID}.tar.gz in the Content-Disposition HTTP response header. The -J -O options tell curl to save the downloaded archive file using that filename.

Let’s define an environment variable named STAGING_BUNDLE_FILE containing name of the downloaded archive file.

# bundle-584.tar.gz
export STAGING_BUNDLE_FILE="bundle-${STAGING_BUNDLE_ID}.tar.gz"

Bundle upload (production)

Given the staging bundle archive identified by the environment variable STAGING_BUNDLE_FILE, you can follow the Uploading Bundles recipe.

This is the one command where you are mixing “staging” and “production” variables. The archive file you obtained from the staging environment is being uploaded to production using the POST /v1/content/{guid}/bundles upload content bundle endpoint.

curl --silent --show-error -L --max-redirs 0 --fail -X POST \
    -H "Authorization: Key ${PROD_API_KEY}" \
    --data-binary @"${STAGING_BUNDLE_FILE}" \
    "${PROD_SERVER}__api__/v1/content/${PROD_CONTENT_GUID}/bundles"
# => {"bundle_id":"242","bundle_size":162991}

Extract the bundle ID from the upload response and assign it to a PROD_BUNDLE_ID environment variable:

export PROD_BUNDLE_ID="242"

Bundle deploy (production)

Given the target production bundle identified by the environment variable PROD_BUNDLE_ID, you can follow the Deploying a Bundle recipe. This uses the POST /v1/content/{guid}/deploy deploy content bundle endpoint.

# Build the JSON input naming the bundle to deploy.
export DATA='{"bundle_id":"'"${PROD_BUNDLE_ID}"'"}'
# Trigger a deployment.
curl --silent --show-error -L --max-redirs 0 --fail -X POST \
    -H "Authorization: Key ${PROD_API_KEY}" \
    --data "${DATA}" \
    "${PROD_SERVER}__api__/v1/content/${PROD_CONTENT_GUID}/deploy"
# => {"task_id":"t0yiLB6bd6RKlesX"}

You can monitor the progress of this deployment by polling against the GET /v1/tasks/{id} get task endpoint, as explained in the Task Polling recipe. Remember to poll against the PROD_SERVER URL, not your staging environment.

Blue-green deployments

Once a piece of content is live in production, updates should be done in a manner that does not cause downtime. You can use a blue-green deployment strategy to ensure that the production version of your content is always available.

Workflow

  1. Following the Deploying Content recipe to deploy your content. Let’s refer to this as the green deployment.

  2. Assign a vanity URL to the content. Users can access the content through this vanity URL.

  3. Deploy the updated version of your content as a new, separate deployment. Let’s refer to this as the blue deployment.

  4. Test the blue deployment via its content_url, rather than the vanity URL (which still routes users to the green deployment). If the blue deployment doesn’t work correctly, you can fix it while users are still accessing the green deployment.

  5. After successful testing, reassign the vanity URL to point to the blue deployment. Users are now accessing the updated content when they visit the vanity URL.

If something goes wrong with the blue deployment after you’ve re-assigned the vanity URL, you can simply re-assign the URL to the green deployment again while you troubleshoot the blue deployment.

Assigning a vanity URL

After deploying your content initially, assign a vanity URL to your content.

VANITY_NAME='sales-forecast'

DATA='{
  "path": "'${VANITY_NAME}'"
}'

curl --silent --show-error -L --max-redirs 0 --fail -X PUT \
    -H "Authorization: Key ${CONNECT_API_KEY}" \
    --data-binary "${DATA}" \
    "${CONNECT_SERVER}__api__/v1/content/${CONTENT_GUID}/vanity"
# => {
# =>   "content_guid":"b99b9b77-a8ae-4ecd-93aa-3c23baf9cefe",
# =>   "path":"sales-forecast",
# =>   "created_at": "2020-10-01T12:34:56-0400"
# => }

Switching deployments

When you are ready to switch from one deployment to the other, assign the vanity URL to the other deployment. The force parameter indicates that the existing vanity URL should be reassigned.

CONTENT_GUID='...'  # blue or green deployment GUID
VANITY_NAME='sales-forecast'

DATA='{
  "path": "'${VANITY_NAME}'"
  "force": "true"
}'

curl --silent --show-error -L --max-redirs 0 --fail -X PUT \
    -H "Authorization: Key ${CONNECT_API_KEY}" \
    --data-binary "${DATA}" \
    "${CONNECT_SERVER}__api__/v1/content/${CONTENT_GUID}/vanity"
# => {
# =>   "content_guid":"ccbd1a41-90a0-4b7b-89c7-16dd9ad47eb5",
# =>   "path":"sales-forecast",
# =>   "created_at": "2020-10-01T13:31:42-0400"
# => }