Skip to main content

CI/CD Guide - Deploy Equinix Metal servers within your GitHub Actions workflow

Deploy Equinix Metal servers within your GitHub Actions workflow.

CI/CD Guide - Deploy Equinix Metal servers within your GitHub Actions workflow

Introduction to GitHub Actions

GitHub Actions, like all CI/CD systems, run their jobs on runners, either virtual machines or containers. Like all CI/CD systems, the description of the actions to take as part of the pipeline is defined in a configuration file. In the case of GitHub Actions, these are yaml files.

If GitHub Actions are new to you, we highly recommend you read the official GitHub Actions documentation.

If you already are familiar, it is helpful to keep the yaml reference handy.

Each yaml file contains a named workflow. Each workflow consists of one or more named jobs. Each job, in turn, consists of a sequence of one or more tasks called steps.

Here is an example of a simple GitHub Actions workflow that runs on a Linux runner and prints a message. This example is modified from the official GitHub Actions documentation.

name: In Flow Testing

on: push

jobs:
  my-job:
    name: My Job
    runs-on: ubuntu-latest
    steps:
      - name: Print a greeting
        env:
          MY_VAR: Hi there! My name is
          FIRST_NAME: Equinix
          LAST_NAME: Metal
        run: |
          echo $MY_VAR $FIRST_NAME $LAST_NAME.

In this case, the single step provided just runs a shell command to print a message.

In addition, steps can run pre-packaged activities, called "actions". Actions are reusable units of code that can be shared with the community. You can find actions in the GitHub Marketplace.

Here is an example of a step that uses an action to check out the repository.

    - name: Checkout code
      uses: actions/checkout@v2

There isn't any code you have to write. It all is packaged up inside the checkout action.

Deploying Equinix Metal servers in a GitHub Actions workflow

There are several methods to deploy a server on Equinix Metal as part of a GitHub Actions workflow.

Deploying a server using the Equinix Metal CLI

One option is to install the metal CLI, authenticate, and run the metal commands directly in the workflow.

Here is an example of a job that installs the metal CLI, authenticates, and creates a server.

name: In Flow Testing

on: push

jobs:
  my-job:
    name: My Job
    runs-on: ubuntu-latest
    steps:
    - name: Install metal CLI
      run: |
        curl -sL https://github.com/equinix/metal-cli/releases/download/v0.22.0/metal-linux-amd64 -o metal
        chmod +x metal
        sudo mv metal /usr/local/bin/metal
    - name: Create Metal project
      id: create-project
      run: |
        OUTPUT=$(metal project create --name ci-cd-project --organization-id $METAL_ORG --output json)
        METAL_PROJECT_ID=$(echo $OUTPUT | jq -r '.id')
        echo projectId=$METAL_PROJECT_ID >> $GITHUB_OUTPUT
    - name: Create Metal device
      run: |
        OUTPUT=$(metal --project-id ${{ steps.create-project.outputs.projectId }} device create --hostname my-server --plan c3.small.x86 --metro da --operating-system ubuntu_20_04 --output json)
        METAL_DEVICE_ID=$(echo $OUTPUT | jq -r '.id')
        echo deviceId=$METAL_DEVICE_ID >> $GITHUB_OUTPUT
    - name: Do testing tasks
      run: |
        # do some testing tasks
    - name: Remove metal device
      run: |
        metal device delete --id ${{ steps.create-device.outputs.deviceId }} --force
    - name: Remove metal project
      run: |
        metal project delete --id ${{ steps.create-project.outputs.projectId }} --force

The above works reasonably well, but it has some drawbacks:

  1. The metal CLI is not pre-installed on the GitHub Actions runners. You have to install it yourself.
  2. The metal CLI requires some steps to manage the output and wrangle it into a useful format.
  3. You need to manage getting that useful output into a store that can be used by subsequent steps.
  4. The steps are written as scripts. The inputs are messy interspersed into the code itself. And this is before pulling out some of the hard-coded values into environment variables or options for the run.

