Note:

Use Packer to create Oracle Cloud Infrastructure custom images

Introduction

This tutorial is about taking an OCI base image, or whatever compute image you already have in OCI and modify it according to the customer needs.

For example, you might need to change the image launch mode from native to paravirtualization or to change the Virtual Network Interface Card (VNIC) attachment. For example, OKE worker nodes need these. Equally you may install additional OS packages or alter the Instance kernel parameters using Packer provisioner (like in Terraform).

Let’s say you need an image on which you want to have PostgreSQL database installed and some ulimit parameters to have other values. All these above can be done by launching a compute from an image and then change whatever you need in it, install the programs, application, packages you need then save the instance as a new Custom Image.

Now imagine you need to create multiple custom image , for multiple architecture and over time you need to add/change the programs or packages added to your custom image. For this, we can use Packer from HashiCorp.

Packer

Packer is HashiCorp’s open-source tool used to create machine images from source configuration. It is lightweight, runs on every major operating system, highly performant. It does not replace configuration management like Chef or Puppet. In fact, when you build images, Packer is able to use tools like Chef or Puppet to install software onto the image. Some of the feature are:

Advantages of using Packer

Packer Authentication

You can authenticate Packer using the following options.

Note: Learn more about Packer Authentication, but the preferable way is to use either the .oci/config file or instance principal.

How Packer works

In the tasks, Packer takes an image specified in the base_image_ocid parameter and deploys a compute instance with the attributes specified by you in the required configuration parameters (i.e. availability*domain, shape, and more).

After that Packer will try to ssh into the instance to run any provisioner (_provisioner “shell”*) you specified in build block on the base image after launching it. For this task you have to consider where you deploy the instance (the subnet_ocid attributes), so Packer is able to ssh in the instance from the machine that runs Packer. At the end Packer snapshots it creates a reusable custom image.

Packer provisioners

Provisioners are important components of Packer that install and configure software within a running machine prior to that machine being turned into a static image. They perform the major work of making the image contain useful software.

Example provisioners include:

Objectives

Prerequisites

Task 1: Create the Packer configuration file

Packer supports HCL or JSON format. In the following task, it’s a HCL format.

  1. Name the file as you want but at the end include .pkr.hcl .

    packer {
        required_plugins {
          oracle = {
            source = "github.com/hashicorp/oracle"
            version = ">= 1.0.3"
          }
        }
    }
    
    source "oracle-oci" "example" {
      availability_domain = "GqIF:EU-FRANKFURT-1-AD-2"
      #you may use an OCID or use a base_image_filter
      #base_image_ocid     = "ocid1.image.oc1.eu-frankfurt-1......"
      base_image_filter  {
        #display_name = "Oracle-Linux-7.9-2023.05.24-0"
        display_name_search =  "^Oracle-Linux-7.9*"
      }
      compartment_ocid    = "ocid1.compartment.oc1......"
      image_name          = "ExampleImage"
      shape               = "VM.Standard1.1"
      ssh_username        = "opc"
      #This is public subnet as I run packer from outside OCI VCN
      subnet_ocid         = "ocid1.subnet.oc1.eu-frankfurt-1....."
      #this attribute will be changed
      nic_attachment_type  = "VFIO"
      #this attribute will be changed
      image_launch_mode = "NATIVE"
      #you may skip the image creation if you are running tests
      #when you want to test set this as true
      skip_create_image = false
    }
    
    build {
      sources = ["source.oracle-oci.example"]
    }
    
  2. The subnet_ocid is the subnet where the instance is provisioned until the custom image is built. This is a subnet that must be reachable from the host running Packer. Normally you wouldn’t want to create development instances with a public IP, as there is no reason for them to be exposed to the internet, and doing so just creates security concerns. One way to avoid using public IP is to run Packer from within OCI or to use the OCI Bastion service. You can also use FastConnect or other VPN to reach the OCI subnet from on-premises without the need to traverse the Internet.

Task 2: Initialize Packer

  1. Run Packer init.

    <path to packer bin> init <packer conf file>
    

    Example:

    <path to packer bin> init oke.pkr.hcl
    

