One place for hosting & domains

      Create

      How To Create an OAuth App with the Linode Python API Library


      Updated by Linode

      Contributed by

      Linode

      Linode supports the OAuth 2 authorization protocol. OAuth 2 allows a user to safely grant a third-party app permission to act on their behalf. This means that a user could authorize an app to access data and / or make changes to their Linode account and services that are exposed by the Linode APIv4. For example, an app could create or destroy Linodes, manage a NodeBalancer, or alter a domain.

      This guide will show you how to create a simple OAuth application using Flask and the Linode Python API library. This app allows a user to log in with their Linode account and create a Linode with a StackScript. The complete code for this example is available in the Linode APIv4 Python library example repository.

      Before You Begin

      1. Normally, in order to create an OAuth app with Linode your server must have HTTPS enabled. The only exceptions to this rule are localhost addresses, which can use HTTP. As this guide is just a primer and is not intended to supply production ready code, we will be working with a local workstation, using localhost. If you choose to create an app for production, you will need to generate SSL certificates for HTTPS access.

      2. Ensure that Python 3 is installed on your workstation.

      Obtaining a Client ID and a Client Secret

      In order for Linode to verify the identity of your app, called a client, you will need to generate a set of credentials, specifically a client ID and a client secret.

      1. Log in to the Linode Cloud Manager and navigate to your Account Profile.

        OAuth Account Profile

      2. From there, click on the My Apps tab and select Create My App. You will be prompted to supply a label for your app and a callback URL. We will discuss the role of the callback URL in depth later in this guide. For now you can supply the following URL:

        http://localhost:5000/auth_callback
        

        Leave Public unchecked and click Submit.

        OAuth Account Profile

      3. A window will appear with your client secret. Copy this down somewhere secure, as once you exit this window you will not be able to retrieve the client secret again, and will be forced to generate a new one.

        OAuth Account Profile

      4. Once you exit the client secret window your app will appear as part of a list of apps. Note your client ID, as this is the last piece of information you will need to verify your app’s identity.

        OAuth Account Profile

      In summary, you should have these three bits of information, with values similar to the ones provided here:

      OAuth 2 Authentication Exchange

      The OAuth 2 workflow is a series of exchanges between your third-party app and Linode. Below is an explanation of these exchanges.

      1. The end user visits your client application’s website and attempts to login.
      2. Your client application redirects the end user to the authentication server (https://login.linode.com) with your client application’s client ID and requested OAuth scopes, which appear in the URL of the login page.
      3. The end user inputs their username and password to the authorization server and authorizes the login.
      4. The authorization server redirects the end user back to your client application with a temporary authorization code (sometimes called an exchange code) in the URL.
      5. The client application issues a POST request to the authentication server containing the authorization code and the client application’s client secret.
      6. The authentication server responds to the client application with a newly issued OAuth access token.

      In the following sections you will write the code to perform each one of these steps, using the Linode Python API library.

      Setup Your Development Environment

      1. Create a project folder and move into that folder.

        mkdir ~/linode-oauth-project && cd ~/linode-oauth-project
        
      2. For this project, you will need to use pip to download and install the required Python libraries. Install pip if you do not already have it:

        apt install python-pip
        
      3. Install the required Python libraries:

        pip install flask flask-session linode_api4
        

      Configure Your App

      In a text editor, create a file named config.py. Add the following variables and values, being sure to change the values to your own.

      The StackScript used in this example is for demo purposes. To explore other available StackScripts, visit the Linode StackScript Library. Note that the stackscript_id does not have quotation marks around it. The secret key is used for serializing session data, and should be a value only you know.

      config.py
      1
      2
      3
      4
      5
      
      client_id = 'ce571a8cdad1ba4a0a7d'
      client_secret = 'fab8e2222e83b9b2f50a76012122ec20a5acb005ed088f3fccda2c9c2c4e1cbd'
      stackscript_id = 320826
      application_name = 'my-application-name'
      secret_key = 'my-secret-key'

      Author an OAuth2 App

      In this section, you will write the code for the app.

      Include Imports

      Ensure you are in the linode-oauth-project directory and create and open a file called app.py in the text editor of your choice. Include the following libraries:

      app.py
      1
      2
      3
      4
      5
      6
      
      import re
      from flask import Flask, redirect, request, render_template, session, send_from_directory
      from flask_session import Session
      from linode_api4 import (LinodeClient, LinodeLoginClient, StackScript, Image, Region, Type, OAuthScopes)
      
      import config

      Set Up Flask and Session Key

      Copy in the following code to set up Flask and the session secret key:

      app.py
      1
      2
      3
      4
      
      ...
      
      app=Flask(__name__)
      app.config['SECRET_KEY'] = config.secret_key

      Create a Function to Return the Linode Login Client

      In app.py add the following function to return the LinodeLoginClient class. The LinodeLoginClient class is the library’s OAuth interface. Note that we are passing the client_id and client_secret parameters from our config.py file to the class:

      ~/linode-oauth-project/app.py
      1
      2
      3
      4
      
      ...
      
      def get_login_client():
          return LinodeLoginClient(config.client_id, config.client_secret)

      Create an Index Route

      In Flask you can create HTTP endpoints with routes. The index route, defined in the code below at the document root /, will be the route the user will see when they navigate to http://localhost:5000/. This route will be responsible for displaying the available Linode plan types, the available regions, and the StackScript-compatible images that a user will choose from when creating their new Linode.

      To query a list of available plan types and regions you can use the LinodeClient class, which is an interface for Linode’s APIv4. Viewing the Linode plan types and regions does not require any sort of authorization, so you can provide a dummy value of no-token to instantiate the class:

      ~/linode-oauth-project/app.py
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      
      ...
      
      @app.route('/')
      def index():
          client = LinodeClient('no-token')
          types = client.linode.types(Type.label.contains("Linode"))
          regions = client.regions()
          stackscript = StackScript(client, config.stackscript_id)
          return render_template('configure.html',
              types=types,
              regions=regions,
              application_name=config.application_name,
              stackscript=stackscript
          )

      It is important to note that the two API queries in the above code are slightly different from one another. The client.regions method is a top-level method, just as it appears in the Linode API. The client.linode.types method, on the other hand, is part of the Linode group, which is a collection of methods that deal with Linodes. Again, this is because Linode endpoints are grouped that way in the API. Some methods in the Linode Python library are top level, such as domain_create, while others, like networking.ip_assign, are part of a group. For more information on the top-level methods and groupings, consult the library documentation.

      In addition to querying the API, the above route also renders the configure.html template by passing it the types, regions, application name, and StackScript object. The StackScript object contains a list of StackScript compatible images. We will cover templating in a later section.

      Create a Login Route

      Next, create a login route in app.py. This route will perform two functions. First, it will serialize the user’s plan type, region, and image selections into the session.

      Second, this route will redirect the user to Linode’s login page where they will be prompted to authorize your client app and the scopes you have requested for it. Scopes are sets of permissions that define the access level of your client app. For instance, to create a Linode, your end user must authorize the OAuthScopes.Linodes.create scope.

      ~/linode-oauth-project/app.py
      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      ...
      
      @app.route('/', methods=["POST"])
      def start_auth():
          login_client = get_login_client()
          session['dc'] = request.form['region']
          session['distro'] = request.form['distribution']
          session['type'] = request.form['type']
          return redirect(login_client.generate_login_url(scopes=OAuthScopes.Linodes.create))

      When the user returns to your app from the Linode login page, they will be directed to the callback URL.

      Note

      Below is a list of available scopes:

      • OAuthScopes.Linodes
      • OAuthScopes.Domains
      • OAuthScopes.StackScripts
      • OAuthScopes.Users
      • OAuthScopes.NodeBalancers
      • OAuthScopes.Tokens
      • OAuthScopes.IPs
      • OAuthScopes.Tickets
      • OAuthScopes.Clients
      • OAuthScopes.Account
      • OAuthScopes.Events
      • OAuthScopes.Volumes

      Each scope is broken into five permissions: view, create, modify, delete, and all. The all permission encompasses the other four permissions.

      Manage the OAuth 2 Callback URL

      The OAuth 2 callback URL has two main responsibilities. Its first responsibility is to help prove the identity of the client application. When a user attempts to log in to Linode through OAuth, instead of redirecting the user back to the page they came from, Linode’s OAuth implementation matches the client ID to the callback URL you have registered with your app on Linode’s system. This ensures that a nefarious third party can’t just steal the client ID, which is public, and attempt to authorize their own app with it.

      The callback URL’s second responsibility is to kick off the process of exchanging an authorization code for an access token. This second process is done over POST, and so it doesn’t require the user to physically leave the page they are returned to after they log in to Linode. Now you will write the code that satisfies this second responsibility.

      In app.py, add the following lines:

      ~/linode-oauth-project/app.py
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      
      ...
      
      @app.route('/auth_callback')
      def auth_callback():
          code = request.args.get('code')
          login_client = get_login_client()
          token, scopes, _, _ = login_client.finish_oauth(code)
      
          # ensure we have sufficient scopes
          if not OAuthScopes.Linodes.create in scopes:
              return render_template('error.html', error='Insufficient scopes granted to deploy {}'
                      .format(config.application_name))
      
          (linode, password) = make_instance(token, session['type'], session['dc'], session['distro'])
      
          get_login_client().expire_token(token)
          return render_template('success.html',
              password=password,
              linode=linode,
              application_name=config.application_name
          )

      Let’s take a look at what each of the parts of this section does.

      First, a route is defined for the callback with @app.route(), then a function called auth_callback is defined that will run whenever this route is accessed:

      ~/linode-oauth-project/app.py
      1
      2
      3
      4
      
      ...
      @app.route('/auth_callback')
      def auth_callback():
      ...

      When the user is returned to the callback URL, an authorization code is appended to the URL. The variable code is set to retrieve this value from the URL’s request arguments:

      ~/linode-oauth-project/app.py
      1
      2
      3
      
      ...
          code = request.args.get('code')
      ...

      Then you retrieve an instance of the LinodeLoginClient class:

      ~/linode-oauth-project/app.py
      1
      2
      3
      
      ...
          login_client = get_login_client()
      ...

      Once you have the LinodeLoginClient class, you can pass the authorization code to the finish_oauth method, which is a helper method that will manage the authorization code to OAuth token exchange. This method returns an OAuth token, and the scopes the user has agreed upon.

      ~/linode-oauth-project/app.py
      1
      2
      3
      
      ...
          token, scopes, _, _ = login_client.finish_oauth(code)
      ...

      The next section compares the scopes your app requested from the user to the scopes returned by Linode’s OAuth login page. If the returned scopes do not include the correct scopes, in this case the OAuthScopes.Linode.create scope, then an error template is rendered and an error message is displayed:

      ~/linode-oauth-project/app.py
      1
      2
      3
      4
      5
      6
      
      ...
          # ensure we have sufficient scopes
          if not OAuthScopes.Linodes.create in scopes:
              return render_template('error.html', error='Insufficient scopes granted to deploy {}'
                      .format(config.application_name))
      ...

      Once your app has determined that it has the correct permissions, it creates the Linode using the Linode plan type, the region, and the image that the app serialized into session storage. You will create the make_instance function in the next step. The make_instance function returns the linode object, which contains the Linode’s label, group, and IP address, and the function also returns a randomly generated password:

      ~/linode-oauth-project/app.py
      1
      2
      3
      
      ...
          (linode, password) = make_instance(token, session['type'], session['dc'], session['distro'])
      ...

      Once the Linode has been created, the app expires the OAuth access token. Expiring tokens after use is a strong security measure but if your app is performing many actions on behalf of the user, you might find that time-based expiration scheme is more suitable to your needs. The app then renders the success template by passing it the linode object, the password, and application name:

      ~/linode-oauth-project/app.py
      1
      2
      3
      4
      5
      6
      7
      
      ...
          get_login_client().expire_token(token)
          return render_template('success.html',
              password=password,
              linode=linode,
              application_name=config.application_name
          )

      Create a Function to Deploy a Linode

      Now, create the make_instance function that you referenced above:

      ~/linode-oauth-project/app.py
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      
      ...
      
      def make_instance(token, type_id, region_id, distribution_id):
          client = LinodeClient('{}'.format(token))
          stackscript = StackScript(client, config.stackscript_id)
          (linode, password) = client.linode.instance_create(type_id, region_id,
                  group=config.application_name,
                  image=distribution_id, stackscript=stackscript.id)
      
          if not linode:
              raise RuntimeError("it didn't work")
          return linode, password

      The make_instance function takes an OAuth access token, the type ID, the region ID, and the image (Linux distribution) ID as parameters. It creates an instance of the LinodeClient class, and unlike the instance of LinodeClient used earlier in the guide, this one requires an OAuth token because you will be using it to create a Linode. The function then creates a Linode using the linode.instance_create method, returning the linode object and the password.

      Finally, if there was an error with the creation of the Linode, the if not linode statement will raise a runtime error.

      Set the name Variable

      At the end of your app.py, paste in the following code to make sure you can run your app:

      ~/linode-oauth-project/app.py
      1
      2
      3
      
      if __name__ == '__main__':
          app.debug=True
          app.run()

      Create App Templates

      Now that you have written the backend code for your app, you’ll need to create a frontend user interface. Begin by creating a templates directory in your project directory and moving into it:

      mkdir ~/linode-oauth-project/templates && cd ~/linode-oauth-project/templates
      

      Using your preferred text editor, create and open base.html. This will be the base template from which your other templates will inherit their stylesheets and JavaScript files:

      ~/linode-oauth-project/templates/base.html
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      
      <html>
      <head>
          <title>Install On Linode</title>
          <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
              integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
          <style>
              body{
                  text-align: center;
                  background-color: #333333;
              }
              .form-group{
                  display: inline-block;
                  text-align: left;
                  width: 250px;
                  border: 1px solid #cccccc;
                  margin: 5px;
                  padding: 5px;
              }
              .form-group label{
                  color: #337ab7;
              }
              .form-group select{
                  font-size: 16px;
                  outline: none;
                  border: 0px solid #000000;
                  box-shadow: inset 0 1px 1px rgba(0,0,0,0);
                  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0);
              }
              .form-group select:focus{
                  box-shadow: inset 0 1px 1px rgba(0,0,0,0);
                  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0);
              }
              .btn-lg{
                  border-radius: 0px;
                  margin-top: 20px;
              }
              .row{
                  margin-bottom: 20px
              }
              .pop{
                  color: #337ab7;
                  font-weight: bold
              }
              code{
                  color: #337ab7;
                  background-color: #eeeeee
              }
              .boxy{
                  border: 1px solid #cccccc;
                  width: 400px;
                  background-color: #f9f9f9;
                  margin: auto;
                  padding: 10px;
              }
          </style>
      </head>
      <body>
          <div class='container' style='background-color: white; border-left: grey; border-right: grey; height: 100%; padding: 20px;'>
              {% block content %}
              {% endblock %}
          </div>
      
          <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
          <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
              integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS"
              crossorigin="anonymous"></script>
      </body>

      The important thing to note in the above template is the Jinja2 templating tags. They are:

      {% block content %}
      {% endblock %}
      

      As you will see, any template that extends the base.html template and includes code between the opening and closing content block, will render the code laid out by base.html.

      Create a file called configure.html, which will be the UI a user will see when they reach the document root endpoint (/). Copy in the following code:

      templates/configure.html
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      
      {% extends 'base.html' %}
      {% block content %}
          <form method="POST">
              <div class='row'>
                  <h1>Deploy <span style="color: #337ab7;">{{application_name}}</span> to a Linode</h1>
                  <p>
                      This will create a brand new Linode running {{application_name}} on your
                      account and give you the credentials.
                  </p>
              </div>
              <div class='row'>
                  <div class='form-group'>
                      <label for='type'>Type</label>
                      <select name='type'i id='type' class='form-control'
                          onblur="blurring(this)" onfocus="focusing(this)">
                          {% for s in types %}
                              <option value="{{s.id}}">{{s.label}}</option>
                          {% endfor %}
                      </select>
                  </div>
                  <div class='form-group'>
                      <label for='region'>Region</label>
                      <select name='region' id='region' class='form-control'
                          onblur="blurring(this)" onfocus="focusing(this)">
                          {% for o in regions %}
                              <option value="{{o.id}}">{{o.id}}</option>
                          {% endfor %}
                      </select>
                  </div>
                  <div class='form-group'>
                      <label for='distribution'>Images</label>
                      <select name='distribution' id='distribution' class='form-control'
                          onblur="blurring(this)" onfocus="focusing(this)">
                          {% for d in stackscript.images %}
                              <option value="{{d.id.id}}">{{d.id.id}}</option>
                          {% endfor %}
                      </select>
                  </div>
              </div>
              <div class='row'>
                  <input type="submit" value="Deploy Linode" class='btn btn-primary btn-lg'/>
              </div>
          </form>
          <script>
              function focusing(ele){
                  ele.parentElement.style.borderColor = "#337ab7";
              }
              function blurring(ele){
                  ele.parentElement.style.borderColor = "#cccccc";
              }
          </script>
      {% endblock %}

      Here the template begins with two statements: {% extends 'base.html' %} and a {% block content %} statement. These two tags tell Jinja2 to extend the code within base.html, and to place everything within {% block content %} ... {% endblock %} in configure.html between the corresponding {% block content %} ... {% endblock %} tags in base.html.

      configure.html includes Jinja2 logic, with the inclusion of for statements like {% for o in regions %}. These statements are like for statements in other languages, and are used to iterate over an array or list. In this example, it is iterating over the regions that we passed to the template from the index route. configure.html also contains variables, which are denoted by double curly brackets: {{ s.id }}.

      Create another file called error.html. This will be the template that appears whenever there is an error in the Linode deployment. Copy in the following code:

      templates/error.html
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      
      {% extends 'base.html' %}
      {% block content %}
          <div class='row'>
              <h1 class="pop">Error</h1>
              <p>{{error}}</p>
          </div>
          <div class='row' style='margin-top: 20px'>
              <a href='/' class='btn btn-lg btn-default'>Try Again</a>
          </div>
      {% endblock %}

      This template works the same way that configure.html does, by extending base.html and providing its own content block.

      Lastly, create another file called success.html. This file follows the pattern set by configure.html and error.html, and will present the user with a confirmation message whenever a Linode is successfully created. This message includes the Linode’s label, group, IP address, and password:

      templates/success.html
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      
      {% extends 'base.html' %}
      {% block content %}
          <div class='row'>
              <h1>Success!</h1>
              <p>{{application_name}} has been deployed to <span class="pop">{{linode.label}}</span> in the {{linode.group}} group.</p>
          </div>
          <div class='row'>
              <div class='boxy'>
                  <p>You can access your Linode with the following command:</p>
                  <code>ssh root@{{linode.ipv4[0]}}</code>
                  <br />
                  <br />
                  <p>Your root password is:</p>
                  <code>{{password}}</code>
              </div>
          </div>
      {% endblock %}

      Run Your App

      You are now ready to run your app. Change back to your project’s main directory:

      cd ~/linode-oauth-project
      

      Run the app.py script:

      python3 app.py
      

      Open your browser to the following URL:

      http://localhost:5000/
      

      You should be greeted with your new app. Select a plan, a region, and an image to deploy a Linode using the Linode API Python library.

      Next Steps

      The app you’ve created shows off some of the aspects of the Linode API Python library. You can use LinodeLoginClient to authorize your OAuth app with the appropriate scopes, and can create Linodes through the use of LinodeClient.

      In extending this app, you might want to add multiple functionalities, like creating NodeBalancers from a list of available Linodes, or managing domains. To achieve this goal you’ll probably want to separate the login logic from the Linode creation logic. One way to do this would be store the OAuth token in the session, implementing a time-based expiration mechanism to expire your tokens instead.

      More Information

      You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.

      Find answers, ask questions, and help others.

      This guide is published under a CC BY-ND 4.0 license.



      Source link

      Create a Terraform Module


      Updated by Linode Contributed by Linode

      Terraform modules allow you to better organize your configuration code and make the code reusable. You can host your Terraform modules on remote version control services, like GitHub, for others to use. The Terraform Module Registry hosts community modules that you can reuse for your own Terraform configurations, or you can publish your own modules for consumption by the Terraform community.

      In this guide you will create a Linode StackScripts module. This module will deploy a Linode instance from a StackScript you will create. This module will include nested modules that split up the required resources between the root module, a linode_instance module, and a stackscripts module.

      Before You Begin

      1. Install Terraform on your local computer using the steps found in the Install Terraform section of the Use Terraform to Provision Linode Environments guide. Your Terraform project directory should be named linode_stackscripts.

      2. Terraform requires an API access token. Follow the Getting Started with the Linode API guide to obtain a token.

      3. Complete the steps in the Configure Git section of the Getting Started with Git guide.

      4. Review Deploy a WordPress Site using Terraform and StackScripts to familiarize yourself with the Linode provider’s StackScript resource.

      Standard Module Structure

      Terraform’s standard module structure provides guidance on file and directory layouts for reusable modules. If you would like to make your module public to the Terraform community, the recommended layout allows Terraform to generate documentation and index modules for the Terraform Module Registry.

      • The primary module structure requirement is that a root module must exist. The root module is the directory that holds the Terraform configuration files that are applied to build your desired infrastructure. These files provide an entry point into any nested modules you might utilize.

      • Any module should include, at minimum, a main.tf, a variables.tf, and an outputs.tf file. This naming convention is recommended, but not enforced.

        • If using nested modules to split up your infrastructure’s required resources, the main.tf file holds all your module blocks and any needed resources not contained within your nested modules. A simple module’s main.tf file, without any nested modules, declares all resources within this file.

        • The variables.tf and outputs.tf files contain input variable and output variable declarations. All variables and outputs should include descriptions.

      • If using nested modules, they should be located in a root module’s subdirectory named modules/.

      • If your modules will be hosted on Terraform’s Module Registry, root modules and any nested modules should contain a README.MD file with a description that explains the module’s intended use.

      • You can provide examples in a root module’s subdirectory named examples.

      Create the Linode StackScripts Module

      The Linode Stackscripts module will included two nested modules that split up the required resources between the root module, a linodes module, and a stackscripts module. When you are done creating all required Terraform files your directory structure will look as follows:

        
      linode_stackscripts/
      ├── main.tf
      ├── outputs.tf
      ├── secrets.tfvars
      ├── terraform
      ├── terraform.tfvars
      ├── variables.tf
      └── modules/
          ├── linodes/
          │   ├── main.tf
          │   ├── variables.tf
          │   └── outputs.tf
          └── stackscripts/
              ├── main.tf
              ├── variables.tf
              └── outputs.tf
      
      

      Note

      Your linode_stackscripts directory will likely contain other files related to the Terraform installation you completed prior to beginning the steps in this guide.

      Create the Linodes Module

      In this section, you will create the linodes module which will be in charge of creating your Linode instance. This module contains a main.tf file and corresponding variables.tf and outputs.tf files.

      1. If your Terraform project directory is not named linode_stackscripts, rename it before beginning and move into that directory:

        mv terraform linode_stackscripts
        cd linode_stackscripts
        
      2. Create the modules and linodes subdirectories:

        mkdir -p modules/linodes
        
      3. Using your preferred text editor, create a main.tf file in modules/linodes/ with the following resources:

        linode_stackscripts/modules/linodes/main.tf
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        
        locals {
            key ="${var.key}"
        }
        
        resource "linode_sshkey" "main_key" {
            label = "${var.key_label}"
            ssh_key = "${chomp(file(local.key))}"
        }
        
        resource "linode_instance" "linode_id" {
            image = "${var.image}"
            label = "${var.label}"
            region = "${var.region}"
            type = "${var.type}"
            authorized_keys = [ "${linode_sshkey.main_key.ssh_key}" ]
            root_pass = "${var.root_pass}"
            stackscript_id = "${var.stackscript_id}"
            stackscript_data = "${var.stackscript_data}"
        }

        The main.tf file declares a linode_instance resource that deploys a Linode using a StackScript. Notice that all argument values use interpolation syntax to access variable values. You will declare the variables next and provide the variable values in the root module’s terraform.tfvars file. Using separate files for variable declaration and assignment parameterizes your configurations and allows them to be reused as modules.

        Let’s take a closer look at each block in the main.tf configuration file.

        1
        2
        3
        4
        5
        6
        7
        8
        
        locals {
            key ="${var.key}"
        }
        
        resource "linode_sshkey" "main_key" {
            label = "${var.key_label}"
            ssh_key = "${chomp(file(local.key))}"
        }
        • The locals stanza declares a local variable key whose value will be provided by an input variable.

        • The linode_sshkey resource will create Linode SSH Keys tied to your Linode account. These keys can be reused for future Linode deployments once the resource has been created. ssh_key = "${chomp(file(local.key))}" uses Terraform’s built-in function file() to provide a local file path to the public SSH key’s location. The location of the file path is the value of the local variable key. The chomp() built-in function removes trailing new lines from the SSH key.

          Note

          If you do not already have SSH keys, follow the steps in the Create an Authentication Key-pair section of the Securing Your Server Guide.
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        
        resource "linode_instance" "linode_id" {
            image = "${var.image}"
            label = "${var.label}"
            region = "${var.region}"
            type = "${var.type}"
            authorized_keys = [ "${linode_sshkey.main_key.ssh_key}" ]
            root_pass = "${var.root_pass}"
            stackscript_id = "${var.stackscript_id}"
            stackscript_data = "${var.stackscript_data}"
        }

        The linode_instance resource creates a Linode instance with the listed arguments. Please note the following information:

        • The authorized_keys argument uses the SSH public key provided by the linode_sshkey resource in the previous stanza. This argument expects a value of type list, so the value must be wrapped in brackets.

        • To use an existing Linode StackScript you must use the stackscript_id argument and provide a valid ID as a value. Every StackScript is assigned a unique ID upon creation. Later on in the guide, you will create your own StackScript and expose its ID as an output variable in order to use its ID to deploy your Linode instance.

        • StackScripts support user defined data. This means a StackScript can use the UDF tag to create a variable whose value must be provided by the user of the script. This allows users to customize the behavior of a StackScript on a per-deployment basis. Any required UDF variable can be defined using the stackscript_data argument.

      4. Create the variables.tf file to define your resource’s required variables:

        linode_stackscripts/modules/linodes/variables.tf
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        
        variable "key" {
          description = "Public SSH Key's path."
        }
        
        variable "key_label" {
          description = "new SSH key label"
        }
        
        variable "image" {
          description = "Image to use for Linode instance"
          default = "linode/ubuntu18.04"
        }
        
        variable "label" {
          description = "The Linode's label is for display purposes only, but must be unique."
          default = "default-linode"
        }
        
        variable "region" {
          description = "The region where your Linode will be located."
          default = "us-east"
        }
        
        variable "type" {
          description = "Your Linode's plan type."
          default = "g6-standard-1"
        }
        
        variable "authorized_keys" {
          description = "SSH Keys to use for the Linode."
          type = "list"
        }
        
        variable "root_pass" {
          description = "Your Linode's root user's password."
        }
        
        variable "stackscript_id" {
          description = "Stackscript ID."
        }
        
        variable "stackscript_data" {
          description = "Map of required StackScript UDF data."
          type = "map"
        }
        • Modules must include a description for each input variable to help document your configuration’s usage. This will make it easier for anyone else to use this module.

        • Every variable can contain a default value. The default value is only used if no other value is provided. For example, if you have a favorite Linux distribution, you may want to provide it as your image variable’s default value. In this case, linode/ubuntu18.04 is set as the default value.

        • You can declare a type for each variable. If no type is provided, the variable will default to type = "string".

        • Notice that the stackscript_data variable is of type = "map". This will allow you to provide values for as many UDF variables as your StackScript requires.

      5. Create the outputs.tf file:

        ~/linode_stackscripts/modules/linodes/outputs.tf
        1
        2
        3
        
        output "sshkey_linode" {
          value = "${linode_sshkey.main_key.ssh_key}"
        }

        The outputs.tf file exposes any values from the resources you declared in the main.tf file. Any exposed values can be used by any other module within the root module. The sshkey_linode output variable exposes the linode_sshkey resource’s public key.

      Now that the linodes module is complete, in the next section, you will create the stackscripts module.

      Create the StackScripts Module

      In this section you will create the StackScripts module. This module creates a linode_stackscripts resource which you can use to create and modify your own Linode StackScript.

      1. Ensure you are in the linode_stackscripts directory and create the stackscripts subdirectory:

        mkdir modules/stackscripts
        
      2. Using your preferred text editor, create a main.tf file in modules/stackscripts/ with the following resource:

        ~/linode_stackscripts/modules/stackscripts/main.tf
        1
        2
        3
        4
        5
        6
        7
        
        resource "linode_stackscript" "default" {
          label = "${var.stackscript_label}"
          description = "${var.description}"
          script = "${var.stackscript}"
          images = [ "${var.stackscript_image}" ]
          rev_note = "${var.rev_note}"
        }

        The main.tf file creates the linode_stackscript resource and provides the required configurations. All argument values use interpolation syntax to access input variable values. You will declare the input variables next and provide the variable values in the root module’s terraform.tfvars file. For more information on StackScripts see the Automate Deployments with StackScripts guide and the Linode APIv4 documentation.

      3. Create the variables.tf file to define your resource’s required variables:

        ~/linode_stackscripts/modules/stackscripts/variables.tf
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        
        variable "stackscript_label" {
          description = "The StackScript's label is for display purposes only."
        }
        
        variable "description" {
          description = "A description for the StackScript."
        }
        
        variable "stackscript" {
          description = "The script to execute when provisioning a new Linode with this StackScript."
        }
        variable "stackscript_image" {
          description = " A list of Image IDs representing the Images that this StackScript is compatible for deploying with."
          type = "list"
        }
        variable "rev_note" {
          description = "This field allows you to add notes for the set of revisions made to this StackScript."
        }
      4. Create the outputs.tf file:

        ~/linode_stackscripts/modules/stackscripts/output.tf
        1
        2
        3
        
        output "stackscript_id" {
          value = "${linode_stackscript.default.id}"
        }

        The outputs.tf file exposes the value of the linode_stackscript resource’s ID. Every StackScript is assigned a unique ID upon creation. You will need this ID when creating your root module.

      You have now created the StackScripts module and are ready to use both modules within the root module. You will complete this work in the next section.

      Create the Root Module

      The root module will call the linode and stackscripts modules, satisfy their required variables and then apply those configurations to build your desired infrastructure. These configurations deploy a Linode based on a StackScript you will define in this section. When using nested modules, the modules will be hidden from your root configuration, so you’ll have to re-expose any variables and outputs you require.

      1. Ensure you are in the linode_stackscripts directory and create the main.tf file:

        linode_stackscripts/main.tf
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        
        provider "linode" {
            token = "${var.token}"
        }
        
        module "stackscripts" {
            source = "./modules/stackscripts"
            stackscript_label = "${var.stackscript_label}"
            description = "${var.description}"
            stackscript = "${var.stackscript}"
            stackscript_image = [ "${var.stackscript_image}" ]
            rev_note = "${var.rev_note}"
        }
        
        module "linodes" {
            source = "./modules/linodes"
            key = "${var.key}"
            key_label = "${var.key_label}"
            image = "${var.image}"
            label = "${var.label}"
            region = "${var.region}"
            type = "${var.type}"
            root_pass = "${var.root_pass}"
            authorized_keys = [ "${module.linodes.sshkey_linode}" ]
            stackscript_id = "${module.stackscripts.stackscript_id}"
            stackscript_data = "${var.stackscript_data}"
        }

        The main.tf file uses the linodes and stackscripts modules that were created in the previous sections and provides the required arguments. All argument values use interpolation syntax to access variable values, which you will declare in a variables.tf file and then provide corresponding values for in a terraform.tfvars file.

        Let’s review each block:

        1
        2
        3
        
        provider "linode" {
            token = "${var.token}"
        }

        The first stanza declares Linode as the provider that will manage the lifecycle of any resources declared throughout the configuration file. The Linode provider requires your Linode APIv4 token for authentication.

        1
        2
        3
        4
        5
        6
        7
        8
        
        module "stackscripts" {
            source = "./modules/stackscripts"
            stackscript_label = "${var.stackscript_label}"
            description = "${var.description}"
            stackscript = "${var.stackscript}"
            stackscript_image = [ "${var.stackscript_image}" ]
            rev_note = "${var.rev_note}"
        }

        The next stanza instructs Terraform to create an instance of the stackscripts module and instantiate any of the resources defined within the module. The source attribute provides the location of the child module’s source code and is required whenever you create an instance of a module. All other attributes are determined by the module. Notice that all the attributes included in the module block correspond to the linode_stackscript resource’s arguments declared in the main.tf file of the stackscripts module.

         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        
        module "linodes" {
            source = "./modules/linodes"
            key = "${var.key}"
            key_label = "${var.key_label}"
            image = "${var.image}"
            label = "${var.label}"
            group = "${var.group}"
            region = "${var.region}"
            type = "${var.type}"
            root_pass = "${var.root_pass}"
            authorized_keys = [ "${module.linodes.sshkey_linode}" ]
            stackscript_id = "${module.stackscripts.stackscript_id}"
            stackscript_data = "${var.stackscript_data}"
        }

        This stanza creates an instance of the linodes module and then instantiates the resources you defined in the module. Notice that authorized_keys = [ "${module.linodes.sshkey_id}" ] and stackscript_id = "${module.stackscripts.stackscript_id}" both access values exposed as output variables by the linodes and stackscripts modules. Any module’s exposed output variables can be referenced in your root module’s main.tf file.

      2. Create the variables.tf file to declare the input variables required by the module instances:

        ~/linode_stackscripts/variables.tf
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        
        variable "token" {
          description = " Linode API token"
        }
        
        variable "stackscript_label" {
          description = "The StackScript's label is for display purposes only."
        }
        
        variable "description" {
          description = "A description for the StackScript."
        }
        
        variable "stackscript" {
          description = "The script to execute when provisioning a new Linode with this StackScript."
        }
        
        variable "stackscript_image" {
          description = "A list of Image IDs representing the Images that this StackScript is compatible for deploying with."
        }
        
        variable "rev_note" {
          description = "This field allows you to add notes for the set of revisions made to this StackScript."
        }
        
        variable "key" {
          description = "Public SSH Key's path."
        }
        
        variable "key_label" {
          description = "New SSH key label."
        }
        
        variable "image" {
          description = "Image to use for Linode instance."
          default = "linode/ubuntu18.04"
        }
        
        variable "label" {
          description = "The Linode's label is for display purposes only, but must be unique."
          default = "default-linode"
        }
        
        variable "region" {
          description = "The region where your Linode will be located."
          default = "us-east"
        }
        
        variable "type" {
          description = "Your Linode's plan type."
          default = "g6-standard-1"
        }
        
        variable "root_pass" {
          description = "Your Linode's root user's password."
        }
        
        variable "stackscript_data" {
          description = "Map of required StackScript UDF data."
          type = "map"
        }
      3. Create the outputs.tf file:

        ~/linode_stackscripts/outputs.tf
        1
        2
        3
        
        output "stackscript_id" {
          value = "${module.stackscripts.stackscript_id}"
        }

        In the outputs.tf file you will re-expose the output variables exposed by the stackscripts module.

      4. Create the terraform.tfvars file to provide values for all input variables defined in the variables.tf file. This file will exclude any values that provide sensitive data, like passwords and API tokens. A file containing sensitive values will be created in the next step:

        ~/linode_stackscripts/terraform.tfvars
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        
        key = "~/.ssh/id_rsa.pub"
        key_label = "my-ssh-key"
        label = "my-linode"
        stackscript_data {
          my_username = "username"
          my_hostname = "linode-hostname"
        }
        stackscript_id = "base-ubuntu-deployment"
        stackscript_label = "base-ubuntu-deployment"
        description = "A base deployment for Ubuntu 18.04 that creates a limited user account."
        stackscript = <<EOF
        #!/bin/bash
        # <UDF name="my_hostname" Label="Linode's Hostname" />
        # <UDF name="my_username" Label="Limited user account" />
        # <UDF name="my_password" Label="Limited user account's password" />
        # <UDF name="my_userpubkey" Label="Limited user account's public key" />
        
        source <ssinclude StackScriptID="1">
        
        set -x
        
        MY_IP=system_primary_ip
        system_set_hostname "$MY_HOSTNAME"
        system_add_host_entry "$MY_IP" "$MY_HOSTNAME"
        user_add_sudo "$MY_USERNAME" "$MY_PASSWORD"
        user_add_pubkey "$MY_USERNAME" "$MY_USERPUBKEY"
        ssh_disable_root
        goodstuff
        EOF
        stackscript_image = "linode/ubuntu18.04"
        rev_note = "First revision of my StackScript created with the Linode Terraform provider."

        The terraform.tfvars file supplies all values required by the linodes and stackscripts modules. Ensure you replace any values with your own values when using this example file.

        The stackscript variable provides the actual contents of the StackScript you create. This example StackScript requires four UDF values: my_hostname, my_username, my_password, and my_userpubkey. The my_hostname and my_username values are supplied by the stackscript_data map. The my_password and my_userpubkey values will be provided in the next step.

        The StackScript will then use these values to create a limited user account; set a hostname; add a host entry; add the created user to the sudo group; disable SSH access for the root user; and install vim, wget, and less. This StackScript uses bash functions defined in the Linode Community StackScript Bash Library.

      5. Create a file named secrets.tfvars to hold any sensitive values:

        ~/linode_stackscripts/secrets.tfvars
        1
        2
        3
        4
        5
        6
        
        token = "my-linode-api-token"
        root_pass = "my-secure-root-password"
        stackscript_data {
          my_password = "my-limited-users-password"
          my_userpubkey = "my-public-ssh-key"
        }

        This file contains all sensitive data needed for your Linode deployment. Ensure you replace all values with your own secure passwords and your Linode account’s APIv4 token. This file should never be tracked in version control software and should be listed in your .gitignore file if using GitHub.

        Note

      You are now ready to apply your linode_stackscripts module’s Terraform configuration. These steps will be completed in the next section.

      Initialize, Plan and Apply the Terraform Configuration

      Whenever a new provider is used in a Terraform configuration, it must first be initialized. The initialization process downloads and installs the provider’s plugin and performs any other steps needed for its use. Before applying your configuration, it is also useful to view your configuration’s execution plan before making any actual changes to your infrastructure. In this section, you will complete all these steps.

      1. Initialize the Linode provider. Ensure you are in the linode_stackscripts directory before running this command:

        terraform init
        

        You will see a message that confirms that the provider plugins have been successfully initialized.

      2. Run the Terraform plan command:

        terraform plan -var-file="secrets.tfvars" -var-file="terraform.tfvars"
        

        Terraform plan won’t take any action or make any changes on your Linode account. Instead, an analysis is done to determine which actions (i.e. Linode instance creations, deletions, or modifications) are required to achieve the state described in your configuration.

      3. You are now ready to create the infrastructure defined in your root module’s main.tf configuration file:

        terraform apply -var-file="secrets.tfvars" -var-file="terraform.tfvars"
        

        Since you are using multiple variable value files, you must call each file individually using the var-file argument. You will be prompted to confirm the apply action. Type yes and hit enter. Terraform will begin to create the resources you’ve defined throughout this guide. This process will take a couple of minutes to complete. Once the infrastructure has been successfully built you will see a similar output:

          
          Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
            
        
      4. To verify the deployment, retrieve your Linode instance’s IP address:

        terraform show | grep 'ip_address'
        

        You should see a similar output:

          
                ip_address = 192.0.2.0
              
        
      5. Open a new shell session and SSH into your Linode using the IP address you retrieved in the previous step and the username you defined in the terraform.tfvars file’s my_username variable:

        ssh username@192.0.2.0
        

        You should be able to access your Linode and then verify that what you defined in the StackScript was executed.

      Version Control Your Terraform Module

      To make the linode_stackscripts module available to other team members, you can version control it using GitHub. Before completing the steps in this section, ensure you have completed the steps in the Configure Git section of the Getting Started with Git guide.

      1. In the linode_stackscripts directory create a .gitignore file:

        ~/linode_stackscripts/.gitignore
        1
        2
        3
        4
        
        secrets.tfvars
        .terraform/
        terraform/
        terraform.tfstate

        Note

        If there are any files related to the Terraform installation steps completed before beginning this guide (i.e zip files and checksum files), you can remove these files from the linode_stackscripts directory, since you should not track them in version control and they are no longer necessary.

      2. Initialize the git repository:

        git init
        

        Stage all the files you’ve created so far for your first commit:

        git add -A
        
      3. Commit all the linode_stackscripts files:

        git commit -m "Initial commit"
        
      4. Navigate to your GitHub account and create a new repository. Ensure you name the repository the same name as that of your Terraform module. In this example, the GitHub repository will be named linode_stackscripts.

      5. At the top of your GitHub repository’s Quick Setup page, copy the remote repository URL.

      6. Return to your local computer’s linode_stackscripts directory and add the URL for the remote repository:

        git remote add origin https://github.com/my-github/linode_stackscripts.git
        
      7. Push your local linode_stackscripts repository to your remote GitHub repository:

        git push -u origin master
        

      Your Terraform module is now tracked via GitHub and can be used, shared and modified by anyone who has access to your GitHub account.

      Invoking Your GitHub-Hosted Module

      In the future, you can source this module from GitHub within your Terraform module declarations. You would write your module block like the following:

      1
      2
      3
      4
      5
      6
      
      module "linode_stackscripts" {
          source = "github.com/username/linode_stackscripts"
      
          VARIABLES HERE
          . . .
      }

      More Information

      You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.

      Find answers, ask questions, and help others.

      This guide is published under a CC BY-ND 4.0 license.



      Source link

      Create a NodeBalancer with Terraform


      Updated by Linode Contributed by Linode

      Terraform allows you to represent Infrastructure as Code (IaC). You can use it to manage infrastructure, speed up deployments, and share your infrastructure’s configuration files within a team. In this guide you will use Terraform to create a NodeBalancer that distributes traffic between two Linodes.

      Caution

      The configurations and commands used in this guide will result in multiple billable resources being added to your account. Be sure to monitor your account closely in the Linode Manager to avoid unwanted charges. See the Billings and Payments guide for more details.

      If you would like to stop billing for the resources created in this guide, remove them when you have finished your work.

      Before You Begin

      1. You should have Terraform installed in your development environment, and have a working knowledge of Terraform resource configuration and the Linode provider. For more information on how to install and use Terraform, check out our Use Terraform to Provision Linode Environments guide.

      2. Terraform requires an API access token. Follow the Getting Started with the Linode API guide to obtain a token.

      3. Create a terraform_nodebalancer directory on your computer for the Terraform project you will create in this guide. All files you create in this guide should be placed in this directory, and you should run all commands from this directory. This new project should not be created inside another Terraform project directory, including the one you may have made when previously following Use Terraform to Provision Linode Environments.

      Create a Terraform Configuration File

      Create a Provider Block

      The first step to take when creating a Terraform configuration file is to create a provider block. This block lets Terraform know which provider to use. The only configuration value that the Linode provider needs is an API access token.

      Create a file named nodebalancer.tf in your Terraform project directory. You will be adding to this file throughout the guide. Add the provider block to the file:

      nodebalancer.tf
      1
      2
      3
      
      provider "linode" {
          token = "${var.token}"
      }

      This provider block uses variable interpolation to access the value of your API token. You will create input variables in a separate variables.tf file later in the Define Terraform Variables section of this guide. Any input variables you define within the variables.tf file are available from the var dictionary using dot notation. You will be using variable interpolation and referencing variables with dot notation throughout this guide.

      Create a NodeBalancer Resource

      Create a NodeBalancer resource in the nodebalancer.tf file:

      nodebalancer.tf
      1
      2
      3
      4
      5
      6
      7
      8
      
      ...
      
      resource "linode_nodebalancer" "example-nodebalancer" {
          label = "examplenodebalancer"
          region = "${var.region}"
      }
      
      ...

      The linode_nodebalancer resource supplies two labels. The first label, example-nodebalancer, is used internally by Terraform. The second label, examplenodebalancer, is used to reference your NodeBalancer in tools like the Manager and the Linode CLI. The region for this NodeBalancer is supplied with the variable region.

      Create NodeBalancer Config Resources

      In addition to the NodeBalancer resource, you must supply at least one NodeBalancer Configuration resource. This resource defines ports, protocol, health checks, and session stickiness, among other options, that the NodeBalancer might use. For this example, you will create a NodeBalancer configuration for HTTP access on port 80, but you could also create one for HTTPS access on port 443 if you have SSL/TLS certificates:

      nodebalancer.tf
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      
      ...
      
      resource "linode_nodebalancer_config" "example-nodebalancer-config" {
          nodebalancer_id = "${linode_nodebalancer.example-nodebalancer.id}"
          port = 80
          protocol = "http"
          check = "http_body"
          check_path = "/healthcheck/"
          check_body = "healthcheck"
          check_attempts = 30
          check_timeout = 25
          check_interval = 30
          stickiness = "http_cookie"
          algorithm = "roundrobin"
      }
      
      ...

      The NodeBalancer Config resource requires a NodeBalancer ID, which is populated in the first line with the variable linode_nodebalancer.example-nodebalancer.id. Because the nodebalancer_id argument references a NodeBalancer that has not been created yet, you can use this variable as a placeholder to reference the NodeBalancer ID. Terraform will automatically know to create the NodeBalancer resource before it creates any other resources that reference it. In this way you can craft intricate infrastructure that references its own parts, without having to worry about the order the resources appear in the Terraform configuration or whether or not the resources already exist.

      As far as settings go, the health check is set to http_body, meaning that the health check will look for the string set by check_body within the body of the page set at check_path. The NodeBalancer will take a node out of rotation after 30 failed check attempts. Each check will wait for a response for 25 seconds before it is considered a failure, with 30 seconds between checks. Additionally, the session stickiness setting has been set to http_cookie. This means that the user will continue to be sent to the same server by the use of a session cookie. The algorithm has been set to roundrobin, which will sort users evenly across your backend nodes based on which server was accessed last.

      Note

      Create NodeBalancer Node Resources

      The third part of setting up a NodeBalancer in Terraform is creating the NodeBalancer Node resource. This resource contains information about the individual Nodes and how they pertain to the NodeBalancer and NodeBalancer Configuration resources.

      nodebalancer.tf
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      
      ...
      
      resource "linode_nodebalancer_node" "example-nodebalancer-node" {
          count = "${var.node_count}"
          nodebalancer_id = "${linode_nodebalancer.example-nodebalancer.id}"
          config_id = "${linode_nodebalancer_config.example-nodebalancer-config.id}"
          label = "example-node-${count.index + 1}"
          address = "${element(linode_instance.example-instance.*.private_ip_address, count.index)}:80"
          mode = "accept"
      }
      
      ...

      This resource’s count argument will be populated with the node_count input variable you will define later on in this guide. The count argument tells Terraform that it should provision node_count number of Nodes.

      Because provisioning more than one node creates a loop in the Terraform process, where the step for creating a node is repeated, you can use the count.index variable to keep track of which iteration Terraform is on in the loop. The interpolation {count.index + 1} in the node’s label argument tells Terraform that you’d like each of the nodes to be labeled sequentially, and because count.index starts at zero, you’d like the count to begin at one.

      linode_instance.example-instance.*.private_ip_address references the private IP addresses of the yet-to-be-created Linode instances. In the next step, the Linode Instance resources will be labeled example-instance. Terraform has access to some attributes for each of the resources it creates, and private_ip_address is one of the available attributes from a Linode Instance resource. Because there will be two Linode instances created during the same step, Terraform groups these sets of attributes in a list. The element() function allows you to grab a single item from a list based on the item index. Here, instead of providing a hard-coded number for the index you can instead provide count.index. In this way the first NodeBalancer Node will reference the private IP address of the first Linode instance, and the second NodeBalancer Node will reference the private IP address of the second instance.

      Create a Linode Instance Resource

      Now that you have the NodeBalancer configured, you need to supply it with a Linode Instance resource. This resource will allow Terraform to know which instances it needs to create to meet the demand of our NodeBalancer example.

      nodebalancer.tf
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      
      ...
      
      resource "linode_instance" "example-instance" {
          count  = "${var.node_count}"
          label  = "example-instance-${count.index + 1}"
          group = "nodebalancer-example"
          tags = ["nodebalancer-example"]
          region = "${var.region}"
          type = "g6-nanode-1"
          image = "linode/ubuntu18.10"
          authorized_keys = ["${chomp(file(var.ssh_key))}"]
          root_pass = "${random_string.password.result}"
          private_ip = true
      
          provisioner "remote-exec" {
              inline = [
                  # install NGINX
                  "export PATH=$PATH:/usr/bin",
      
                  "apt-get -q update",
                  "mkdir -p /var/www/html/",
                  "mkdir -p /var/www/html/healthcheck",
                  "echo healthcheck > /var/www/html/healthcheck/index.html",
                  "echo node ${count.index + 1} > /var/www/html/index.html",
                  "apt-get -q -y install nginx",
              ]
      
              connection {
                  type = "ssh"
                  user = "root"
                  password = "${random_string.password.result}"
              }
          }
      }
      
      ...

      The above resource uses the same count argument as the NodeBalancer Node resource that was configured in the previous step. Also, the label argument is being sequentially incremented in a similar fashion to the NodeBalancer Node.

      The authorized_keys argument is supplied an SSH key input variable that will be defined later in this guide. It is passed to the file() function, which reads the contents of a file into a string. That string is then passed to the chomp() function, which strips any extra whitespace.

      root_pass is given the result of the random_string resource that will be defined later in this guide.

      The last thing of note in this Linode Instance resource is the remote-exec Provisioner block. This block contains two other components, the inline list and connection block. inline is a list of commands that Terraform will execute on the Linode instance once the Linode instance has booted. In this example, the inline commands will: update the Linode instance, create the necessary directory structure for NGINX, create the health check file and the more generalized index.html file, and install NGINX.

      The connection block explains to Terraform how it should gain access to the Linode instance in order to run the commands supplied by the inline list. In this case, Terraform will gain access over SSH, logging in as the root user.

      Create an Output

      The last step that you’ll take in creating nodebalancer.tf is adding an output. Terraform will add this information to the end of it’s output in the terminal. Outputs can be any information from your configuration you would like to expose. Below is an example that will display the public IP address of the NodeBalancer:

      nodebalancer.tf
      1
      2
      3
      4
      5
      
      ...
      
      output "NodeBalancer IP Address" {
          value = "${linode_nodebalancer.example-nodebalancer.0.ipv4}"
      }

      Define Terraform Variables

      You will now declare all variables required by your Terraform configuration in a variables.tf file.

      1. Create a file called variables.tf. This file will create the variables referenced in the configuration of your NodeBalancer and Nodes. You will supply values to the variables in another step.

        variables.tf
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        
        variable "token" {
            description = "Your APIv4 Access Token"
        }
        
        variable "region" {
            description = "The datacenter where your NodeBalancer and Nodes will reside. E.g., 'us-east'."
            default = "us-west"
        }
        
        variable "node_count" {
            description = "The amount of backend Nodes to create."
        }
        
        variable "ssh_key" {
            description = "The local file location of the SSH key that will be transferred to each Linode."
            default = "~/.ssh/id_rsa.pub"
        }
        
        resource "random_string" "password" {
            length = 32
            special = true
            upper = true
            lower = true
            number = true
        }

        Terraform allows each variable to have its own description and default value. These variables will have their values populated through the use of a terraform.tfvars file that you will create in the next step. Separating the variable definitions from their values helps to keep sensitive data from entering your Terraform code, should you choose to include your code in a version control system like Git.

        In addition to the variables you defined above, there is also a random_string resource with the label password. This resource is provided by the Random provider, and will generate a random string of 32 characters, including uppercase characters, lowercase characters, and numbers. This string will be the root password for your backend Nodes. If you would rather have exact control over your passwords, you can define a password here in variables.tf and set the value for that password in terraform.tfvars in the next step.

      2. Create the terraform.tfvars file and supply values for the token, region, and node_count variables. This example uses the us-east regional datacenter, and the node_count is two.

        terraform.tfvars
        1
        2
        3
        
        token = "your_api_token"
        region = "us-east"
        node_count = 2

        When Terraform runs, it looks for a file named terraform.tfvars, or files with the extension *.auto.tfvars, and populates the Terraform variables with those values. If your SSH key is at a file location that is different than the default value, i.e., it does not exist at ~/.ssh/id_rsa.pub, then you will need to add that value to terraform.tfvars.

        Note

        If you want to use an input variable’s default value defined in the variables.tf file, you can omit providing a value for that variable in the terraform.tfvars file.

        Feel free to change any of the values in the terraform.tfvars file to your liking. For a list of regional datacenter IDs, you can use the cURL command to query the API:

        curl https://api.linode.com/v4/regions
        

      Initializing, Planning, and Applying the Terraform State

      Because this guide employs two providers (Linode and Random) that you might not have installed on your local development environment, you’ll need to run the init command to install them.

      terraform init
      

      You should see a message that Terraform has been successfully initialized.

      To review the Terraform plan of action defined in the nodebalancer.tf file, run the plan command:

      terraform plan
      

      You should see a lengthy output with all the create actions that will take place. Review the output, taking note that the <computed> values you see will be defined on creation. Once you are satisfied with the proposed actions, it’s time to apply them.

      Run the apply command:

      terraform apply
      

      You will be prompted to approve the apply action. Type yes and hit Enter. Terraform will begin to create the resources you have configured in the previous steps. This will take a few minutes, after which you will start to see the output of the the remote-exec commands you defined in your Linode instance resource. Once all of the actions are completed you should see output like the following:

        
      Apply complete! Resources: 7 added, 0 changed, 0 destroyed.
      
      Outputs:
      
      NodeBalancer IP Address = 104.237.148.131
      
      

      Navigate to your NodeBalancer IP address and view your NodeBalancer in action. You have successfully created a NodeBalancer and backend nodes in Terraform.

      (Optional) Remove the NodeBalancer Resources

      If you are done with the resources you just created, you can remove them with the destroy command

      terraform destroy
      

      This command will prompt you to confirm the action.

      More Information

      You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.

      Find answers, ask questions, and help others.

      This guide is published under a CC BY-ND 4.0 license.



      Source link