This can be better, thanks to the hard-working staff of Equinix Metal.

Deploying a server using Equinix Metal Actions

Recall the first example, with a single action that handles all of the cloning of the repository. Equinix Metal has pre-packaged Actions available to handle all of the work you did above.

The Actions available are:

Convert the scripts into a workflow that uses those actions.

name: In Flow Testing

on: push

jobs:
  my-job:
    name: My Job
    runs-on: ubuntu-latest
    steps:
    - name: Create temporary project
      id: metal-project
      uses: equinix-labs/metal-project-action@v0.14.1
      with:
        userToken: ${{ secrets.METAL_AUTH_TOKEN }}
    - name: Create device in temporary project
      uses: equinix-labs/metal-device-action@v0.2.1
      continue-on-error: true
      with:
        metal_auth_token: ${{ steps.metal-project.outputs.projectToken }}
        metal_project_id: ${{ steps.metal-project.outputs.projectID }}
        metro: da
        plan: m3.small.x86
        os: ubuntu_22_04
    - name: Do testing tasks
      run: |
        # do some testing tasks
    - name: Delete temporary project & device
      uses: equinix-labs/metal-sweeper-action@v0.6.1
      with:
        authToken: ${{ secrets.METAL_AUTH_TOKEN }}
        projectID: ${{ steps.metal-project.outputs.projectID }}

This code is much cleaner. The inputs are all in the with section, which are where the parameters are passed to the actions. The outputs are all in the outputs section. There are no scripts that pertain to creating or deploying resources, and no ugly lines that have to be structured just right.

Take one more step to make this even cleaner: put the common parameters into the env section.

name: In Flow Testing

on: push

env:
  PLAN: m3.small.x86
  OS: ubuntu_22_04
  METRO: da

jobs:
  my-job:
    name: My Job
    runs-on: ubuntu-latest
    steps:
    - name: Create temporary project
      id: metal-project
      uses: equinix-labs/metal-project-action@v0.14.1
      with:
        userToken: ${{ secrets.METAL_AUTH_TOKEN }}
    - name: Create device in temporary project
      uses: equinix-labs/metal-device-action@v0.2.1
      continue-on-error: true
      with:
        metal_auth_token: ${{ steps.metal-project.outputs.projectToken }}
        metal_project_id: ${{ steps.metal-project.outputs.projectID }}
        metro: ${{ env.METRO }}
        plan: ${{ env.PLAN }}
        os: ${{ env.OS }}
    - name: Do testing tasks
      run: |
        # do some testing tasks
    - name: Delete temporary project & device
      uses: equinix-labs/metal-sweeper-action@v0.6.1
      with:
        authToken: ${{ secrets.METAL_AUTH_TOKEN }}
        projectID: ${{ steps.metal-project.outputs.projectID }}

The pre-packaged Equinix Metal Actions have one other distinct benefit. Normally, when you create a device in Metal - whether using the Web UI, API or metal CLI - it does not create the device, but rather creates the request to create the device. You then need to check back to see if the device is ready or not.

The equinix-device-action comes with a built-in timeout. It waits until the device is ready before proceeding to the next step. However, this means that the step might take longer to complete. If you need to deploy multiple servers, we recommend using GitHub Actions' parallel execution within a single job for deployment, and then another job for running tests.

name: In Flow Testing

on: push

env:
  PLAN: m3.small.x86
  OS: ubuntu_22_04
  METRO: da

