Packer を Systems Manager オートメーションランブック AWS-RunPacker で実行してみた

Packer の実行環境を管理する必要がなくなります。

コンバンハ、千葉(幸)です。

Packer に入門したての私がウロウロと AWS ドキュメントをうろついていると、AWS-RunPacker というイカした名称の Systems Manager オートメーションランブックが目に入ってきました。

見つけてしまったからには試さずにはいられないので、試してみました。

AWS-RunPacker で何ができるのか

Packer を AWS Systems Manager オートメーションによる一時的な実行基盤で実行させる事ができます。

ランブックの説明に入る前に Packer について簡単におさらいしておきましょう。( Packer 自体は様々な環境で使用できますが、ここでは AWS における AMI の作成に限定して言及します。)

Packer の用途は AMI の作成です。テンプレートであるべき姿を定義しておき、それをビルドする事で以下の流れで AMI の作成が実行されます。

  • 一時的なインスタンスの作成
  • インスタンスへの SSH 接続および定義された処理の実行
  • 処理が完了したインスタンスから AMI の作成
  • 一時的なリソースの削除

Packer の実行は手元の端末からリモートで実行することもできますし、例えば EC2 インスタンスにセットアップすればそこから実行することもできます。

「どこから実行するか」に加えて、テンプレートをどう管理するかや、実行に必要な IAM の認証情報をどう管理するかも考慮が必要な点となります。

AWS-RunPacker を使用することで、以下のイメージとなります。

  • 実行環境:Systems Manager オートメーションによる一時的な実行環境が作成される
  • テンプレート:S3 バケットに配置した Packer テンプレートが環境にダウンロードされる
  • 認証情報:オートメーション用ロール(もしくは操作者)の認証情報が使用される

手元の端末から Packer を実行可能だしチームでなく一人で使っている、という環境ではあまりありがたみはなさそうですが、専用の実行サーバなどを用意して使っていた場合には AWS-RunPacker に乗り換えることでメリットは出そうです。

AWS-RunPacker の基本情報

インプットパラメーター

以下のパラメーターを指定できます。

パラメータ名 指定 概要
TemplateS3BucketName 必須 Packer テンプレートが格納されたバケット名
TemplateFileName 必須 テンプレートのファイル名(パスを含む)
Mode 必須 Build,Validate,Fixのいずれか。デフォルトはBuild
Force 必須 Build のオプション。true か falseでデフォルトは true
AutomationAssumeRole オプション オートメーション実行時に引き受けるロール

いくつかさらに補足をしておきます。

Mode

Packer コマンドのいくつかをここで指定できます。

  • Build:テンプレートからアーティファクト(ここでは AMI )を作成
  • Validate:テンプレートの構文が正しいかを確認
  • Fix:テンプレートの下位互換性の無い部分を見つけて最新バージョンの Packer で使用できるように修正

詳細は以下を参照してください。

Force

Force は build コマンドのオプションです。以前のビルドのアーティファクトにより実行が妨げられた場合に、強制的に続行する場合に true を指定します。

例えば作成予定の AMI と同じ名称の AMI が存在している場合 通常はビルドが失敗しますが、--forceオプションを付与することで既存の AMI の削除を伴う形でビルドが成功します。

アウトプットパラメーター

出力として以下が定義されています。

パラメータ名 概要
RunPackerProcessTemplate.output Packer ツールによる標準出力
RunPackerProcessTemplate.s3_bucket Fix されたテンプレートの格納バケット
RunPackerProcessTemplate.fixed_template_key Fix されたテンプレートのファイル名(パスを含む)

一点目は手元で Packer を実行した際に標準出力されるものと同じ内容です。ただし、オートメーションの処理が完了してからでないと出力されないことに注意が必要です。(手元で実行する際は進捗状況に応じて随時出力される。)

二点目と三点目は Mode に Fix を指定した場合のみ有効な出力です。

コンテンツ

2021/06/08現在のコンテンツの内容は以下の通りです。

ドキュメントの作成は 2021/05/28 となっていたので、ある程度定期的に内容は更新されているようです。使用する上ではコンテンツの中身をすべて理解する必要はないので、大まかな流れを抑えたい場合にご参照ください。

