NAT インスタンス用の AMI を Packer で作ってみた

Packer でぱっかーん。

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

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

先日、NAT インスタンスを Amazon Linux2 AMI から手作りしてみました。

久しぶりに OS 上でコマンドを叩いてテンションが上がったので、これまた久しぶりに Packer を使ってみるか、ということでパカることにしました。

今回やること

大まかに書くと以下の通りです。

  1. Packer により NAT インスタンス用 AMI をビルド
  2. NAT インスタンスのセットアップ
  3. クライアントインスタンスから疎通確認

NAT インスタンスを配置する VPC やクライアントインスタンスはあらかじめ作成済みの前提とします。

1. Packer によるビルド

今回使用する Packer は以下バージョンです。

% packer --version
1.8.4

あらかじめ、必要な権限を有する IAM ユーザーのクレデンシャルを AWS CLI プロファイルに設定しています。 *1

テンプレートの準備

nat-instance.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
      },
      "associate_public_ip_address": true,
      "ami_name": "NAT-AmazonLinux2-{{isotime | clean_resource_name}}",
      "tags": {
        "Base_AMI_ID": "{{ .SourceAMI }}",
        "Base_AMI_NAME": "{{ .SourceAMIName }}"
      }
    }
  ],
    "provisioners": [
      {
        "type":  "shell",
        "inline": [
          "sudo sysctl -w net.ipv4.ip_forward=1 | sudo tee -a /etc/sysctl.conf",
          "sudo /sbin/iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE",
          "sudo yum install -y iptables-services",
          "sudo service iptables save",
          "sudo systemctl enable iptables"
        ]
      }
    ]
}

このテンプレートにより、簡単に言うと以下が行われます。

  • Amazon Linux2 の最新の AMI をベースに一時的なインスタンスを起動
  • クライアントから一時的なインスタンスに ec2-user で SSH 接続し各種コマンドを実行
  • 実行後 AMI を取得し、AMI 名の末尾に作成時刻を付与
  • AMI 作成完了後、一時的なインスタンスを削除

一時的なインスタンスの起動サブネットを指定していないので、デフォルトの VPC で作成が行われます。

テンプレートの検証

packer validetの後にテンプレートファイルを指定することで検証できます。

% packer validate nat-instance.json
The configuration is valid.

おまけ:Packer テンプレートを Json から HCL2 に変換

Packer のテンプレートは Json だけでなく HCL2(HashiCorp Configuration Language 2)もサポートされています。

個人的に Json のほうが慣れているのですが、簡単に変換してくれるコマンドがあるので試してみます。

% packer hcl2_upgrade nat-instance.json
Successfully created nat-instance.json.pkr.hcl. Exit 0

変換後のテンプレートはこのようになっていました。一部変換が正常に行われていないですね……。

% cat nat-instance.json.pkr.hcl

data "amazon-ami" "autogenerated_1" {
  filters = {
    name = "amzn2-ami-hvm-*-x86_64-gp2"
  }
  most_recent = true
  owners      = ["137112412989"]
  region      = "ap-northeast-1"
}

# 1 error occurred upgrading the following block:
# unhandled "clean_resource_name" call:
# there is no way to automatically upgrade the "clean_resource_name" call.
# Please manually upgrade to use custom validation rules, `replace(string, substring, replacement)` or `regex_replace(string, substring, replacement)`
# Visit https://packer.io/docs/templates/hcl_templates/variables#custom-validation-rules , https://www.packer.io/docs/templates/hcl_templates/functions/string/replace or https://www.packer.io/docs/templates/hcl_templates/functions/string/regex_replace for more infos.

source "amazon-ebs" "autogenerated_1" {
  ami_name                    = "NAT-AmazonLinux2-{{ clean_resource_name `${timestamp()}` }}"
  associate_public_ip_address = true
  instance_type               = "t3.micro"
  region                      = "ap-northeast-1"
  source_ami                  = "${data.amazon-ami.autogenerated_1.id}"
  ssh_username                = "ec2-user"
  tags = {
    Base_AMI_ID   = "{{ .SourceAMI }}"
    Base_AMI_NAME = "{{ .SourceAMIName }}"
  }
}

