如何让我的EC2实例在每次启动时都运行cloud-init启动脚本?

71

我有一个运行在基于Amazon Linux AMI的EC2实例。像所有这样的AMI一样,它支持cloud-init系统,用于根据传递给每个实例的用户数据运行启动脚本。在这种特定情况下,我的用户数据输入恰好是一个包含文件,其中包含了几个其他的启动脚本:

#include
http://s3.amazonaws.com/path/to/script/1
http://s3.amazonaws.com/path/to/script/2

第一次启动实例时,cloud-init启动脚本运行正常。但是,如果我对实例进行软重启(例如,运行sudo shutdown -r now),实例会在第二次启动时运行启动脚本。如果我进入系统日志,可以看到:

Running cloud-init user-scripts
user-scripts already ran once-per-instance
[  OK  ]

这不是我想要的——我能理解每个实例生命周期只运行一次启动脚本的实用性,但在我的情况下,这些脚本应该在每次实例启动时都运行,就像普通的启动脚本一样。
我意识到一个可能的解决方案是在第一次运行后手动将我的脚本插入到rc.local中。然而,这似乎很繁琐,因为cloud-init和rc.d环境有微妙的差异,我现在必须分别调试首次启动和所有后续启动的脚本。
有人知道如何告诉cloud-init始终运行我的脚本吗?这肯定是cloud-init设计者考虑过的事情。

嗨!我将一个Bash脚本文件复制到了/var/lib/cloud/scripts/per-instance文件夹中,但是当我实例化一个实例时,该脚本并没有运行。请帮忙解决。 - Harith
9个回答

69

在11.10、12.04以及之后的版本中,您可以通过使“scripts-user”始终运行来实现此目的。在 /etc/cloud/cloud.cfg 文件中,您会看到类似以下的内容:

cloud_final_modules:
 - rightscale_userdata
 - scripts-per-once
 - scripts-per-boot
 - scripts-per-instance
 - scripts-user
 - keys-to-console
 - phone-home
 - final-message

此项设置可以在启动后进行修改,或者通过用户数据(user-data)插入覆盖此段落的云配置数据(cloud-config data)。也就是说,在用户数据中,您可以提供以下信息:

#cloud-config
cloud_final_modules:
 - rightscale_userdata
 - scripts-per-once
 - scripts-per-boot
 - scripts-per-instance
 - [scripts-user, always]
 - keys-to-console
 - phone-home
 - final-message

您在描述中所做的那样,可以将其包含为“#include”。但很遗憾,目前您无法修改“cloud_final_modules”,只能覆盖它。我希望能够在某个时间点添加修改配置部分的功能。

有关此信息的更多详细信息,请参见cloud-config文档: https://github.com/canonical/cloud-init/tree/master/doc/examples

或者,您可以将文件放在/var/lib/cloud/scripts/per-boot中,它们将由“scripts-per-boot”路径运行。


1
我希望能够在某个时候添加修改配置部分的功能。现在是否已经添加了这个功能?我看到最新的cloud-init中有一个“合并器”功能,但我无法弄清楚如何仅更改“scripts-user”行。它会覆盖整个列表,而不管我传递的选项是什么。 - Meta
3
这是一个可以进行行内修改的一行代码:sed -i 's/scripts-user$/\[scripts-user, always\]/' /etc/cloud/cloud.cfg - wjordan
将文件放入/var/lib/cloud/scripts/per-boot似乎更容易,我可以使用它来设置自动EC2关机 - eQ19
2
截至2017年,数据已经移动到其他地方。请使用 /etc/cloud/cloud.cfg.d/ 并在那里放置一个新文件。 - eco
1
@Chetabahana 你知道那个还能用吗?即使我使用了 sudo chmod a+x run.shsudo chown root:root run.sh,我仍然无法让它工作。编辑:实际上,它可以运行内置的东西,但无论我尝试什么,都无法运行我的可执行文件。我不得不使用 crontab。 - Gumby The Green
显示剩余2条评论

21
/etc/init.d/cloud-init-user-scripts中,编辑这一行:
/usr/bin/cloud-init-run-module once-per-instance user-scripts execute run-parts ${SCRIPT_DIR} >/dev/null && success || failure

to

 /usr/bin/cloud-init-run-module always user-scripts execute run-parts ${SCRIPT_DIR} >/dev/null && success || failure

祝你好运!


13

现在cloud-init本身就支持这个功能,可以在文档中查看runcmd与bootcmd命令的描述(http://cloudinit.readthedocs.io/en/latest/topics/examples.html#run-commands-on-first-boot):

"runcmd":

#cloud-config

# run commands
# default: none
# runcmd contains a list of either lists or a string
# each item will be executed in order at rc.local like level with
# output to the console
# - runcmd only runs during the first boot
# - if the item is a list, the items will be properly executed as if
#   passed to execve(3) (with the first arg as the command).
# - if the item is a string, it will be simply written to the file and
#   will be interpreted by 'sh'
#
# Note, that the list has to be proper yaml, so you have to quote
# any characters yaml would eat (':' can be problematic)
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 ]
"

bootcmd:

"
#cloud-config

# boot commands
# default: none
# this is very similar to runcmd, but commands run very early
# in the boot process, only slightly after a 'boothook' would run.
# bootcmd should really only be used for things that could not be
# done later in the boot process.  bootcmd is very much like
# boothook, but possibly with more friendly.
# - bootcmd will run on every boot
# - the INSTANCE_ID variable will be set to the current instance id.
# - you can use 'cloud-init-per' command to help only run once
bootcmd:
 - echo 192.168.1.130 us.archive.ubuntu.com >> /etc/hosts
 - [ cloud-init-per, once, mymkfs, mkfs, /dev/vdb ]

还要注意bootcmd中的"cloud-init-per"命令示例。从它的帮助文档可以看到:

Usage: cloud-init-per frequency name cmd [ arg1 [ arg2 [ ... ] ]
   run cmd with arguments provided.

   This utility can make it easier to use boothooks or bootcmd
   on a per "once" or "always" basis.

   If frequency is:
      * once: run only once (do not re-run for new instance-id)
      * instance: run only the first boot for a given instance-id
      * always: run every boot

通过官方文档引用更新答案,并为参考链接到原始文档。 - Erich Eichinger
5
bootcmd部分在系统未完全启动时执行,可能无法按预期工作。 - Nico
您如何在EC2 UserData中使用cloud-init-per来在每次启动时运行cfn-init脚本? - Maria Dorohin

8

一种可能性,虽然有些hackish,是删除cloud-init使用的锁定文件,以确定用户脚本是否已经运行。在我的情况下(Amazon Linux AMI),这个锁定文件位于/var/lib/cloud/sem/,名为user-scripts.i-7f3f1d11(结尾的哈希部分每次启动都会更改)。因此,将以下用户数据脚本添加到Include文件的末尾即可解决问题:

#!/bin/sh
rm /var/lib/cloud/sem/user-scripts.*

我不确定这是否会对其他任何事情产生不良影响,但在我的实验中它是有效的。


1
“hash部分”似乎是亚马逊机器ID,不是吗? - theist
1
看起来像是AWS实例ID,如果是的话,它将在每次实例启动时更改,但跨同一实例的停止和重新启动保持不变。 - froggythefrog
对我来说,/var/lib/cloud/sem/ 没有包含任何有用的内容,但是删除以下信号量起了作用:sudo rm /var/lib/cloud/instance/sem/config_write_filessudo rm /var/lib/cloud/instance/sem/config_runcmd - casper

3
如果有人想在CDK上做这件事,这里有一个Python示例。
对于Windows用户数据,有一个特殊的persist标签,但对于Linux,则需要使用MultiPart用户数据来首先设置cloud-init。此Linux示例使用cloud-config(请参见ref blog)部分类型而不是需要cloud-init-per(也请参见bootcmd)调用的cloud-boothook,我无法测试(例如:cloud-init-pre always)。
Linux示例:
    # Create some userdata commands
    instance_userdata = ec2.UserData.for_linux()
    instance_userdata.add_commands("apt update")
    # ...
    # Now create the first part to make cloud-init run it always
    cinit_conf = ec2.UserData.for_linux();
    cinit_conf .add_commands('#cloud-config');
    cinit_conf .add_commands('cloud_final_modules:');
    cinit_conf .add_commands('- [scripts-user, always]');
    multipart_ud = ec2.MultipartUserData()
    #### Setup to run every time instance starts
    multipart_ud.add_part(ec2.MultipartBody.from_user_data(cinit_conf , content_type='text/cloud-config'))
    #### Add the commands desired to run every time
    multipart_ud.add_part(ec2.MultipartBody.from_user_data(instance_userdata));

    ec2.Instance(
        self, "myec2",
        userdata=multipart_ud,
        #other required config...
    )

Windows示例:

    instance_userdata = ec2.UserData.for_windows()
    # Bootstrap
    instance_userdata.add_commands("Write-Output 'Run some commands'")
    # ...
    # Making all the commands persistent - ie: running on each instance start
    data_script = instance_userdata.render()
    data_script += "<persist>true</persist>"
    ud = ec2.UserData.custom(data_script)
    ec2.Instance(
        self, "myWinec2",
        userdata=ud,
        #other required config...
    )

我使用了类似的方法,发现用户数据命令脚本已经运行了两次。你遇到过这个问题吗? - aykcandem

3

我为这个问题苦苦挣扎了将近两天,尝试了所有能找到的解决方案,最终结合几种方法,得出了以下解决方案:

MyResource:
  Type: AWS::EC2::Instance
  Metadata:
    AWS::CloudFormation::Init:
      configSets:
        setup_process:
          - "prepare"
          - "run_for_instance"
      prepare:
        commands:
          01_apt_update:
            command: "apt-get update"
          02_clone_project:
            command: "mkdir -p /replication && rm -rf /replication/* && git clone https://github.com/awslabs/dynamodb-cross-region-library.git /replication/dynamodb-cross-region-library/"
          03_build_project:
            command: "mvn install -DskipTests=true"
            cwd: "/replication/dynamodb-cross-region-library"
          04_prepare_for_apac:
            command: "mkdir -p /replication/replication-west && rm -rf /replication/replication-west/* && cp /replication/dynamodb-cross-region-library/target/dynamodb-cross-region-replication-1.2.1.jar /replication/replication-west/replication-runner.jar"
      run_for_instance:
        commands:
          01_run:
            command: !Sub "java -jar replication-runner.jar --sourceRegion us-east-1 --sourceTable ${TableName} --destinationRegion ap-southeast-1 --destinationTable ${TableName} --taskName -us-ap >/dev/null 2>&1 &"
            cwd: "/replication/replication-west"
  Properties:
    UserData:
      Fn::Base64:
        !Sub |
          #cloud-config
          cloud_final_modules:
           - [scripts-user, always]
          runcmd:
           - /usr/local/bin/cfn-init -v -c setup_process --stack ${AWS::StackName} --resource MyResource --region ${AWS::Region}
           - /usr/local/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource MyResource --region ${AWS::Region}

这是DynamoDb跨区域复制流程的设置。

你说过你“结合了许多方法”。能否详细说明一下,你在上面的 CloudFormation 模板中使用了哪些方法来确保你的用户数据脚本每次启动时都能运行?谢谢。 - CBP
@CBP,基本上,AWS::CloudFormation::Init会在每次启动时运行代码。而run_for_instance步骤将运行复制脚本。 - Enigo
“prepare” 和 “run_for_instance” 有什么区别?它们都在每次设置中运行吗? - Maria Dorohin
是的。这只是为了清晰分离而已。把它看作两种不同的方法。 - Enigo

1

另一种方法是在您的用户数据脚本中使用#cloud-boothook。 来自文档

Cloud Boothook

  • 以#cloud-boothook或Content-Type:text/cloud-boothook开头。
  • 此内容是bootook数据。 它存储在/var/lib/cloud下的文件中,然后立即执行。
  • 这是最早可用的“hook”。 没有提供仅运行一次的机制。 Boothook必须自己处理这个问题。 它在环境变量INSTANCE_ID中提供了实例ID。 使用此变量提供每个实例一次性的boothook数据集。

1

请在您的bash脚本之前使用以下脚本。

示例:这里我将“hello world”打印到我的文件中。

在添加到用户数据之前停止实例。

脚本

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." >> /var/tmp/sdksdfjsdlf
--//

请仅返回翻译后的文本,不要从aws文档中复制粘贴,需要添加一些解释。 - dev
x-shellscript-per-boot 是一个关键字,类似于:cloud-init devel make-mime -a always.sh:x-shellscript-per-boot -a instance.sh:x-shellscript-per-instance -a once.sh:x-shellscript-per-once从以下链接复制而来: https://cloudinit.readthedocs.io/en/latest/explanation/format.html#examples - undefined

0
在Amazon Linux 2上对我有效的方法是删除/var/lib/cloud/instance/sem/下的信号量。
sudo rm /var/lib/cloud/instance/sem/config_write_files
sudo rm /var/lib/cloud/instance/sem/config_runcmd
sudo rm /var/lib/cloud/instance/sem/config_scripts_user

理论上,可以创建一个定时任务来定期删除这些文件。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接