in AWS, is user_data executed before cloud-init?
Asked Answered
E

1

8

I use terraform to create an EC2 instance, and I use user_data to place a file in /var/lib/cloud/scripts/per-once. This is not executed - my question now is: is cloud-init run before user_data?

===EDIT===

A longer reply to Dude0001's very helpful answer:

I have tried the following, now - this is my user_data:

#!/bin/bash
cat >/var/lib/cloud/scripts/per-once/install_mysql <<!
#cloud-config

package_update: true

packages:
  - mysql-server

!

cat >>/root/.bashrc <<!
set -o vi
unalias -a
alias ll='ls -lp'
!

cat >>/home/admin/.bashrc <<!
set -o vi
unalias -a
alias ll='ls -lp'
!

cat /root/.vimrc <<!
set t_ti= t_te=
set compatible
set expandtab ts=2 sw=2 ai
!

cat >/home/admin/.vimrc <<!
set t_ti= t_te=
set compatible
set expandtab ts=2 sw=2 ai
!

This creates all the files, as expected (I'm really old-fashioned and don't like most of vim's new features). I tried to reboot after the instance was created: no mysqld. I changed the permissions, chmod 755 /var/lib/cloud/scripts/per-once/install_mysql, and rebooted: no result either (the reason I changed permissions is that it appears from the python code that cloud-init looks for executables only).

===EDIT===

Some explanations to my user_data above:

This construction may mystify some, since it isn't too common:

cat >/some/path/to/a/file <<!
...
!

cat is a command that simply read from the standard input and writes to the standard output without change - it is often used with redirection < and >. In the construction above, I direct any output to a file /some/path/to/a/file. The other part, involving <<! and ! is known as a here document, something that has its origin in the JCL language used on mainframes, I suspect, but it is really useful. What is means is read the following lines until the end-marker (here: !, but it could be any string). So, all in all, it says create a file with the following content: ....

The first file, /var/lib/cloud/scripts/per-once/install_mysql, contains:

#cloud-config

package_update: true

packages:
  - mysql-server

My hope is that this should tell cloud-init to update the package repository and install mysql-server - this doesn't happen.

The next 4 files are just some setup in the root and admin users' environments; basically, I create a .vimrc and add a few lines to .bashrc to ensure that certain things are set up to my liking.

The files are all created, but the one with #cloud-config doesn't seem to get touched at all. I have done a few experiments yesterday, by placing this file in different directories under /var/lib/cloud/scripts/, but it looks a lot as if these files aren't in place for when cloud-init reads the directories. Reading through cloud-init's source code, it looks as if it runs through 10 stages - user_data is fetched in stage 5, and it should be read in stage 7. I can also see that it seems to require the execute permission bit to be set; however this is what is in the log after a reboot:

2019-10-02 08:06:52,884 - handlers.py[DEBUG]: start: modules-final/config-scripts-per-boot: running config-scripts-per-boot with frequency always
2019-10-02 08:06:52,884 - helpers.py[DEBUG]: Running config-scripts-per-boot using lock (<cloudinit.helpers.DummyLock object at 0x7f677362acc0>)
2019-10-02 08:06:52,885 - util.py[DEBUG]: Running command ['/var/lib/cloud/scripts/per-boot/install_mysql'] with allowed return codes [0] (shell=False, capture=False)
2019-10-02 08:06:52,887 - util.py[WARNING]: Failed running /var/lib/cloud/scripts/per-boot/install_mysql [-]
2019-10-02 08:06:52,887 - util.py[DEBUG]: Failed running /var/lib/cloud/scripts/per-boot/install_mysql [-]
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/cloudinit/util.py", line 1992, in subp
    env=env, shell=shell)
  File "/usr/lib/python3.7/subprocess.py", line 775, in __init__
    restore_signals, start_new_session)
  File "/usr/lib/python3.7/subprocess.py", line 1522, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
OSError: [Errno 8] Exec format error: b'/var/lib/cloud/scripts/per-boot/install_mysql'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/cloudinit/util.py", line 835, in runparts
    subp(prefix + [exe_path], capture=False)
  File "/usr/lib/python3/dist-packages/cloudinit/util.py", line 2000, in subp
    stderr="-" if decode else b"-")
cloudinit.util.ProcessExecutionError: Exec format error. Missing #! in script?
Command: ['/var/lib/cloud/scripts/per-boot/install_mysql']
Exit code: -
Reason: [Errno 8] Exec format error: b'/var/lib/cloud/scripts/per-boot/install_mysql'
Stdout: -
Stderr: -
2019-10-02 08:06:52,897 - cc_scripts_per_boot.py[WARNING]: Failed to run module scripts-per-boot (per-boot in /var/lib/cloud/scripts/per-boot)
2019-10-02 08:06:52,898 - handlers.py[DEBUG]: finish: modules-final/config-scripts-per-boot: FAIL: running config-scripts-per-boot with frequency always
2019-10-02 08:06:52,898 - util.py[WARNING]: Running module scripts-per-boot (<module 'cloudinit.config.cc_scripts_per_boot' from '/usr/lib/python3/dist-packages/cloudinit/config/cc_scripts_per_boot.py'>) failed
2019-10-02 08:06:52,898 - util.py[DEBUG]: Running module scripts-per-boot (<module 'cloudinit.config.cc_scripts_per_boot' from '/usr/lib/python3/dist-packages/cloudinit/config/cc_scripts_per_boot.py'>) failed
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/cloudinit/stages.py", line 800, in _run_modules
    freq=freq)
  File "/usr/lib/python3/dist-packages/cloudinit/cloud.py", line 54, in run
    return self._runners.run(name, functor, args, freq, clear_on_fail)
  File "/usr/lib/python3/dist-packages/cloudinit/helpers.py", line 187, in run
    results = functor(*args)
  File "/usr/lib/python3/dist-packages/cloudinit/config/cc_scripts_per_boot.py", line 41, in handle
    util.runparts(runparts_path)
  File "/usr/lib/python3/dist-packages/cloudinit/util.py", line 842, in runparts
    % (len(failed), len(attempted)))
RuntimeError: Runparts: 1 failures in 1 attempted commands

So, it definitely doesn't like the format of the file - it wants to see a #!... or perhaps a binary executable.

I will try out Dude0001's suggestions in more detail now.

===EDIT===

In the end, what does work is using the multipart/mixed format, as suggested by Dude0001:

Content-Type: multipart/mixed; boundary="//"
MIME-Version: 1.0

--//
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="cloud-config.txt"

#cloud-config
package_update: yes
package_upgrade: all

packages:
 - mariadb-server
 - apt-file

--//
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="userdata.txt"

#!/bin/bash
cat >>/root/.bashrc <<!
set -o vi
unalias -a
alias ll='ls -lp'
!

cat >>/home/admin/.bashrc <<!
set -o vi
unalias -a
alias ll='ls -lp'
!

cat /root/.vimrc <<!
set t_ti= t_te=
set compatible
set expandtab ts=2 sw=2 ai
!

cat >/home/admin/.vimrc <<!
set t_ti= t_te=
set compatible
set expandtab ts=2 sw=2 ai
!

--//

Just specifying #cloud-config doesn't seem to work, but this way does. For me, at least. In the present moment.

Evangelize answered 30/9, 2019 at 16:4 Comment(0)
C
10

Short answer:

A user_data value set to a shell script will cause the given shell script to be ran during in the final stage of cloud-init (and I believe after the cloud-init directives in the one-time folder you reference).

If you want to use a custom cloud-init directive and a shell script both in EC2 user_data property you need to use the multipart/mixed mime format https://aws.amazon.com/premiumsupport/knowledge-center/execute-user-data-ec2/

Long answer:

The user_data can hold raw data to be read through the EC2 meta data, a script or a cloud-init directive. Additionally, you can set it up as a multipart/mixed mime type and provide each of these.

If user_data is raw data, it can be fetched with a curl command inside the EC2 instance. It is up to the calling command to interpret the data, it can be whatever the user chooses.

[ec2-user ~]$ curl http://169.254.169.254/latest/user-data

If user_data is a script (e.g. #!/bin/bash in the first line), it is ran as a step in cloud-init in the final stage of cloud-init https://cloudinit.readthedocs.io/en/latest/topics/boot.html#final.

If user_data is a cloud-init directive (e.g. #cloud-config in the first line), it is ran as the specified cloud-init directive.

From https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html#user-data-cloud-init

"To pass cloud-init directives to an instance with user_data... enter your cloud-init directive text in the user_data text."

Like so

#cloud-config
repo_update: true
repo_upgrade: all

packages:
 - httpd
 - mariadb-server

runcmd:
 - [ sh, -c, "amazon-linux-extras install -y lamp-mariadb10.2-php7.2 php7.2" ]
 - systemctl start httpd
 - sudo systemctl enable httpd
 - [ sh, -c, "usermod -a -G apache ec2-user" ]
 - [ sh, -c, "chown -R ec2-user:apache /var/www" ]
 - chmod 2775 /var/www
 - [ find, /var/www, -type, d, -exec, chmod, 2775, {}, \; ]
 - [ find, /var/www, -type, f, -exec, chmod, 0664, {}, \; ]
 - [ sh, -c, 'echo "<?php phpinfo(); ?>" > /var/www/html/phpinfo.php' ]

The multipart/mixed mime format is described here https://aws.amazon.com/premiumsupport/knowledge-center/execute-user-data-ec2/, with the example

Content-Type: multipart/mixed; boundary="//"
MIME-Version: 1.0

--//
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="cloud-config.txt"

#cloud-config
cloud_final_modules:
- [scripts-user, always]

--//
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="userdata.txt"

#!/bin/bash
/bin/echo "Hello World" >> /tmp/testfile.txt
--//
Conjugate answered 30/9, 2019 at 18:47 Comment(6)
That is very helpful, thanks. I tried the simple #cloud-config version, in different guises, in order to install mysql-server, but no effect. What is the correct way to do this? I only want to install once, of course, not every time I reboot - should I place it in .../per-once and then reboot?Evangelize
I tried out a few things inspired by your answer and put the results in an edit.Evangelize
It doesn't look like you are using the correct syntax still, you are missing the Content-Type: multipart/mixed part and a lot of other syntax from the example I sent. I'm also not familiar with what the line in your shell script (or the cloud-init script for that matter) is doing, can you maybe add some comments to your user_data to explain what you expect each line to? Sorry, I'm more familiar with AWS and cloud-init than the other things you are doing.Conjugate
Sorry, I didn't see your comment before - I'll edit my question with some more details.Evangelize
That took a while, but I finally got things to work - when I used the multipart/mixed format. I'll update my post with what I did - thank you for helping!Evangelize
Awesome! You'll learn a lot spinning up your own services like this. Have fun!Conjugate

© 2022 - 2024 — McMap. All rights reserved.