build {
  sources = ["source.amazon-ebs.autogenerated_1"]

  provisioner "shell" {
    inline = ["sudo sysctl -w net.ipv4.ip_forward=1 | sudo tee -a /etc/sysctl.conf", "sudo /sbin/iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE", "sudo yum install -y iptables-services", "sudo service iptables save", "sudo systemctl enable iptables"]
  }

}

(読み方が……読み方がわからん……!!!)

おとなしく諦めて Json のテンプレートを引き続き使用することにします。

Packer のビルドの実行

packer build テンプレート名でビルドが実行できます。

今回は一連の流れが 3 分ほどで完了しました。ログを眺めていると何をしているのかが見えて楽しいです。

% packer build nat-instance.json
amazon-ebs: output will be in this color.

==> amazon-ebs: Prevalidating any provided VPC information
==> amazon-ebs: Prevalidating AMI Name: NAT-AmazonLinux2-2022-11-07T13-45-43Z
    amazon-ebs: Found Image ID: ami-046a961f907758d0d
==> amazon-ebs: Creating temporary keypair: packer_63690c07-48c7-f172-95ef-9e0825e6879b
==> amazon-ebs: Creating temporary security group for this instance: packer_63690c09-f5ea-c49c-c1ce-e5cc509d2f98
==> amazon-ebs: Authorizing access to port 22 from [0.0.0.0/0] in the temporary security groups...
==> amazon-ebs: Launching a source AWS instance...
    amazon-ebs: Instance ID: i-0258b976379c25086
==> amazon-ebs: Waiting for instance (i-0258b976379c25086) to become ready...
==> amazon-ebs: Using SSH communicator to connect: 35.77.64.135
==> amazon-ebs: Waiting for SSH to become available...
==> amazon-ebs: Connected to SSH!
==> amazon-ebs: Provisioning with shell script: /var/folders/_5/20lflls12nd16km90bdpry700000gp/T/packer-shell2491648282
    amazon-ebs: net.ipv4.ip_forward = 1
    amazon-ebs: Loaded plugins: extras_suggestions, langpacks, priorities, update-motd
==> amazon-ebs: Existing lock /var/run/yum.pid: another copy is running as pid 2297.
==> amazon-ebs: Another app is currently holding the yum lock; waiting for it to exit...
==> amazon-ebs:   The other application is: yum
==> amazon-ebs:     Memory : 161 M RSS (454 MB VSZ)
==> amazon-ebs:     Started: Mon Nov  7 13:46:01 2022 - 00:07 ago
==> amazon-ebs:     State  : Running, pid: 2297
==> amazon-ebs: Another app is currently holding the yum lock; waiting for it to exit...
==> amazon-ebs:   The other application is: yum
==> amazon-ebs:     Memory : 169 M RSS (461 MB VSZ)
==> amazon-ebs:     Started: Mon Nov  7 13:46:01 2022 - 00:09 ago
==> amazon-ebs:     State  : Running, pid: 2297
    amazon-ebs: Resolving Dependencies
    amazon-ebs: --> Running transaction check
    amazon-ebs: ---> Package iptables-services.x86_64 0:1.8.4-10.amzn2.1.2 will be installed
    amazon-ebs: --> Finished Dependency Resolution
    amazon-ebs:
    amazon-ebs: Dependencies Resolved
    amazon-ebs:
    amazon-ebs: ================================================================================
    amazon-ebs:  Package               Arch       Version                  Repository      Size
    amazon-ebs: ================================================================================
    amazon-ebs: Installing:
    amazon-ebs:  iptables-services     x86_64     1.8.4-10.amzn2.1.2       amzn2-core      58 k
    amazon-ebs:
    amazon-ebs: Transaction Summary
    amazon-ebs: ================================================================================
    amazon-ebs: Install  1 Package
    amazon-ebs:
    amazon-ebs: Total download size: 58 k
    amazon-ebs: Installed size: 24 k
    amazon-ebs: Downloading packages:
    amazon-ebs: Running transaction check
    amazon-ebs: Running transaction test
    amazon-ebs: Transaction test succeeded
    amazon-ebs: Running transaction
    amazon-ebs:   Installing : iptables-services-1.8.4-10.amzn2.1.2.x86_64                  1/1
    amazon-ebs:   Verifying  : iptables-services-1.8.4-10.amzn2.1.2.x86_64                  1/1
    amazon-ebs:
    amazon-ebs: Installed:
    amazon-ebs:   iptables-services.x86_64 0:1.8.4-10.amzn2.1.2
    amazon-ebs:
    amazon-ebs: Complete!
    amazon-ebs: iptables: Saving firewall rules to /etc/sysconfig/iptables: [  OK  ]