jobs:
  create-project:
    name: Create Project
    runs-on: ubuntu-latest
    outputs:
      projectID: ${{ steps.metal-project.outputs.projectID }}
      projectToken: ${{ steps.metal-project.outputs.projectToken }}
    steps:
    - name: Create temporary project
      id: metal-project
      uses: equinix-labs/metal-project-action@v0.14.1
      with:
        userToken: ${{ secrets.METAL_AUTH_TOKEN }}

  deploy-servers:
    name: Deploy Servers
    runs-on: ubuntu-latest
    needs: create-project
    strategy:
      matrix:
        count: [1, 2, 3]
    outputs:
        deviceid_1: ${{ steps.create-device.outputs.deviceid_1 }}
        ipaddress_1: ${{ steps.create-device.outputs.ipaddress_1 }}
        deviceid_2: ${{ steps.create-device.outputs.deviceid_2 }}
        ipaddress_2: ${{ steps.create-device.outputs.ipaddress_2 }}
        deviceid_3: ${{ steps.create-device.outputs.deviceid_3 }}
        ipaddress_3: ${{ steps.create-device.outputs.ipaddress_3 }}
    steps:
    - name: Create device in temporary project
      id: create-device
      uses: equinix-labs/metal-device-action@v0.2.1
      continue-on-error: true
      with:
        metal_auth_token: ${{ needs.create-project.outputs.projectToken }}
        metal_project_id: ${{ needs.create-project.outputs.projectID }}
        metro: ${{ env.METRO }}
        plan: ${{ env.PLAN }}
        os: ${{ env.OS }}
    - name: Set device ID output
      id: outputs
      run: |
        echo "deviceid_${{ matrix.count }}=${{ steps.create-device.outputs.deviceid }}" >> $GITHUB_OUTPUTS
        echo "ipaddress_${{ matrix.count }}=${{ steps.create-device.outputs.ipaddress }}" >> $GITHUB_OUTPUTS


  test-servers:
    name: Run Tests
    runs-on: ubuntu-latest
    needs: deploy-servers
    steps:
    - name: Do testing tasks
      run: |
        # do some testing tasks
        # to access individual server ipaddress and deviceid, use ${{ needs.deploy-servers.outputs.ipaddress_1 }} and ${{ needs.deploy-servers.outputs.deviceid_1 }} etc.

  clean-up:
    name: Clean Up
    runs-on: ubuntu-latest
    needs: [test-servers,create-project]
    steps:
    - name: Delete temporary project & devices
      uses: equinix-labs/metal-sweeper-action@v0.6.1
      with:
        authToken: ${{ secrets.METAL_AUTH_TOKEN }}
        projectID: ${{ needs.create-project.outputs.projectID }}

This is looking pretty good.

  • You have a create-project job that creates a project.
  • You have a deploy-servers job that creates servers in that project. Because of the matrix, it creates three of them in parallel. Any one failure will stop the entire job and workflow, while they are deployed in parallel.
  • You have a test-servers job that runs tests on the servers. It only runs if the deploy-servers job is successful.
  • You have a clean-up job that deletes the project and all the servers. It runs even if the tests fail.
  • The common parameters are extracted into env.

One last piece is missing. Notice the reference to secrets.METAL_AUTH_TOKEN? We need to set that up in the GitHub repository. When you go to your repository, you will be able to add secrets in the "Settings" tab. This workflow expects to find it there. Do not store the secret hard-coded into the workflow.

Apply this all together.

  1. Go to GitHub and create a new repository, or, if you already have one you want to use, skip this step.
  2. Enable Actions in the repository in "Settings".
  3. Create a secret in the repository called METAL_AUTH_TOKEN with the value of your Equinix Metal API token.
  4. Create a new file in the .github/workflows directory called metal.yml, of the filename of your choice.
  5. Copy the above workflow into the file.
  6. Edit the workflow as you see fit.

Some things you may want to edit in the workflow:

  • The values of PLAN, OS, and METRO in the env section.
  • The number of servers to deploy in the matrix section.
  • The tests. In this guide, the tests are left empty - customize it by adding the tests that best suit your project.

Now you can deploy Equinix Metal projects and servers, and clean them up, as part of your GitHub Actions workflows!

Last updated

03 June, 2024

Category

Tagged

Article