> ## Documentation Index
> Fetch the complete documentation index at: https://developer.upsun.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Scheduling resources

export const PostMeta = ({data = {}}) => {
  const {author, date, image} = data;
  const authors = Array.isArray(author) ? author : author ? [author] : [];
  const resolveAuthor = slug => {
    const entry = AUTHOR_MAP[slug] || ({});
    const name = entry.name || slug;
    const github = entry.github || null;
    const linkedin = entry.linkedin || null;
    const url = github ? `https://github.com/${github}` : linkedin || null;
    const avatarUrl = github ? `https://github.com/${github}.png?size=64` : null;
    return {
      name,
      url,
      avatarUrl
    };
  };
  const formattedDate = date ? new Date(date).toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }) : null;
  if (!image && authors.length === 0 && !formattedDate) return null;
  const AUTHOR_MAP = {
    "aaron-collier": {
      "name": "Aaron Collier"
    },
    "aaron-dudenhofer": {
      "name": "Aaron Dudenhofer"
    },
    "aaron-porter": {
      "name": "Aaron Porter"
    },
    "adriaan-odendaal": {
      "name": "Adriaan Odendaal"
    },
    "ajmal": {
      "name": "Ajmal Siddiqui"
    },
    "akalipetis": {
      "name": "Antonis Kalipetis"
    },
    "alexander-varwijk": {
      "name": "Alexander Varwijk"
    },
    "alicia-bevilacqua": {
      "name": "Alicia Bevilacqua"
    },
    "amelie-deguerry": {
      "name": "Amelie Deguerry"
    },
    "anacidre": {
      "name": "Ana Cidre",
      "linkedin": "https://www.linkedin.com/in/ana-cidre"
    },
    "andoni": {
      "name": "Andoni Auzmendi"
    },
    "andrei-taranu": {
      "name": "Andrei (Alex) Taranu",
      "linkedin": "https://www.linkedin.com/in/andrei-alex-taranu/"
    },
    "andrew-baxter": {
      "name": "Andrew Baxter"
    },
    "andrew-melck": {
      "name": "Andrew Melck"
    },
    "antoine-crochet-damais": {
      "name": "Antoine Crochet Damais"
    },
    "augustin-delaporte": {
      "name": "Augustin Delaporte",
      "linkedin": "https://www.linkedin.com/in/augustindelaporte/"
    },
    "branislav-bujisic": {
      "name": "Branislav Bujisic"
    },
    "carl-smith": {
      "name": "Carl Smith"
    },
    "caroline-leroy": {
      "name": "Caroline Leroy"
    },
    "cati-mayer": {
      "name": "Cati Mayer"
    },
    "catplat": {
      "name": "C Trinkwon"
    },
    "ceelolulu": {
      "name": "Celeste van der Watt"
    },
    "chadwcarlson": {
      "name": "Chad Carlson",
      "github": "chadwcarlson",
      "linkedin": "https://www.linkedin.com/in/chadwcarlson"
    },
    "chris-ward": {
      "name": "Chris Ward"
    },
    "chris-yates": {
      "name": "Chris Yates"
    },
    "christian-sieber": {
      "name": "Christian Sieber"
    },
    "christopher-lockheardt": {
      "name": "Christopher Lockheardt"
    },
    "christopher-skene": {
      "name": "Christopher Skene"
    },
    "chuck-morgan": {
      "name": "Chuck Morgan"
    },
    "corey-dockendorf": {
      "name": "Corey Dockendorf"
    },
    "crell": {
      "name": "Crell"
    },
    "damz": {
      "name": "Damz"
    },
    "dan-morrison": {
      "name": "Dan Morrison"
    },
    "davidbonachera": {
      "name": "David Bonachera",
      "github": "davidbonachera",
      "linkedin": "https://www.linkedin.com/in/davidbonachera"
    },
    "dereliahmet1": {
      "name": "Ahmet Faruk Dereli"
    },
    "devicezero": {
      "name": "Jonas Kröger",
      "github": "devicezero",
      "linkedin": "https://www.linkedin.com/in/jonaskroeger/"
    },
    "doug-goldberg": {
      "name": "Doug Goldberg"
    },
    "duncan-naves": {
      "name": "Duncan Naves",
      "github": "duncannaves",
      "linkedin": "https://www.linkedin.com/in/duncan-naves-a94423aa"
    },
    "erika-bustamante": {
      "name": "Erika Bustamante"
    },
    "fabpot": {
      "name": "Fabien Potencier"
    },
    "flovntp": {
      "name": "Florent Huck",
      "github": "flovntp",
      "linkedin": "https://www.linkedin.com/in/florenthuck"
    },
    "fred-plais": {
      "name": "Fred Plais"
    },
    "gauthier-garnier": {
      "name": "Gauthier Garnier"
    },
    "gilzow": {
      "name": "Paul Gilzow"
    },
    "gmoigneu": {
      "name": "Guillaume Moigneu",
      "github": "gmoigneu",
      "linkedin": "https://www.linkedin.com/in/guillaumemoigneu/"
    },
    "gregqualls": {
      "name": "Greg Qualls"
    },
    "guguss": {
      "name": "Augustin Delaporte"
    },
    "haylee-millar": {
      "name": "Haylee Millar"
    },
    "ivana-kotur": {
      "name": "Ivana Kotur"
    },
    "jackrabbithanna": {
      "name": "Mark Hanna"
    },
    "jared-wright": {
      "name": "Jared Wright",
      "github": "jww-sh",
      "linkedin": "https://www.linkedin.com/in/jaredwaynewright"
    },
    "jessica-orozco": {
      "name": "Jessica Orozco"
    },
    "joey-stanford": {
      "name": "Joey Stanford"
    },
    "john-grubb": {
      "name": "John Grubb"
    },
    "jonas-kruger": {
      "name": "Jonas Kruger"
    },
    "kathryn-frazer": {
      "name": "Kathryn Frazer"
    },
    "kemiojo": {
      "name": "Kemi Elizabeth Ojogbede"
    },
    "kieronsambrook-smith": {
      "name": "Kieronsambrook Smith"
    },
    "laurent-arnoud": {
      "name": "Laurent Arnoud"
    },
    "letoya-boyne": {
      "name": "Letoya Boyne"
    },
    "lolautruche": {
      "name": "Jérôme Vieilledent"
    },
    "lyly-lepinay": {
      "name": "Lyly Lepinay"
    },
    "manauwar-alam": {
      "name": "Manauwar Alam"
    },
    "marc-antoine-porri": {
      "name": "Marc Antoine Porri"
    },
    "maria-antinkaapo": {
      "name": "Maria Antinkaapo"
    },
    "maria-de-anton": {
      "name": "Maria De Anton"
    },
    "mark-dorison": {
      "name": "Mark Dorison"
    },
    "markus-hausammann": {
      "name": "Markus Hausammann"
    },
    "mary-thomas": {
      "name": "Mary Thomas"
    },
    "mathias-bolt-lesniak": {
      "name": "Mathias Bolt Lesniak"
    },
    "mathieu-strauch": {
      "name": "Mathieu Strauch"
    },
    "matthias-van-woensel": {
      "name": "Matthias Van Woensel",
      "linkedin": "https://www.linkedin.com/in/matthias-van-woensel-267a069"
    },
    "michael-sharp": {
      "name": "Michael Sharp"
    },
    "mupsi": {
      "name": "Marine Gandy"
    },
    "natalie-harper": {
      "name": "Natalie Harper"
    },
    "ngommenginger": {
      "name": "Nicolas Gommenginger",
      "linkedin": "https://www.linkedin.com/in/nicolas-gommenginger"
    },
    "nicholas-bennison": {
      "name": "Nicholas Bennison"
    },
    "nicholas-vahalik": {
      "name": "Nicholas Vahalik"
    },
    "nick-hardiman": {
      "name": "Nick Hardiman"
    },
    "nickanderegg": {
      "name": "Nickanderegg"
    },
    "nicolas-grekas": {
      "name": "Nicolas Grekas",
      "github": "nicolas-grekas",
      "linkedin": "https://www.linkedin.com/in/nicolasgrekas/"
    },
    "niti-malwade": {
      "name": "Niti Malwade"
    },
    "opensocialteam": {
      "name": "Opensocialteam"
    },
    "ori-pekelman": {
      "name": "Ori Pekelman"
    },
    "otavio-santana": {
      "name": "Otavio Santana"
    },
    "palwandi": {
      "name": "Pawan Alwandi",
      "github": "pawpy",
      "linkedin": "https://www.linkedin.com/in/pawanalwandi"
    },
    "patrick-boest": {
      "name": "Patrick Boest"
    },
    "patrick-dawkins": {
      "name": "Patrick Dawkins",
      "github": "pjcdawkins",
      "linkedin": "https://www.linkedin.com/in/patrickdawkins"
    },
    "patrick-klima": {
      "name": "Patrick Klima"
    },
    "pjcdawkins": {
      "name": "Pjcdawkins"
    },
    "prineet-kaurbhurji": {
      "name": "Prineet Kaurbhurji"
    },
    "quentin-sinig": {
      "name": "Quentin Sinig"
    },
    "ralt": {
      "name": "Florian Margaine",
      "github": "ralt",
      "linkedin": "https://www.linkedin.com/in/florian-margaine-43971136"
    },
    "ramanathanramakrishnamurthy": {
      "name": "Ramanathanramakrishnamurthy"
    },
    "remi-lejeune": {
      "name": "Rémi Lejeune"
    },
    "ribel": {
      "name": "Taras Kruts"
    },
    "robert-douglass": {
      "name": "Robert Douglass"
    },
    "rudy-weber": {
      "name": "Rudy Weber"
    },
    "ryan-hicks": {
      "name": "Ryan Hicks"
    },
    "sabri-helal": {
      "name": "Sabri Helal"
    },
    "savannah-bergeron": {
      "name": "Savannah Bergeron"
    },
    "shannon-vettes": {
      "name": "Shannon Vettes"
    },
    "shawn-ogasawara": {
      "name": "Shawn Ogasawara",
      "linkedin": "https://www.linkedin.com/in/shawn-ogasawara-83a9a0/"
    },
    "shawna-spoor": {
      "name": "Shawna Spoor"
    },
    "shedrack-akintayo": {
      "name": "Shedrack Akintayo"
    },
    "simon-ruggier": {
      "name": "Simon Ruggier"
    },
    "sophie-van-der-kindere": {
      "name": "Sophie Van Der Kindere"
    },
    "stefanos-thampis": {
      "name": "Stefanos Thampis"
    },
    "stephen-weinberg": {
      "name": "Stephen Weinberg"
    },
    "sukhman-virk": {
      "name": "Sukhman Virk"
    },
    "sumaira-nazir": {
      "name": "Sumaira Nazir"
    },
    "sumer": {
      "name": "Sümer Cip"
    },
    "syed-raza": {
      "name": "Syed Raza"
    },
    "tamara-bacchia": {
      "name": "Tamara Bacchia"
    },
    "tara-arnold": {
      "name": "Tara Arnold"
    },
    "theosakamg": {
      "name": "Mickael Gaillard",
      "github": "theosakamg"
    },
    "thomasdiluccio": {
      "name": "Thomas di Luccio"
    },
    "tim-anderson": {
      "name": "Tim Anderson"
    },
    "tom-helmer-hansen": {
      "name": "Tom Helmer Hansen"
    },
    "tylermills": {
      "name": "Tyler Mills"
    },
    "upsun": {
      "name": "Upsun"
    },
    "veronika-tolkachova": {
      "name": "Veronika Tolkachova",
      "linkedin": "https://www.linkedin.com/in/veronika-tolkachova-169167a2"
    },
    "vince-parker": {
      "name": "Vince Parker"
    },
    "vinnie-russo": {
      "name": "Vincenzo Russo"
    },
    "vrobert78": {
      "name": "Vincent Robert",
      "github": "vrobert78",
      "linkedin": "https://www.linkedin.com/in/vincent-robert-498a883"
    },
    "yuriy-babenko": {
      "name": "Yuriy Babenko"
    },
    "yuriy-gerasimov": {
      "name": "Yuriy Gerasimov"
    }
  };
  return <div className="post-meta">
      {(authors.length > 0 || formattedDate) && <div className="post-meta-info">
          {authors.length > 0 && <div className="post-meta-authors">
              {authors.map(slug => {
    const {name, url, avatarUrl} = resolveAuthor(slug);
    const inner = <>
                    {avatarUrl && <img src={avatarUrl} alt={name} className="post-meta-avatar" />}
                    <span className="post-meta-author-name">{name}</span>
                  </>;
    return url ? <a key={slug} href={url} target="_blank" rel="noopener noreferrer" className="post-meta-author">
                    {inner}
                  </a> : <span key={slug} className="post-meta-author">{inner}</span>;
  })}
            </div>}
          {authors.length > 0 && formattedDate && <span className="post-meta-separator" aria-hidden="true">·</span>}
          {formattedDate && <span className="post-meta-date">{formattedDate}</span>}
        </div>}
      {image && <img src={image} alt="" className="post-meta-image" aria-hidden="true" />}
    </div>;
};

