linuxbashterraformcloud-init

How I can provide arguments into my script if run as userdata in terraform?


I made a script that setups a LEMP stack:

#!/usr/bin/env bash

if tput colors >/dev/null 2>&1; then
  RED='\033[0;31m'
  YELLOW='\033[1;33m'
  CYAN='\033[1;35m'
  NC='\033[0m' # No Color
else
  RED=''
  GREEN=''
  YELLOW=''
  NC=''
fi

print_help() {
  echo -e "Usage: ${YELLOW}$0${NC} [options]"
  echo -e "${CYAN}Options:${NC}"
  echo "  --php_ver <version>        Specify PHP version (default is 8.2)"
  echo "  --nodb                     Do not install any database"
  echo "  --db_root_password <pass>  Set root password for the database"
  echo "  -h, --help                 Show this help message"
}

cleanup () {
  echo -e "${CYAN}Cleanup${NC}"
  apt-get autoremove && apt-get autoclean
  exit 0;
}

if [ "$EUID" -ne 0 ]; then 
  echo -e "${RED}ERROR: Run this script as root or via using sudo.${NC}"
  echo
  print_help
  exit 1;
fi

export DEBIAN_FRONTEND=noninteractive

PHP_VERSION="8.2"
DB_TYPE="mariadb"


while [ "$1" != "" ]; do
  case $1 in
    "--php_ver")
      PHP_VERSION=$2
      shift 2
      ;;
    
    "--nodb") 
        DB_TYPE="none"
        shift
        ;;

    "--db_root_password")
      DB_ROOT_PASSWORD=$2
      shift 2
      ;;

    "-h" | "--help")
      print_help
      exit 0
      ;;

    *)
      echo -e " ${RED}Invalid option: ${YELLOW}$1${NC}"
      exit 1
      ;;
  esac
done

 apt-get update &&  apt-get upgrade -y


if [ "$PHP_VERSION"  == "" ]; then
    echo -e "${RED}No php version provided defaulting into 8.2${NC}"
    PHP_VERSION="8.2"
fi

echo -e "${CYAN}PHP ${YELLOW}$PHP_VERSION${CYAN} will be installed ${NC}"

apt-get install -y nginx ca-certificates apt-transport-https software-properties-common
add-apt-repository -y ppa:ondrej/php 
apt-get update

apt-get install -y php${PHP_VERSION}-fpm  \
    php${PHP_VERSION}-mbstring \
    php${PHP_VERSION}-mysql \
    php${PHP_VERSION}-oauth \
    php${PHP_VERSION}-opcache \
    php${PHP_VERSION}-readline \
    php${PHP_VERSION}-xml

if [ "$DB_TYPE" == 'none' ];then
  echo -e  "${YELLOW}No Db support will be installed${NC}"
  cleanup
  exit 0;
fi

POOL_CONF="/etc/php/${PHP_VERSION}/fpm/pool.d/www.conf"
if [ -f "$POOL_CONF" ]; then
  echo -e "${CYAN}Configuring PHP-FPM to listen on ${YELLOW}127.0.0.1:9000${NC}"
  sed -i "s|^listen = .*|listen = 127.0.0.1:9000|" "$POOL_CONF"
  systemctl restart php${PHP_VERSION}-fpm
else
  echo -e "${RED}Failed to configure PHP-FPM: ${POOL_CONF} not found${NC}"
  exit 1
fi

echo -e "${CYAN}Configuring default Vhost${NC}"

rm -rf /var/www/html/*

echo "<?php phpinfo();" > /var/www/html/index.php
systemctl stop nginx

cat >/etc/nginx/sites-available/default <<EOL
server {
    listen 80 default_server;
    listen [::]:80 default_server;

    root /var/www/html;

    index index.php index.html index.htm index.nginx-debian.html;

    server_name _;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
    
    
        # With php-cgi (or other tcp sockets):
        fastcgi_pass 127.0.0.1:9000;
    }

    location ~ /\.ht {
        deny all;
    }
}
EOL

systemctl start nginx

echo -e "${CYAN}Installing ${YELLOW}${DB_TYPE}${NC}"

apt-get -y install mariadb-server mariadb-client


if [ "$DB_ROOT_PASSWORD" == "" ]; then
  echo -e "${YELLOW}DB Root password is missing. skipping${NC}"
  cleanup
  exit 0;
fi

echo "${CYAN}Provisioning Root User${NC}"

# Make sure that NOBODY can access the server without a password
 mysql -e "UPDATE mysql.user SET Password = PASSWORD('${DB_ROOT_PASSWORD}') WHERE User = 'root'"
# Kill the anonymous users
 mysql -e "DROP USER ''@'localhost'"
# Because our hostname varies we'll use some Bash magic here.
 mysql -e "DROP USER ''@'$(hostname)'"
# Kill off the demo database
 mysql -e "DROP DATABASE IF EXISTS test"
# Make our changes take effect
 mysql -e "FLUSH PRIVILEGES"

As you can see I need to provide the following arguments:

The script is located alongside the terraform modules. Therefore how I can execute it but also provide the nessesary arguments towards the script as well:

resource "aws_instance" "instance" {
    ami="ami-0d342235295932397"
    instance_type="t3a.micro"
    key_name = "ssh_key"
    iam_instance_profile = "myInstance"

    root_block_device {
        volume_size = 30
        volume_type = "gp3"
    }

    # rest of nessesary arguments
    userdata= # execute script here
}

I want to avoid modifying the script so I can use it outside terraform as well.


Solution

  • As far as Terraform and AWS are concerned, user_data is literally just a bunch of bytes that get saved in the EC2 API without any specific meaning. Software running inside your EC2 instance then retrieves that data using the Instance metadata and user data API and decides for itself how to interpret that data.

    For most general-purpose Linux machine images, the software handling user_data is cloud-init, and so the interpretation of your user_data content is as described in User data formats.

    You are currently using the User data script interpretation, which just writes the given data to disk as an executable file and tries to execute it. In that case cloud-init doesn't pass any arguments to the script, so your current script is not suitable for use in this way.

    The most flexible option is to set user_data to Cloud config data, which is a YAML format defined by cloud-init that allows describing various different actions cloud-init should take when it runs during system boot.


    cloud-init has two "modules" that are potentially useful for your requirement: Write Files to write arbitrary small files into the filesystem, and Bootcmd to run an inline script once during early boot.

    The following configuration generates a Cloud Config user_data that instructs cloud-init to first write the bootstrapping script to a specific location on disk, and then to run another small script that executes that file with specific arguments:

    resource "aws_instance" "instance" {
      # ...
    
      user_data = <<-EOT
        #cloud-config
        ${yamlencode({
          write_files = [
            {
              encoding    = "b64"
              content     = filebase64("${path.module}/setup-lemp.sh")
              owner       = "root:root"
              path        = "/usr/local/bin/setup-lemp"
              permissions = "0755"
            },
          ]
          bootcmd = [
            [
              "cloud-init-per", "once", "setup-lemp",
              "/usr/local/bin/setup-lemp",
              "--db_root_password", "foo",
            ],
          ]
        })}
      EOT
    }
    

    The bootcmd module is configured to run the following command line:

    cloud-init-per once setup-lemp /usr/local/bin/setup-lemp --db_root_password foo
    

    This uses a cloud-init helper tool called cloud-init-per, which arranges for the given command to be executed later in the boot process. bootcmd actually runs before write_files in a typical cloud-init configuration, so this extra helper allows deferring the actual execution of the script until later in the process after the file should already have been written.


    Some considerations and caveats to keep in mind: