Deploy local files to instances without using Terraform file provisioners - terraform

As several other users who have posted to StackOverflow, I ran into problems with file provisioners, and the Terraform documentation says we should not rely on them.
What's the best way to work around file provisioners - specifically for local config files and scripts?

One solution, which works very well and does not require a direct connection to the instance, is to use the userdata as a hook to "install" the files from the base64 version of the file(s).
We can actually embed the files as base64 strings in the userdata initialization scripts. This works for both Windows and Linux instances in AWS, and is compatible also with having a userdata script run on startup.
Solution Description:
During terraform plan, encode whatever local files you need as base64 strings using terraform functions base64encode(file("path/to/file")).
(Optional) Save a marker file (_INIT_STARTED_) at the start of userdata execution; this file will have the creation timestamp of when the userdata execution started.
Before running the actual userdata script, write the base64 strings to text files. (The actual command varies between windows and linux, see examples below.)
Run the userdata script itself (userdata_win.bat or userdata_lin.sh)
(Optional) Finally, save a second marker file (_INIT_COMPLETE_) which will have the creation timestamp of when the userdata script completed. (The absence of this file is also helpful to detect script failures and/or still-running scripts after logging into the instance.)
For AWS Linux instances:
data "template_file" "userdata_lin" {
template = <<EOF
#!/bin/bash
mkdir -p /home/ubuntu/setup-scripts
cd /home/ubuntu/setup-scripts
touch _INIT_STARTED_
echo ${base64encode(file("${path.module}/userdata_lin.sh"))} | base64 --decode > userdata.sh
echo ${base64encode(file("${path.module}/config.json"))} | base64 --decode > config.json
${file("${path.module}/userdata_lin.sh")}
sudo chmod 777 *
touch _INIT_COMPLETE_
EOF
}
# ...
resource "aws_instance" "my_linux_instance" {
# ...
user_data = data.template_file.userdata_lin.rendered
}
For AWS Windows instances:
data "template_file" "userdata_win" {
template = <<EOF
<script>
mkdir C:\Users\Administrator\setup-scripts
cd C:\Users\Administrator\setup-scripts
echo "" > _INIT_STARTED_
echo ${base64encode(file("${path.module}/userdata_win.bat"))} > tmp1.b64 && certutil -decode tmp1.b64 userdata.bat
echo ${base64encode(file("${path.module}/config.json"))} > tmp2.b64 && certutil -decode tmp2.b64 config.json
${file("${path.module}/userdata_win.bat")}
echo "" > _INIT_COMPLETE_
</script>
<persist>false</persist>
EOF
}
# ...
resource "aws_instance" "my_windows_instance" {
# ...
user_data = data.template_file.userdata_win.rendered
}

Related

Unable to create files in $HOME directory using user_data

I've always been puzzled why I cannot create files in $HOME directory using user_data when using an aws_instance resource. Even a simple "touch a.txt" in user_data would not create the file.
I have worked around this by creating files in other directories (e.g. /etc/some_file.txt) instead. But I am really curious what's the reason behind this & if there is a way to create files in $HOME with user_data.
Thank you.
----- 1st edit -----
Sample code:
resource "aws_instance" "ubuntu" {
ami = var.ubuntu_ami
instance_type = var.ubuntu_instance_type
subnet_id = aws_subnet.ubuntu_subnet.id
associate_public_ip_address = "true"
key_name = var.key_name
vpc_security_group_ids = [aws_security_group.standard_sg.id]
user_data = <<-BOOTSTRAP
#!/bin/bash
touch /etc/1.txt # this file is created in /etc/1.txt
touch 2.txt # 2.txt is not created in $HOME/2.txt
BOOTSTRAP
tags = {
Name = "${var.project}_eks_master_${count.index + 1}"
}
}
I am not sure what is the default path used at user_data but I did a simple test and I found the solution to your problem.
In an EC2 Instance, I tried this in my user_data
user_data = <<-EOF
#! /bin/bash
sudo bash -c "pwd > /var/www/html/path.html"
The result was this:
root#ip-10-0-10-10:~# cat /var/www/html/path.html
/
Did you check if you have this file created?
ls -l /2.txt
Feel free to reach me if you have any doubts.
I think I found the answer to my own question. The $HOME environment variable does not exist at the time the user_data script is run.
I tried to 'echo $HOME >> /etc/a.txt' and I got a blank line. And instead of creating a file using 'touch $HOME/1.txt', I tried 'touch /home/ubuntu/1.txt' and the file 1.txt was created.
So, I can only conclude that $HOME does not exist at the time user_data was run.
----- Update 1 -----
Did some further testing to support my findings above. When I ran sudo bash -c 'echo $HOME > /etc/a.txt', it gave me the result of /root in the file /etc/a.txt. But when I ran echo $HOME > /etc/b.txt, the file /etc/b.txt contained 0xA (just a single linefeed character).
Did another test by running set > /etc/c.txt to see if $HOME was defined & $HOME didn't exist amongst the environment variables listed in /etc/c.txt. But once the instance was up, and I ran set via an SSH session, $HOME existed & had the value /home/ubuntu.
I also wondered who was running during the initialization so I tried who am i > /etc/d.txt. And /etc/d.txt was a 0-byte file. So, now I don't know which user is running during the EC2 instantiation.