折り畳み
#
# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
# software and associated documentation files (the "Software"), to deal in the Software
# without restriction, including without limitation the rights to use, copy, modify,
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
---
description: |
  ### Document Name - AWS-RunPacker

  ## What does this document do?
  This document uses the HashiCorp [packer](https://www.packer.io/) tool to validate, fix, or build packer templates that are used to create machine images. This document is using Packer v1.7.2.

  ## Input Parameters
  * TemplateS3BucketName: The name of the Amazon S3 bucket containing the packer template.
  * TemplateFileName: The name, or key, of the template file in the S3 bucket.
  * Mode: The mode, or command, in which to use Packer when validating against the template:
      * [Build](https://www.packer.io/docs/commands/build.html): (Default) Runs all the builds within the template in order to generate a set of artifacts.
      * [Validate](https://www.packer.io/docs/commands/validate.html): Validates the syntax and configuration of the template.
      * [Fix](https://www.packer.io/docs/commands/fix.html): Finds backwards incompatible content in the template and updates it for use with the latest version of Packer. It then uploads the fixed template to the S3 bucket that you specify. The name of the fixed template is identical to the template provided by the user, but with "fixed-" prepended to the name.
  * [Force](https://www.packer.io/docs/commands/build.html#force): Forces a builder to run when artifacts from a previous build otherwise prevent a build from running.
      * True: (Default) Force flag is used
      * False: Force flag is not used
  * AutomationAssumeRole: The ARN of the role that allows Automation to perform the actions on your behalf.

  ## Output Parameters
  * RunPackerProcessTemplate.output: The stdout from the Packer tool.
  * RunPackerProcessTemplate.fixed_template_key: The name of the template stored in an S3 bucket to use only when running in "Fix" mode.
  * RunPackerProcessTemplate.s3_bucket: The name of the S3 bucket that contains the fixed template to use only when running in "Fix" mode.
schemaVersion: '0.3'
assumeRole: "{{ AutomationAssumeRole }}"
parameters:
  TemplateS3BucketName:
    type: String
    description: "(Required) AWS S3 bucket name that stores the template. eg. my-packer-bucket"
  TemplateFileName:
    type: String
    description: "(Required) Packer template file key in the bucket. eg. path/to/packer-template.json"
  Mode:
    type: String
    description: "(Required) The mode in which to use Packer when validating against the template. Default Value - Build"
    allowedValues:
      - Validate
      - Fix
      - Build
    default: 'Build'
  Force:
    type: String
    description: "(Optional) Forces a builder to run when artifacts from a previous build otherwise prevent a build from running. Default Value - True"
    allowedValues:
      - 'True'
      - 'False'
    default: 'True'
  AutomationAssumeRole:
    type: String
    description: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf.
    default: ''
outputs:
  - RunPackerProcessTemplate.output
  - RunPackerProcessTemplate.fixed_template_key
  - RunPackerProcessTemplate.s3_bucket
mainSteps:
- name: RunPackerProcessTemplate
  action: aws:executeScript
  timeoutSeconds: 600
  description: |
    ## RunPackerProcessTemplate
    Runs the selected mode against the template using the Packer tool
    ## Outputs
    * output: The stdout from the packer tool
    * Fixed_template_key: The name of the fixed template in S3 if run in "Fix" mode. No value otherwise
    * s3_bucket: The name of your s3 that contains the fixed template if run in "Fix" mode. No value otherwise
  inputs:
    Runtime: python3.6
    Handler: run_packer_handler
    Attachment: packer_1.7.2_linux_amd64.zip
    InputPayload:
        TemplateFileName: '{{TemplateFileName}}'
        TemplateS3BucketName: '{{TemplateS3BucketName}}'
        Mode: '{{Mode}}'
        Force: '{{Force}}'

    Script: |
      import os
      import glob
      import boto3
      import subprocess
      from pathlib import Path
      import json

      def get_current_dir():
        current_path = os.path.realpath(__file__)
        p = Path(current_path)
        return str(p.parent)

      def execute_packer_command(cmdArray):
        p = subprocess.Popen(cmdArray, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        out, err = [(x.decode("utf-8")) for x in p.communicate()]
        return_code = p.returncode
        if err != "":
          raise Exception("ERROR IN EXECUTING PACKER", err)
        if return_code != 0:
          raise Exception("FAILED", out)
        return out

      def download_file(path, key, bucket_name):
        s3 = boto3.client('s3')
        s3.download_file(bucket_name, key, path)

      def upload_file(path, key, bucket_name):
        s3 = boto3.client('s3')
        s3.upload_file(path, bucket_name, key)

      def validate_template(packer_cmd, template_path):
        validate_template_cmd = [packer_cmd, "validate", template_path]
        out = execute_packer_command(validate_template_cmd)
        print("Template validated successfully")
        return out

      def build_template(packer_cmd, template_path, force_build):
        build_command = [packer_cmd, "build"]
        if force_build == "True":
            build_command.extend(["-machine-readable", "-force"])
        build_command.append(template_path)
        out = execute_packer_command(build_command)
        print("Template built")
        return out

      def fix_template(packer_cmd, local_template_path, s3_template_path, bucket_name):
          s3_path_head, template_file_name = os.path.split(s3_template_path)
          fixed_template_file_name = "fixed-" + template_file_name
          fixed_template_path = os.path.join('/tmp/', fixed_template_file_name)
          fixed_template_cmd = [packer_cmd, "fix", local_template_path]
          out = execute_packer_command(fixed_template_cmd)
          fixed_template_file = open(fixed_template_path, "w")
          fixed_template_file.write(out)
          fixed_template_file.close()
          fixed_s3_key = os.path.join(s3_path_head, fixed_template_file_name)
          upload_file(fixed_template_path, fixed_s3_key, bucket_name)
          print ("Template fixed from " + s3_template_path + " to " + fixed_s3_key)
          return out, fixed_s3_key

      def run_packer_handler(events, context):
        current_dir = get_current_dir()
        packer_cmd = current_dir + "/" + "packer"
        s3_template_path = events['TemplateFileName']
        bucket_name = events['TemplateS3BucketName']
        mode = events['Mode']
        force_build = events['Force']
        s3_path_head, file_name = os.path.split(s3_template_path)
        local_template_path = os.path.join("/tmp/", file_name)
        download_file(local_template_path, s3_template_path, bucket_name)

        os.system("chmod u=x " + packer_cmd)

        fixed_template_path = ""
        ret_bucket_value = ""
        if mode == "Validate":
            out = validate_template(packer_cmd, local_template_path)
        elif mode == "Build":
            out = build_template(packer_cmd, local_template_path, force_build)
        elif mode == "Fix":
            out, fixed_template_path = fix_template(packer_cmd, local_template_path, s3_template_path, bucket_name)
            ret_bucket_value = bucket_name
        return {
          'output': json.dumps(out),
          'fixed_template_key': fixed_template_path,
          's3_bucket': ret_bucket_value
        }
  isEnd: true
  outputs:
    - Name: output
      Selector: $.Payload.output
      Type: String
    - Name: fixed_template_key
      Selector: $.Payload.fixed_template_key
      Type: String
    - Name: s3_bucket
      Selector: $.Payload.s3_bucket
      Type: String
files:
  packer_1.7.2_linux_amd64.zip:
    checksums:
      sha256: 9429c3a6f80b406dbddb9b30a4e468aeac59ab6ae4d09618c8d70c4f4188442e
    size: 28738303
...

アタッチメント

ドキュメントの詳細画面から添付ファイルを確認できます。 Packer のバージョンは現時点で最新の 1.7.2 が使用されるようです。

AWSSystemsManagerDocuments_RunPacker

ここからダウンロードすることもできます。

自身のロールを使用して実行する

一通り確認が完了したので実際に動かしてみましょう。

事前準備

あらかじめ以下のテンプレートを用意しました。

amazonlinux2.json

{
  "builders": [
    {
      "type": "amazon-ebs",
      "region": "ap-northeast-1",
      "instance_type": "t3.micro",
      "ssh_username": "ec2-user",
      "source_ami_filter": {
        "filters": {
          "name": "amzn2-ami-hvm-*-x86_64-gp2"
        },
        "owners": ["137112412989"],
        "most_recent": true
      },
      "vpc_id": "vpc-0e4acafc38414468c",
      "subnet_id": "subnet-0caa45223899b4b73",
      "associate_public_ip_address": true,
      "ami_name": "AmazonLinux2-{{isotime | clean_resource_name}}",
      "tags": {
        "Base_AMI_ID": "{{ .SourceAMI }}",
        "Base_AMI_NAME": "{{ .SourceAMIName }}"
      }
    }
  ],
    "provisioners": [
      {
        "type":  "shell",
        "inline": [
          "sudo yum -y install tree"
        ]
      }
    ]
}

ざっくり以下の内容です。

  • 最新の Amazon Linux2 の AMI をソース AMI とする
  • 一時インスタンスの配置先として特定の VPC とサブネット(パブリック)を指定しパブリック IP アドレスの割り当てを許可する
  • AMI 名の末尾に実行時間のタイムスタンプを付与し、ソース AMI の ID と 名称をタグとして付与する
  • tree を yum インストールする

上記のテンプレートを S3 バケットchibayuki-templateのトップディレクトリ(という表現は正確ではありませんが)に格納してあります。

オートメーションの実行(コンソール)

マネジメントコンソールから実行していきます。なお、今回はAdministratorAccessを持つ IAM ロールにスイッチして作業しています。

ドキュメントの詳細画面から「オートメーションの実行」を押下することで実行画面に遷移できますが、お手軽に以下の URL にアクセスします。

https://ap-northeast-1.console.aws.amazon.com/systems-manager/automation/execute/AWS-RunPacker

実行方法として「シンプルな実行」を選択し、各種パラメータを入力します。

パラメータ名
TemplateS3BucketName chibayuki-template
TemplateFileName amazonlinux2.json
Mode Build
Force True
AutomationAssumeRole なし

AWS-RunPackerRun

画面下部で実行リンクと AWS CLI コマンドが生成できます。

AWS-RunPacker-linkandcli

前者の例は以下、

https://ap-northeast-1.console.aws.amazon.com/systems-manager/automation/execute/AWS-RunPacker?region=ap-northeast-1#TemplateS3BucketName=chibayuki-template&TemplateFileName=amazonlinux2.json&Mode=Build&Force=True&AutomationAssumeRole=&tagsOnCreate=%5B%5D

後者の例は以下です。(なお、AutomationAssumeRoleの値がブランクのままだと CLI は失敗するので、指定しない場合はキーごと削除する必要があります。)

aws ssm start-automation-execution --document-name "AWS-RunPacker" --document-version "\$DEFAULT" --parameters '{"TemplateS3BucketName":["chibayuki-template"],"TemplateFileName":["amazonlinux2.json"],"Mode":["Build"],"Force":["True"],"AutomationAssumeRole":[""]}' --region ap-northeast-1

コンソールから何度も実行したい場合は、パラメータをつど入力するのが手間なので実行リンクを控えておくといいでしょう。

最後に「実行」を押下すれば実行処理が開始されます。

以下のような実行詳細画面に遷移します。

AWS-RunPacker-execution

AWS-RunPacker は単一のステップからなるドキュメントのため、ステップの進み具合を確認する、ということはできません。処理が終わるまでひたすら待ちましょう。

環境によりますが、短くても 3 分程度はかかるでしょう。完了するまで標準出力( Packer の実行ログ)が確認できないのは少しやきもきします。

5 分足らずで実行が完了しました。出力を確認してみましょう。

AWS-RunPacker-finished

RunPackerProcessTemplate.outputとして Packer の実行ログが確認できます。

AWSRun-Packer-output

せっかくなのでログを載せておきます。

折り畳み
"1623084960,,ui,say,==> amazon-ebs: Force Deregister flag found%!(PACKER_COMMA) skipping prevalidating AMI Name
1623084960,,ui,message,    amazon-ebs: Found Image ID: ami-0ca38c7440de1749a
1623084960,,ui,say,==> amazon-ebs: Creating temporary keypair: packer_60be4f9f-20cd-34ea-3572-bf7056d76453
1623084960,,ui,say,==> amazon-ebs: Creating temporary security group for this instance: packer_60be4fa0-ea14-2e60-6593-168770925689
1623084960,,ui,say,==> amazon-ebs: Authorizing access to port 22 from [0.0.0.0/0] in the temporary security groups...
1623084961,,ui,say,==> amazon-ebs: Launching a source AWS instance...
1623084961,,ui,say,==> amazon-ebs: Adding tags to source instance
1623084961,,ui,message,    amazon-ebs: Adding tag: \"Name\": \"Packer Builder\"
1623084962,,ui,message,    amazon-ebs: Instance ID: i-002ca33d541534f99
1623084962,,ui,say,==> amazon-ebs: Waiting for instance (i-002ca33d541534f99) to become ready...
1623084977,,ui,say,==> amazon-ebs: Using ssh communicator to connect: 18.183.91.191
1623084977,,ui,say,==> amazon-ebs: Waiting for SSH to become available...
1623084990,,ui,say,==> amazon-ebs: Connected to SSH!
1623084990,,ui,say,==> amazon-ebs: Provisioning with shell script: /tmp/packer-shell059834347
1623084991,,ui,message,    amazon-ebs: Loaded plugins: extras_suggestions%!(PACKER_COMMA) langpacks%!(PACKER_COMMA) priorities%!(PACKER_COMMA) update-motd
1623084991,,ui,error,==> amazon-ebs: Existing lock /var/run/yum.pid: another copy is running as pid 2398.
1623084991,,ui,error,==> amazon-ebs: Another app is currently holding the yum lock; waiting for it to exit...
1623084991,,ui,error,==> amazon-ebs:   The other application is: yum
1623084991,,ui,error,==> amazon-ebs:     Memory : 105 M RSS (418 MB VSZ)
1623084991,,ui,error,==> amazon-ebs:     Started: Mon Jun  7 16:56:25 2021 - 00:06 ago
1623084991,,ui,error,==> amazon-ebs:     State  : Running%!(PACKER_COMMA) pid: 2398
1623084993,,ui,error,==> amazon-ebs: Existing lock /var/run/yum.pid: another copy is running as pid 2532.
1623084993,,ui,error,==> amazon-ebs: Another app is currently holding the yum lock; waiting for it to exit...
1623084993,,ui,error,==> amazon-ebs:   The other application is: yum
1623084993,,ui,error,==> amazon-ebs:     Memory :  53 M RSS (269 MB VSZ)
1623084993,,ui,error,==> amazon-ebs:     Started: Mon Jun  7 16:56:31 2021 - 00:02 ago
1623084993,,ui,error,==> amazon-ebs:     State  : Running%!(PACKER_COMMA) pid: 2532
1623084995,,ui,error,==> amazon-ebs: Another app is currently holding the yum lock; waiting for it to exit...
1623084995,,ui,error,==> amazon-ebs:   The other application is: yum
1623084995,,ui,error,==> amazon-ebs:     Memory : 143 M RSS (360 MB VSZ)
1623084995,,ui,error,==> amazon-ebs:     Started: Mon Jun  7 16:56:31 2021 - 00:04 ago
1623084995,,ui,error,==> amazon-ebs:     State  : Running%!(PACKER_COMMA) pid: 2532
1623084997,,ui,message,    amazon-ebs: Resolving Dependencies
1623084997,,ui,message,    amazon-ebs: --> Running transaction check
1623084997,,ui,message,    amazon-ebs: ---> Package tree.x86_64 0:1.6.0-10.amzn2.0.1 will be installed
1623084997,,ui,message,    amazon-ebs: --> Finished Dependency Resolution
1623084997,,ui,message,    amazon-ebs:
1623084997,,ui,message,    amazon-ebs: Dependencies Resolved
1623084997,,ui,message,    amazon-ebs:
1623084997,,ui,message,    amazon-ebs: ================================================================================
1623084997,,ui,message,    amazon-ebs:  Package     Arch          Version                      Repository         Size
1623084997,,ui,message,    amazon-ebs: ================================================================================
1623084997,,ui,message,    amazon-ebs: Installing:
1623084997,,ui,message,    amazon-ebs:  tree        x86_64        1.6.0-10.amzn2.0.1           amzn2-core         47 k
1623084997,,ui,message,    amazon-ebs:
1623084997,,ui,message,    amazon-ebs: Transaction Summary
1623084997,,ui,message,    amazon-ebs: ================================================================================
1623084997,,ui,message,    amazon-ebs: Install  1 Package
1623084997,,ui,message,    amazon-ebs:
1623084997,,ui,message,    amazon-ebs: Total download size: 47 k
1623084997,,ui,message,    amazon-ebs: Installed size: 83 k
1623084997,,ui,message,    amazon-ebs: Downloading packages:
1623084997,,ui,message,    amazon-ebs: Running transaction check
1623084997,,ui,message,    amazon-ebs: Running transaction test
1623084997,,ui,message,    amazon-ebs: Transaction test succeeded
1623084997,,ui,message,    amazon-ebs: Running transaction
1623084997,,ui,message,    amazon-ebs:   Installing : tree-1.6.0-10.amzn2.0.1.x86_64                               1/1
1623084997,,ui,message,    amazon-ebs:   Verifying  : tree-1.6.0-10.amzn2.0.1.x86_64                               1/1
1623084997,,ui,message,    amazon-ebs:
1623084997,,ui,message,    amazon-ebs: Installed:
1623084997,,ui,message,    amazon-ebs:   tree.x86_64 0:1.6.0-10.amzn2.0.1
1623084997,,ui,message,    amazon-ebs:
1623084997,,ui,message,    amazon-ebs: Complete!
1623084997,,ui,say,==> amazon-ebs: Stopping the source instance...
1623084997,,ui,message,    amazon-ebs: Stopping instance
1623084998,,ui,say,==> amazon-ebs: Waiting for the instance to stop...
1623085013,,ui,say,==> amazon-ebs: Creating AMI AmazonLinux2-2021-06-07T16-55-59Z from instance i-002ca33d541534f99
1623085013,,ui,message,    amazon-ebs: AMI: ami-0b50d7d2aafa48580
1623085013,,ui,say,==> amazon-ebs: Waiting for AMI to become ready...
1623085195,,ui,say,==> amazon-ebs: Adding tags to AMI (ami-0b50d7d2aafa48580)...
1623085195,,ui,say,==> amazon-ebs: Tagging snapshot: snap-0cec58bfdd01d9530
1623085195,,ui,say,==> amazon-ebs: Creating AMI tags
1623085195,,ui,message,    amazon-ebs: Adding tag: \"Base_AMI_NAME\": \"amzn2-ami-hvm-2.0.20210427.0-x86_64-gp2\"
1623085195,,ui,message,    amazon-ebs: Adding tag: \"Base_AMI_ID\": \"ami-0ca38c7440de1749a\"
1623085195,,ui,say,==> amazon-ebs: Creating snapshot tags
1623085195,,ui,say,==> amazon-ebs: Terminating the source AWS instance...
1623085210,,ui,say,==> amazon-ebs: Cleaning up any extra volumes...
1623085211,,ui,say,==> amazon-ebs: No volumes to clean up%!(PACKER_COMMA) skipping
1623085211,,ui,say,==> amazon-ebs: Deleting temporary security group...
1623085211,,ui,say,==> amazon-ebs: Deleting temporary keypair...
1623085211,,ui,say,Build 'amazon-ebs' finished after 4 minutes 11 seconds.
1623085211,,ui,say,\
==> Wait completed after 4 minutes 11 seconds
1623085211,,ui,say,\
==> Builds finished. The artifacts of successful builds are:
1623085211,amazon-ebs,artifact-count,1
1623085211,amazon-ebs,artifact,0,builder-id,mitchellh.amazonebs
1623085211,amazon-ebs,artifact,0,id,ap-northeast-1:ami-0b50d7d2aafa48580
1623085211,amazon-ebs,artifact,0,string,AMIs were created:\
ap-northeast-1: ami-0b50d7d2aafa48580\

1623085211,amazon-ebs,artifact,0,files-count,0
1623085211,amazon-ebs,artifact,0,end
1623085211,,ui,say,--> amazon-ebs: AMIs were created:\
ap-northeast-1: ami-0b50d7d2aafa48580\

"

ともかく、 Packer による AMI 作成が正常に完了しました。

オートメーション用ロールを指定して実行する

先ほどは操作者の IAM 権限を使用して実行しましたが、オートメーション用ロールを作成し、そちらの権限を使用して実行するパターンを試してみます。

そうすることにより、操作者に与える権限を最小(オートメーションを実行できる)にしつつも、 Packer による AMI 作成などの操作を実現できます。

事前準備

オートメーション用ロール

以下ブログに記載の CloudFormation テンプレートを利用し作成しました。

折り畳み
AWSTemplateFormatVersion: "2010-09-09"
Description: "Template to create sample IAM role to execute packer automation using SSM"
Parameters: 
  PackerTemplateS3BucketLocation: 
    Type: String
    Description: Enter the name of the bucket where the packer templates will be stored. This is used to add permissions to the policy. For example, my-packer-bucket
Resources:
    SSMAutomationPackerRole:
        Type: "AWS::IAM::Role"
        Properties:
            RoleName: "SSMAutomationPackerCF"
            ManagedPolicyArns: [
              'arn:aws:iam::aws:policy/service-role/AmazonSSMAutomationRole'
            ]
            AssumeRolePolicyDocument: 
                Version: "2012-10-17"
                Statement: 
                  - 
                    Effect: "Allow"
                    Action: 
                      - "sts:AssumeRole"
                    Principal: 
                        Service: 
                          - "ec2.amazonaws.com"
                          - "ssm.amazonaws.com"

    SSMAutomationPackerInstanceProfile:
        Type: "AWS::IAM::InstanceProfile"
        Properties:
            InstanceProfileName: "SSMAutomationPackerCF"
            Roles:
              - !Ref SSMAutomationPackerRole

    SSMAutomationPackerInlinePolicy:
        Type: "AWS::IAM::Policy"
        Properties:
            PolicyName: "SSMAutomationPackerInline"
            PolicyDocument: 
                Version: "2012-10-17"
                Statement: 
                  - 
                    Effect: "Allow"
                    Action: 
                      - "iam:GetInstanceProfile"
                    Resource: 
                      - "arn:aws:iam::*:instance-profile/*"
                  - 
                    Effect: "Allow"
                    Action: 
                      - "logs:CreateLogStream"
                      - "logs:DescribeLogGroups"
                    Resource: 
                      - "arn:aws:logs:*:*:log-group:*"
                  - 
                    Effect: "Allow"
                    Action: 
                      - "s3:ListBucket"
                    Resource: 
                      - !Sub 'arn:aws:s3:::${PackerTemplateS3BucketLocation}'
                  - 
                    Effect: "Allow"
                    Action: 
                      - "s3:GetObject"
                    Resource: 
                      - !Sub 'arn:aws:s3:::${PackerTemplateS3BucketLocation}/*'
                  - 
                    Effect: "Allow"
                    Action: 
                      - "ec2:DescribeInstances"
                      - "ec2:CreateKeyPair"
                      - "ec2:DescribeRegions"
                      - "ec2:DescribeVolumes"
                      - "ec2:DescribeSubnets"
                      - "ec2:DeleteKeyPair"
                      - "ec2:DescribeSecurityGroups"
                    Resource: 
                      - "*"
            Roles: 
              - !Ref SSMAutomationPackerRole

    SSMAutomationPackerPassrolePolicy:
        Type: "AWS::IAM::Policy"
        Properties:
            PolicyName: "SSMAutomationPackerPassrole"
            PolicyDocument: 
                Version: "2012-10-17"
                Statement: 
                  - 
                    Sid: "SSMAutomationPackerPassrolePolicy"
                    Effect: "Allow"
                    Action: "iam:PassRole"
                    Resource: !GetAtt SSMAutomationPackerRole.Arn
            Roles: 
              - !Ref SSMAutomationPackerRole

これにより、以下ロールが作成さました。

  • ロール名:SSMAutomationPackerCF
  • インスタンスプロファイル名:同上
  • 信頼されたエンティティ:ec2.amazonaws.com,ssm.amazonaws.com
  • アタッチされたポリシー
    • AmazonSSMAutomationRole( AWS 管理ポリシー)
    • SSMAutomationPackerInline(インラインポリシー)
    • SSMAutomationPackerPassrole(インラインポリシー)

インラインポリシーの内訳はそれぞれ以下の通りです。

SSMAutomationPackerInline

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:DescribeInstances",
                "ec2:CreateKeyPair",
                "ec2:CreateSecurityGroup",
                "ec2:DeleteSecurityGroup",
                "ec2:DescribeRegions",
                "ec2:DescribeVolumes",
                "ec2:DescribeSubnets",
                "ec2:DeleteKeyPair",
                "ec2:DescribeSecurityGroups"
            ],
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:DescribeLogGroups",
                "iam:GetInstanceProfile",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:logs:*:*:log-group:*",
                "arn:aws:iam::*:instance-profile/*",
                "arn:aws:s3:::chibayuki-template"
            ]
        },
        {
            "Sid": "VisualEditor2",
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::chibayuki-template/*"
        }
    ]
}

↑ 今回使用する Packer テンプレートを前提にするとハイライト部の SecuriryGroup に関する操作権限が不足していることに気づいたので、手動で追記しました。

SSMAutomationPackerPassrole

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "iam:PassRole",
            "Resource": "arn:aws:iam::012345678910:role/SSMAutomationPackerCF",
            "Effect": "Allow",
            "Sid": "SSMAutomationPackerPassrolePolicy"
        }
    ]
}

操作者用の IAM ポリシー

以下の IAM ポリシーを割り当てた IAM ユーザーで後続の作業を行います。

  • AmazonSSMFullAccess(AWS管理ポリシー)
  • PassRole 用ポリシー(インラインポリシー)

インラインポリシーはオートメーション用ロールに付与したものと同一です。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "iam:PassRole",
            "Resource": "arn:aws:iam::012345678910:role/SSMAutomationPackerCF",
            "Effect": "Allow",
            "Sid": "SSMAutomationPackerPassrolePolicy"
        }
    ]
}

これらの権限設定は以下ドキュメントを参考にしました。

SSM フルアクセスから更に絞れないかなと考えたのですが、沼にハマりそうなのでやめておきました。

オートメーションの実行( AWS CLI )

先ほどのコンソールから生成したコマンドを加工して、以下を作成しました。

aws ssm start-automation-execution\
  --document-name "AWS-RunPacker"\
  --document-version "\$DEFAULT"\
  --parameters '{"TemplateS3BucketName":["chibayuki-template"],"TemplateFileName":["amazonlinux2.json"],"Mode":["Build"],"Force":["True"],"AutomationAssumeRole":["arn:aws:iam::012345678910:role/SSMAutomationPackerCF"]}'\
  --region ap-northeast-1

テンプレートは前回と同じものを指定しています。コマンドを実行すると、実行 ID が返却されます。

% aws ssm start-automation-execution\
  --document-name "AWS-RunPacker"\
  --document-version "\$DEFAULT"\
  --parameters '{"TemplateS3BucketName":["chibayuki-template"],"TemplateFileName":["amazonlinux2.json"],"Mode":["Build"],"Force":["True"],"AutomationAssumeRole":["arn:aws:iam::012345678910:role/SSMAutomationPackerCF"]}'\
  --region ap-northeast-1
{
    "AutomationExecutionId": "162c4e17-de8d-4af6-a3b2-7b0dd676d25b"
}

ここから更に CLI を駆使して色々参照したいところですが、「ちょっとめんどくさいな……」という思いが勝ったので、以降はコンソールから確認していきます。

戻り値として返ってきたオートメーション実行 ID が進行中であることが確認できます。

AWSSSMCLI-3143539

こちらも数分後に成功ステータスに遷移しました。これよりあとはコンソールからの実行と差異がないので割愛します。

余談:オートメーションの実行環境が使用するグローバル IP は何?

今回はテンポラリなインスタンスが使用する SecuriryGroup をテンプレートで定義していないため、一時的な SecuriryGroup が作成されます。ここでは 0.0.0.0/0 からの 22 ポートのインバウンドが許可されています。

インスタンスも含めて一時的なものであることからリスクは低いと判断しそのままにしましたが、送信元を絞りたい場合もあるかと思います。

では許可する送信元として何を指定すればいいのか?ということになりますが、実測できた範囲では以下の IP が使用されていました。

  • 3.112.59.252
  • 3.112.110.150
  • 3.113.18.247
  • 3.113.181.141
  • 3.115.18.230
  • 13.230.73.125
  • 13.231.159.156
  • 18.181.201.35
  • 18.183.46.53
  • 18.183.122.218
  • 18.183.136.15
  • 35.73.127.132
  • 54.249.3.81

これらは Packer テンプレート内でsudo netstat -anp | grep ESTABLISHEDの処理を追加し、実行ログから接続元の グローバル IP アドレスを確認しました。該当箇所のログを抽出して並べると以下の通りです。

1623072997,,ui,message,    amazon-ebs: tcp        0      0 192.168.0.174:22        3.113.18.247:35368      ESTABLISHED 2462/sshd: ec2-user
1623084997,,ui,message,    amazon-ebs: tcp        0      0 192.168.0.152:22        3.113.181.141:60238     ESTABLISHED 2424/sshd: ec2-user
1623141688,,ui,message,    amazon-ebs: tcp        0      0 192.168.0.233:22        3.112.59.252:58288      ESTABLISHED 2438/sshd: ec2-user
1623142506,,ui,message,    amazon-ebs: tcp        0      0 192.168.0.245:22        18.183.136.15:33272     ESTABLISHED 2453/sshd: ec2-user
1623142535,,ui,message,    amazon-ebs: tcp        0      0 192.168.0.104:22        18.183.46.53:41644      ESTABLISHED 2424/sshd: ec2-user
1623142548,,ui,message,    amazon-ebs: tcp        0      0 192.168.0.109:22        13.231.159.156:46420    ESTABLISHED 2501/sshd: ec2-user
1623142560,,ui,message,    amazon-ebs: tcp        0      0 192.168.0.243:22        18.181.201.35:53014     ESTABLISHED 2437/sshd: ec2-user
1623142524,,ui,message,    amazon-ebs: tcp        0      0 192.168.0.168:22        3.112.110.150:36682     ESTABLISHED 2441/sshd: ec2-user
1623142657,,ui,message,    amazon-ebs: tcp        0      0 192.168.0.117:22        35.73.127.132:37828     ESTABLISHED 2409/sshd: ec2-user
1623142999,,ui,message,    amazon-ebs: tcp        0      0 192.168.0.99:22         13.230.73.125:58012     ESTABLISHED 2458/sshd: ec2-user
1623143221,,ui,message,    amazon-ebs: tcp        0      0 192.168.0.163:22        3.115.18.230:54616      ESTABLISHED 2402/sshd: ec2-user
1623143357,,ui,message,    amazon-ebs: tcp        0     36 192.168.0.46:22         54.249.3.81:55662       ESTABLISHED 2430/sshd: ec2-user
1623143713,,ui,message,    amazon-ebs: tcp        0      0 192.168.0.197:22        18.183.122.218:51354    ESTABLISHED 2449/sshd: ec2-user

これらの IP アドレスは、以下から「東京リージョンのEC2サービス」が使用する IP レンジに含まれているところまで確認できました。

% jq -r '.prefixes[] | select(.region=="ap-northeast-1" and .service=="EC2") | .ip_prefix' < ip-ranges.json | sort -n
3.112.0.0/14
3.5.152.0/21
13.112.0.0/14
13.230.0.0/15
15.177.79.0/24
15.193.1.0/24
18.176.0.0/15
18.178.0.0/16
18.179.0.0/16
18.180.0.0/15
18.182.0.0/16
18.183.0.0/16
35.72.0.0/13
46.51.224.0/19
52.192.0.0/15
52.194.0.0/15
52.196.0.0/14
52.68.0.0/15
52.94.248.80/28
52.95.243.0/24
52.95.255.48/28
54.150.0.0/16
54.168.0.0/16
54.178.0.0/16
54.199.0.0/16
54.238.0.0/16
54.248.0.0/15
54.250.0.0/16
54.64.0.0/15
54.92.0.0/17
54.95.0.0/16
64.252.110.0/24
64.252.111.0/24
64.252.112.0/24
64.252.113.0/24
99.150.48.0/21
99.77.139.0/24
99.77.160.0/24
103.4.8.0/21
175.41.192.0/18
176.32.64.0/19
176.34.0.0/19
176.34.32.0/19

ここから更に絞れるかは確認する手段が思いつかなかったので、もし送信元 IP 制限をかけるなら上記をすべて使用することになりそうです。ちょっと厳しいですね。テンポラリな SecuriryGroup を許容する方が楽ではありそうです。

終わりに

AWS-RunPacker を試してみました。

操作者のロールを使用する場合でもオートメーション用ロールを使用する場合でも、Packer テンプレート内では特に認証情報に関する定義を入れなくても済む事が少し意外でした。

以下ブログではiam_instance_profileとしてオートメーション用ロールを指定していましたが、今回のような使い方であれば不要なようです。

何度か試してみて、 Packer によるビルドの進行状況が一通りオートメーションの実行が終わってからでないと確認できない(アウトプットに出力されない)というのが個人的には少し辛みがありました。

検証環境などで一人で気楽にバチバチ叩ける環境では従来通り手元の端末から実行して、お作法にのっとる必要がある場合には使用を検討する、というのが良さそうです。

手段の一つとして覚えておいてはいかがでしょうか。

以上、 チバユキ (@batchicchi) がお送りしました。