每月 Golden Image 构建流程使用 Packer 与 Ansible

发布: (2025年12月23日 GMT+8 12:43)
7 分钟阅读
原文: Dev.to

Source: Dev.to

(请提供您希望翻译的正文内容,我将为您翻译成简体中文。)

介绍

在 IT 运维中,想象我们在一家云组织工作,每月部署数百个 EC2 实例。每个实例都需要安全且合规。手动配置每个实例简直是噩梦。相反,你希望拥有一个 golden image —— 一个可重复使用的 AMI,已预先加固并配备所有必要工具。

这正是 Packer 发挥作用的时机。

问题陈述

在大多数组织中,EC2 实例会频繁启动以支持各种工作负载。但每个实例必须:

  • 安全且合规
  • 配备监控和安全代理
  • 配置一致

Source:

实时场景

假设您是一个注重安全的企业的一员。每个 EC2 实例必须:

  • 符合 CIS 基准
  • 安装 CrowdStrike 和 Qualys 代理

与其在实例启动后再进行配置,您希望创建一个已经加固并预装好的黄金 AMI。该镜像将作为所有未来部署的基础——节省时间并确保一致性。

涉及的工具

  • Packer
  • AWS EC2
  • Ansible
  • GitLab CI/CD
  • Amazon SSM

架构图

架构图

工作流概览

  1. 从基础镜像启动临时 EC2 实例。
  2. 运行配置脚本以:
    • 应用操作系统加固(CIS 基准、 防火墙规则、SSH 配置)。
  3. 从已配置的实例创建新的 AMI。
  4. 终止临时实例。

实施步骤

步骤 1 – 安装 Packer

Packer 安装截图

步骤 2 – 使用 Ansible 进行 Packer 模板创建

以下 Packer 模板通过在特定的 AWS VPC 和子网中启动临时 EC2 实例,并使用指定的 SSH 密钥对进行安全访问,自动化创建自定义 Amazon Machine Image(AMI)的过程。

packer {
  required_plugins {
    amazon = {
      version = ">= 1.2.8"
      source  = "github.com/hashicorp/amazon"
    }
    ansible = {
      version = "~> 1"
      source  = "github.com/hashicorp/ansible"
    }
  }
}

variable "ami_prefix" {
  type    = string
  default = ""
}

variable "reference_image" {
  type    = string
  default = ""
}

locals {
  timestamp = regex_replace(timestamp(), "[- TZ:]", "")
}

variable "privatekey" {
  type    = string
  default = ""
}

source "amazon-ebs" "amazon_linux" {
  ami_name                     = "${var.ami_prefix}-${local.timestamp}"
  instance_type                = "t2.micro"
  region                       = "ap-south-1"
  vpc_id                       = "vpc-07b2ce11f9b189f3b"
  subnet_id                    = "subnet-063ebc661edd9fb37"
  security_group_id            = "sg-04e9ae673095b02e9"
  ssh_interface                = "private_ip"
  associate_public_ip_address  = true
  ssh_keypair_name             = "runner_key"
  ssh_private_key_file         = var.privatekey

  source_ami_filter {
    filters = {
      name                 = "${var.reference_image}"
      root-device-type     = "ebs"
      virtualization-type = "hvm"
    }
    most_recent = true
    owners      = [""]
  }

  ssh_username = "ec2-user"
}

build {
  name    = "learn-packer"
  sources = ["source.amazon-ebs.amazon_linux"]

  provisioner "shell" {
    inline = [
      "sleep 20",
      "echo '--- Running AMI pre‑check ---'",
      "set -e",
      "sudo mkdir -p /usr/lib",
      "# Ensure SFTP subsystem path exists",
      "if [ ! -f /usr/lib/sftp-server ]; then",
      "  if [ -f /usr/libexec/openssh/sftp-server ]; then",
      "    sudo ln -s /usr/libexec/openssh/sftp-server /usr/lib/sftp-server",
      "    echo 'Linked /usr/libexec/openssh/sftp-server -> /usr/lib/sftp-server';",
      "  else",
      "    echo 'Warning: sftp-server not found, installing openssh-server...';",
      "    sudo yum install -y openssh-server || sudo apt-get install -y openssh-server;",
      "  fi;",
      "fi",
      "# Basic network sanity check",
      "sudo yum clean all || true",
      "sudo yum update -y || true",
      "echo '--- Pre‑check complete ---'"
    ]
  }

  provisioner "shell" {
    inline = [
      "sudo mkdir -p /tmp/.ansible",
      "sudo chmod 777 /tmp/.ansible"
    ]
  }

  provisioner "ansible" {
    playbook_file   = "./playbook/main.yml"
    use_proxy       = false
    extra_arguments = [
      "--vault-password-file=/home/gitlab-runner/.vault_pass",
      "-e", "ansible_remote_tmp=/tmp/.ansible",
      "-e", "ansible_local_tmp=/tmp/.ansible",
      "-e", "ansible_scp_if_ssh=True",
      "-e", "ansible_python_interpreter=/usr/bin/python3",
      "-e", "ansible_ssh_transfer_method=scp"
    ]
  }
}