Task 3: Build the image

  1. At this point you have a custom image that will have image_launch_mode = "NATIVE" and nic_attachment_type = "VFIO". There are other attributes you can change. All are described in official doc.

  2. You can run custom commands or script (there are examples attached).

    <path to packer bin> build oke.pkr.hcl
    
    oracle-oci.example: output will be in this color.
    
    ==> oracle-oci.example: Creating temporary ssh key for instance...
    ==> oracle-oci.example: Creating instance...
    ==> oracle-oci.example: Created instance (ocid1.instance.........).
    ==> oracle-oci.example: Waiting for instance to enter 'RUNNING' state...
    ==> oracle-oci.example: Instance 'RUNNING'.
    ==> oracle-oci.example: Instance has IP: 130.61.X.X
    ==> oracle-oci.example: Using SSH communicator to connect: 130.61.X.X
    ==> oracle-oci.example: Waiting for SSH to become available...
    ==> oracle-oci.example: Connected to SSH!
    ==> oracle-oci.example: Creating image from instance...
    ==> oracle-oci.example: Updating image schema...
    ==> oracle-oci.example: Created image (ocid1.image........).
    ==> oracle-oci.example: Terminating instance (ocid1.instance.....)...
    ==> oracle-oci.example: Terminated instance.
    Build 'oracle-oci.example' finished after 4 minutes 37 seconds.
    
    ==> Wait completed after 4 minutes 37 seconds
    
    ==> Builds finished. The artifacts of successful builds are:
    --> oracle-oci.example: An image was created: 'ExampleImage' (OCID: ocid1.image..........'
    

    Example using provisioner:

    • This example uses provisioner (shell and file).

    • It would create 2 images (specified in source).

      variable "VAREXAMPLE" {
        type = string
        default = "val Example"
      }
      
      variable "deploy_in_comp" {
        type = string
        default = "ocid1.compartment.oc1......."
      }
      
      variable "OCI_PROFILE" {
        type = string
        default = "DEFAULT"
      }
      
      locals {
        pkr_root    = "${path.cwd}"           #the directory from where Packer was started
        pkr_scripts = "${path.root}/scripts"  #the directory of the input HCL file or the input folder
        root        = path.root
      }
      
      source "oracle-oci" "img1" {
        access_cfg_file_account = var.OCI_PROFILE
        availability_domain = "GqIF:US-ASHBURN-AD-3"
        base_image_ocid     = "ocid1.image.oc1.iad....."
        compartment_ocid    = "ocid1.compartment.oc1......"
        image_name          = "img1"
        shape               = "VM.Standard.E4.Flex"
        #usage of shape config
        shape_config {
        ocpus               = "1"
        }
        ssh_username        = "opc"
        subnet_ocid         = "ocid1.subnet.oc1.iad....."
      }
      
      source "oracle-oci" "img2" {
        access_cfg_file_account = var.OCI_PROFILE
        availability_domain = "GqIF:US-ASHBURN-AD-3"
        base_image_ocid     = "ocid1.image.oc1.iad....."
        compartment_ocid    = "ocid1.compartment.oc1........."
        image_name          = "img2"
        shape               = "VM.Standard.E4.Flex"
        shape_config {
        ocpus               = "2"
        }
        ssh_username        = "opc"
        subnet_ocid         = "ocid1.subnet.oc1.iad........"
        image_compartment_ocid = var.deploy_in_comp
      }
      
      build {
        # here we secific both source above, img1 and img2
        sources = [
          "source.oracle-oci.img1",
          "source.oracle-oci.img2"
        ]
        provisioner "shell" {
          # this will run on all sources
          inline = [
            "echo to run on all sources ${var.VAREXAMPLE}"
          ]
        }
        provisioner "shell" {
          # This provisioner only runs for img1 source.
          only = ["oracle-oci.img1"]
          inline = [
            "echo To run in img1",
          ]
        }
        provisioner "shell" {
          # This provisioner  runs on all but img1 source.
          # Mnd that there is no "source" string but only the name of the source
          except = ["oracle-oci.img1"]
          inline = [
            "echo To run in all except img1",
          ]
        }
        provisioner "file" {
          only = ["oracle-oci.img2"]
          source = "${local.pkr_scripts}/test.txt"
          destination = "/tmp/test.txt"
        }
        # This will run local on the machine you run packer
        provisioner "shell-local" {
          inline = ["echo ${local.pkr_root} and ${local.pkr_scripts}"]
        }
        # This will run local on the machine you run packer
        provisioner "shell-local" {
          inline = ["echo VAR from file :${var.VAREXAMPLE}"]
        }
      }
      
      
    • It uses variables (like in Terraform).

      image_compartment_ocid = var.deploy_in_comp
      
    • the Auth method is with .oci/config file (the default) but it use an atttribute to specify wich profile within the file to use.

      access_cfg_file_account = var.OCI_PROFILE
      
    • In this we use provisioner “shell” and provisioner “file” and provisioner “shell-local”.

    • The shell Packer provisioner provisions machines built by Packer using shell scripts. Shell provisioning is the easiest way to get software installed and configured on a machine.

    • You might either run shell command on the machine (use inline) or to execute e script (use script).

    • We used the file provisioner as well. The file Packer provisioner uploads files to machines built by Packer.

    • You might see more example in the hcl file that you can download prov-example.pkr.hcl .

    Example using variable files and Ansible provisioners :

    • We use:

      • variable files.
      • a different Auth method - using variables.
    • Instead of using shell to install packages we are using ansible to runs playbooks stored in a separate folder.

    • This method is for more complex situation that can be handled better by Ansible.

    • There are two ansible provisioner:

      • ansible-local : Needs ansible installed on the host that Packer will build (you can use provisioner shell to install Ansible).
      • ansible: It will run ansible from host that runs Packer so no need to be present on the instance you build.
    • there are multiple configuration files involved so you have to specify the directory name when you build.

      packer build <directory name>
      
    • Here the example file ansible-example.pkr.hcl. Below is the ansible-example.pkr.hcl.

      # mind there is a variables.pkr.hcl file where
      # other vars are set
      # so when build use: packer build <directory name>
      # so all the files within dir will be loaded
      
      
      source "oracle-oci" "img1" {
      #auth vars
      region        = var.REGION
      tenancy_ocid  = var.TENANCY_OCID
      user_ocid     = var.USER_OCID
      key_file      = var.KEY_FILE
      fingerprint   = var.FINGERPRINT
      #
      availability_domain = var.AD
      base_image_ocid     = var.BASE_IMG_OCID
      compartment_ocid    = var.COMP_IMG_OCID
      image_name          = var.IMG_NAME
      shape               = var.SHAPE
      shape_config {
      ocpus               = var.SHAPE_OCPUS
      }
      ssh_username        = "opc"
      subnet_ocid         = var.SUBNET_OCID
      # for testing and debug. remove it after
      skip_create_image   = true
      }
      
      source "oracle-oci" "img2" {
      #auth vars
      region        = var.REGION
      tenancy_ocid  = var.TENANCY_OCID
      user_ocid     = var.USER_OCID
      key_file      = var.KEY_FILE
      fingerprint   = var.FINGERPRINT
      #
      availability_domain = var.AD
      base_image_ocid     = var.BASE_IMG_OCID
      compartment_ocid    = var.COMP_IMG_OCID
      image_name          = var.IMG_NAME1
      shape               = var.SHAPE
      shape_config {
      ocpus               = var.SHAPE_OCPUS
      }
      ssh_username        = "opc"
      subnet_ocid         = var.SUBNET_OCID
      # for testing and debug. remove it after
      skip_create_image   = true
      }
      
      build {
      sources = [
        "source.oracle-oci.img1",
        "source.oracle-oci.img2"
      ]
      
      #need ansible installed on VM that packer will build
      #so you need to install ansible beforehand
      
      provisioner "shell" {
        # this will run on all sources
        inline = [
          "sudo yum install ansible -y"
        ]
      }
      
      # this provisioner ansible-local needs ansible installed
      # on the host that packer will builds
      
      provisioner "ansible-local" {
        playbook_file   = "${path.root}/ansible/pb1.yaml"
      }
      
      # This provisioner ansible
      # will run ansible playbook from host that runs packer
      # so you don't need to inmstall ansible on VM that packer wil create
      
      #provisioner "ansible" {
      #  playbook_file   = "${path.root}/ansible/pb-remote.yaml"
      #  user = "opc"
      #  # in case you need to debug ansible tasks
      #  extra_arguments = [ "-vvvv" ]
      #}
      }
      
      
    • Here is the vars.pkr.hcl. In this file we declare and initialize them.

      variable "AD" {
      type = string
      default = "GqIF:US-ASHBURN-AD-3"
      }
      
      variable "BASE_IMG_OCID" {
      type = string
      default = "ocid1.image.oc1.iad......"
      }
      
      variable "COMP_IMG_OCID" {
      type = string
      default = "ocid1.compartment.oc1......."
      }
      
      variable "IMG_NAME" {
      type = string
      default = "img1"
      }
      
      variable "IMG_NAME1" {
      type = string
      default = "img2"
      }
      
      variable "SHAPE" {
      type = string
      default = "VM.Standard.E3.Flex"
      }
      
      variable "SHAPE_OCPUS" {
      type = string
      default = "1"
      }
      
      variable "SUBNET_OCID" {
      type = string
      default = "ocid1.subnet.oc1.iad......"
      }
      
    • Here is the auth-vars.pkr.hcl, we use this file to setup the authentication.

      variable "REGION" {
        type = string
        default = "us-ashburn-1"
      }
      
      variable "TENANCY_OCID" {
        type = string
        default = "ocid1.tenancy.oc1......."
      }
      
      variable "USER_OCID" {
        type = string
        default = "ocid1.user.oc1......"
      }
      
      variable "KEY_FILE" {
        type = string
        default = ".....oci_api_key.pem"
      }
      
      variable "FINGERPRINT" {
        type = string
        default = "...."
      }
      

Acknowledgments

Author - Francisc Vass (Principal Cloud Architect)

More Learning Resources

Explore other labs on docs.oracle.com/learn or access more free learning content on the Oracle Learning YouTube channel. Additionally, visit education.oracle.com/learning-explorer to become an Oracle Learning Explorer.

For product documentation, visit Oracle Help Center.