==> amazon-ebs: Created symlink from /etc/systemd/system/basic.target.wants/iptables.service to /usr/lib/systemd/system/iptables.service.
==> amazon-ebs: Stopping the source instance...
    amazon-ebs: Stopping instance
==> amazon-ebs: Waiting for the instance to stop...
==> amazon-ebs: Creating AMI NAT-AmazonLinux2-2022-11-07T13-45-43Z from instance i-0258b976379c25086
    amazon-ebs: AMI: ami-0457c2a5f19548b0f
==> amazon-ebs: Waiting for AMI to become ready...
==> amazon-ebs: Skipping Enable AMI deprecation...
==> amazon-ebs: Adding tags to AMI (ami-0457c2a5f19548b0f)...
==> amazon-ebs: Tagging snapshot: snap-0407c0fb3a23b2d7a
==> amazon-ebs: Creating AMI tags
    amazon-ebs: Adding tag: "Base_AMI_ID": "ami-046a961f907758d0d"
    amazon-ebs: Adding tag: "Base_AMI_NAME": "amzn2-ami-hvm-2.0.20221004.0-x86_64-gp2"
==> amazon-ebs: Creating snapshot tags
==> amazon-ebs: Terminating the source AWS instance...
==> amazon-ebs: Cleaning up any extra volumes...
==> amazon-ebs: No volumes to clean up, skipping
==> amazon-ebs: Deleting temporary security group...
==> amazon-ebs: Deleting temporary keypair...
Build 'amazon-ebs' finished after 2 minutes 51 seconds.

==> Wait completed after 2 minutes 51 seconds

==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:
ap-northeast-1: ami-0457c2a5f19548b0f

指定した名称・タグで AMI が作成されていました。

NAT_Packer__EC2_Management_Console

2. NAT インスタンスのセットアップ

NAT インスタンスの起動

Packer により作成された AMI からインスタンスを起動します。

  • パブリックサブネットに配置しパブリック IP 有効化
  • IAM ロールをアタッチしてSystems Manager の権限付与
  • t3.micro、gp3、8 GiB

意図した設定値になっているかの確認のため、ユーザーデータに以下を指定しました。

#!/bin/bash
sysctl -n net.ipv4.ip_forward
systemctl status iptables
iptables -t nat --list

EC2 コンソールからシステムログを取得して確認すると、きちんと意図通りになっていることが確認できました。 *2