<PostMeta data={{ author: ["dman"], date: "2024-10-24T13:45:55+07:00", image: "/images/posts/how-tos/schedule-resource-availability/red-background.webp" }} />

As Upsun does purely usage-driven billing,
rapid provisioning, and no-downtime scaling,
you have the ability to turn things off when they are not being used.
In this example, we will look at a way to scale down your development environments
and take them offline overnight when they are not in use.
This has the potential to reduce your hosting costs significantly.
...And reduce needless energy consumption also!

> [Platform.sh is committed to "Green Hosting" and better environmental sustainability.](https://platform.sh/blog/b-corp-certification/)

## Overview

To keep things simple to start with,
We will set up two small cron jobs
that will put your development environments to sleep at the end of the day,
and wake them up again before you start work.

The ability to turn things on and off requires access to an API key that can be used to invoke the `upsun` CLI utility.

<Note>
  **Assumptions:**

  * You already have a project hosted on Upsun.
    * with at least one development environment
    * and resources to add one more
  * You have the Upsun CLI installed.
</Note>

## Steps

### Create a feature branch environment

We will be adding and testing this new work in a feature branch.

Open a CLI in the project root and:

```bash {filename="Terminal"} theme={null}
FEATURE_BRANCH='pauseSun'
git checkout -b $FEATURE_BRANCH
git push --set-upstream upsun $FEATURE_BRANCH
upsun environment:activate -y --environment=$FEATURE_BRANCH 
```

> Upsun also [provides the equivalent of these git commands](https://docs.upsun.com/administration/cli.html#choose-between-the-cli-and-git-commands) - that can useful.

### Create an API token

You can do this either [via the console](https://docs.upsun.com/administration/cli/api-tokens.html#2-create-an-api-token), or with the CLI as outlined below:

<Info>
  Generating a new token cannot be done using a CLI token.
  Need to force browser login to make this work.
</Info>

```bash {filename="Terminal"} theme={null}
unset UPSUN_CLI_TOKEN
upsun auth:logout
upsun auth:browser-login
new_token_response=$(upsun api:curl users/"$(upsun auth:info id)"/api-tokens --json '{"name":"pauseSun runner"}')
new_token=$(echo $new_token_reponse | jq -r ".token")
```

### Embed the token into the feature branch environment

Do this the easy way [by setting an environment variable in the console](https://docs.upsun.com/administration/cli/api-tokens.html#authenticate-in-an-environment).

... or the hard way

```bash {filename="Terminal"} theme={null}
upsun variable:create \
   -e $FEATURE_BRANCH \
   --level environment \
   --prefix 'env' \
   --name UPSUN_CLI_TOKEN \
   --sensitive true \
   --value "$new_token" \
   --inheritable false \
   --visible-build true \
   --no-interaction
```

### Install `upsun` CLI

Now [add a build hook to your app configuration](https://docs.upsun.com/administration/cli/api-tokens.html#authenticate-in-an-environment) to install the CLI as part of the build process.

```yaml {filename=".upsun/config.yaml",hl_lines=["7-8"],linenostart=1} theme={null}
applications:
  app:
    hooks:
      #...
      build: |
        set -e
        echo "Installing Upsun CLI"
        curl -fsSL https://raw.githubusercontent.com/platformsh/cli/main/installer.sh | VENDOR=upsun bash
```

### Verify

If you push the code with the build hook now, you should see the message
`Upsun CLI has been installed successfully.` in your build log.

You can also `upsun ssh` into the environment and ensure that running `upsun` commands there works as expected and the token is valid.

```bash {filename="Terminal"} theme={null}
me@Platform Upsun-Tools-Demo % upsun ssh
 _   _                   
| | | |_ __ ____  _ _ _  
| |_| | '_ (_-< || | ' \ 
 \___/| .__/__/\_,_|_||_|
      |_|                

 Welcome to Upsun.
Environment: pausesun-varzi3y
Branch: pauseSun
Project: ce4ywrmdpxgbk

web@app.0:~$ upsun

Warning:
An API token is set. Anyone with SSH access to this environment can read the token.
Please ensure the token only has strictly necessary access.

Welcome to Upsun!

Project: Drupal10-Platformsh (ce4rmdpywxgbk)
Environment: pauseSun (type: development)
Application name: static
```

### Write the script

This could be done in any language you are comfortable with, but seeing as we are mostly just running commands on the CLI, I will stay in `bash`.

<Info>
  The default shell in `upsun` environments is actually `dash`, but that won't make a difference here.
</Info>

This script will be stored in a `scripts/` directory *inside your app folder*. So in my case with my application in  a folder called `app/`, this will be `app/scripts/` in the repository, but just `scripts/` from the runtime environment application root.

It's very easy to ask the API for the list of environments.
I want only `active`, `development` environments - don't want to touch the `production` environment!

```bash {filename="Terminal"} theme={null}
upsun environment:list --columns=ID --no-header --format=plain --type=development --status=active
```

Then, use the API to turn environments on and off within a script:

```bash {filename="scripts/pause_environments"} theme={null}
#!/usr/bin/env bash
# Pause all development environments

ENVIRONMENTS=$(upsun environment:list --no-interaction --columns=ID --no-header --format=plain --type=development --status=active)

for ENVIRONMENT in $ENVIRONMENTS; do
  # Exclude self.
  if [ "$ENVIRONMENT" != "$PLATFORM_BRANCH" ]; then 
    echo "$0 is pausing $ENVIRONMENT.."
    upsun environment:pause --no-interaction --no-wait --environment="$ENVIRONMENT"
  fi
done
```

Here I use `--no-wait` and loop through all the environments. This will result in the activities being queued by the orchestration system, while this command returns immediately.
If you have a number of environments, it may take a few minutes for them to all process sequentially.
Check out the activity log to watch the queue in action.

We can set this script to be executable now, and `git add`, `commit`, `push`, and test it.

```bash {filename="Terminal"} theme={null}
chmod a+x app/scripts/pause_environments
git add app/scripts/pause_environments
git commit -m "Added script to pause all environments"
git push
```

### Optional - publish this as a runtime operation

This will make it easy to turn everything on or off with one button!

```yaml {filename=".upsun/config.yaml",hl_lines=["5-9"],linenostart=1} theme={null}
application:
  app:
    operations:
      #...
      pause_environments:
        role: admin
        commands:
          start: scripts/pause_environments
```

<img src="https://mintcdn.com/upsun-c9761871/eXV27YLeu5lESGxN/images/posts/how-tos/schedule-resource-availability/pauseSun-runtime-operation.webp?fit=max&auto=format&n=eXV27YLeu5lESGxN&q=85&s=d6492c47393015bb29d0cea616a3f2e7" alt="You can now trigger the runtime operation from the Web Console" width="923" height="471" data-path="images/posts/how-tos/schedule-resource-availability/pauseSun-runtime-operation.webp" />

### Set a schedule

This is as simple as [adding a line in the `crons` section](https://docs.upsun.com/create-apps/app-reference/single-runtime-image.html#crons) of your `.upsun/config.yaml`

```yaml {filename=".upsun/config.yaml",hl_lines=["5-9"],linenostart=1} theme={null}
application:
  app:
    crons:
      #...
      pause_environments_at_7pm:
        spec: '0 19 * * *'
        commands:
          start: scripts/pause_environments
```

<Info>
  Take care for time zones!!
  [`cron` will run on the system timezone. Adjust as needed.](https://docs.upsun.com/create-apps/app-reference/single-runtime-image.html#cron-job-timing)
</Info>

Once you `git add`, `commit`, and `push` this to your environment, all should be ready to run.

You can test the operation immediately by invoking it from the console via "Run runtime operation"

### Repeat this for turning back on in the morning

Almost the same additions can be made to reverse the process.

We use `upsun environment:resume` (not `environment:activate`) so as to not accidentally turn on new environments.

```bash {filename="scripts/resume_environments"} theme={null}
#!/usr/bin/env bash
# Wake up all development environments

ENVIRONMENTS=$(upsun environment:list --no-interaction --columns=ID --no-header --format=plain --type=development --status=paused)

for ENVIRONMENT in $ENVIRONMENTS; do
  if [ "$ENVIRONMENT" != "$PLATFORM_BRANCH" ]; then 
    echo "$0 is activating $ENVIRONMENT.."
    upsun environment:resume --no-interaction --no-wait --environment="$ENVIRONMENT"
  fi
done
```

In `upsun/config.yaml`, add the cron job and runtime operation to turn things on:

```yaml {filename=".upsun/config.yaml"} theme={null}
application:
  app:
    operations:
      #...
      resume_environments:
        role: admin
        commands:
          start: scripts/resume_environments

    ...
 
    crons:
      #...
      resume_environments_at_7am:
        spec: '0 7 * * 1-5'
        commands:
          start: scripts/resume_environments 
```

DONE!

## Conclusions

You now have a short, simple way to schedule downtime for your development environments when you are not using them.

This simple example doesn't contain much business logic, though I do try to leave things off over the weekend also.
For real use you may want to maybe adjust the schedule to be more selective about which environments really have to be turned on every day.

In this example, I developed and tested in a dedicated branch environment.
For real use, I could now merge this addition into the `production` branch, and have the schedule running from there.
This is because the `production` is expected to be always-on, and we can't put the scheduler into an environment that might itself become paused.

You could even put this script (and the corresponding `upsun` CLI and token) in a separate hosting environment altogether. This tutorial is an example of the building blocks that you can adjust to your own workflows.