Ansible 主 Playbook

---
- name: Create users and provide sudo access
  hosts: all
  become: true
  gather_facts: true
  vars_files:
    - ../vars/useradd.yml
    - ../vars/vault.yml
  roles:
    - ../roles/useradd
    - ../roles/sudo

- name: Set hostnames
  hosts: all
  become: true
  gather_facts: false
  vars_files:
    - ../vars/var.yml
  roles:
    - ../roles/hostnamectl

- name: Enable or set miscellaneous services
  hosts: all
  become: true
  gather_facts: false
  roles:
    - ../roles/ssh
    - ../roles/login_banner
    - ../roles/services
    - ../roles/timezone
    # - ../roles/fs_integrity
    # - ../roles/selinux
    # - ../roles/firewalld
    # - ../roles/log_management
    - ../roles/rsyslog
    # - ../roles/cron

./roles/journald

---
- hosts: all
  become: true
  gather_facts: true
  vars_files:
    - ../vars/useradd.yml
    - ../vars/vault.yml
  roles:
    - ../roles/useradd

Outcome

此模板通过以下方式构建自定义 AMI:

  • 在特定的 VPC 和子网中启动虚拟机。
  • 使用已定义的 SSH 密钥对进行访问。
  • 运行 Shell 脚本和 Ansible 来配置实例。
  • 将最终镜像保存为唯一名称,以便后续使用。

注意: 我还在 GitLab 中配置了一个 CI/CD 变量,用于安全存储用于 SSH 访问的私钥内容,以供 Packer 构建使用。

GitLab CI/CD Variable for Private Key

在 GitLab 中,CI/CD 变量允许我们安全地存储敏感数据(密码、令牌、SSH 密钥)。我创建了一个变量(例如 PRIVATE_KEY),其中包含 完整的 私钥内容(而不仅仅是路径)。该变量在运行时注入到流水线中,使得 Packer 等工具可以使用它,而无需在代码库中硬编码密钥或泄露密钥。

packer validate -var-file="ami.pkrvars.hcl" -var "privatekey=runner_key.pem" aws-linux.pkr.hcl

第 3 步:GitLab 流水线阶段

在此流水线中,GitLab CI/CD 作业通过以下方式自动化 AMI 创建过程:

  1. 从 CI/CD 变量中安全注入 SSH 私钥。
  2. 验证并构建 Packer 模板。
  3. 在特定 VPC/子网中配置 EC2 实例。
  4. 将最终镜像保存以供后续使用。
default: 
  tags:
    - gitlab_runner

stages:
  - image_build

Image Build: 
  stage: image_build
  script: 
    - echo "$SSH_PRIVATE_KEY" > runner_key.pem
    - chmod 400 runner_key.pem
    - packer init .
    - echo "Validating packer template..."
    - packer validate -var-file="ami.pkrvars.hcl" -var "privatekey=runner_key.pem" aws-linux.pkr.hcl
    - echo "Building AMI..."
    - packer build -var-file="ami.pkrvars.hcl" -var "privatekey=runner_key.pem" aws-linux.pkr.hcl

结论

在本文中,我们解决了云运维中的一个常见挑战——在无需人工干预的情况下,确保每个 EC2 实例 安全合规配置一致

通过结合 PackerAnsibleGitLab CI/CD,我们构建了一个全自动化流水线,实现了:

  • 启动临时 EC2 实例。
  • 应用 CIS 加固并安装安全代理。
  • 保存用于后续使用的黄金 AMI。
  • 使用 GitLab CI/CD 变量来保护凭证。

这种方法不仅提升了安全性和合规性,还节省了大量人工工作时间,降低了人为错误,并确保每次部署都基于可信的基线。

谢谢,

Susseta Bose

Back to Blog

相关文章

阅读更多 »