Is there a way that i can download files from different URL's based on user input?

There are two releases
1. Dev available at https://example.com/foo/new-package.txt
2. GA available at https://example.com/bar/new-package.txt
I want the user to enter his choice of Dev or GA and based on that need to download the files, in the shell script is there a better way to do it?
There is a file which has environment variables that I'm sourcing inside another script.
env_var.sh
#!/bin/bash
echo "Enter your release"
export release='' #either Dev or GA
This file will be sourced from another script as
download.sh
#!/bin/bash
. ./env_var.sh #sourcing a environment var file
wget https://<Dev or GA URL>/new-package.txt
My main problem is how to set the wget URL based on the release set in env_var file.
Any help is appreciated.
Have you considered using read to get the user input?
read -p 'Selection: ' choice
You could then pass ${choice} to a function that has case statements for the urls:
get_url() {
case $1 in
'dev' ) wget https://example.com/foo/new-package.txt ;;
'ga' ) wget https://example.com/bar/new-package.txt ;;
\? ) echo "Invalid choice" ;;
esac
}
For more information on read, a good reference is TLDP's guide on user input.
Edit: To source a config file, run the command source ${PATH_TO_FILE}. You would then be able to pass the variable to the get_url() function for the same result.

How does one create a wrapper around a program?

I want to learn to create a wrapper around a program in linux. How does one do this? A tutorial reference web-page/link or example will do. To clarify what I want to learn, I will explain with an example.
I use vim for editing text files. And use rcs as my simple revision control system. rcs allows you to check-in and checkout-files. I would like to create a warpper program named vir which when I type in the shell as:
$ vir temp.txt
will load the file temp.txt into rcs with ci -u temp.txt and then allows me to edit the file using vim.
When I get out and go back in, It will need to check out the file first, using ci -u temp.txt and allow me to edit the file as one normally does with vim, and then when I save and exit, it should check-in the file using co -u temp.txt and as part of that I should be able to add a version control comment.
Basically, all I want to be doing on the command line is:
$ vir temp.txt
as one would with vim. And the wrapper should take care of the version control for me.
Take a look at rcsvers.vim, a vim plugin for automatically saving versions in RCS; you could modify that. There are also other RCS plugins for vim at vim.org
I have a wrapper to enhance the ping command (using zsh) it could, maybe help you:
# ping command wrapper - Last Change: out 27 2019 18:47
# source: https://www.cyberciti.biz/tips/unix-linux-bash-shell-script-wrapper-examples.html
ping(){
# Name: ping() wrapper
# Arg: (url|domain|ip)
# Purpose: Send ping request to domain by removing urls, protocol, username:pass using system /usr/bin/ping
local array=( $# ) # get all args in an array
local host=${array[-1]} # get the last arg
local args=${array[1,-2]} # get all args before last arg in $#
#local _ping="/usr/bin/ping"
local _ping="/bin/ping"
local c=$(_getdomainnameonly "$host")
[ "$host" != "$c" ] && echo "Sending ICMP ECHO_REQUEST to \"$c\"..."
# pass args and host
# $_ping $args $c
# default args for ping
$_ping -n -c 2 -i 1 -W1 $c
}
_getdomainnameonly(){
# Name: _getdomainnameonly
# Arg: Url/domain/ip
# Returns: Only domain name
# Purpose: Get domain name and remove protocol part, username:password and other parts from url
# get url
local h="$1"
# upper to lowercase
local f="${h:l}"
# remove protocol part of hostname
f="${f#http://}"
f="${f#https://}"
f="${f#ftp://}"
f="${f#scp://}"
f="${f#scp://}"
f="${f#sftp://}"
# Remove username and/or username:password part of hostname
f="${f#*:*#}"
f="${f#*#}"
# remove all /foo/xyz.html*
f=${f%%/*}
# show domain name only
echo "$f"
}
What it hides the local ping using a function called "ping", so if your script has precedence on your path it will find at first the function ping. Then inside the script I define an internal variable called ping that points out to the real ping command:
local _ping="/bin/ping"
You can also notice that the args are stored in one array.

How can I write own cloud-config in cloud-init?

cloud-init is powerful to inject user-data in to VM instance, and its existing module provides lots of possibility.
While to make it more easy to use, I want to define my own tag like coreos below, see detail in running coreos in openstack
#cloud-config
coreos:
etcd:
# generate a new token for each unique cluster from https://discovery.etcd.io/new
discovery: https://discovery.etcd.io/<token>
# multi-region and multi-cloud deployments need to use $public_ipv4
addr: $private_ipv4:4001
peer-addr: $private_ipv4:7001
units:
- name: etcd.service
command: start
- name: fleet.service
command: start
So I could have something like below using my own defined tag/config myapp
#cloud-config
myapp:
admin: admin
database: 192.168.2.3
I am new to cloud-init, is it called module ? it is empty in document http://cloudinit.readthedocs.org/en/latest/topics/modules.html
Can you provide some information to describe how I can write my own module ?
You need to write a "cc" module in a suitable directory, and modify a few configurations. It is not terribly easy, but certainly doable (we use it a lot).
Find the directory for cloudconfig modules. On Amazon Linux, this is /usr/lib/python2.6/site-packages/cloudinit/config/, but the directory location differs in different cloud init versions and distributions. The easiest way to find this is to find a file named cc_mounts.py.
Add a new file there, in your case cc_myapp.py. Copy some existing script as a base to know what to write there. The important function is def handle(name,cfg,cloud,log,args): which is basically the entrypoint for your script.
Implement your logic. The cfg parameter has a python object which is the parsed YAML config file. So for your case you would do something like:
myapp = cfg.get('myapp')
admin = myapp.get('admin')
database = myapp.get('database')
Ensure your script gets called by cloud-init. If your distribution uses the standard cloud-init setup, just adding the file might work. Otherwise you might need to add it to /etc/cloud/cloud.cfg.d/defaults.cfg or directly to /etc/cloud/cloud.cfg. There are keys called cloud_init_modules, cloud_config_modules, etc. which correspond to different parts of the init process where you can get your script run. If this does not work straight out of the box, you'll probably have to do a bit of investigation to find out how the modules are called on your system. For example, Amazon Linux used to have a hardcoded list of modules inside the init.d script, ignoring any lists specified in configuration files.
Also note that by default your script will run only once per instance, meaning that rerunning cloud-init will not run your script again. You need to either mark the script as being per boot by setting frequency to always in the configuration file listing your module, or remove the marker file saying that the script has run, which lives somewhere under /var/lib/cloud like in /var/lib/cloud/instances/i-86ecc763/sem/config_mounts.
paste my note for you:
config: after installed cloud-init in VM,if u want to have root permission to access with passwd, do simple config below
modify /etc/cloud/cloud.cfg like below
users:
- defaults
disable_root:0
ssh_pwauth: 1
Note: ssh_pwauth: "it will modify PasswordAuthentication in sshd_config automatically, 1 means yes
Usage:
the behavior of cloud-init can configured using user data. user data can be filled by user during the start of instance (user data is limited to 16K).
Mainly there are several ways to do (tested):
user-data script
$ cat myscript.sh
#!/bin/sh
echo "Hello World. The time is now $(date -R)!" | tee /root/output.txt
when starting instance, add parameter --user-data myscript.sh, and the instance will run the script once during startup and only once.
cloud config syntax:
It is YAML-based, see http://bazaar.launchpad.net/~cloud-init-dev/cloud-init/trunk/files/head:/doc/examples/
run script
#cloud-config
runcmd:
- [ ls, -l, / ]
- [ sh, -xc, "echo $(date) ': hello world!'" ]
- [ sh, -c, echo "=========hello world'=========" ]
- ls -l /root
- [ wget, "http://slashdot.org", -O, /tmp/index.html ]
change hostname, password
#cloud-config
chpasswd:
list: |
root:123456
expire: False
ssh_pwauth: True
hostname: test
include format
run url script, it will download URL script and execute them sequence, this can help to manage the scripts centrally.
#include
http://hostname/script1
http://hostname/scrpt2

Can I customize the svnadmin create process?

I have many different repositories setup on my server. I need to have an identical post-commit hook file in every one of those repos. Simple enough for existing, but is there a way to have calls to svnadmin create automatically copy a post-commit stub file to the new hooks directory? Essentially I'm looking for a post-svnadmin-create hook. Thanks!
I think your best bet would be to wrap the call to svnadmin create in a script that creates the hooks after the repo.
Agreed as long as there is not some built-in way, which there seems not to be. I would have expected subversion to sport something like the customizable skeleton directory for new Linux users. Too bad.
Here is my wrapper with comments if anyone can find it useful - should be fairly extendable. If anyone notices any glaring gotchas in it, don't hesitate - I'm neither a bash nor Linux expert but I think I got most of it covered, and it works :)
# -----------------------------------------------------------------------
# A wrapper for svnadmin to allow post operations following repo creation - copying custom
# hook files into repo in this case. This should be run as root.
# capture input args; note that args[0] == $#[1] (this script name is not captured here)
args=("$#");
# redirect args to svnadmin in all cases - this script should not modify the behavior of svnadmin.
# note: the original binary "/binary_path/svnadmin" has been renamed "/binary_path/svnadmin-wrapped" and
# this script was then named "/binary_path/svnadmin" and given identical user:group & permissions as
# the original.
sudo -u svnuser svnadmin-wrapped ${args[#]};
# capture return code so we can return on exit; svnadmin returns 0 for success
eCode=$?;
# find out if sub-command to svnadmin was "create" and, if so, note the index of the directory arg,
# which is not necessarily going to be in the same position each time (options may be specified
# before the sub-command).
path_idx=0;
found=0;
for i in ${args[#]}
do
# track index; pre-incerement
((path_idx++));
if [ $i == "create" ]
then
# found repo path
((found++));
break;
fi
done
# we now know if the subcommand was create and where the repo path is - finish up as needed.
# note that this block assumes that our hook file stubs are /stub_path/ (owned by root)
# and that there exists a custom log file at /stub_path/cust-log (also owned by root).
d=`date`;
if [ $found != 0 ]
then
# check that the command succeeded
if [ $eCode == 0 ]
then
# check that the directory exists
if [ -d "${args[$path_idx]}/hooks" ]
then
# copy our custom hooks into place
sudo -u svnuser cp "/stub_path/post-commit" "${args[$path_idx]}/hooks/post-commit";
sudo -u svnuser cp "/stub_path/post-revprop-change" "${args[$path_idx]}/hooks/post-revprop-change";
else
# unlikey failure; set custom error code here; log issue
echo "$d svnadmin wrapper error: svnadmin 'create' succeeded but the 'hooks' directory was not found! Params: ${args[#]}" >> "/stub_path/cust-log";
let "eCode=1325";
fi
else
# tried to create but svnadmin failed; log issue
echo "$d svnadmin wrapper error: svnadmin 'create' was called but failed! Params: ${args[#]}" >> "/stub_path/cust-log";
fi
fi
exit $eCode;
-Thanks to all who host and post!

Resources