...略...
ip-172-31-27-71 login: [   15.908072] cloud-init[2330]: Cloud-init v. 19.3-45.amzn2 running 'modules:final' at Mon, 07 Nov 2022 14:03:51 +0000. Up 15.85 seconds.
[   15.922281] cloud-init[2330]: 1
[   15.927948] cloud-init[2330]: ● iptables.service - IPv4 firewall with iptables
[   15.928235] cloud-init[2330]: Loaded: loaded (/usr/lib/systemd/system/iptables.service; enabled; vendor preset: disabled)
[   15.928589] cloud-init[2330]: Active: active (exited) since Mon 2022-11-07 14:03:42 UTC; 9s ago
[   15.929005] cloud-init[2330]: Process: 1695 ExecStart=/usr/libexec/iptables/iptables.init start (code=exited, status=0/SUCCESS)
[   15.929429] cloud-init[2330]: Main PID: 1695 (code=exited, status=0/SUCCESS)
[   15.929828] cloud-init[2330]: CGroup: /system.slice/iptables.service
[   15.930259] cloud-init[2330]: Nov 07 14:03:41 ip-172-31-19-34.ap-northeast-1.compute.internal systemd[1]: Starting IPv4 firewall with iptables...
[   15.930824] cloud-init[2330]: Nov 07 14:03:42 ip-172-31-19-34.ap-northeast-1.compute.internal iptables.init[1695]: iptables: Applying firewall rules: [  OK  ]
[   15.931194] cloud-init[2330]: Nov 07 14:03:42 ip-172-31-19-34.ap-northeast-1.compute.internal systemd[1]: Started IPv4 firewall with iptables.
[   15.933461] cloud-init[2330]: Chain PREROUTING (policy ACCEPT)
[   15.933902] cloud-init[2330]: target     prot opt source               destination
[   15.934274] cloud-init[2330]: Chain INPUT (policy ACCEPT)
[   15.934690] cloud-init[2330]: target     prot opt source               destination
[   15.935103] cloud-init[2330]: Chain OUTPUT (policy ACCEPT)
[   15.935520] cloud-init[2330]: target     prot opt source               destination
[   15.935948] cloud-init[2330]: Chain POSTROUTING (policy ACCEPT)
[   15.936396] cloud-init[2330]: target     prot opt source               destination
[   15.936770] cloud-init[2330]: MASQUERADE  all  --  anywhere             anywhere
...略...

送信元/送信先チェックの無効化

デフォルトでは送信元/送信先チェックが有効になっているので、「アクション」から無効にしておきます。

NAT_Packer__EC2_Management_Console-7830202


ちなみにインスタンスの起動時にこれをコントロールできたりはしないんですね。(以下はインスタンス起動ウイザードの「高度なネットワーク設定」の画面。)

NAT_Packer_EC2_Management_Console

3. クライアントインスタンスからの疎通確認

クライアントインスタンスが属するサブネットのルートテーブル編集

クライアントインスタンスはプライベートサブネットに属しています。

そのサブネットのルートテーブルで、宛先を0.0.0.0/0としターゲットを NAT インスタンスとするルートを追加します。

NAT_Packer_VPC_Management_Console

ルートの編集画面では「インスタンス」でも「ネットワークインターフェース」でも指定できますが、保存は「ネットワークインターフェース」に統一されます。

クライアントインスタンスにセッションマネージャー接続

クライアントインスタンスにセッションマネージャー接続を試みます。SSM エージェントのインストール、IAM ロールでの権限の付与は済んでいるので、NAT インスタンス経由でエンドポイントにアウトバウンド通信が確立できれば接続できる状態です。

NAT_Packer_client_instance__EC2_Management_Console

問題なく接続できました。また、checkip.amazonaws.com宛ての curl も成功し、NAT インスタンスのグローバル IP アドレスが返却されました。

NAT_Packer_AWS_Systems_Manager_-_Session_Manager

終わりに

NAT インスタンス用の AMI を Packer で作ってみました。

たまに触ると Packer 面白いですね。パカっという感じがします。似たようなことをされようとしている方の参考になれば幸いです。

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

参考

脚注

  1. AssumeRole するパターンのほうがいいんでしょうけどね、セットアップの仕方がよく分かってないので横着してます。
  2. とは言えシステムログが取得できるようになるまで少し時間がかかるので、セッションマネージャーで接続して自分でコマンド叩いたほうが早かったりはします。