2024年10月

概述

线程本地变量。当使用 ThreadLocal 维护变量时, ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。

每个线程都有一个 ThreadLocalMap ( ThreadLocal 内部类),Map中元素的
键为 ThreadLocal
,而值对应线程的变量副本。

ThreadLocal原理

如何实现线程隔离

具体关于为线程分配变量副本的代码如下:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap threadLocals = getMap(t);
    if (threadLocals != null) {
        ThreadLocalMap.Entry e = threadLocals.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
  • 首先获取当前线程对象t, 然后从线程t中获取到ThreadLocalMap的成员属性threadLocals

  • 如果当前线程的threadLocals已经初始化(即不为null) 并且存在以当前ThreadLocal对象为Key的值, 则直接返回当前线程要获取的对象(本例中为Connection);

  • 如果当前线程的threadLocals已经初始化(即不为null)但是不存在以当前ThreadLocal对象为Key的的对象, 那么重新创建一个Connection对象, 并且添加到当前线程的threadLocals Map中,并返回

  • 如果当前线程的threadLocals属性还没有被初始化, 则重新创建一个ThreadLocalMap对象, 并且创建一个Connection对象并添加到ThreadLocalMap对象中并返回。

如果存在则直接返回很好理解, 那么对于如何初始化的代码又是怎样的呢?

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
  • 首先调用上面写的重载过后的initialValue方法

  • 继续查看当前线程的threadLocals是不是空的, 如果ThreadLocalMap已被初始化, 那么直接将产生的对象添加到ThreadLocalMap中, 如果没有初始化, 则创建并添加对象到其中;

同时, ThreadLocal还提供了直接操作Thread对象中的threadLocals的方法

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

这样也可以不实现initialValue:

public Connection getConnection() {
    Connection connection = dbConnectionLocal.get();
    if (connection == null) {
        try {
            connection = DriverManager.getConnection("", "", "");
            dbConnectionLocal.set(connection);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    return connection;
}

看过代码之后就很清晰的知道了为什么ThreadLocal能够实现变量的多线程隔离了; 其实就是用了Map的数据结构给当前线程缓存了, 要使用的时候就从本线程的threadLocals对象中获取就可以了, key就是当前线程;

当然了在当前线程下获取当前线程里面的Map里面的对象并操作肯定没有线程并发问题了, 当然能做到变量的线程间隔离了;

ThreadLocalMap对象是什么

本质上来讲, 它就是一个Map, 但是这个ThreadLocalMap与平时见到的Map有点不一样

  • 它没有实现Map接口;

  • 它没有public的方法, 最多有一个default的构造方法, 因为这个ThreadLocalMap的方法仅仅在ThreadLocal类中调用, 属于静态内部类

  • ThreadLocalMap的Entry实现继承了WeakReference<ThreadLocal<?>>

  • 该方法仅仅用了一个Entry数组来存储Key, Value; Entry并不是链表形式, 而是每个bucket里面仅仅放一个Entry;

要了解ThreadLocalMap的实现, 我们先从入口开始, 就是往该Map中添加一个值:

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

//这里用的是Hash冲突的开放定址法的线性探测
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

先进行简单的分析, 对该代码表层意思进行解读:

  • 看下当前threadLocal的在数组中的索引位置 比如: i = 2,看i = 2位置上面的元素(Entry)的Key是否等于threadLocal 这个 Key, 如果等于就很好说了, 直接将该位置上面的Entry的Value替换成最新的就可以了;

  • 如果当前位置上面的 Entry 的 Key为空, 说明ThreadLocal对象已经被回收了, 那么就调用replaceStaleEntry

  • 如果清理完无用条目(ThreadLocal被回收的条目)、并且数组中的数据大小 > 阈值的时候对当前的Table进行重新哈希 所以, 该HashMap是处理冲突检测的机制是向后移位, 清除过期条目 最终找到合适的位置;

了解完Set方法, 后面就是Get方法了:

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

先找到ThreadLocal的索引位置, 如果索引位置处的entry不为空并且键与threadLocal是同一个对象, 则直接返回; 否则去后面的索引位置继续查找

Entry对象

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);//父类是WeakReference,也就是相当于new了一个弱引用(k)
        //也就相当于 map中的key是弱引用的
        value = v;
    }
}

这里的key指向的ThreadLocal是弱引用,是为了防止ThreadLocal对象永远不会被回收。因为,若key为强引用,当ThreadLocal不想用了,那么就令 tl = null,但是此时key中还有一个强引用指向ThreadLocal,因此也就永远无法进行回收(除非ThreadLocalMap不用了),所以会有内存泄露;但如果key使用的是弱引用,只要GC,就会回收

但是还会有内存泄漏存在,ThreadLocal被回收,就导致key=null,此时map中也就无法访问到value,无法访问到的value也就无用了,也就是说,这个k-v对无用了,那么value也应该被回收,但实际上value可能没有被回收,因此依然存在内存泄露

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

弱引用:GC时,若没有强引用指向这个对象了,只剩下弱引用,就会直接进行回收。原因就在于GC时无关内存是否足够,弱引用会被直接回收。所以,只要tl=null了,那么GC时,key指向的ThreadLocal对象就会被回收

ThreadLocal内存泄漏的原因?

每个线程都有⼀个 ThreadLocalMap 的内部属性,map的key是 ThreaLocal ,定义为弱引用,value是强引用类型。垃圾回收的时候会⾃动回收key,而value的回收取决于Thread对象的生命周期。

一般会通过线程池的方式复用线程节省资源,而如果用线程池来操作ThreadLocal 对象确实会造成内存泄露, 因为对于线程池里面不会销毁的线程, 里面总会存在着<ThreadLocal, LocalVariable>的强引用, 因为final static 修饰的 ThreadLocal 并不会释放, 而ThreadLocalMap 对于 Key 虽然是弱引用, 但是强引用不会释放, 弱引用当然也会一直有值, 同时创建的LocalVariable对象也不会释放, 就造成了内存泄露; 如果LocalVariable对象不是一个大对象的话, 其实泄露的并不严重, 泄露的内存 = 核心线程数 * LocalVariable对象的大小;

所以, 为了避免出现内存泄露的情况, ThreadLocal提供了一个清除线程中对象的方法, 即 remove, 其实内部实现就是调用 ThreadLocalMap 的remove方法:

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

应用场景

每个线程维护了一个“序列号”

public class SerialNum {
    // The next serial number to be assigned
    private static int nextSerialNum = 0;

    private static ThreadLocal serialNum = new ThreadLocal() {
        protected synchronized Object initialValue() {
            return new Integer(nextSerialNum++);
        }
    };

    public static int get() {
        return ((Integer) (serialNum.get())).intValue();
    }
}

Session的管理

Web 应用中的请求处理:在 Web 应用中,一个请求通常会被多个线程处理,每个线程需要访问自己的数据,使用 ThreadLocal 可以确保数据在每个线程中的独立性。

经典的另外一个例子:

private static final ThreadLocal threadSession = new ThreadLocal();  
  
public static Session getSession() throws InfrastructureException {  
    Session s = (Session) threadSession.get();  
    try {  
        if (s == null) {  
            s = getSessionFactory().openSession();  
            threadSession.set(s);  
        }  
    } catch (HibernateException ex) {  
        throw new InfrastructureException(ex);  
    }  
    return s;  
}  

在线程内部创建ThreadLocal

线程池中的线程对象共享数据:线程池中的线程对象是可以被多个任务共享的,如果线程对象中需要保存任务相关的数据,使用 ThreadLocal 可以保证线程安全。

当然,在使用线程池时,ThreadLocal 可能会导致线程重用时的数据残留,从而影响程序的正确性。因此,在使用线程池时,要确保在任务执行前后清理 ThreadLocal 的值,以避免线程重用时的数据残留。

线程类内部创建ThreadLocal,基本步骤如下:

  • 在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。

  • 在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。

  • 在ThreadDemo类的run()方法中,通过调用getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。

public class ThreadLocalTest implements Runnable{
    
    ThreadLocal<Student> StudentThreadLocal = new ThreadLocal<Student>();

    @Override
    public void run() {
        String currentThreadName = Thread.currentThread().getName();
        System.out.println(currentThreadName + " is running...");
        Random random = new Random();
        int age = random.nextInt(100);
        System.out.println(currentThreadName + " is set age: "  + age);
        Student Student = getStudentt(); //通过这个方法,为每个线程都独立的new一个Studentt对象,每个线程的的Studentt对象都可以设置不同的值
        Student.setAge(age);
        System.out.println(currentThreadName + " is first get age: " + Student.getAge());
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println( currentThreadName + " is second get age: " + Student.getAge());
        
    }
    
    private Student getStudentt() {
        Student Student = StudentThreadLocal.get();
        if (null == Student) {
            Student = new Student();
            StudentThreadLocal.set(Student);
        }
        return Student;
    }

    public static void main(String[] args) {
        ThreadLocalTest t = new ThreadLocalTest();
        Thread t1 = new Thread(t,"Thread A");
        Thread t2 = new Thread(t,"Thread B");
        t1.start();
        t2.start();
    }
    
}

class Student{
    int age;
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    
}

java 开发手册中推荐的 ThreadLocal

看看阿里巴巴 java 开发手册中推荐的 ThreadLocal 的用法:

import java.text.DateFormat;
import java.text.SimpleDateFormat;
 
public class DateUtils {
    public static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
}

然后再要用到 DateFormat 对象的地方,这样调用:

DateUtils.df.get().format(new Date());

面试题专栏

Java面试题专栏
已上线,欢迎访问。

  • 如果你不知道简历怎么写,简历项目不知道怎么包装;
  • 如果简历中有些内容你不知道该不该写上去;
  • 如果有些综合性问题你不知道怎么答;

那么可以私信我,我会尽我所能帮助你。

兴趣是最好的老师,
HelloGitHub
让你对编程感兴趣!

简介

HelloGitHub
分享 GitHub 上有趣、入门级的开源项目。

github.com/521xueweihan/HelloGitHub

这里有实战项目、入门教程、黑科技、开源书籍、大厂开源项目等,涵盖多种编程语言 Python、Java、Go、C/C++、Swift...让你在短时间内感受到开源的魅力,对编程产生兴趣!


以下为本期内容|每个月
28
号更新

C 项目

1、
rawdrawandroid
:仅用 C 语言开发 Android 应用。这是一个 Android 应用开发框架,可以让开发者不用 Java,仅用 C 和 Make 开发 Android 应用。它轻量且跨平台,支持 OpenGL ES、陀螺仪、多点触控及 Android 键盘,并能直接访问 USB 设备。

2、
taisei
:免费开源的东方 Project 系列的射击游戏。该项目是基于东方 Project 世界观的弹幕射击类游戏,拥有独立原创的故事情节、音乐和游戏机制。这款名为“泰西”的游戏,采用 C11、SDL2 和 OpenGL 开发,完全免费且开源,支持在 Windows、Linux、macOS 和 Chrome 等浏览器上运行。

C# 项目

3、
Bulk-Crap-Uninstaller
:免费的 Windows 应用卸载神器。这是一个用 C# 开发的 Windows 软件卸载工具,能够快速删除大量不需要的应用程序。它完全免费、开箱即用,支持批量和强制卸载、清理残留文件、检测隐藏或受保护的已注册应用等功能。虽然面向 IT 专业人员设计,但其简单的默认设置,让任何人都能轻松上手。来自
@猎隼丶止戈reNo7
的分享

4、
Macro-Deck
:将手机变成 Stream Deck 的工具。该项目可以将手机、平板等带浏览器的设备变成类似 Stream Deck 的远程自定义按键板,实现一键执行单步或多步操作,适用于直播和简化日常任务等场景。

C++ 项目

5、
aria2
:超快的命令行下载工具。这个跨平台命令行下载工具由 C++ 开发,支持 HTTP(S)、FTP、SFTP、BitTorrent 等多种协议。它操作简单、体积小、下载速度快,并提供后台运行、速度限制、分段下载和 BitTorrent 扩展等功能。来自
@刘睿华
的分享

6、
fast_float
:速度与精准兼具的 C++ 数字解析库。该项目是用于快速解析数字字符串的 C++ 库,实现了类似 from_charts 函数的功能。它是一个速度极快、仅头文件的库,比标准库快数倍。支持解析 float、double 和整数类型的字符串,已被广泛应用在 Chromium、Redis 和 LLVM 等知名项目中。

#include "fast_float/fast_float.h"
#include <iostream>

int main() {
    const std::string input =  "3.1416 xyz ";
    double result;
    auto answer = fast_float::from_chars(input.data(), input.data()+input.size(), result);
    if(answer.ec != std::errc()) { std::cerr << "parsing failure\n"; return EXIT_FAILURE; }
    std::cout << "parsed the number " << result << std::endl;
    return EXIT_SUCCESS;
}

7、
mame
:开源的街机模拟器。这是一款支持海量街机游戏的模拟器。它通过模拟多种硬件平台,实现了在电脑上运行各种复古软件的功能。不仅支持街机,还有老式电脑和游戏机。

Go 项目

8、
beszel
:轻量级高颜值的 Docker 监控平台。这是一个轻量级的服务器监控平台,包括 Docker 统计、历史数据和警报功能。它拥有友好的 Web 界面,配置简单、开箱即用,支持自动备份、多用户、OAuth 认证和 API 访问等功能。

9、
envd
:高效的 AI 开发环境搭建工具。这是一个为 AI/ML 项目提供可复现开发环境的命令行工具。只需简单的配置语言和命令,即可快速创建基于容器的开发环境,支持远程构建、依赖缓存和导入远程仓库等功能。

10、
gophish
:开源的网络钓鱼平台。该项目提供了一个开箱即用的网络钓鱼平台,可用于模拟钓鱼攻击。它拥有友好的 Web 管理后台,支持邮件模板、批量发送邮件、网站克隆和数据可视化,适用于企业安全培训和渗透测试等场景。

11、
opentofu
:实现基础设施即代码的开源方案。该项目是一个开源的基础设施即代码工具,专注于自动化地创建、管理和部署本地和云服务基础设施。作为 Terraform 的一个分支,它由社区驱动,支持使用高级配置语法描述基础设施、生成执行计划和构建资源依赖图,从而减少人为操作失误,实现复杂变更的自动化。来自
@刘睿华
的分享

12、
photoview
:极简的照片管理平台。这是一款用于自建云相册的 Web 应用,它拥有直观的用户界面和丰富的功能,支持自动整理照片、生成缩略图、共享相册、EXIF 解析和多用户管理。还提供了 iOS 应用,方便用户在手机上访问。来自
@刘睿华
的分享

Java 项目

13、
GoGoGo
:开源的 Android 虚拟定位应用。该项目是一个基于 Android 调试 API 和百度地图实现的虚拟定位工具,无需 ROOT 权限即可修改地理位置。它支持位置搜索和手动输入坐标,并提供了一个可自由移动的摇杆来模拟位移。

14、
karate
:开源的 API 自动测试框架。这是一款基于 Java 的 API 测试框架,可与 Spring Boot、Maven 等 Java 生态系统无缝集成。它整合了 API 测试自动化、模拟、性能测试和 UI 自动化等功能,支持使用类似 Cucumber 的语法编写测试用例,并提供了一个跨平台的可执行文件,即使对 Java 不熟悉也能轻松上手。

JavaScript 项目

15、
icones
:极简的图标搜索网站。这是一个用于快速查找各种图标的网站,支持分类过滤和多选模式。用户可将选择的图标打包为字体或直接下载 SVG 格式。

16、
media-chrome
:打造现代化网页播放器界面的组件库。这是一个用于定制网页音频和视频播放器界面的库,兼容各种 JavaScript 框架。它高度可定制,开发者可以轻松调整组件的外观和功能,支持字幕、投屏、快捷键、倍速、预览缩略图、移动端和静音按钮等功能。

<media-controller audio>
  <audio
    slot="media"
    src="xxxxxx"
  ></audio>
  <media-control-bar>
    <media-play-button></media-play-button>
    <media-time-display showduration></media-time-display>
    <media-time-range></media-time-range>
    <media-playback-rate-button></media-playback-rate-button>
    <media-mute-button></media-mute-button>
    <media-volume-range></media-volume-range>
  </media-control-bar>
</media-controller>

17、
Moe-Counter
:可爱的网站计数器。该项目是一个用于统计页面访问人数的计数器。它不仅简单易用,还提供多种可爱风格的主题,用户可根据个人喜好进行选择。

18、
piscina
:灵活高效的 Node.js 线程池。该项目是用 TypeScript 编写的高性能 Node Worker 线程池,旨在简化 Node.js 多线程编程。它提供简单易用的 API,支持线程间通信、动态调整线程池大小、取消任务、设置内存限制和异步任务跟踪等功能。

const path = require('path');
const Piscina = require('piscina');

const piscina = new Piscina({
  filename: path.resolve(__dirname, 'worker.js')
});

(async function() {
  const result = await piscina.run({ a: 4, b: 6 });
  console.log(result);  // Prints 10
})();

19、
swapy
:轻松实现拖动交换布局的库。该项目可以将任意布局转换为可拖动交换的形式,仅需几行代码即可实现。它支持设置交互动画,可以在 React、Vue、Svelte 等框架中使用,适用于各种需要交互式布局的场景。

import { createSwapy } from 'swapy'

const container = document.querySelector('.container')

const swapy = createSwapy(container, {
  animation: 'dynamic' // or spring or none
})

// You can disable and enable it anytime you want
swapy.enable(true)

Kotlin 项目

20、
etchdroid
:在手机上制作 USB 启动盘的工具。这是一个开源的 Android 应用,专为在手机上制作操作系统 USB 启动盘而设计。它无需 ROOT 权限,即可将操作系统镜像写入 USB 设备,支持 Ubuntu、树莓派等多个系统,适用在无法使用电脑时制作启动 U 盘。

21、
KeyMapper
:Android 按键重映射应用。这是一个免费开源的 Android 应用,可以自定义 Android 设备的按键、指纹和手势操作。无需 ROOT 权限,支持蓝牙和有线键盘,提供灵活的按键重映射体验。

Python 项目

22、
backtrader
:Python 量化交易回测框架。该项目是用 Python 编写的回测库,专为开发和测试交易策略而设计。它可以从 CSV 文件、在线数据源和 pandas 中提取数据,支持多策略同步运行、生成交易策略的可视化图表等功能。内置 100 多种指标,包括趋势、成交量和波动性等指标。

from datetime import datetime
import backtrader as bt

class SmaCross(bt.SignalStrategy):
    def __init__(self):
        sma1, sma2 = bt.ind.SMA(period=10), bt.ind.SMA(period=30)
        crossover = bt.ind.CrossOver(sma1, sma2)
        self.signal_add(bt.SIGNAL_LONG, crossover)

cerebro = bt.Cerebro()
cerebro.addstrategy(SmaCross)

data0 = bt.feeds.YahooFinanceData(dataname='MSFT', fromdate=datetime(2011, 1, 1),
                                  todate=datetime(2012, 12, 31))
cerebro.adddata(data0)

cerebro.run()
cerebro.plot()

23、
core
:开源的智能家居平台。这是一个用 Python 编写的智能家居平台,旨在整合不同品牌的智能设备,提供个性化的家庭自动化体验。它解决了传统系统互操作性(Interoperability)差的问题,允许用户在同一平台上自由控制和联动 Apple HomeKit、米家、Aqara、涂鸦等设备,极大提升了智能家居的灵活性和便捷性。适合希望打破单一平台限制的用户,尤其是追求高性价比的 DIY 智能家居爱好者。来自
@无间之钟
的分享

24、
paperless-ngx
:纸质文档数字化存档工具。这是一个基于 Django 的文档管理系统,可将纸质文档转换成可搜索的在线存档。不同于普通的扫描仪将实体书变为难以检索的图片或 PDF 格式,它通过文档扫描器实现电子化,转化为易于检索的格式。

25、
pipreqs
:快速生成 Python 项目依赖文件的工具。该项目可以根据 Python 项目中的导入语句,生成 requirements.txt 文件。它能够自动识别项目中使用的库,无需安装即可生成依赖库列表。

26、
pokeapi
:宝可梦数据的 API 服务。这是一个基于 Django 构建的宝可梦数据 RESTful API 服务,为开发者提供全面的宝可梦数据库,包括小精灵的动作、属性、技能和进化信息等详细资料。

Rust 项目

27、
insta
:Rust 的快照测试库。这是一个用于 Rust 项目的快照测试库,特别适用于参考值非常大或经常变化的场景。它提供了 VSCode 插件和命令行工具,当测试因参考值变动而失败时,可以通过 review 命令查看问题,并一键更新快照(参考值),从而快速通过单元测试。

fn split_words(s: &str) -> Vec<&str> {
    s.split_whitespace().collect()
}

#[test]
fn test_split_words() {
    let words = split_words("hello from the other side");
    insta::assert_yaml_snapshot!(words);
}

28、
oha
:Rust 驱动的 HTTP 压测工具。这是一个用 Rust 开发的 HTTP 请求压测工具,它操作简单、带 TUI 动画界面,支持生成请求延迟、吞吐量等指标的报告,以及动态 URL 和更灵活的请求间隔(burst-delay)等功能。

29、
steel
:基于 Rust 的嵌入式 Scheme 解释器。这是一个用 Rust 编写的嵌入式 Scheme 解释器,旨在提供轻量级且快速的脚本语言支持。它解决了在嵌入式环境或小型应用中对高效、灵活脚本引擎的需求。来自
@无间之钟
的分享

Swift 项目

30、
aural-player
:灵感来自 Winamp 的 macOS 音乐播放器。该项目是受经典的 Winamp 播放器启发,用 Swift 编程语言开发的适用于 macOS 的音乐播放器。它内置音效和均衡器,支持多种音频格式、回放、歌词显示、自定义界面等功能。

31、
DockDoor
:适用于 macOS 的窗口预览工具。该项目是用 Swift 和 SwiftUI 开发的 Dock 窗口预览工具。只需将鼠标悬停在 Dock 上的应用图标,即可预览其打开的窗口,还支持类似 Windows 的 Alt+Tab 切换和自定义快捷键的功能。

人工智能

32、
moondream
:小型的视觉语言模型。这是一个可在资源受限的设备上运行的小型视觉语言模型,它能够理解并生成与图像相关的自然语言描述,支持图像识别、生成描述和问答等功能。

from transformers import AutoModelForCausalLM, AutoTokenizer
from PIL import Image

model_id = "vikhyatk/moondream2"
revision = "2024-08-26"
model = AutoModelForCausalLM.from_pretrained(
    model_id, trust_remote_code=True, revision=revision
)
tokenizer = AutoTokenizer.from_pretrained(model_id, revision=revision)

image = Image.open('<IMAGE_PATH>')
enc_image = model.encode_image(image)
print(model.answer_question(enc_image, "Describe this image.", tokenizer))

33、
Prompt_Engineering
:全面的提示工程实战指南。这份教程致力于帮助用户掌握与大型语言模型(LLM)沟通的技巧。内容涵盖从基础到高级的提示工程技术,附有详细的实现指南和示例代码。

34、
spaCy
:强大的自然语言处理 Python 库。这是一个工业级的自然语言处理(NLP)库,支持 70 多种语言的分词和训练。它采用 Python 编写,可实现标注、解析和文本分类等功能,并支持模型打包与部署。

# pip install -U spacy
# python -m spacy download en_core_web_sm
import spacy

# Load English tokenizer, tagger, parser and NER
nlp = spacy.load("en_core_web_sm")

# Process whole documents
text = ("When Sebastian Thrun started working on self-driving cars at "
        "Google in 2007, few people outside of the company took him "
        "seriously. “I can tell you very senior CEOs of major American "
        "car companies would shake my hand and turn away because I wasn’t "
        "worth talking to,” said Thrun, in an interview with Recode earlier "
        "this week.")
doc = nlp(text)

# Analyze syntax
print("Noun phrases:", [chunk.text for chunk in doc.noun_chunks])
print("Verbs:", [token.lemma_ for token in doc if token.pos_ == "VERB"])

# Find named entities, phrases and concepts
for entity in doc.ents:
    print(entity.text, entity.label_)

35、
ultralytics
:先进的对象检测和跟踪模型。该项目是基于之前的 YOLO 版本,增加了新功能并改进了模型,在对象检测、跟踪、实例分割和图像分类等任务中表现出色。

from ultralytics import YOLO

# Load a model
model = YOLO("yolo11n.pt")

# Train the model
train_results = model.train(
    data="coco8.yaml",  # path to dataset YAML
    epochs=100,  # number of training epochs
    imgsz=640,  # training image size
    device="cpu",  # device to run on, i.e. device=0 or device=0,1,2,3 or device=cpu
)

# Evaluate model performance on the validation set
metrics = model.val()

# Perform object detection on an image
results = model("path/to/image.jpg")
results[0].show()

# Export the model to ONNX format
path = model.export(format="onnx")  # return path to exported model

其它

36、
BilibiliSponsorBlock
:B 站视频空降助手。这是一款能够自动跳过 B 站视频中恰饭片段和开场、结尾动画的浏览器插件,所有标注数据均由网友贡献,支持 Chrome、Edge 和 FireFox 浏览器。

37、
cognitive-load
:降低开发者认知负荷的建议。这是一篇关于如何在软件开发过程中,降低认知负荷的文章。即简化代码、提高代码的可读性,减轻开发者在阅读和理解代码时的负担。

38、
dockerc
:将 Docker 镜像编译为独立可执行文件的工具。该项目能将 Docker 镜像转化为二进制可执行文件,无需配置 Docker 环境或安装依赖,简化了软件的分发和运行流程。来自
@kero990
的分享

39、
kubernetes-goat
:Kubernetes 安全攻防演练平台。该项目是用于构建漏洞百出、易受攻击的集群环境,让开发者可以在真实场景中学习 K8s 攻击和防御技巧。

40、
pilipala
:开源的 bilibili 第三方客户端。该项目是用 Flutter 开发的 B 站第三方客户端,支持 Android 和 iOS 平台。它提供了推荐视频列表、热门直播、番剧、离线缓存、回复评论、弹幕和搜索等功能。来自
@Micro·J
的分享

41、
Sensor-Watch
:卡西欧 F-91W 手表的开源电路板。该项目是为经典 Casio F-91W 手表自制电路板,采用 ARM Cortex-M0+ 微控制器(SAM L22)。配备十位数段液晶显示屏、五个指示段、LED 背光和三个按钮,支持用户通过 USB 编程,在手表上运行自定义程序。

42、
themostdangerouswritingapp
:挑战写作效率极限的工具。这是一个帮助用户进入写作“心流”状态的 Web 应用。如果你停止输入超过 5 秒,屏幕上的文字就会逐渐变得模糊,最终会彻底消失。来自
@孤胆枪手
的分享

开源书籍

43、
udlbook
:《Understanding Deep Learning》理解深度学习。该书是由 Simon J.D. Prince 编写的一本关于深度学习的专业书籍,内容涵盖深度学习的理论基础、性能评估、卷积网络、Transformers、图神经网络、生成对抗网络(GANs)、扩散模型(Diffusion Models)、强化学习等主题,并附有大量练习题。

最后

感谢参与分享开源项目的小伙伴们,欢迎更多的开源爱好者来 HelloGitHub 自荐/推荐开源项目。如果你发现了 GitHub 上有趣的项目,就
点击这里
分享给大家伙吧!

本期有你感兴趣的开源项目吗?如果有的话就留言告诉我吧~如果还没看过瘾,可以
点击阅读
往期内容。

感谢您的阅读,如果觉得本期内容还不错的话
求赞、求分享
❤️

在云计算的浪潮中,基础设施管理变得越来越复杂。如何高效地配置和管理云资源,成为了每个开发者和运维工程师必须面对的挑战。Terraform,作为一种强大的基础设施即代码(IaC)工具,为我们提供了一种简洁而有效的解决方案。

在这篇博客中,我将深入探讨Terraform的功能与使用场景,帮助你理解其在云资源管理中的重要性。同时,我会详细介绍Terraform的安装步骤,以便你能快速上手。

这篇博客特别适合入门级读者,内容详尽易懂,确保即使是初学者也能顺利跟随。我将通过实际操作,使用Terraform在AWS上创建各种基础设施资源,包括VPC、子网、路由表、网关、安全组、EC2实例、EBS卷和弹性IP(EIP)。此外,我还将分享如何创建EKS的IAM角色,定义EKS的Terraform配置文件,以及如何配置EKS Node Group的IAM角色和节点组,一步一步创建EKS集群。

每个Terraform文件都将配有详尽的解释,让你清晰理解每一行代码的意义和作用。无论你是云计算新手还是希望提升技能的专业人士,这篇博客都将为你提供实用的指导和深入的见解,帮助你轻松迈入Terraform的世界。让我们一起开始这段充满乐趣的学习之旅吧!

一、
Terraform 简介

1. 特性与使用场景

Terraform 是一个开源的基础设施即代码(Infrastructure as Code, IaC)工具,由 HashiCorp 开发。它允许用户通过配置文件以编程方式管理云基础设施、物理设备以及其他服务的资源。以下是 Terraform 的一些关键特性和使用场景:

基础设施即代码

Terraform 使用简单的声明性配置语言(HCL,HashiCorp Configuration Language),使用户能够定义和管理他们的基础设施。这种方式带来了多个优势:

  • 版本控制
    : 通过将配置文件存储在版本控制系统中,用户可以跟踪基础设施的历史变更,轻松回滚到先前的状态。
  • 共享和复用
    : 配置文件可以作为代码库的一部分进行共享,促进团队成员之间的协作和最佳实践的传递。
  • 审计和合规性
    : 明确的配置文件使得基础设施的审计和合规性检查变得简单,通过对比配置文件和实际状态,可以快速识别不一致性。

多云支持

Terraform 支持多种云服务提供商,包括 AWS、Azure、Google Cloud、阿里云等,用户可以在一个配置文件中同时管理不同云环境的资源。这种多云管理能力带来了如下好处:

  • 灵活性
    : 企业可以根据需求和成本优化选择合适的云服务提供商,而无需重新编写大量配置代码。
  • 灾难恢复
    : 可以在不同云环境中实现资源备份和故障转移,提升业务连续性。
  • 集成能力
    : 不同云提供商的服务可以无缝集成,构建跨云应用架构。

状态管理

Terraform 会维护一个关于当前基础设施状态的文件(状态文件),以便在后续的变更中跟踪和管理资源的状态。状态管理的优势包括:

  • 一致性
    : 状态文件确保用户对资源的操作是基于最新状态的,防止并发修改导致的冲突。
  • 变更检测
    : 在应用新配置之前,Terraform 会根据状态文件与配置文件的比较,提供清晰的变更计划,确保用户了解即将进行的操作。
  • 远程状态
    : 支持将状态文件存储在远程后端(如 S3),便于团队协作和提高安全性。

资源依赖管理

Terraform 能够自动处理资源之间的依赖关系,确保在创建或修改资源时,按照正确的顺序进行操作。这减少了手动处理依赖的复杂性,提高了自动化水平。具体来说:

  • 自动排序
    : 用户不必手动指定创建顺序,Terraform 会根据资源之间的引用关系自动处理。
  • 并行处理
    : 通过识别独立资源,Terraform 可以并行创建或删除资源,缩短整体执行时间。

可扩展性

Terraform 提供了丰富的插件和模块,用户可以通过自定义模块来扩展 Terraform 的功能,实现更复杂的基础设施架构。可扩展性的特点包括:

  • 社区贡献
    : Terraform 拥有一个活跃的社区,用户可以方便地找到现成的模块并进行集成,减少重复工作。
  • 模块化设计
    : 用户可以将常用的配置封装成模块,提高配置的复用性和可读性。

跨团队协作

Terraform 的配置文件可以与 Git 等版本控制系统结合使用,支持团队协作。团队协作的优势包括:

  • 代码审查
    : 团队成员可以对基础设施的变更进行审查,确保变更经过充分验证,提升基础设施的稳定性。
  • 透明性
    : 通过版本控制,所有团队成员都能清楚地看到基础设施的变更历史和决策过程,促进知识的共享。

使用场景

  • 创建和管理云基础设施
    : 通过 Terraform,用户可以轻松创建和管理各种云资源,如 VPC、EC2 实例、RDS 数据库、EKS 集群等,提升管理效率。
  • 实现持续集成和持续交付(CI/CD)中的基础设施自动化
    : 将基础设施配置纳入 CI/CD 流程,确保环境一致性,降低手动操作的风险。
  • 配置和管理多个环境
    : 使用 Terraform,用户可以轻松配置和管理开发、测试和生产环境的基础设施,保证环境之间的一致性。
  • 通过模块化设计实现基础设施的复用
    : 用户可以创建和共享模块,以便在不同项目中复用相同的基础设施配置。

总之,Terraform 是一个强大且灵活的工具,能够帮助开发团队以代码的方式高效管理云基础设施,提升运维效率和灵活性。通过 Terraform,用户能够在多云环境中实现自动化和标准化,适应快速变化的业务需求。

2. Terraform 工作原理和工作流程

Terraform 是一个基础设施即代码(IaC)工具,通过以下几个步骤来管理基础设施:

  1. 配置文件(.tf 文件)
    : 用户首先通过编写 Terraform 配置文件来定义所需的基础设施。这些文件使用 HCL(HashiCorp Configuration Language)语言,描述资源的类型、属性和配置。

  2. 初始化(terraform init)
    : 在开始使用 Terraform 之前,用户需要运行
    terraform init
    命令。这一步会初始化工作目录,下载所需的提供程序(如 AWS、Azure 等),并准备后续的操作。

  3. 生成执行计划(terraform plan)
    : 使用
    terraform plan
    命令,Terraform 会读取配置文件并生成执行计划,展示将要执行的操作(如创建、更新或删除资源)。这一步允许用户预览即将进行的变更,避免意外操作。

  4. 应用变更(terraform apply)
    : 在确认执行计划后,用户可以运行
    terraform apply
    命令,Terraform 会根据生成的计划实际执行相应的操作,创建、更新或删除云资源。

  5. 状态管理
    : Terraform 会维护一个状态文件(
    terraform.tfstate
    ),记录当前基础设施的状态。这个文件用于跟踪资源的实际状态,以便在后续操作中进行对比和管理。

  6. 变更管理
    : 当需要对基础设施进行更改时,用户只需修改配置文件,然后重复执行
    plan

    apply
    流程。Terraform 会自动识别资源的变更,并进行相应的更新。

  7. 销毁资源(terraform destroy)
    : 当不再需要某些资源时,用户可以运行
    terraform destroy
    命令,Terraform 会删除所有配置文件中定义的资源,确保清理工作整洁。

通过这些步骤,Terraform 能够以一致性和可预测的方式管理和部署基础设施,使用户在整个基础设施生命周期中保持对资源的控制和管理。

3. Terraform基本使用概念

3.1 Provider

  • 定义
    : Provider 是 Terraform 与外部服务(如 AWS、Azure、Google Cloud 等)进行交互的插件。它们负责管理资源的生命周期。
  • 使用
    : 在
    provider.tf
    文件中,通常会定义所需的云服务提供商。例如,对于 AWS,你可能会写如下内容:
    terraform {
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 3.0"
        }
      }
    }
    
    provider "aws" {
      region     = "us-west-1"
    #  access_key = var.aws_access_key  #本文使用aws cli那块定义的key
    #  secret_key = var.aws_secret_key
    }
  • 这里,
    required_providers
    定义了所需的 AWS 提供商及其版本。
  • provider
    块则设置了访问该服务的具体参数,如区域和凭证。

3.2  Terraform状态文件

  • 定义
    :
    terraform.tfstate
    是 Terraform 用来跟踪已管理资源状态的文件。它存储了当前基础设施的详细信息。
  • 作用
    : 这个文件帮助 Terraform 在执行计划(
    terraform plan
    )和应用(
    terraform apply
    )时了解哪些资源已经创建、哪些需要更新或删除。
  • 注意事项
    : 状态文件敏感且重要,应妥善保管,避免直接修改。为了提高安全性,通常建议使用远程后端(如 S3)存储状态文件。

3.3 Terraform配置文件

  • 定义
    : Terraform 的配置文件以
    .tf
    为扩展名,包含了所有基础设施资源的定义和配置。
  • 内容
    : 每个配置文件可以包含资源定义、变量、输出等。例如,
    main.tf
    中可以包含 VPC、子网、EC2 实例的定义。
  • 示例
    :
    resource "aws_vpc" "my_vpc" {
      cidr_block = "10.0.0.0/16"
      enable_dns_support = true
      enable_dns_hostnames = true
    }

3.4 变量文件

  • 定义
    : 变量文件(通常命名为
    variables.tf
    )用于定义可以在多个地方使用的变量,以提高灵活性。
  • 使用
    : 你可以在配置中引用这些变量,以便根据不同环境(如开发、测试、生产)进行定制。例如:
    variable "region" {
      description = "The AWS region to deploy resources"
      type        = string
      default     = "us-west-1"
    }

3.5 输出文件

  • 定义
    : 输出文件(通常命名为
    outputs.tf
    )用于定义在 Terraform 执行后希望输出的信息,方便用户获取资源的关键信息。
  • 作用
    : 输出可以是 EC2 实例的公共 IP 地址、安全组 ID 等。例如:
    output "instance_ip" {
      value = aws_instance.my_instance.public_ip
    }

二、
环境准备

1. 安装 Terraform

请根据自己的操作系统参考 https://developer.hashicorp.com/terraform/install,本文之列出常见的操作系统安装方式。

macOS

brew tap hashicorp/tap
brew install hashicorp/tap/terraform

Windows

https://releases.hashicorp.com/terraform/1.9.8/terraform_1.9.8_windows_amd64.zip

Ubuntu/Debian

wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

CentOS/RHEL

sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum -y install terraform

2. 配置 AWS CLI

确保配置的访问密钥拥有足够的权限。

详情可参考:
https://www.cnblogs.com/Sunzz/p/18432935

2.1. 创建
~/.aws/config文件

内容如下:

[default]
region = us-west-1

其中region请根据你的实际情况进行修改即可

2.2 创建
~/.aws/credentials文件

内容如下:

[default]
aws_access_key_id = AKIA2LXD....
aws_secret_access_key = ZvQllpYL.....

转载请著名原文地址:
https://www.cnblogs.com/Sunzz/p/18498915

3. 初始化
terraform

3.1 创建
variables.tf
文件

variables.tf用来存多次用到的变量

内容如下:

variable "aws_region" {
  default = "us-west-1"
}

定义了使用的区域,这里使用us-west-1,请根据你的实际情况进行修改。

3.2 创建
provider.tf
文件

配置定义了 Terraform 使用的
providers

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

解释

  • terraform

    :指定 Terraform 的基本设置,包括依赖的 providers。

  • required_providers
    :定义 Terraform 项目所依赖的 providers 列表,这些 providers 用于与特定平台交互,例如 AWS 或生成 TLS 密钥。


    • aws
      provider


      • source
        :指明此 provider 的来源,即
        hashicorp/aws
        ,表示使用 HashiCorp 官方发布的 AWS provider。
      • version
        :指定使用
        4.0
        版本及其以上的最新版本,但不会升级到
        5.0
        以上。

3.3 初始化

初始化过程中会用到国外的一些网络资源,由于众所周知的原因,下载的时候可能出现一些问题,这里建议直接使用你的工具即可。

根据实际情况修改ip和端口

export https_proxy=http://127.0.0.1:7890
export http_proxy=http://127.0.0.1:7890
terraform init

输出如下:

Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching ">= 4.0.0"...
- Installing hashicorp/aws v5.72.1...
- Installed hashicorp/aws v5.72.1 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

三、创建aws 网络资源

本文每次创建之前我都会先创建对应资源的tf文件,一种资源一个tf文件,比如所有的ec2都放在一个ec2.tf文件中。

然后都会执行
terraform
plan -out=tf.plan
来预演一下执行结果,防止出错。

terraform plan -out=tf.plan
是一个“预演”工具。它不会真的去创建或改动资源,而是生成一个详细的计划,告诉我们“如果执行,会做哪些具体更改”。这个计划可以保存成一个文件(比如这里的
tf.plan
),这样我们可以先检查它,确保没问题后,再真正去执行。这不仅减少了出错的机会,还让我们随时知道哪些资源会被创建、修改或删除。

1. 创建vpc

编写vpc.tf文件

resource "aws_vpc" "tf_vpc" {
  cidr_block = "10.10.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "tf-vpc"
  }
}

解释:

  • resource "aws_vpc" "tf_vpc"
    : 定义了一个 AWS VPC 资源,名称为
    tf_vpc
    。你可以在 Terraform 配置中引用这个名称。

  • cidr_block = "10.10.0.0/16"
    : 这是 VPC 的 CIDR(无类域间路由)块,指定了 IP 地址范围。
    10.10.0.0/16
    表示该 VPC 可以使用从
    10.10.0.0

    10.10.255.255
    的所有 IP 地址。

  • enable_dns_hostnames = true
    : 启用 DNS 主机名。设置为
    true
    时,AWS 将为 VPC 中的 EC2 实例分配 DNS 主机名,这样你可以通过 DNS 名称而不是 IP 地址访问这些实例。

  • enable_dns_support = true
    : 启用 DNS 支持。这意味着 VPC 将能够解析 DNS 名称。这对于在 VPC 内部使用 AWS 服务和实例之间的通信非常重要。

  • tags = { Name = "tf-vpc" }
    : 为 VPC 添加标签。在 AWS 控制台中,标签可以帮助你识别和管理资源。这里的标签将 VPC 命名为 "tf-vpc"。

预执行

terraform plan -out=tf.plan
terraform plan
 terraform plan -out=tf.plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # aws_vpc.tf_vpc will be created
  + resource "aws_vpc" "tf_vpc" {
      + arn                                  = (known after apply)
      + cidr_block                           = "10.10.0.0/16"
      + default_network_acl_id               = (known after apply)
      + default_route_table_id               = (known after apply)
      + default_security_group_id            = (known after apply)
      + dhcp_options_id                      = (known after apply)
      + enable_dns_hostnames                 = true
      + enable_dns_support                   = true
      + enable_network_address_usage_metrics = (known after apply)
      + id                                   = (known after apply)
      + instance_tenancy                     = "default"
      + ipv6_association_id                  = (known after apply)
      + ipv6_cidr_block                      = (known after apply)
      + ipv6_cidr_block_network_border_group = (known after apply)
      + main_route_table_id                  = (known after apply)
      + owner_id                             = (known after apply)
      + tags                                 = {
          + "Name" = "tf-vpc"
        }
      + tags_all                             = {
          + "Name" = "tf-vpc"
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tf.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "tf.plan"

创建vpc

terraform apply tf.plan
aws_vpc.tf_vpc: Creating...
aws_vpc.tf_vpc: Still creating... [10s elapsed]
aws_vpc.tf_vpc: Creation complete after 13s [id=vpc-0f2e1cdca0cf5a306]

转载请著名原文地址:
https://www.cnblogs.com/Sunzz/p/18498915

2. 创建子网

新建变量


variables.tf添加如下内容:

variable "az_1" {
  description = "Availability Zone for the first subnet"
  type        = string
  default     = "us-west-1a"
}

variable "az_2" {
  description = "Availability Zone for the second subnet"
  type        = string
  default     = "us-west-1b"
}

解释:

variable "az_1/2"
:

  • description
    : 提供了一个简短的说明,指明这个变量代表第一个子网的可用区。
  • type
    : 指定变量的数据类型为字符串 (
    string
    )。
  • default
    : 设置默认值为
    "us-west-1a/b"
    。如果在 Terraform 配置中没有为此变量提供其他值,则将使用这个默认值。

定义子网配置的subnet.tf文件

# 定义第一个子网 tf-subnet01 (10.10.1.0/24, 使用变量指定可用区)
resource "aws_subnet" "tf_subnet01" {
  vpc_id            = aws_vpc.tf_vpc.id
  cidr_block        = "10.10.1.0/24"
  availability_zone = var.az_1  # 使用变量代替硬编码的可用区
  tags = {
    Name = "tf-subnet01"
  }
}

# 定义第二个子网 tf-subnet02 (10.10.2.0/24, 使用变量指定可用区)
resource "aws_subnet" "tf_subnet02" {
  vpc_id            = aws_vpc.tf_vpc.id
  cidr_block        = "10.10.2.0/24"
  availability_zone = var.az_2
  tags = {
    Name = "tf-subnet02"
  }
}

解释:

  • resource "aws_subnet" "tf_subnet01"
    : 声明创建一个名为
    tf_subnet01
    的子网资源。

  • vpc_id = aws_vpc.tf_vpc.id
    : 关联此子网到之前定义的虚拟私有云(VPC)。
    aws_vpc.tf_vpc.id
    引用已创建的 VPC 的 ID。

  • cidr_block = "10.10.1.0/24"
    : 定义子网的 CIDR 块,这里表示子网的 IP 地址范围是
    10.10.1.0

    10.10.1.255
    ,总共有 256 个地址(包括网络地址和广播地址)。

  • availability_zone = var.az_1
    : 指定此子网所在的可用区,使用了之前定义的变量
    az_1
    ,而不是硬编码。这使得配置更灵活,便于修改和维护。

预执行

terraform plan -out=tf.plan
terraform plan
 terraform plan -out=tf.plan
aws_vpc.tf_vpc: Refreshing state... [id=vpc-0f2e1cdca0cf5a306]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # aws_subnet.tf_subnet01 will be created
  + resource "aws_subnet" "tf_subnet01" {
      + arn                                            = (known after apply)
      + assign_ipv6_address_on_creation                = false
      + availability_zone                              = "us-west-1a"
      + availability_zone_id                           = (known after apply)
      + cidr_block                                     = "10.10.1.0/24"
      + enable_dns64                                   = false
      + enable_resource_name_dns_a_record_on_launch    = false
      + enable_resource_name_dns_aaaa_record_on_launch = false
      + id                                             = (known after apply)
      + ipv6_cidr_block_association_id                 = (known after apply)
      + ipv6_native                                    = false
      + map_public_ip_on_launch                        = false
      + owner_id                                       = (known after apply)
      + private_dns_hostname_type_on_launch            = (known after apply)
      + tags                                           = {
          + "Name" = "tf-subnet01"
        }
      + tags_all                                       = {
          + "Name" = "tf-subnet01"
        }
      + vpc_id                                         = "vpc-0f2e1cdca0cf5a306"
    }

  # aws_subnet.tf_subnet02 will be created
  + resource "aws_subnet" "tf_subnet02" {
      + arn                                            = (known after apply)
      + assign_ipv6_address_on_creation                = false
      + availability_zone                              = "us-west-1b"
      + availability_zone_id                           = (known after apply)
      + cidr_block                                     = "10.10.2.0/24"
      + enable_dns64                                   = false
      + enable_resource_name_dns_a_record_on_launch    = false
      + enable_resource_name_dns_aaaa_record_on_launch = false
      + id                                             = (known after apply)
      + ipv6_cidr_block_association_id                 = (known after apply)
      + ipv6_native                                    = false
      + map_public_ip_on_launch                        = false
      + owner_id                                       = (known after apply)
      + private_dns_hostname_type_on_launch            = (known after apply)
      + tags                                           = {
          + "Name" = "tf-subnet02"
        }
      + tags_all                                       = {
          + "Name" = "tf-subnet02"
        }
      + vpc_id                                         = "vpc-0f2e1cdca0cf5a306"
    }

Plan: 2 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tf.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "tf.plan"

创建子网

terraform apply "tf.plan"
terraform apply "tf.plan"
aws_subnet.tf_subnet01: Creating...
aws_subnet.tf_subnet02: Creating...
aws_subnet.tf_subnet01: Creation complete after 2s [id=subnet-08f8e4b2c62e27989]
aws_subnet.tf_subnet02: Creation complete after 2s [id=subnet-019490723ad3e940a]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

3.创建网关

创建定义网关的internet_gateway.tf文件

resource "aws_internet_gateway" "tf_igw" {
  vpc_id = aws_vpc.tf_vpc.id
  tags = {
    Name = "tf-igw"
  }
}

解释

  • resource "aws_internet_gateway" "tf_igw"
    : 声明创建一个名为
    tf_igw
    的互联网网关资源。

  • vpc_id = aws_vpc.tf_vpc.id
    : 将此互联网网关与之前创建的虚拟私有云(VPC)关联。通过引用
    aws_vpc.tf_vpc.id
    ,确保该网关可以与指定的 VPC 一起使用。

  • tags
    : 为互联网网关添加标签,
    Name = "tf-igw"
    。标签有助于用户在 AWS 控制台中管理和识别该资源。

预执行

terraform plan -out=tf.plan
tf plan
 Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # aws_internet_gateway.tf_igw will be created
  + resource "aws_internet_gateway" "tf_igw" {
      + arn      = (known after apply)
      + id       = (known after apply)
      + owner_id = (known after apply)
      + tags     = {
          + "Name" = "tf-igw"
        }
      + tags_all = {
          + "Name" = "tf-igw"
        }
      + vpc_id   = "vpc-0f2e1cdca0cf5a306"
    }

Plan: 1 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tf.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "tf.plan"

执行创建网关

terraform apply "tf.plan"

输出如下:

aws_internet_gateway.tf_igw: Creating...
aws_internet_gateway.tf_igw: Creation complete after 2s [id=igw-08ec2f3357e8725df]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

4. 创建路由表

定义route_table.tf

resource "aws_route_table" "tf_route_table" {
  vpc_id = aws_vpc.tf_vpc.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.tf_igw.id
  }
  tags = {
    Name = "tf-route-table"
  }
}

解释

  • vpc_id = aws_vpc.tf_vpc.id
    : 将此路由表与之前创建的虚拟私有云(VPC)关联。通过引用
    aws_vpc.tf_vpc.id
    ,确保路由表适用于指定的 VPC。

  • route { ... }
    :   该块定义了路由表中的一条路由。


    • cidr_block = "0.0.0.0/0"
      :
      • 指定目标 CIDR 块为
        0.0.0.0/0
        ,表示该路由适用于所有流量(即互联网流量)。
    • gateway_id = aws_internet_gateway.tf_igw.id
      :
      • 将流量指向之前创建的互联网网关。这意味着任何发往互联网的流量都将通过这个互联网网关。

预执行创建路由表

terraform plan
 terraform plan -out=tf.plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # aws_route_table.tf_route_table will be created
  + resource "aws_route_table" "tf_route_table" {
      + arn              = (known after apply)
      + id               = (known after apply)
      + owner_id         = (known after apply)
      + propagating_vgws = (known after apply)
      + route            = [
          + {
              + cidr_block                 = "0.0.0.0/0"
              + gateway_id                 = "igw-08ec2f3357e8725df"
                # (12 unchanged attributes hidden)
            },
        ]
      + tags             = {
          + "Name" = "tf-route-table"
        }
      + tags_all         = {
          + "Name" = "tf-route-table"
        }
      + vpc_id           = "vpc-0f2e1cdca0cf5a306"
    }

Plan: 1 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tf.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "tf.plan"

执行创建路由表

terraform apply "tf.plan"

输出如下:

aws_route_table.tf_route_table: Creating...
aws_route_table.tf_route_table: Creation complete after 3s [id=rtb-0ae4b29ae8d6881ed]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

5. 关联路由表和子网

创建route_table_association.tf

# 关联子网和路由表
resource "aws_route_table_association" "tf_route_table_association_01" {
  subnet_id      = aws_subnet.tf_subnet01.id
  route_table_id = aws_route_table.tf_route_table.id
}

resource "aws_route_table_association" "tf_route_table_association_02" {
  subnet_id      = aws_subnet.tf_subnet02.id
  route_table_id = aws_route_table.tf_route_table.id
}

解释

  • resource "aws_route_table_association" "tf_route_table_association_01"
    : 声明创建一个名为
    tf_route_table_association_01
    的路由表关联资源。该资源用于将子网和路由表连接起来。

  • subnet_id = aws_subnet.tf_subnet01.id
    : 指定要关联的子网,引用之前创建的子网
    tf_subnet01
    的 ID。这意味着该路由表将应用于这个子网中的所有实例。

  • route_table_id = aws_route_table.tf_route_table.id
    : 指定要关联的路由表,引用之前创建的路由表
    tf_route_table
    的 ID。通过这个引用,确保路由表与指定的子网关联。

预执行
terraform plan -out=tf.plan

查看代码
  terraform plan -out=tf.plan
  
 Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # aws_route_table_association.tf_route_table_association_01 will be created
  + resource "aws_route_table_association" "tf_route_table_association_01" {
      + id             = (known after apply)
      + route_table_id = "rtb-0ae4b29ae8d6881ed"
      + subnet_id      = "subnet-08f8e4b2c62e27989"
    }

  # aws_route_table_association.tf_route_table_association_02 will be created
  + resource "aws_route_table_association" "tf_route_table_association_02" {
      + id             = (known after apply)
      + route_table_id = "rtb-0ae4b29ae8d6881ed"
      + subnet_id      = "subnet-019490723ad3e940a"
    }

Plan: 2 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tf.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "tf.plan"
转载请著名原文地址:https://www.cnblogs.com/Sunzz/p/18498915

执行关联

terraform apply "tf.plan"

输出如下:

aws_route_table_association.tf_route_table_association_01: Creating...
aws_route_table_association.tf_route_table_association_02: Creating...
aws_route_table_association.tf_route_table_association_01: Creation complete after 1s [id=rtbassoc-0999e44cc1cfb7f09]
aws_route_table_association.tf_route_table_association_02: Creation complete after 1s [id=rtbassoc-0190cb61bd5850d86]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

四、创建ec2

1. 创建密钥对

生成密钥对

 ssh-keygen -t rsa -b 4096 -f ~/.ssh/tf-keypair

创建key_pair.tf文件

resource "aws_key_pair" "tf-keypair" {
  key_name   = "tf-keypair"
  public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC42p8Ly5xXtaQPbBoKiVVSuU0HKhK38I5DtPhijhZrVZmhRpW5yD6pbCXmFLnIFTFNb....."
}

解释:

  • resource "aws_key_pair" "tf-keypair"
    : 声明创建一个名为
    tf-keypair
    的密钥对资源。这是一个 AWS EC2 密钥对,用于通过 SSH 访问 EC2 实例。

  • key_name = "tf-keypair"
    : 指定密钥对的名称为
    tf-keypair
    。在 AWS 控制台中,该密钥对将以这个名称显示。

  • public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC42p8Ly5xXtaQPbBoKiVVSuU0HKhK38I5DtPhijhZrVZmhRpW5yD6pbCXmFLnIFTFNb....."
    :


    • 提供公钥的内容,使用 SSH 公钥格式。这个公钥将被存储在 AWS 中,而相应的私钥则由用户保管,用于通过 SSH 连接到 EC2 实例。
    • 注意:公钥必须是有效的 SSH 公钥格式,且通常会以
      ssh-rsa
      开头,后面跟着密钥数据和可选的注释。
    • 其中public_key 就是
      ~/.ssh/tf-keypair.pub的内容

预执行

terraform plan -out=tf.plan
terraform plan
 terraform plan -out=tf.plan
aws_vpc.tf_vpc: Refreshing state... [id=vpc-0f2e1cdca0cf5a306]
aws_subnet.tf_subnet01: Refreshing state... [id=subnet-08f8e4b2c62e27989]
aws_subnet.tf_subnet02: Refreshing state... [id=subnet-019490723ad3e940a]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # aws_key_pair.tf-keypair will be created
  + resource "aws_key_pair" "tf-keypair" {
      + arn             = (known after apply)
      + fingerprint     = (known after apply)
      + id              = (known after apply)
      + key_name        = "tf-keypair"
      + key_name_prefix = (known after apply)
      + key_pair_id     = (known after apply)
      + key_type        = (known after apply)
      + public_key      = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC42p8Ly5xXtaQPbBoKiVVSuU0HKhK38ua0arfBYQF++/QFRJZ7+/fmeES7P0+//+vKjWnwdf67BIu0RyoA+MFpztYn58hDKdAmSeEXCpp4cOojgFmgnf1+p3MdaOvnT379YT....."
      + tags_all        = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tf.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "tf.plan"

执行创建密钥对

terraform apply "tf.plan"

结果如下:

aws_key_pair.tf-keypair: Creating...
aws_key_pair.tf-keypair: Creation complete after 1s [id=tf-keypair]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

2.创建安全组

创建security_group.tf文件

resource "aws_security_group" "tf_security_group" {
  name        = "tf-security-group"
  description = "Security group for allowing specific inbound traffic"
  vpc_id      = aws_vpc.tf_vpc.id

  # ICMP (ping) 入站规则
  ingress {
    from_port   = -1
    to_port     = -1
    protocol    = "icmp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow ICMP (ping) traffic"
  }

  # SSH (22) 入站规则
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow SSH traffic"
  }

  # HTTP (80) 入站规则
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow HTTP traffic"
  }

  # HTTPS (443) 入站规则
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow HTTPS traffic"
  }

  # 默认出站规则:允许所有出站流量
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow all outbound traffic"
  }

  tags = {
    Name = "tf-security-group"
  }
}

解释

  • ingress
    规则


    • icmp
      放行所有 ICMP 流量,用于允许 ping。
    • tcp
      规则放行 22 (SSH)、80 (HTTP)、443 (HTTPS) 端口。
  • egress
    规则


    • 出站流量默认允许所有协议和端口。

预执行

terraform plan -out=tf.plan
terraform plan
 terraform plan -out=tf.plan
aws_key_pair.tf-keypair: Refreshing state... [id=tf-keypair]
aws_vpc.tf_vpc: Refreshing state... [id=vpc-0f2e1cdca0cf5a306]
aws_subnet.tf_subnet01: Refreshing state... [id=subnet-08f8e4b2c62e27989]
aws_subnet.tf_subnet02: Refreshing state... [id=subnet-019490723ad3e940a]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # aws_security_group.tf_security_group will be created
  + resource "aws_security_group" "tf_security_group" {
      + arn                    = (known after apply)
      + description            = "Security group for allowing specific inbound traffic"
      + egress                 = [
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + description      = "Allow all outbound traffic"
              + from_port        = 0
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "-1"
              + security_groups  = []
              + self             = false
              + to_port          = 0
            },
        ]
      + id                     = (known after apply)
      + ingress                = [
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + description      = "Allow HTTP traffic"
              + from_port        = 80
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "tcp"
              + security_groups  = []
              + self             = false
              + to_port          = 80
            },
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + description      = "Allow HTTPS traffic"
              + from_port        = 443
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "tcp"
              + security_groups  = []
              + self             = false
              + to_port          = 443
            },
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + description      = "Allow ICMP (ping) traffic"
              + from_port        = -1
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "icmp"
              + security_groups  = []
              + self             = false
              + to_port          = -1
            },
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + description      = "Allow SSH traffic"
              + from_port        = 22
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "tcp"
              + security_groups  = []
              + self             = false
              + to_port          = 22
            },
        ]
      + name                   = "tf-security-group"
      + name_prefix            = (known after apply)
      + owner_id               = (known after apply)
      + revoke_rules_on_delete = false
      + tags                   = {
          + "Name" = "tf-security-group"
        }
      + tags_all               = {
          + "Name" = "tf-security-group"
        }
      + vpc_id                 = "vpc-0f2e1cdca0cf5a306"
    }

Plan: 1 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tf.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "tf.plan"

执行创建安全组

terraform apply "tf.plan"

输出如下:

terraform apply "tf.plan"
aws_security_group.tf_security_group: Creating...
aws_security_group.tf_security_group: Creation complete after 5s [id=sg-0907b4ae2d4bd9592]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

3.创建ec2

先定义ami内容,方便后边作用变量直接使用,编辑
variables.tf新增如下内容。这里使用了amazon linux和ubuntu 24.04的镜像

修改variables.tf

variable "amazon_linux_ami" {
  description = "AMI ID for Amazon Linux"
  type        = string
  default     = "ami-0cf4e1fcfd8494d5b"  # 替换为你的Amazon Linux AMI ID
}

variable "ubuntu_ami" {
  description = "AMI ID for Ubuntu"
  type        = string
  default     = "ami-0da424eb883458071"  # 替换为你的Ubuntu 24.04 AMI ID
}

创建ec2.tf文件

# 第一个 EC2 实例
resource "aws_instance" "tf-ec2-01" {
  ami           = var.amazon_linux_ami
  instance_type = "t2.micro"
  subnet_id     = aws_subnet.tf_subnet01.id
  key_name      = aws_key_pair.tf-keypair.key_name
  vpc_security_group_ids = [aws_security_group.tf_security_group.id]

  root_block_device {
    volume_size = 10
  }

  tags = {
    Name = "tf-ec2-01"
  }
}

# 第二个 EC2 实例
resource "aws_instance" "tf-ec2-02" {
  ami           = var.ubuntu_ami
  instance_type = "t2.micro"
  subnet_id     = aws_subnet.tf_subnet02.id 
  key_name      = aws_key_pair.tf-keypair.key_name
  vpc_security_group_ids = [aws_security_group.tf_security_group.id]

  root_block_device {
    volume_size = 10
  }

  tags = {
    Name = "tf-ec2-02"
  }
}

配置说明

  • AMI ID 参数化
    :  以便在不同环境中灵活指定 AMI
  • instance_type
    : 指定实例规格
  • subnet_id
    : 指定子网,这里使用前边创建的
  • 安全组和密钥对

    key_name
    和 vpc_security_group_ids 分别设置为之前创建的
    tf-keypair

    tf-security-group
  • root_block_device
    :将
    volume_size
    设置为 10GB,以指定每个实例的系统盘大小。

转载请著名原文地址:
https://www.cnblogs.com/Sunzz/p/18498915

预执行

terraform plan -out=tf.plan
terraform plan
terraform plan -out=tf.plan
aws_key_pair.tf-keypair: Refreshing state... [id=tf-keypair]
aws_vpc.tf_vpc: Refreshing state... [id=vpc-0f2e1cdca0cf5a306]
aws_subnet.tf_subnet02: Refreshing state... [id=subnet-019490723ad3e940a]
aws_subnet.tf_subnet01: Refreshing state... [id=subnet-08f8e4b2c62e27989]
aws_security_group.tf_security_group: Refreshing state... [id=sg-0907b4ae2d4bd9592]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # aws_instance.tf-ec2-01 will be created
  + resource "aws_instance" "tf-ec2-01" {
      + ami                                  = "ami-0cf4e1fcfd8494d5b"
      + arn                                  = (known after apply)
      + associate_public_ip_address          = (known after apply)
      + availability_zone                    = (known after apply)
      + cpu_core_count                       = (known after apply)
      + cpu_threads_per_core                 = (known after apply)
      + disable_api_stop                     = (known after apply)
      + disable_api_termination              = (known after apply)
      + ebs_optimized                        = (known after apply)
      + get_password_data                    = false
      + host_id                              = (known after apply)
      + host_resource_group_arn              = (known after apply)
      + iam_instance_profile                 = (known after apply)
      + id                                   = (known after apply)
      + instance_initiated_shutdown_behavior = (known after apply)
      + instance_state                       = (known after apply)
      + instance_type                        = "t2.micro"
      + ipv6_address_count                   = (known after apply)
      + ipv6_addresses                       = (known after apply)
      + key_name                             = "tf-keypair"
      + monitoring                           = (known after apply)
      + outpost_arn                          = (known after apply)
      + password_data                        = (known after apply)
      + placement_group                      = (known after apply)
      + placement_partition_number           = (known after apply)
      + primary_network_interface_id         = (known after apply)
      + private_dns                          = (known after apply)
      + private_ip                           = (known after apply)
      + public_dns                           = (known after apply)
      + public_ip                            = (known after apply)
      + secondary_private_ips                = (known after apply)
      + security_groups                      = (known after apply)
      + source_dest_check                    = true
      + subnet_id                            = "subnet-08f8e4b2c62e27989"
      + tags                                 = {
          + "Name" = "tf-ec2-01"
        }
      + tags_all                             = {
          + "Name" = "tf-ec2-01"
        }
      + tenancy                              = (known after apply)
      + user_data                            = (known after apply)
      + user_data_base64                     = (known after apply)
      + user_data_replace_on_change          = false
      + vpc_security_group_ids               = [
          + "sg-0907b4ae2d4bd9592",
        ]

      + capacity_reservation_specification (known after apply)

      + cpu_options (known after apply)

      + ebs_block_device (known after apply)

      + enclave_options (known after apply)

      + ephemeral_block_device (known after apply)

      + maintenance_options (known after apply)

      + metadata_options (known after apply)

      + network_interface (known after apply)

      + private_dns_name_options (known after apply)

      + root_block_device {
          + delete_on_termination = true
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + throughput            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = 10
          + volume_type           = (known after apply)
        }
    }

  # aws_instance.tf-ec2-02 will be created
  + resource "aws_instance" "tf-ec2-02" {
      + ami                                  = "ami-0da424eb883458071"
      + arn                                  = (known after apply)
      + associate_public_ip_address          = (known after apply)
      + availability_zone                    = (known after apply)
      + cpu_core_count                       = (known after apply)
      + cpu_threads_per_core                 = (known after apply)
      + disable_api_stop                     = (known after apply)
      + disable_api_termination              = (known after apply)
      + ebs_optimized                        = (known after apply)
      + get_password_data                    = false
      + host_id                              = (known after apply)
      + host_resource_group_arn              = (known after apply)
      + iam_instance_profile                 = (known after apply)
      + id                                   = (known after apply)
      + instance_initiated_shutdown_behavior = (known after apply)
      + instance_state                       = (known after apply)
      + instance_type                        = "t2.micro"
      + ipv6_address_count                   = (known after apply)
      + ipv6_addresses                       = (known after apply)
      + key_name                             = "tf-keypair"
      + monitoring                           = (known after apply)
      + outpost_arn                          = (known after apply)
      + password_data                        = (known after apply)
      + placement_group                      = (known after apply)
      + placement_partition_number           = (known after apply)
      + primary_network_interface_id         = (known after apply)
      + private_dns                          = (known after apply)
      + private_ip                           = (known after apply)
      + public_dns                           = (known after apply)
      + public_ip                            = (known after apply)
      + secondary_private_ips                = (known after apply)
      + security_groups                      = (known after apply)
      + source_dest_check                    = true
      + subnet_id                            = "subnet-019490723ad3e940a"
      + tags                                 = {
          + "Name" = "tf-ec2-02"
        }
      + tags_all                             = {
          + "Name" = "tf-ec2-02"
        }
      + tenancy                              = (known after apply)
      + user_data                            = (known after apply)
      + user_data_base64                     = (known after apply)
      + user_data_replace_on_change          = false
      + vpc_security_group_ids               = [
          + "sg-0907b4ae2d4bd9592",
        ]

      + capacity_reservation_specification (known after apply)

      + cpu_options (known after apply)

      + ebs_block_device (known after apply)

      + enclave_options (known after apply)

      + ephemeral_block_device (known after apply)

      + maintenance_options (known after apply)

      + metadata_options (known after apply)

      + network_interface (known after apply)

      + private_dns_name_options (known after apply)

      + root_block_device {
          + delete_on_termination = true
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + throughput            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = 10
          + volume_type           = (known after apply)
        }
    }

Plan: 2 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tf.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "tf.plan"

执行创建ec2

terraform apply "tf.plan"

输出:

aws_instance.tf-ec2-01: Creating...
aws_instance.tf-ec2-02: Creating...
aws_instance.tf-ec2-02: Still creating... [10s elapsed]
aws_instance.tf-ec2-01: Still creating... [10s elapsed]
aws_instance.tf-ec2-01: Creation complete after 16s [id=i-0f8d63e600d93f6b0]
aws_instance.tf-ec2-02: Creation complete after 16s [id=i-0888d477cdf36aea0]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

4.创建ebs

新增ebs.tf文化

resource "aws_ebs_volume" "ebs_ec2_01" {
  availability_zone = var.az_1  # 使用变量代替硬编码的可用区
  size              = 20
  type              = "gp3"
  tags = {
    Name = "ebs-ec2-01"
  }
}

resource "aws_ebs_volume" "ebs_ec2_02" {
  availability_zone = var.az_2
  size              = 20
  type              = "gp3"
  tags = {
    Name = "ebs-ec2-02"
  }
}

解释

  • resource "aws_ebs_volume" "ebs_ec2_01"
    : 声明创建一个名为
    ebs_ec2_01
    的 EBS 卷资源。

  • availability_zone = var.az_1
    : 指定该 EBS 卷的可用区,使用之前定义的变量
    az_1
    ,这样可以灵活地选择卷所在的可用区而不需要硬编码。

  • size = 20
    : 设置 EBS 卷的大小为 20 GB。这个参数决定了卷的存储容量。

  • type = "gp3"
    : 指定 EBS 卷的类型为
    gp3
    ,这是 AWS 提供的一种通用型 SSD 卷类型,适合大多数工作负载。

  • tags = { Name = "ebs-ec2-01" }
    : 为 EBS 卷添加标签,指定其名称为
    ebs-ec2-01
    ,便于在 AWS 控制台中识别和管理。

预执行

terraform plan -out=tf.plan
terraform plan
 terraform plan -out=tf.plan
aws_key_pair.tf-keypair: Refreshing state... [id=tf-keypair]
aws_vpc.tf_vpc: Refreshing state... [id=vpc-0f2e1cdca0cf5a306]
aws_subnet.tf_subnet02: Refreshing state... [id=subnet-019490723ad3e940a]
aws_subnet.tf_subnet01: Refreshing state... [id=subnet-08f8e4b2c62e27989]
aws_security_group.tf_security_group: Refreshing state... [id=sg-0907b4ae2d4bd9592]
aws_instance.tf-ec2-02: Refreshing state... [id=i-0888d477cdf36aea0]
aws_instance.tf-ec2-01: Refreshing state... [id=i-0f8d63e600d93f6b0]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # aws_ebs_volume.ebs_ec2_01 will be created
  + resource "aws_ebs_volume" "ebs_ec2_01" {
      + arn               = (known after apply)
      + availability_zone = "us-west-1a"
      + encrypted         = (known after apply)
      + final_snapshot    = false
      + id                = (known after apply)
      + iops              = (known after apply)
      + kms_key_id        = (known after apply)
      + size              = 20
      + snapshot_id       = (known after apply)
      + tags              = {
          + "Name" = "ebs-ec2-01"
        }
      + tags_all          = {
          + "Name" = "ebs-ec2-01"
        }
      + throughput        = (known after apply)
      + type              = "gp3"
    }

  # aws_ebs_volume.ebs_ec2_02 will be created
  + resource "aws_ebs_volume" "ebs_ec2_02" {
      + arn               = (known after apply)
      + availability_zone = "us-west-1b"
      + encrypted         = (known after apply)
      + final_snapshot    = false
      + id                = (known after apply)
      + iops              = (known after apply)
      + kms_key_id        = (known after apply)
      + size              = 20
      + snapshot_id       = (known after apply)
      + tags              = {
          + "Name" = "ebs-ec2-02"
        }
      + tags_all          = {
          + "Name" = "ebs-ec2-02"
        }
      + throughput        = (known after apply)
      + type              = "gp3"
    }

Plan: 2 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tf.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "tf.plan"

执行创建ebs

terraform apply "tf.plan"

输出如下:

terraform apply "tf.plan"
aws_ebs_volume.ebs_ec2_02: Creating...
aws_ebs_volume.ebs_ec2_01: Creating...
aws_ebs_volume.ebs_ec2_02: Still creating... [10s elapsed]
aws_ebs_volume.ebs_ec2_01: Still creating... [10s elapsed]
aws_ebs_volume.ebs_ec2_01: Creation complete after 12s [id=vol-0aac9f1302376328a]
aws_ebs_volume.ebs_ec2_02: Creation complete after 12s [id=vol-06bd472f44eadaf02]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

5.将 EBS 磁盘附加到 EC2

新增ebs_attachment.tf文件

resource "aws_volume_attachment" "attach_ebs_to_ec2_01" {
  device_name = "/dev/xvdh"                # 设备名称,可根据需求更改
  volume_id   = aws_ebs_volume.ebs_ec2_01.id
  instance_id = aws_instance.tf-ec2-01.id
}

resource "aws_volume_attachment" "attach_ebs_to_ec2_02" {
  device_name = "/dev/xvdh"
  volume_id   = aws_ebs_volume.ebs_ec2_02.id
  instance_id = aws_instance.tf-ec2-02.id
}

解释

  • resource "aws_volume_attachment" "attach_ebs_to_ec2_01"
    : 声明了一个名为
    attach_ebs_to_ec2_01
    的 EBS 卷附件资源,属于
    aws_volume_attachment
    类型。这个资源负责将 EBS 卷与 EC2 实例关联。

  • device_name = "/dev/xvdh"
    : 指定 EBS 卷在 EC2 实例上的设备名称。这里使用的是
    /dev/xvdh
    ,可以根据需求更改。这个名称在实例中将用于引用该 EBS 卷。

  • volume_id = aws_ebs_volume.ebs_ec2_01.id
    : 引用之前创建的 EBS 卷
    ebs_ec2_01
    的 ID,以指定要附加的卷。通过引用资源的 ID,可以确保操作的是正确的资源。

  • instance_id = aws_instance.tf-ec2-01.id
    : 引用要将 EBS 卷附加到的 EC2 实例
    tf-ec2-01
    的 ID。这样可以明确指定哪个实例将使用该 EBS 卷。

预执行

terraform plan -out=tf.plan
terraform plan
 terraform plan -out=tf.plan
aws_ebs_volume.ebs_ec2_02: Refreshing state... [id=vol-06bd472f44eadaf02]
aws_vpc.tf_vpc: Refreshing state... [id=vpc-0f2e1cdca0cf5a306]
aws_ebs_volume.ebs_ec2_01: Refreshing state... [id=vol-0aac9f1302376328a]
aws_key_pair.tf-keypair: Refreshing state... [id=tf-keypair]
aws_subnet.tf_subnet02: Refreshing state... [id=subnet-019490723ad3e940a]
aws_subnet.tf_subnet01: Refreshing state... [id=subnet-08f8e4b2c62e27989]
aws_security_group.tf_security_group: Refreshing state... [id=sg-0907b4ae2d4bd9592]
aws_instance.tf-ec2-01: Refreshing state... [id=i-0f8d63e600d93f6b0]
aws_instance.tf-ec2-02: Refreshing state... [id=i-0888d477cdf36aea0]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # aws_volume_attachment.attach_ebs_to_ec2_01 will be created
  + resource "aws_volume_attachment" "attach_ebs_to_ec2_01" {
      + device_name = "/dev/xvdh"
      + id          = (known after apply)
      + instance_id = "i-0f8d63e600d93f6b0"
      + volume_id   = "vol-0aac9f1302376328a"
    }

  # aws_volume_attachment.attach_ebs_to_ec2_02 will be created
  + resource "aws_volume_attachment" "attach_ebs_to_ec2_02" {
      + device_name = "/dev/xvdh"
      + id          = (known after apply)
      + instance_id = "i-0888d477cdf36aea0"
      + volume_id   = "vol-06bd472f44eadaf02"
    }

Plan: 2 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tf.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "tf.plan"

执行附加磁盘

terraform apply "tf.plan"

输出:

aws_volume_attachment.attach_ebs_to_ec2_01: Creating...
aws_volume_attachment.attach_ebs_to_ec2_02: Creating...
aws_volume_attachment.attach_ebs_to_ec2_02: Still creating... [10s elapsed]
aws_volume_attachment.attach_ebs_to_ec2_01: Still creating... [10s elapsed]
aws_volume_attachment.attach_ebs_to_ec2_01: Still creating... [20s elapsed]
aws_volume_attachment.attach_ebs_to_ec2_02: Still creating... [20s elapsed]
aws_volume_attachment.attach_ebs_to_ec2_02: Still creating... [30s elapsed]
aws_volume_attachment.attach_ebs_to_ec2_01: Still creating... [30s elapsed]
aws_volume_attachment.attach_ebs_to_ec2_02: Creation complete after 33s [id=vai-439503465]
aws_volume_attachment.attach_ebs_to_ec2_01: Creation complete after 33s [id=vai-1312740159]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

6. 创建eip和关联eip到ec2实例

新增eip.tf文件

# 为 tf-ec2-01 创建 EIP
resource "aws_eip" "tf_eip_01" {
  vpc = true
  tags = {
    Name = "tf-eip-01"
  }
}

# 为 tf-ec2-02 创建 EIP
resource "aws_eip" "tf_eip_02" {
  vpc = true
  tags = {
    Name = "tf-eip-02"
  }
}

解释

  • resource "aws_eip" "tf_eip_01"
    : 声明了一个名为
    tf_eip_01
    的弹性 IP 资源,属于
    aws_eip
    类型。弹性 IP 是 AWS 提供的一种静态 IPv4 地址,可以在不同的 EC2 实例之间动态迁移。

  • vpc = true
    : 指定这个弹性 IP 是用于 VPC(虚拟私有云)的。如果设置为
    true
    ,则弹性 IP 将与 VPC 关联。这是因为在 VPC 中使用的弹性 IP 地址与传统的 EC2 实例弹性 IP 有所不同。

  • tags = { Name = "tf-eip-01" }
    : 为该弹性 IP 添加一个标签,名称为
    tf-eip-01
    。标签用于管理和识别资源,使得在 AWS 控制台或使用其他工具时能够更方便地找到和管理该资源。

新增eip_association.tf文件

# 关联 EIP 到 tf-ec2-01 实例
resource "aws_eip_association" "tf_eip_association_01" {
  instance_id   = aws_instance.tf-ec2-01.id
  allocation_id = aws_eip.tf_eip_01.id
}

# 关联 EIP 到 tf-ec2-02 实例
resource "aws_eip_association" "tf_eip_association_02" {
  instance_id   = aws_instance.tf-ec2-02.id
  allocation_id = aws_eip.tf_eip_02.id
}

解释

  • resource "aws_eip_association" "tf_eip_association_01"
    : 声明了一个名为
    tf_eip_association_01
    的资源,属于
    aws_eip_association
    类型。这个资源用于建立弹性 IP 和 EC2 实例之间的关联。

  • instance_id = aws_instance.tf-ec2-01.id
    : 指定要与弹性 IP 关联的 EC2 实例的 ID。这里引用了之前定义的 EC2 实例
    tf-ec2-01
    的 ID。

  • allocation_id = aws_eip.tf_eip_01.id
    : 指定要关联的弹性 IP 的分配 ID。这里引用了之前创建的弹性 IP
    tf_eip_01
    的 ID。

预执行

terraform plan -out=tf.plan

terraform plan
terraform plan -out=tf.plan
aws_key_pair.tf-keypair: Refreshing state... [id=tf-keypair]
aws_ebs_volume.ebs_ec2_01: Refreshing state... [id=vol-0aac9f1302376328a]
aws_ebs_volume.ebs_ec2_02: Refreshing state... [id=vol-06bd472f44eadaf02]
aws_vpc.tf_vpc: Refreshing state... [id=vpc-0f2e1cdca0cf5a306]
aws_internet_gateway.tf_igw: Refreshing state... [id=igw-08ec2f3357e8725df]
aws_subnet.tf_subnet02: Refreshing state... [id=subnet-019490723ad3e940a]
aws_subnet.tf_subnet01: Refreshing state... [id=subnet-08f8e4b2c62e27989]
aws_security_group.tf_security_group: Refreshing state... [id=sg-0907b4ae2d4bd9592]
aws_route_table.tf_route_table: Refreshing state... [id=rtb-0ae4b29ae8d6881ed]
aws_instance.tf-ec2-01: Refreshing state... [id=i-0f8d63e600d93f6b0]
aws_instance.tf-ec2-02: Refreshing state... [id=i-0888d477cdf36aea0]
aws_route_table_association.tf_route_table_association_02: Refreshing state... [id=rtbassoc-0190cb61bd5850d86]
aws_route_table_association.tf_route_table_association_01: Refreshing state... [id=rtbassoc-0999e44cc1cfb7f09]
aws_volume_attachment.attach_ebs_to_ec2_01: Refreshing state... [id=vai-1312740159]
aws_volume_attachment.attach_ebs_to_ec2_02: Refreshing state... [id=vai-439503465]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # aws_eip.tf_eip_01 will be created
  + resource "aws_eip" "tf_eip_01" {
      + allocation_id        = (known after apply)
      + association_id       = (known after apply)
      + carrier_ip           = (known after apply)
      + customer_owned_ip    = (known after apply)
      + domain               = (known after apply)
      + id                   = (known after apply)
      + instance             = (known after apply)
      + network_border_group = (known after apply)
      + network_interface    = (known after apply)
      + private_dns          = (known after apply)
      + private_ip           = (known after apply)
      + public_dns           = (known after apply)
      + public_ip            = (known after apply)
      + public_ipv4_pool     = (known after apply)
      + tags                 = {
          + "Name" = "tf-eip-01"
        }
      + tags_all             = {
          + "Name" = "tf-eip-01"
        }
      + vpc                  = true
    }

  # aws_eip.tf_eip_02 will be created
  + resource "aws_eip" "tf_eip_02" {
      + allocation_id        = (known after apply)
      + association_id       = (known after apply)
      + carrier_ip           = (known after apply)
      + customer_owned_ip    = (known after apply)
      + domain               = (known after apply)
      + id                   = (known after apply)
      + instance             = (known after apply)
      + network_border_group = (known after apply)
      + network_interface    = (known after apply)
      + private_dns          = (known after apply)
      + private_ip           = (known after apply)
      + public_dns           = (known after apply)
      + public_ip            = (known after apply)
      + public_ipv4_pool     = (known after apply)
      + tags                 = {
          + "Name" = "tf-eip-02"
        }
      + tags_all             = {
          + "Name" = "tf-eip-02"
        }
      + vpc                  = true
    }

  # aws_eip_association.tf_eip_association_01 will be created
  + resource "aws_eip_association" "tf_eip_association_01" {
      + allocation_id        = (known after apply)
      + id                   = (known after apply)
      + instance_id          = "i-0f8d63e600d93f6b0"
      + network_interface_id = (known after apply)
      + private_ip_address   = (known after apply)
      + public_ip            = (known after apply)
    }

  # aws_eip_association.tf_eip_association_02 will be created
  + resource "aws_eip_association" "tf_eip_association_02" {
      + allocation_id        = (known after apply)
      + id                   = (known after apply)
      + instance_id          = "i-0888d477cdf36aea0"
      + network_interface_id = (known after apply)
      + private_ip_address   = (known after apply)
      + public_ip            = (known after apply)
    }

Plan: 4 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tf.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "tf.plan"

执行创建eip和关联ec2

terraform apply "tf.plan"

结果如下:

aws_eip.tf_eip_02: Creating...
aws_eip.tf_eip_01: Creating...
aws_eip.tf_eip_01: Creation complete after 2s [id=eipalloc-0a9cdbc84013614f5]
aws_eip.tf_eip_02: Creation complete after 2s [id=eipalloc-0ed1c932d9a7a305a]
aws_eip_association.tf_eip_association_01: Creating...
aws_eip_association.tf_eip_association_02: Creating...
aws_eip_association.tf_eip_association_02: Creation complete after 1s [id=eipassoc-0b517a49d76639054]
aws_eip_association.tf_eip_association_01: Creation complete after 1s [id=eipassoc-0e0359ad952266802]

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

转载请著名原文地址:
https://www.cnblogs.com/Sunzz/p/18498915

7.通过控制后台查看创建的结果

通过控制台可以看到,实例名字、类型、可用区、公网IP、安全组、密钥、磁盘等都是符合我们在tf中定义的。

再直接登录到服务器上看下,确保网络、安全组都是可用的

ssh ec2-user@52.9.19.52 -i ~/.ssh/tf-keypair

五、创建EKS

1. 创建EKS所需的网络资源

创建eks所用的子网并关联路由表文件

eks_subnets.tf内容如下

resource "aws_subnet" "tf_eks_subnet1" {
  vpc_id            = aws_vpc.tf_vpc.id
  cidr_block        = "10.10.81.0/24"
  availability_zone = var.az_1
  map_public_ip_on_launch = true

  tags = {
    Name = "tf_eks_subnet1"
  }
}

resource "aws_subnet" "tf_eks_subnet2" {
  vpc_id            = aws_vpc.tf_vpc.id
  cidr_block        = "10.10.82.0/24"
  availability_zone = var.az_2
  map_public_ip_on_launch = true

  tags = {
    Name = "tf_eks_subnet2"
  }
}


# 将路由表关联到子网tf_eks_subnet1
resource "aws_route_table_association" "tf_eks_subnet1_association" {
  subnet_id      = aws_subnet.tf_eks_subnet1.id 
  route_table_id = aws_route_table.tf_route_table.id
}

# 将路由表关联到子网tf_eks_subnet2
resource "aws_route_table_association" "tf_eks_subnet2_association" {
  subnet_id      = aws_subnet.tf_eks_subnet2.id  
  route_table_id = aws_route_table.tf_route_table.id
}

解释

  • resource "aws_subnet" "tf_eks_subnet1"
    : 声明了一个名为
    tf_eks_subnet1
    的资源,类型为
    aws_subnet
    ,用于创建一个新的子网。

  • vpc_id = aws_vpc.tf_vpc.id
    : 指定子网所属的虚拟私有云(VPC)的 ID。这里引用了之前定义的 VPC
    tf_vpc
    的 ID,以将此子网关联到相应的 VPC。

  • cidr_block = "10.10.81.0/24"
    : 指定子网的 CIDR 块(Classless Inter-Domain Routing),这表示该子网的 IP 地址范围为
    10.10.81.0

    10.10.81.255
    ,可以容纳 256 个 IP 地址。

  • availability_zone = var.az_1
    : 指定子网所在的可用区(Availability Zone),这里使用了变量
    az_1
    ,允许灵活配置子网的可用区。

  • map_public_ip_on_launch = true
    : 指定在此子网中启动的实例是否自动分配公共 IP 地址。设置为
    true
    表示新启动的实例将自动获得公共 IP,允许它们直接访问互联网。

  • resource "aws_route_table_association" "tf_eks_subnet1_association"
    : 声明了一个名为
    tf_eks_subnet1_association
    的资源,类型为
    aws_route_table_association
    ,用于创建路由表与子网之间的关联。

  • subnet_id = aws_subnet.tf_eks_subnet1.id
    : 指定要关联的子网的 ID。这里引用了之前定义的子网
    tf_eks_subnet1
    的 ID。

  • route_table_id = aws_route_table.tf_route_table.id
    : 指定要关联的路由表的 ID。这里引用了之前定义的路由表
    tf_route_table
    的 ID。

预创建

 terraform plan -out=tf.plan
tf plan
 Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
  + create

Terraform will perform the following actions:

  # aws_route_table_association.tf_eks_subnet1_association will be created
  + resource "aws_route_table_association" "tf_eks_subnet1_association" {
      + id             = (known after apply)
      + route_table_id = "rtb-0ae4b29ae8d6881ed"
      + subnet_id      = (known after apply)
    }

  # aws_route_table_association.tf_eks_subnet2_association will be created
  + resource "aws_route_table_association" "tf_eks_subnet2_association" {
      + id             = (known after apply)
      + route_table_id = "rtb-0ae4b29ae8d6881ed"
      + subnet_id      = (known after apply)
    }

  # aws_subnet.tf_eks_subnet1 will be created
  + resource "aws_subnet" "tf_eks_subnet1" {
      + arn                                            = (known after apply)
      + assign_ipv6_address_on_creation                = false
      + availability_zone                              = "us-west-1a"
      + availability_zone_id                           = (known after apply)
      + cidr_block                                     = "10.10.81.0/24"
      + enable_dns64                                   = false
      + enable_resource_name_dns_a_record_on_launch    = false
      + enable_resource_name_dns_aaaa_record_on_launch = false
      + id                                             = (known after apply)
      + ipv6_cidr_block_association_id                 = (known after apply)
      + ipv6_native                                    = false
      + map_public_ip_on_launch                        = true
      + owner_id                                       = (known after apply)
      + private_dns_hostname_type_on_launch            = (known after apply)
      + tags                                           = {
          + "Name" = "tf_eks_subnet1"
        }
      + tags_all                                       = {
          + "Name" = "tf_eks_subnet1"
        }
      + vpc_id                                         = "vpc-0f2e1cdca0cf5a306"
    }

  # aws_subnet.tf_eks_subnet2 will be created
  + resource "aws_subnet" "tf_eks_subnet2" {
      + arn                                            = (known after apply)
      + assign_ipv6_address_on_creation                = false
      + availability_zone                              = "us-west-1b"
      + availability_zone_id                           = (known after apply)
      + cidr_block                                     = "10.10.82.0/24"
      + enable_dns64                                   = false
      + enable_resource_name_dns_a_record_on_launch    = false
      + enable_resource_name_dns_aaaa_record_on_launch = false
      + id                                             = (known after apply)
      + ipv6_cidr_block_association_id                 = (known after apply)
      + ipv6_native                                    = false
      + map_public_ip_on_launch                        = true
      + owner_id                                       = (known after apply)
      + private_dns_hostname_type_on_launch            = (known after apply)
      + tags                                           = {
          + "Name" = "tf_eks_subnet2"
        }
      + tags_all                                       = {
          + "Name" = "tf_eks_subnet2"
        }
      + vpc_id                                         = "vpc-0f2e1cdca0cf5a306"
    }

Plan: 4 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tf.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "tf.plan"

创建子网

terraform apply "tf.plan"

输出如下:

aws_subnet.tf_eks_subnet2: Creating...
aws_subnet.tf_eks_subnet1: Creating...
aws_subnet.tf_eks_subnet1: Still creating... [10s elapsed]
aws_subnet.tf_eks_subnet2: Still creating... [10s elapsed]
aws_subnet.tf_eks_subnet2: Creation complete after 13s [id=subnet-0a30534a829758774]
aws_route_table_association.tf_eks_subnet2_association: Creating...
aws_subnet.tf_eks_subnet1: Creation complete after 13s [id=subnet-01b5d98060f0063ef]
aws_route_table_association.tf_eks_subnet1_association: Creating...
aws_route_table_association.tf_eks_subnet1_association: Creation complete after 1s [id=rtbassoc-08fef5fee4d037035]
aws_route_table_association.tf_eks_subnet2_association: Creation complete after 1s [id=rtbassoc-0ec12dc9868d6316a]

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

2. 创建EKS安全组

eks_security_group.tf

这里放开所有只是为了演示,请勿用在生产环境中

resource "aws_security_group" "eks_allow_all" {
  name        = "eks_allow_all"
  description = "Security group that allows all inbound and outbound traffic"
  vpc_id      = aws_vpc.tf_vpc.id


  // 允许所有入站流量
  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"  // -1 表示所有协议
    cidr_blocks = ["0.0.0.0/0"]  // 允许来自所有 IP 的流量
  }

  // 允许所有出站流量
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"  // -1 表示所有协议
    cidr_blocks = ["0.0.0.0/0"]  // 允许流量发送到所有 IP
  }
}

解释

  1. resource "aws_security_group" "eks_allow_all"
    : 声明一个名为
    eks_allow_all
    的资源,类型为
    aws_security_group
    ,用于创建安全组。

  2. name = "eks_allow_all"
    : 设置安全组的名称为
    eks_allow_all

  3. description = "Security group that allows all inbound and outbound traffic"
    : 为安全组提供描述,说明这个安全组的作用是允许所有入站和出站流量。

  4. vpc_id = aws_vpc.tf_vpc.id
    : 指定安全组所属的 VPC,引用了之前定义的 VPC 的 ID。

入站规则(
ingress

  1. ingress { ... }
    :
    • 定义入站规则,允许流量进入安全组。

    • from_port = 0

      to_port = 0

      : 这表示允许所有端口的流量(0到0表示所有端口)。

    • protocol = "-1"
      :
      -1
      表示所有协议,包括 TCP、UDP 和 ICMP 等。

    • cidr_blocks = ["0.0.0.0/0"]
      : 允许来自所有 IP 地址的流量(0.0.0.0/0 表示任意 IP)。

出站规则(
egress

  1. egress { ... }
    :
    • 定义出站规则,允许流量离开安全组。

    • from_port = 0

      to_port = 0

      : 同样允许所有端口的流量。

    • protocol = "-1"
      : 表示所有协议。

    • cidr_blocks = ["0.0.0.0/0"]
      : 允许流量发送到所有 IP 地址。

预创建

terraform plan -out=tf.plan
tf plan
 Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
  + create

Terraform will perform the following actions:

  # aws_security_group.eks_allow_all will be created
  + resource "aws_security_group" "eks_allow_all" {
      + arn                    = (known after apply)
      + description            = "Security group that allows all inbound and outbound traffic"
      + egress                 = [
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + from_port        = 0
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "-1"
              + security_groups  = []
              + self             = false
              + to_port          = 0
                # (1 unchanged attribute hidden)
            },
        ]
      + id                     = (known after apply)
      + ingress                = [
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + from_port        = 0
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "-1"
              + security_groups  = []
              + self             = false
              + to_port          = 0
                # (1 unchanged attribute hidden)
            },
        ]
      + name                   = "eks_allow_all"
      + name_prefix            = (known after apply)
      + owner_id               = (known after apply)
      + revoke_rules_on_delete = false
      + tags_all               = (known after apply)
      + vpc_id                 = "vpc-0f2e1cdca0cf5a306"
    }

Plan: 1 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tf.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "tf.plan"

创建安全组

terraform apply "tf.plan"

输出如下:

aws_security_group.eks_allow_all: Creating...
aws_security_group.eks_allow_all: Creation complete after 7s [id=sg-0db88cd4ca4b95099]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

3. 创建 EKS 集群 IAM 角色

创建
eks_iam_roles.tf文件

data "aws_iam_policy_document" "assume_role" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["eks.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "eks-cluster" {
  name               = "eks-cluster"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_role_policy_attachment" "eks-cluster-AmazonEKSClusterPolicy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
  role       = aws_iam_role.eks-cluster.name
}

resource "aws_iam_role_policy_attachment" "eks-cluster-AmazonEKSVPCResourceController" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSVPCResourceController"
  role       = aws_iam_role.eks-cluster.name
}

解释

这段代码定义了一个 IAM 角色及其权限策略,用于 Amazon EKS 集群的创建和管理。以下是详细解释:

数据源部分

  1. data "aws_iam_policy_document" "assume_role"
    :
    • 这是一个 IAM 策略文档的数据源,用于定义角色的信任策略。

    • statement { ... }
      :


      • effect = "Allow"
        : 允许该角色的使用。
      • principals { ... }
        : 定义可以使用此角色的主体。
        • type = "Service"
          : 指定主体类型为服务。
        • identifiers = ["eks.amazonaws.com"]
          : 允许 EKS 服务来假设此角色。
      • actions = ["sts:AssumeRole"]
        : 允许上述主体执行的操作,即假设角色的权限。

IAM 角色部分

  1. resource "aws_iam_role" "eks-cluster"
    :
    • 创建一个名为
      eks-cluster
      的 IAM 角色。

    • assume_role_policy = data.aws_iam_policy_document.assume_role.json
      : 将之前定义的信任策略应用于该角色。

IAM 角色策略附件

  1. resource "aws_iam_role_policy_attachment" "eks-cluster-AmazonEKSClusterPolicy"
    :


    • 关联 Amazon EKS Cluster Policy 到 IAM 角色。
    • policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
      : 指定要附加的策略 ARN。
    • role = aws_iam_role.eks-cluster.name
      : 将策略附加到之前创建的
      eks-cluster
      角色。
  2. resource "aws_iam_role_policy_attachment" "eks-cluster-AmazonEKSVPCResourceController"
    :


    • 关联 Amazon EKS VPC Resource Controller 策略到 IAM 角色。
    • policy_arn = "arn:aws:iam::aws:policy/AmazonEKSVPCResourceController"
      : 指定要附加的策略 ARN。
    • role = aws_iam_role.eks-cluster.name
      : 将策略附加到之前创建的
      eks-cluster
      角色。

预创建

terraform plan -out=tf.plan
tf plan
 Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
  + create

Terraform will perform the following actions:

  # aws_iam_role.eks-cluster will be created
  + resource "aws_iam_role" "eks-cluster" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "eks.amazonaws.com"
                        }
                      + Sid       = ""
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "eks-cluster"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + role_last_used        = (known after apply)
      + tags_all              = (known after apply)
      + unique_id             = (known after apply)

      + inline_policy (known after apply)
    }

  # aws_iam_role_policy_attachment.eks-cluster-AmazonEKSClusterPolicy will be created
  + resource "aws_iam_role_policy_attachment" "eks-cluster-AmazonEKSClusterPolicy" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
      + role       = "eks-cluster"
    }

  # aws_iam_role_policy_attachment.eks-cluster-AmazonEKSVPCResourceController will be created
  + resource "aws_iam_role_policy_attachment" "eks-cluster-AmazonEKSVPCResourceController" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSVPCResourceController"
      + role       = "eks-cluster"
    }

Plan: 3 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tf.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "tf.plan"

创建eks iam role

terraform apply "tf.plan"

输出如下:

aws_iam_role.eks-cluster: Creating...
aws_iam_role.eks-cluster: Creation complete after 2s [id=eks-cluster]
aws_iam_role_policy_attachment.eks-cluster-AmazonEKSVPCResourceController: Creating...
aws_iam_role_policy_attachment.eks-cluster-AmazonEKSClusterPolicy: Creating...
aws_iam_role_policy_attachment.eks-cluster-AmazonEKSVPCResourceController: Creation complete after 1s [id=eks-cluster-20241027124651622300000001]
aws_iam_role_policy_attachment.eks-cluster-AmazonEKSClusterPolicy: Creation complete after 1s [id=eks-cluster-20241027124651968900000002]

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

4. 创建EKS集群

编写eks_cluster.tf
文件

resource "aws_eks_cluster" "tf-eks" {
  name     = "tf-eks"
  version  = var.eks_version  # 指定 EKS 版本
  role_arn = aws_iam_role.eks-cluster.arn

  vpc_config {
    subnet_ids = [
	  aws_subnet.tf_eks_subnet1.id, 
	  aws_subnet.tf_eks_subnet2.id
	]
    security_group_ids      = [aws_security_group.eks_allow_all.id]    # 引用之前创建的安全组
    endpoint_public_access  = true     # 公有访问
    endpoint_private_access = true     # 私有访问
    public_access_cidrs = ["0.0.0.0/0"] # 允许从任何地方访问
  }
#  # 启用日志
#  enabled_cluster_log_types = [
#           "api",
#           "audit",
#           "authenticator",
#           "controllerManager",
#           "scheduler",
#  ]

  depends_on = [
    aws_iam_role_policy_attachment.eks-cluster-AmazonEKSClusterPolicy,
    aws_iam_role_policy_attachment.eks-cluster-AmazonEKSVPCResourceController,
  ]
}

参数解释

  • name
    :指定集群的名称为
    tf-eks
  • version
    :指定 EKS 的版本,使用变量
    var.eks_version
    ,这样可以方便地在不同环境中调整。
  • role_arn
    :指定用于 EKS 集群的 IAM 角色 ARN,通常这个角色需要有相应的权限策略。
  • subnet_ids
    :指定 EKS 集群所在的子网,允许使用多个子网 ID,以便在高可用性场景中部署。
  • security_group_ids
    :引用之前创建的安全组,用于控制集群的网络流量。
  • endpoint_public_access
    :设置为
    true
    ,表示允许通过公共网络访问 EKS API 端点。
  • endpoint_private_access
    :设置为
    true
    ,表示允许在 VPC 内部访问 EKS API 端点。
  • public_access_cidrs
    :允许访问集群的 CIDR 范围,这里设置为
    ["0.0.0.0/0"]
    ,表示允许任何 IP 地址访问,这可能会带来安全风险。
  • 日志部分注释了,若启用,可以指定需要记录的集群日志类型,包括 API、审计、身份验证器、控制器管理器和调度程序等。
  • depends_on
    :确保在创建 EKS 集群之前,所需的 IAM 角色策略已经附加,确保资源的创建顺序正确

预创建

terraform plan -out=tf.plan
tf plan
 Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
  + create

Terraform will perform the following actions:

  # aws_eks_cluster.tf-eks will be created
  + resource "aws_eks_cluster" "tf-eks" {
      + arn                   = (known after apply)
      + certificate_authority = (known after apply)
      + cluster_id            = (known after apply)
      + created_at            = (known after apply)
      + endpoint              = (known after apply)
      + id                    = (known after apply)
      + identity              = (known after apply)
      + name                  = "tf-eks"
      + platform_version      = (known after apply)
      + role_arn              = "arn:aws:iam::xxxxxxxx:role/eks-cluster"
      + status                = (known after apply)
      + tags_all              = (known after apply)
      + version               = "1.31"

      + kubernetes_network_config (known after apply)

      + vpc_config {
          + cluster_security_group_id = (known after apply)
          + endpoint_private_access   = true
          + endpoint_public_access    = true
          + public_access_cidrs       = [
              + "0.0.0.0/0",
            ]
          + security_group_ids        = [
              + "sg-0db88cd4ca4b95099",
            ]
          + subnet_ids                = [
              + "subnet-01b5d98060f0063ef",
              + "subnet-0a30534a829758774",
            ]
          + vpc_id                    = (known after apply)
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tf.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "tf.plan"

创建

terraform apply "tf.plan"

输出如下:

aws_eks_cluster.tf-eks: Creating...
aws_eks_cluster.tf-eks: Still creating... [10s elapsed]
aws_eks_cluster.tf-eks: Still creating... [20s elapsed]
aws_eks_cluster.tf-eks: Still creating... [30s elapsed]
......
.......
aws_eks_cluster.tf-eks: Still creating... [7m21s elapsed]
aws_eks_cluster.tf-eks: Still creating... [7m31s elapsed]
aws_eks_cluster.tf-eks: Creation complete after 7m35s [id=tf-eks]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

5. 创建Node Group IAM

创建tf文件

eks_node_group_iam.tf

resource "aws_iam_role" "eks-nodegroup-role" {
  name = "eks-nodegroup-role"
  assume_role_policy = jsonencode({
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "ec2.amazonaws.com"
      }
    }]
    Version = "2012-10-17"
  })
}

resource "aws_iam_role_policy_attachment" "eks-nodegroup-role-AmazonEKSWorkerNodePolicy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
  role       = aws_iam_role.eks-nodegroup-role.name
}

resource "aws_iam_role_policy_attachment" "eks-nodegroup-role-AmazonEKS_CNI_Policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
  role       = aws_iam_role.eks-nodegroup-role.name
}

resource "aws_iam_role_policy_attachment" "eks-nodegroup-role-AmazonEC2ContainerRegistryReadOnly" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
  role       = aws_iam_role.eks-nodegroup-role.name
}

解释

这段代码定义了一个 IAM 角色,用于 Amazon EKS 节点组,并附加了必要的权限策略。以下是详细解释:

IAM 角色定义

  1. resource "aws_iam_role" "eks-nodegroup-role"
    :
    • 创建一个名为
      eks-nodegroup-role
      的 IAM 角色,供 EKS 节点使用。

    • assume_role_policy = jsonencode({ ... })
      : 定义角色的信任策略,允许特定服务假设此角色。


      • Statement = [{ ... }]
        : 定义角色的权限声明。
        • Action = "sts:AssumeRole"
          : 允许的操作,即假设角色。
        • Effect = "Allow"
          : 该声明的效果是允许。
        • Principal = { Service = "ec2.amazonaws.com" }
          : 允许
          ec2.amazonaws.com
          服务(即 EC2 实例)来假设此角色,这样 EKS 节点才能获得权限。
    • Version = "2012-10-17"
      : 定义策略语言的版本。

IAM 角色策略附件

  1. resource "aws_iam_role_policy_attachment" "eks-nodegroup-role-AmazonEKSWorkerNodePolicy"
    :


    • 将 Amazon EKS Worker Node Policy 附加到节点组角色。
    • policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
      : 指定要附加的策略 ARN。
    • role = aws_iam_role.eks-nodegroup-role.name
      : 将策略附加到之前创建的
      eks-nodegroup-role
      角色。
  2. resource "aws_iam_role_policy_attachment" "eks-nodegroup-role-AmazonEKS_CNI_Policy"
    :


    • 将 Amazon EKS CNI Policy 附加到节点组角色。
    • policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
      : 指定要附加的策略 ARN。
    • role = aws_iam_role.eks-nodegroup-role.name
      : 将策略附加到之前创建的
      eks-nodegroup-role
      角色。
  3. resource "aws_iam_role_policy_attachment" "eks-nodegroup-role-AmazonEC2ContainerRegistryReadOnly"
    :


    • 将 Amazon EC2 Container Registry Read Only Policy 附加到节点组角色。
    • policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
      : 指定要附加的策略 ARN。
    • role = aws_iam_role.eks-nodegroup-role.name
      : 将策略附加到之前创建的
      eks-nodegroup-role
      角色。

预创建

terraform plan -out=tf.plan
tf plan
 Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
  + create

Terraform will perform the following actions:

  # aws_iam_role.eks-nodegroup-role will be created
  + resource "aws_iam_role" "eks-nodegroup-role" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ec2.amazonaws.com"
                        }
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "eks-nodegroup-role"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + role_last_used        = (known after apply)
      + tags_all              = (known after apply)
      + unique_id             = (known after apply)

      + inline_policy (known after apply)
    }

  # aws_iam_role_policy_attachment.eks-nodegroup-role-AmazonEC2ContainerRegistryReadOnly will be created
  + resource "aws_iam_role_policy_attachment" "eks-nodegroup-role-AmazonEC2ContainerRegistryReadOnly" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
      + role       = "eks-nodegroup-role"
    }

  # aws_iam_role_policy_attachment.eks-nodegroup-role-AmazonEKSWorkerNodePolicy will be created
  + resource "aws_iam_role_policy_attachment" "eks-nodegroup-role-AmazonEKSWorkerNodePolicy" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
      + role       = "eks-nodegroup-role"
    }

  # aws_iam_role_policy_attachment.eks-nodegroup-role-AmazonEKS_CNI_Policy will be created
  + resource "aws_iam_role_policy_attachment" "eks-nodegroup-role-AmazonEKS_CNI_Policy" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
      + role       = "eks-nodegroup-role"
    }

Plan: 4 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tf.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "tf.plan"

创建node group iam

 terraform apply "tf.plan"

输出:

aws_iam_role.eks-nodegroup-role: Creating...
aws_iam_role.eks-nodegroup-role: Creation complete after 2s [id=eks-nodegroup-role]
aws_iam_role_policy_attachment.eks-nodegroup-role-AmazonEKS_CNI_Policy: Creating...
aws_iam_role_policy_attachment.eks-nodegroup-role-AmazonEC2ContainerRegistryReadOnly: Creating...
aws_iam_role_policy_attachment.eks-nodegroup-role-AmazonEKSWorkerNodePolicy: Creating...
aws_iam_role_policy_attachment.eks-nodegroup-role-AmazonEC2ContainerRegistryReadOnly: Creation complete after 1s [id=eks-nodegroup-role-20241027130604526800000001]
aws_iam_role_policy_attachment.eks-nodegroup-role-AmazonEKS_CNI_Policy: Creation complete after 1s [id=eks-nodegroup-role-20241027130604963000000002]
aws_iam_role_policy_attachment.eks-nodegroup-role-AmazonEKSWorkerNodePolicy: Creation complete after 2s [id=eks-nodegroup-role-20241027130605372700000003]

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

转载请著名原文地址:
https://www.cnblogs.com/Sunzz/p/18498915

6. 创建Node Group

定义
eks_node_group.tf文件

resource "aws_eks_node_group" "node_group1" {
  cluster_name    = aws_eks_cluster.tf-eks.name
  node_group_name = "node_group1"
  ami_type        = "AL2_x86_64"
  capacity_type   = "ON_DEMAND"
  disk_size       = 20
  instance_types   = ["t3.medium"]
  node_role_arn   = aws_iam_role.eks-nodegroup-role.arn
  subnet_ids = [
      aws_subnet.tf_eks_subnet1.id,
      aws_subnet.tf_eks_subnet2.id
    ]

  scaling_config {
    desired_size = 1
    max_size     = 2
    min_size     = 1
  }

  update_config {
    max_unavailable = 1
  }

  depends_on = [
    aws_iam_role_policy_attachment.eks-nodegroup-role-AmazonEKSWorkerNodePolicy,
    aws_iam_role_policy_attachment.eks-nodegroup-role-AmazonEKS_CNI_Policy,
    aws_iam_role_policy_attachment.eks-nodegroup-role-AmazonEC2ContainerRegistryReadOnly,
  ]

#  remote_access {
#    ec2_ssh_key = aws_key_pair.tf-keypair.key_name
#    source_security_group_ids = [
#	   aws_security_group.tf_security_group.id
#    ]
#  } 
}

resource "aws_eks_node_group" "node_group2" {
  cluster_name    = aws_eks_cluster.tf-eks.name
  node_group_name = "node_group2"
  ami_type        = "AL2_x86_64"
  capacity_type   = "ON_DEMAND"
  disk_size       = 20
  instance_types  = ["t3.medium"]
  node_role_arn   = aws_iam_role.eks-nodegroup-role.arn
  subnet_ids = [
      aws_subnet.tf_eks_subnet1.id,
      aws_subnet.tf_eks_subnet2.id
    ]

  scaling_config {
    desired_size = 1
    max_size     = 2
    min_size     = 1
  }

  update_config {
    max_unavailable = 1
  }

  depends_on = [
    aws_iam_role_policy_attachment.eks-nodegroup-role-AmazonEKSWorkerNodePolicy,
    aws_iam_role_policy_attachment.eks-nodegroup-role-AmazonEKS_CNI_Policy,
    aws_iam_role_policy_attachment.eks-nodegroup-role-AmazonEC2ContainerRegistryReadOnly,
  ]

#  remote_access {
#    ec2_ssh_key = aws_key_pair.tf-keypair.key_name
#    source_security_group_ids = [
#	   aws_security_group.tf_security_group.id
#    ]
#  } 
}

解释

EKS 节点组定义

  1. resource "aws_eks_node_group" "node_group1"
    : 创建一个名为
    node_group1
    的 EKS 节点组。

  2. cluster_name = aws_eks_cluster.tf-eks.name
    : 指定该节点组所属的 EKS 集群,引用之前创建的
    tf-eks
    集群的名称。

  3. node_group_name = "node_group1"
    : 设置节点组的名称为
    node_group1

  4. ami_type = "AL2_x86_64"
    : 指定节点组使用的 Amazon Machine Image (AMI) 类型,这里使用的是 Amazon Linux 2 (AL2) 的 x86_64 架构。可选的有
    AL2_x86_64 AL2_x86_64_GPU AL2_ARM_64 CUSTOM BOTTLEROCKET_ARM_64 BOTTLEROCKET_x86_64 BOTTLEROCKET_ARM_64_NVIDIA BOTTLEROCKET_x86_64_NVIDIA WINDOWS_CORE_2019_x86_64 WINDOWS_FULL_2019_x86_64 WINDOWS_CORE_2022_x86_64 WINDOWS_FULL_2022_x86_64],

  5. capacity_type = "ON_DEMAND"
    : 设置实例的容量类型为按需(On-Demand),即按使用付费,而非预留。

  6. disk_size = 20
    : 为每个节点指定根卷的大小,这里设置为 20 GB。

  7. instance_types = ["t3.medium"]
    : 指定节点的实例类型,这里使用的是
    t3.medium
    类型。

  8. node_role_arn = aws_iam_role.eks-nodegroup-role.arn
    : 指定节点组的 IAM 角色 ARN,允许节点访问必要的 AWS 服务。

  9. subnet_ids = [ ... ]
    : 定义节点组将要使用的子网,引用
    tf_eks_subnet1

    tf_eks_subnet2
    的 ID。这些子网是 EKS 节点运行的网络环境。

扩展和更新配置

  1. scaling_config { ... }
    :


    • 设置节点组的扩展配置。
    • desired_size = 1
      : 默认启动一个节点。
    • max_size = 2
      : 节点组最多可以扩展到 2 个节点。
    • min_size = 1
      : 节点组至少保留一个节点。
  2. update_config { ... }
    :


    • 配置节点组的更新策略。
    • max_unavailable = 1
      : 更新过程中,最多可以有一个节点不可用。

依赖关系

  1. depends_on = [ ... ]
    : 指定该资源的依赖关系,确保在创建节点组之前,相关的 IAM 角色策略附加操作已经完成。

远程访问配置(被注释掉)

  1. remote_access { ... }
    (被注释掉):
    • 此部分配置远程访问选项,允许 SSH 访问节点组。
    • ec2_ssh_key = aws_key_pair.tf-keypair.key_name
      : 指定用于 SSH 访问的 EC2 密钥对。
    • source_security_group_ids = [ ... ]
      : 指定允许 SSH 访问的安全组。

预创建

 terraform plan -out=tf.plan
tf plan
 Terraform will perform the following actions:

  # aws_eks_node_group.node_group1 will be created
  + resource "aws_eks_node_group" "node_group1" {
      + ami_type               = "AL2_x86_64"
      + arn                    = (known after apply)
      + capacity_type          = "ON_DEMAND"
      + cluster_name           = "tf-eks"
      + disk_size              = 20
      + id                     = (known after apply)
      + instance_types         = [
          + "t3.medium",
        ]
      + node_group_name        = "node_group1"
      + node_group_name_prefix = (known after apply)
      + node_role_arn          = "arn:aws:iam::xxxxxx:role/eks-nodegroup-role"
      + release_version        = (known after apply)
      + resources              = (known after apply)
      + status                 = (known after apply)
      + subnet_ids             = [
          + "subnet-01b5d98060f0063ef",
          + "subnet-0a30534a829758774",
        ]
      + tags_all               = (known after apply)
      + version                = (known after apply)

      + scaling_config {
          + desired_size = 1
          + max_size     = 2
          + min_size     = 1
        }

      + update_config {
          + max_unavailable = 1
        }
    }

  # aws_eks_node_group.node_group2 will be created
  + resource "aws_eks_node_group" "node_group2" {
      + ami_type               = "AL2_x86_64"
      + arn                    = (known after apply)
      + capacity_type          = "ON_DEMAND"
      + cluster_name           = "tf-eks"
      + disk_size              = 20
      + id                     = (known after apply)
      + instance_types         = [
          + "t3.medium",
        ]
      + node_group_name        = "node_group2"
      + node_group_name_prefix = (known after apply)
      + node_role_arn          = "arn:aws:iam::xxxxx:role/eks-nodegroup-role"
      + release_version        = (known after apply)
      + resources              = (known after apply)
      + status                 = (known after apply)
      + subnet_ids             = [
          + "subnet-01b5d98060f0063ef",
          + "subnet-0a30534a829758774",
        ]
      + tags_all               = (known after apply)
      + version                = (known after apply)

      + scaling_config {
          + desired_size = 1
          + max_size     = 2
          + min_size     = 1
        }

      + update_config {
          + max_unavailable = 1
        }
    }

Plan: 2 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tf.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "tf.plan"

创建Node Group

terraform apply "tf.plan"

输出如下:

aws_eks_node_group.node_group2: Creating...
aws_eks_node_group.node_group1: Creating...
aws_eks_node_group.node_group1: Still creating... [10s elapsed]
......
aws_eks_node_group.node_group1: Creation complete after 1m41s [id=tf-eks:node_group1]
aws_eks_node_group.node_group2: Still creating... [1m50s elapsed]
aws_eks_node_group.node_group2: Creation complete after 1m52s [id=tf-eks:node_group2]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

7. 获取EKS信息

新增eks_output.tf

# 输出 EKS 集群的名称
output "eks_cluster_name" {
  value = aws_eks_cluster.tf-eks.name
  description = "The name of the EKS cluster"
}

# 输出 EKS 集群的 ARN(Amazon Resource Name)
output "eks_cluster_arn" {
  value = aws_eks_cluster.tf-eks.arn
  description = "The ARN of the EKS cluster"
}

# 输出 EKS 集群的 API 服务器端点
output "eks_cluster_endpoint" {
  value = aws_eks_cluster.tf-eks.endpoint
  description = "The endpoint of the EKS cluster"
}

# 输出 EKS 集群的当前状态
output "eks_cluster_status" {
  value = aws_eks_cluster.tf-eks.status
  description = "The status of the EKS cluster"
}

# 输出与 EKS 集群关联的 VPC ID
output "eks_cluster_vpc_id" {
  value = aws_eks_cluster.tf-eks.vpc_config[0].vpc_id
  description = "The VPC ID associated with the EKS cluster"
}

# 输出与 EKS 集群关联的安全组 ID
output "eks_cluster_security_group_ids" {
  value = aws_eks_cluster.tf-eks.vpc_config[0].cluster_security_group_id
  description = "The security group IDs associated with the EKS cluster"
}

# 输出用于访问 EKS 集群的 kubeconfig 配置
output "kubeconfig" {
  value = <<EOT
apiVersion: v1
clusters:
- cluster:
    server: ${aws_eks_cluster.tf-eks.endpoint}
    certificate-authority-data: ${aws_eks_cluster.tf-eks.certificate_authority[0].data}
  name: ${aws_eks_cluster.tf-eks.name}
contexts:
- context:
    cluster: ${aws_eks_cluster.tf-eks.name}
    user: ${aws_eks_cluster.tf-eks.name}
  name: ${aws_eks_cluster.tf-eks.name}
current-context: ${aws_eks_cluster.tf-eks.name}
kind: Config
preferences: {}
users:
- name: ${aws_eks_cluster.tf-eks.name}
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      command: aws
      args:
        - eks
        - get-token
        - --cluster-name
        - ${aws_eks_cluster.tf-eks.name}
EOT
  description = "Kubeconfig for accessing the EKS cluster"
}

由于output.tf只是获取已经创建的资源信息,不涉及资源的修改,所以可以直接apply

terraform
apply

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

eks_cluster_arn = "arn:aws:eks:us-west-1:xxxxx:cluster/tf-eks"
eks_cluster_endpoint = "https://D59BB0103962C6BEABC8271AC16B34EC.gr7.us-west-1.eks.amazonaws.com"
eks_cluster_name = "tf-eks"
eks_cluster_security_group_ids = "sg-0159f56ebd2d93a38"
eks_cluster_status = "ACTIVE"
eks_cluster_vpc_id = "vpc-0361291552eab4047"
kubeconfig = <<EOT
apiVersion: v1
clusters:
- cluster:
    server: https://D59BB0103962C6BEABC8271AC16B34EC.gr7.us-west-1.eks.amazonaws.com
    certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJSG83cjJJV.....
  name: tf-eks
contexts:
- context:
    cluster: tf-eks
    user: tf-eks
  name: tf-eks
current-context: tf-eks
kind: Config
preferences: {}
users:
- name: tf-eks
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1 
      command: aws
      args:
        - eks
        - get-token
        - --cluster-name
        - tf-eks

EOT

8. 配置kubeconfig

方式1

根据上边生成的kubeconfig内容配置~/.kube/config即可

方式2

执行命令生成kubeconfig文件

aws eks update-kubeconfig --region us-west-1 --name tf-eks

查看集群节点数量

 kubectl get no
 kubectl get no
NAME                                         STATUS   ROLES    AGE     VERSION
ip-10-10-81-13.us-west-1.compute.internal    Ready    <none>   3m48s   v1.31.0-eks-a737599
ip-10-10-82-102.us-west-1.compute.internal   Ready    <none>   4m1s    v1.31.0-eks-a737599

查看集群允许的pod数量

kubectl get po -A
NAMESPACE     NAME                       READY   STATUS    RESTARTS   AGE
kube-system   aws-node-8zjcn             2/2     Running   0          4m55s
kube-system   aws-node-n4ns8             2/2     Running   0          4m42s
kube-system   coredns-6486b6fd59-hkcnb   1/1     Running   0          20m
kube-system   coredns-6486b6fd59-hz75m   1/1     Running   0          20m
kube-system   kube-proxy-fbdv9           1/1     Running   0          4m42s
kube-system   kube-proxy-nnb2r           1/1     Running   0          4m55s

9. 创建nginx应用

编辑nginx-deployment.yaml文件

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3  # 副本数量
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.26  # 指定 Nginx 镜像版本
          ports:
            - containerPort: 80  # 暴露容器端口

创建nginx-deployment

kubectl apply -f nginx-deployment.yaml

10.回顾创建的tf文件

至此我们已经创建了一堆tf文件,大家也可以一起创建所有的tf文件,最后再
terraform plan

详情如下:

9.销毁资源

terraform destroy

总结

经过一段时间的深入探索与编写,这篇博客成为了我迄今为止最费时间和最长的一篇作品。从研究Terraform的功能到实际操作在AWS上创建各种资源,每一个步骤都需要细致的推敲与反复的验证。这不仅让我更加熟悉Terraform的强大功能,也让我在分享知识的过程中收获了许多。

在这篇博文中,我力求以详尽的内容和清晰的解释帮助读者轻松入门,尤其是针对那些初学者。我希望这些努力能为读者提供实用的指导,帮助他们在云计算的道路上迈出坚实的第一步。这段旅程虽然漫长,但每一个字都承载着我对Terraform的热情和对知识分享的期待。希望你们能在这篇博客中找到灵感和启发!

传统的目标检测模型通常受到其训练数据和定义的类别逻辑的限制。随着语言-视觉模型的近期兴起,出现了不受这些固定类别限制的新方法。尽管这些开放词汇检测模型具有灵活性,但与传统的固定类别模型相比,仍然在准确性上存在不足。同时,更加准确的数据特定模型在需要扩展类别或合并不同数据集进行训练时面临挑战。后者通常由于逻辑或冲突的类别定义而无法组合,这使得在不损害模型性能的情况下提升模型变得困难。

CerberusDet
是一个旨在处理多目标检测任务的多头模型框架,该模型基于
YOLO
架构,能够有效地共享来自主干和
NECK
部分组件的视觉特征,同时保持独立的任务头。这种方法使得
CerberusDet
能够高效地执行,同时仍然提供最佳结果。


PASCAL VOC
数据集和
Objects365
数据集上评估了该模型,以展示其能力。
CerberusDet
实现了最先进的结果,推理时间减少了
36%
。同时训练的任务越多,所提出的模型与顺序运行单独模型相比,效率越高。

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: CerberusDet: Unified Multi-Dataset Object Detection

Introduction


向一个使用目标检测(
OD
)的现有实时应用程序中添加新类别涉及几个重大挑战。一个关键问题是,在一个数据集中标注的对象类别在另一个数据集中可能没有标注,即使这些对象本身出现在后者的图像中。此外,由于标注逻辑的不同和类别重叠的不完整,合并不同的数据集往往是不可能的。同时,这类应用程序需要高效的管道,这限制了独立的数据特定模型的使用。

论文的目标是构建一个在多个数据集上训练的统一模型,其准确性不会低于单独训练模型的表现,同时利用更少的计算资源。论文提出了
CerberusDet
,用于同时在多个数据集上训练单个检测神经网络。论文还展示了一种识别最佳模型架构的方法,因为并不是所有任务都可以一起训练。一个显著的挑战在于确定哪些参数在不同任务之间共享,任务的次优分组可能导致负迁移,即在无关任务之间共享信息的问题。此外,在计算资源有限的情况下,所提方法能够选择满足要求的架构。在开放数据的实验中,
CerberusDet
使用一个统一的神经网络获得的结果与分离数据特定的最先进模型相媲美。

扩展检测器模型以包含新类别的另一种方法是使用开放词汇目标检测器(
OVDs
),这一方法最近变得越来越流行。然而,
OVDs
通常缺乏数据特定检测器的准确性,需要大量的训练数据,并且容易对基础类别过拟合。论文优先考虑高准确性,而非
OVDs
的灵活性。论文提出的架构能够根据需要添加新类别,同时保持之前学习类别的准确性,更适合实际的需求。值得注意的是,这种方法已经在生产环境中部署并验证,证明了其在实际应用中的鲁棒性和可靠性。

论文的主要贡献如下:

  1. 对多数据集和多任务检测的各种方法进行了研究,探索了不同的参数共享策略和训练程序。

  2. 展示了使用开放数据集的几个实验结果,提供了各种方法有效性的见解。

  3. 提出了一种新的多分支目标检测模型
    CerberusDet
    ,可以根据不同的计算需求和任务进行定制。

  4. 公开发布了训练和推理代码,以及训练好的模型,鼓励该领域进一步的研究和发展。

Model


Method

CerberusDet
模型允许在一个共享模型中学习多个检测任务。每个检测任务都是一个独立的任务,使用其自己的数据集和独特的标签集。

CerberusDet
模型建立在
YOLO
架构之上,通过在任务之间共享所有主干参数来优化计算资源,而每个任务保留其自己独特的
HEAD
部分参数集。
NECK
部分层可以是共享的,也可以是特定于任务的。图
2
展示了基于
YOLOv8

CerberusDet
架构在三个任务下的一个可能变体。使用标准的
YOLOv8x
架构和
640
的输入图像分辨率,该模型的主干由
184
层和
3000
万参数组成。
NECK
部份有
6
个可共享模块,包含
134
层和
2800
万参数。每个
HEAD
部分由
54
层和
800
万参数组成。

通过在多个任务之间共享主干,训练方法相比于对每个任务分别使用单独模型的顺序推理,实现了显著的计算预算节约。图
3
展示了基于
YOLOv8x
架构的
CerberusDet
的推理速度。该图比较了两种场景的推理时间:一种是所有
NECK
部分参数依赖于特定任务,另一种是这些参数在任务间共享。结果突出显示了通过参数共享所获得的计算效率。

Parameters sharing

鉴于硬参数共享技术在多任务学习中的高效性及其通过在训练过程中利用任务间的信息来增强每个任务的预测质量,论文决定采用该技术。硬参数共享允许任务之间共享一组参数,并保留一组任务特定的参数。基于
YOLO
架构,在模块级别具有可共享的参数集。例如,
YOLOv8x

6
个参数化的
NECK
部分模块,因此每个任务可以与另一个任务共享其中的任一模块。

为了决定哪些模块在不同任务之间共享,采用表示相似性分析(
Representation Similarity Analysis

RSA
)方法来估计每个
NECK
部分模块的任务相似性,这些模块可以是共享的或特定于任务的。然后,对于每个可能的架构变体,计算基于
RSA
的相似性评分(
\(\mathit{rsa\ score}\)
)和
\(\mathit{computational\ score}\)
。第一个评分显示了架构的潜在性能,而第二个评分评估了其计算效率。在可用的计算预算内,选择具有最佳
\(\mathit{rsa\ score}\)
的架构。设该架构包含
\(l\)
个可共享模块并且有
\(N\)
个任务,选择该架构的算法如下所示:

  1. 从每个任务的测试集中选择一个小的代表性图像子集。
  2. 使用特定于任务的模型,从每个模块中提取所选图像的特征。
  3. 根据提取的特征,计算双重性图相似性(
    Duality Diagram Similarity

    DDS
    )——计算每对选定图像的成对(不)相似性。矩阵的每个元素是(
    1 -
    皮尔逊相关系数)的值。
  4. 使用中心化核对齐(
    Centered Kernel Alignment

    CKA
    )方法对
    DDS
    矩阵进行计算,生成表示不相似度矩阵(
    Representation Dissimilarity Matrices

    RDMs
    )——每个模块一个
    \(N \times N\)
    矩阵。矩阵的每个元素表示两个任务之间的相似性系数。
  5. 对于每种可能的架构,使用来自
    RDM
    矩阵的值计算
    \(\mathit{rsa\ score}\)
    ,即可共享模型层中每个位置的任务不相似度得分之和。定义为
    \(\mathit{rsa\ score} = \sum_{m=1}^{l} S_m\)
    ,其中
    \(S_m\)
    (公式 `ref`{eq:rsa})通过对模块 l 中共享任务的不相似度得分之间的最大距离取平均得到。
  6. 对于每种可能的架构,使用公式
    2
    计算
    \(\mathit{computational\ score}\)
  7. 选择具有最佳
    \(\mathit{rsa\ score}\)

    \(\mathit{computational\ score}\)
    组合的架构(越低越好),或者在
    \(\mathit{computational\ score}\)
    的设定约束下选择具有最低
    \(\mathit{rsa\ score}\)
    的架构。

\[\begin{align}
\label{eq:rsa}
S_m &= \frac{1}{|\tau_i, \ldots, \tau_k\}|} \times \\
&\sum_{j=i}^{k} \max \left\{ RDM(j, i), \ldots, RDM(j, k) \right\} \notag
\end{align}
\]

其中
\(\{\tau_i, \ldots, \tau_k\}\)
是模块
\(l\)
中的共享任务。

\[\begin{equation}
\label{eq:comp_score}
\mathit{computational\ score} = \frac{\mathit{inference\_time}}{(N * \mathit{single\_inference\_time})}
\end{equation}
\]

为了评估所选择的方法,选择了
4
个具有不同
RSA
分数和计算分数的架构,训练模型并比较了平均指标值。图
4
表明,随着
RSA
分数的下降和计算复杂度的增加,模型的准确性也在提高。为了计算计算分数,使用了
V100 GPU
,批处理大小为
1

Training procedure

考虑一组任务
\(\{\mathit{{\tau}_1, \ldots, \tau_n}\}\)
,这些任务的不同组合可能共享一组模型参数。设
\(\theta_{shared} = \{\theta_{i..k}, \ldots \theta_{j..m}\}\)
为不同任务组之间的共享参数集合
\(\{i, \ldots,k\}, \ldots, \{j, \ldots,m\}\)
。算法
1
展示了所提出的
CerberusDet
模型的端到端学习过程。在训练过程中,遍历任务,从相应的数据集中抽取小批量数据,计算与当前任务相关的参数的损失和梯度。接下来,对每个任务组的共享参数进行梯度平均,并根据公式
3
更新它们的值。

\[\begin{equation}
\label{eq:average}
\theta_{\{i,\ldots,k\}} = \theta_{\{i,\ldots,k\}} - (\alpha * \frac{1}{|\{i,\ldots,k\}|} * \sum_{j \in \{i,\ldots,k\}} \dfrac{\partial L_j}{\partial \theta_{\{i,\ldots,k\}}})
\end{equation}
\]

其中
\(\{i,\ldots,k\}\)
表示具有共享参数
\(\theta_{\{i,\ldots,k\}}\)
的任务组,
\(\alpha\)
是学习率,
\(L_j\)
是任务
\(j\)
的损失。

联合训练的速度和有效性受到各个任务损失函数的强烈影响。由于这些损失函数可能具有不同的性质和规模,因此正确权衡它们是至关重要的。为了找到损失函数的最佳权重以及其他训练超参数,采用超参数演化方法。

在训练过程中,如果每个批次中的样本没有经过仔细和全面的平衡,模型的性能会显著下降。为了解决这个问题,需要确保每个迭代中所有类别都根据它们在数据集中的频率得到充分表示。

The impact of training settings


1
展示了前面所述的每种技术影响的结果。在这些实验中使用专有数据,因为它们展示了足够的任务间一致性,以确保实验的清晰性。模型被训练用于
3
个任务,其中基线是一个架构,在该架构中,模型的所有参数(除了
HEAD
部分)都是在各任务之间共享的。

第一个任务的数据集包含
22
个类别,训练集中有
27,146
张图像,验证集中有
3,017
张图像。第二个任务的数据集包含
18
个类别,训练集中有
22,365
张图像,验证集中有
681
张图像。第三个任务的数据集包含
16
个类别,训练集中有
17,012
张图像,验证集中有
3,830
张图像。为了比较架构搜索方法对结果的影响,论文还训练了一个模型,其中所有的
NECK
部分参数都是任务特定的,然后将发现的架构的准确性提升与之进行比较。

提到的模型是在
YOLOv5x
基础上构建的,输入图像分辨率为
640x640
,测量是在
V100 GPU
上以
FP16
精度进行的。

Open-source datasets experiments




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

Canvas简历编辑器-选中绘制与拖拽多选交互设计

在之前我们聊了聊如何基于
Canvas
与基本事件组合实现了轻量级
DOM
,并且在此基础上实现了如何进行管理事件以及多层级渲染的能力设计。那么此时我们就依然在轻量级
DOM
的基础上,关注于实现选中绘制与拖拽多选交互设计。

关于
Canvas
简历编辑器项目的相关文章:

选中绘制

我们先来聊一聊最基本的节点点击选中以及拖拽的交互,而在聊具体的代码实现之前,我们先来看一下对于图形的绘制问题。在
Canvas
中我们绘制路径的话,我们可以通过
fill
来填充路径,也可以通过
stroke
来描边路径,而在我们描边的时候,如果不注意的话可能会陷入一些绘制的问题。假如此时我们要绘制一条线,我们可以分别来看下使用
stroke

fill
的绘制方法实现,此时如果在高清
ctx.scale(devicePixel, devicePixel)
情况下,则能明显地看出来绘制位置差
0.5px
,而如果基准为
1px
的话则会出现
1px
的差值以及色值偏差。

ctx.beginPath();
ctx.strokeStyle = "blue";
ctx.lineWidth = 1;
ctx.moveTo(5, 5);
ctx.lineTo(100, 5);
ctx.closePath();
ctx.stroke();
ctx.fillStyle = "red";
ctx.beginPath();
ctx.moveTo(100, 5);
ctx.lineTo(200, 5);
ctx.lineTo(200, 6);
ctx.lineTo(100, 6);
ctx.closePath();
ctx.fill();

在先前的选中图形
frame
中,我们都是用
stroke
来实现的,然后最近我想将其真正作为外边框来绘制,然后就发现想绘制
inside stroke
确实不是一件容易的事。从
MDN
上阅读
stroke
的文档可以得到其是以路径的中心线为基准的,也就是说
stroke
是由基准分别向内外扩展的,那么问题就来了,假如我们绘制了一条线,而这条线本身是存在
1px
宽度的,那么初步理解按照文档所说其本身结构应该是以这
1px
本身的中心点也就是
0.5px
的位置为中心点向外发散,然而其实际效果是以
1px
的外边缘为基准发散,那么就会导致
1px
的线在
stroke
之后会多出
0.5px
的宽度,这个效果可以通过
lineTo(0, 100)
外加
lineWith=1
来测试,可以发现其可见宽度只有
0.5px
,这点可以通过再画一个
1px

Path
来对比。

ctx.beginPath();
ctx.lineWidth = 6;
ctx.strokeStyle = "blue";
ctx.moveTo(0, 0);
ctx.lineTo(100, 0);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = "red";
ctx.moveTo(100, 3);
ctx.lineTo(200, 3);
ctx.closePath();
ctx.stroke();

那么这里的
Strokes are aligned to the center of a path
可能与我理解的
center of a path
并不相同,或许其只是想表达
stroke
是分别向两侧绘制描边的,而并不是解释其基准位置。关于这个问题我咨询了一下,这里主要是理解有偏差,在我们使用
API
绘制路径时,本身并没有设置宽度的信息,而坐标信息定义的是路径的轮廓或边界,因此我们在最开始定义的路径结构
1px
是不成立的。在图形学的上下文中,路径
path
通常是指一个几何形状的轮廓或线条,路径本身是数学上的抽象概念,没有宽度,只是一个由点和线段构成的轨迹,因此当我们提到描边
stroke
时,指的是一个可视化过程,即在路径的周围绘制有宽度的线条。

实际上这里如果仅仅是处理
frame
的问题的话,可能并没有太大的问题,然而在处理节点的时候,发现由于是使用
stroke
绘制的操作节点,那么实际上其总是会超出原始宽度的,也就是上边说的描边问题,而因为超出的这
0.5px
的边缘节点,使得我一直认为绘制节点的边缘与填充是没问题的,然而今天才发现这里的顺序反了,描边的内部会被填充覆盖掉,也就是说实现的
border
宽度总是会被除以
2
的,因此要先填充再描边才是正确的绘制方式。此外,无论是
frame
节点的绘制还是类似
border
的绘制,在
Firefox

inside stroke
总是会出现兼容性问题,仅有组合
fill
以及使用
fill
配合
Path2D + clip
才能绘制正常的
inside stroke

ctx.save();
ctx.beginPath();
ctx.arc(70, 75, 50, 0, 2 * Math.PI);
ctx.stroke();
ctx.fillStyle = "white";
ctx.fill();
ctx.closePath();
ctx.restore();

ctx.save();
ctx.beginPath();
ctx.arc(200, 75, 50, 0, 2 * Math.PI);
ctx.fillStyle = "white";
ctx.fill();
ctx.stroke();
ctx.closePath();
ctx.restore();

那么我们就可以利用三种方式绘制
inside stroke
,当然还有借助
lineTo/fillRect
分别绘制
4
条边的方式我们没有列举,因为这种方式自然不会出现什么问题,其本身就是使用
fill
的方式绘制的,而我们这里主要是讨论
stroke
的绘制问题,只是借助
Path2D
同样也是
fill
的方式绘制的,但是这里需要讨论一下
clip

fillRule-nonzero/evenodd
的问题。那么借助
stroke
的特性,方式
1
是我们绘制两倍的
lineWidth
,然后裁剪掉外部的描边部分,这样就能够正确保留内部的描边了,方式
2
则是我们主动校准了描边的位置,将其向内缩小
0.5px
的位置,由此来绘制完整的描边,方式
3
是借助
evenodd
的填充规则,通过
clip
来生成规则保留内部的描边,再来实际填充即可实现。

<canvas id="canvas" width="800" height="800"></canvas>
<script>
  // https://stackoverflow.com/questions/36615592/canvas-inner-stroke
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");
  const devicePixelRatio = Math.ceil(window.devicePixelRatio || 1);
  const width = canvas.clientWidth;
  const height = canvas.clientHeight;
  canvas.width = width * devicePixelRatio;
  canvas.height = height * devicePixelRatio;
  canvas.style.width = width + "px";
  canvas.style.height = height + "px";
  ctx.scale(devicePixelRatio, devicePixelRatio);

  ctx.save();
  ctx.beginPath();
  ctx.rect(10, 10, 150, 100);
  ctx.clip();
  ctx.closePath();
  ctx.lineWidth = 2;
  ctx.strokeStyle = "blue";
  ctx.stroke();
  ctx.restore();

  ctx.save();
  ctx.beginPath();
  ctx.rect(170 + 0.5, 10 + 0.5, 150 - 1, 100 - 1);
  ctx.closePath();
  ctx.lineWidth = 1;
  ctx.strokeStyle = "blue";
  ctx.stroke();
  ctx.restore();

  ctx.save();
  ctx.beginPath();
  const region = new Path2D();
  region.rect(330, 10, 150, 100);
  region.rect(330 + 1, 10 + 1, 150 - 2, 100 - 2);
  ctx.clip(region, "evenodd");
  ctx.rect(330, 10, 150, 100);
  ctx.closePath();
  ctx.fillStyle = "blue";
  ctx.fill();
  ctx.restore();
</script>

那么先前我们也提到了在
Firefox
浏览器的兼容性问题,那么我们将上述的实现方式在
Firefox
中进行测试,可以发现
inside stroke
的绘制是有些许问题的,第一个图形明显左上的线比右下的线细一些,第二个图形则明显会粗糙一些,第三个图形则看起来绘制更细致更符合
1px
的绘制。因此我们如果想要兼容绘制
inside stroke
的话最好的方式还是选择方式三,当然像最开始的实现中借助
lineTo/fillRect
分别绘制
4
条边的方式自然也是没问题的,两者的性能对比在后边也可以尝试实验一下。

那么接着我们就回到在轻量级
DOM
上实现选中的绘制,首先我们对基本节点的事件做一些通用的实现,我们先来实现点击的选取。因为在之前我们已经定义好了事件的基本传递,那么我们此时只需要在
Element
节点上实现事件的响应即可,那么在这里我们就可以直接操作选区模块,直接将当前的活跃节点
id
设置为节点组的内容即可。

// packages/core/src/canvas/dom/element.ts
export class ElementNode extends Node {
  protected onMouseDown = (e: MouseEvent) => {
    this.editor.selection.setActiveDelta(this.id);
  };
}

而当我们触发选区的节点设置之后,在选区模块则会将此时所有的
active
节点组合起来形成新的
Range
,然后在新的
Range
基础上判断当前是否应该触发选区变换的事件,这里的事件分发比较重要,整个编辑器的选区变化事件都会在此处分发。

// packages/core/src/selection/index.ts
export class Selection {
  public set(range: Range | null) {
    if (this.editor.state.get(EDITOR_STATE.READONLY)) return this;
    const previous = this.current;
    if (Range.isEqual(previous, range)) return this;
    this.current = range;
    this.editor.event.trigger(EDITOR_EVENT.SELECTION_CHANGE, {
      previous,
      current: range,
    });
    return this;
  }

  public setActiveDelta(...deltaIds: string[]) {
    this.active.clear();
    deltaIds.forEach(id => this.active.add(id));
    this.compose();
  }

  public compose() {
    const active = this.active;
    if (active.size === 0) {
      this.set(null);
      return void 0;
    }
    let range: Range | null = null;
    active.forEach(key => {
      const delta = this.editor.deltaSet.get(key);
      if (!delta) return void 0;
      const deltaRange = Range.from(delta);
      range = range ? range.compose(deltaRange) : deltaRange;
    });
    this.set(range);
  }
}

那么在事件分发之后,我们必须要在选区变换之后绘制新的选区,实际上在选区变换后我们理论上仅仅需要将节点绘制出来即可,而按照我们先前的调度设计而言,我们需要主动按需触发要绘制的区域,并且由于选区是由其他的位置变换到当前区域的,因此绘制时就需要将先前的区域同时绘制。那么按照我们先前的设计,
SelectNode
本身既是事件处理器又是渲染器,基本与
DOM
节点基本一致,只是我们绑定事件和绘制都是直接由类控制而已,而在
drawingMask

Shape.frame
绘制中,就是我们最开始聊的描边与填充绘制问题。

// packages/core/src/canvas/dom/node.ts
export class SelectNode extends Node {
  protected onSelectionChange = (e: SelectionChangeEvent) => {
    const { current, previous } = e;
    this.editor.logger.info("Selection Change", current);
    const range = current || previous;
    if (range) {
      const refresh = range.compose(previous).compose(current);
      this.editor.canvas.mask.drawingEffect(refresh.zoom(RESIZE_OFS));
    }
  };

  public drawingMask = (ctx: CanvasRenderingContext2D) => {
    const selection = this.editor.selection.get();
    if (selection) {
      const { x, y, width, height } = selection.rect();
      Shape.frame(ctx, { x, y, width, height, borderColor: BLUE_6 });
    }
  };
}

拖拽多选

当我们已经成功实现图形单选以及节点绘制之后,我们很容易想到两个交互问题,首先是图形的多选,因为我们在选中节点的时候可能不会仅仅选一个节点,例如全选的场景,其次则是选中图形的拖拽,这个就是常见的交互方式了,无论是单选还是多选的时候,都可以通过拖拽图形来调整位置。那么我们首先来看一下多选,实际上在上边我们的设计中本就是支持多选的,我们在选区的
active
就是
Set<string>
类型,以及
Selection

compose
方法也是支持多选的,那么我们只需要在选中节点的时候,将节点的
id
添加到
active
中即可。

// packages/core/src/canvas/dom/element.ts
export class ElementNode extends Node {
  protected onMouseDown = (e: MouseEvent) => {
    if (e.shiftKey) {
      this.editor.selection.addActiveDelta(this.id);
    } else {
      this.editor.selection.setActiveDelta(this.id);
    }
  };
}

除了按住
shiftKey
键进行多选之外,我们使用鼠标以某个点为起点拖拽选区进行选择也是一种多选的方式,那么在这里我们将这个交互方式设计在了
FrameNode
内,而这里有点不同的是我们的起始行为需要归并到
Root
节点上,因为只有点击在
Root
节点上的事件我们才认为是起始,否则是认为点击到了节点本身上,而框选这个交互的本身事件则主要是判断当前的选区大小,以及其覆盖的节点范围,将覆盖的节点
id
全部放置于选区模块即可。

// packages/core/src/canvas/dom/frame.ts
export class FrameNode extends Node {
  private onRootMouseDown = (e: MouseEvent) => {
    this.savedRootMouseDown(e);
    this.unbindOpEvents();
    this.bindOpEvents();
    this.landing = Point.from(e.x, e.y);
    this.landingClient = Point.from(e.clientX, e.clientY);
  };

  private onMouseMoveBridge = (e: globalThis.MouseEvent) => {
    if (!this.landing || !this.landingClient) return void 0;
    const point = Point.from(e.clientX, e.clientY);
    const { x, y } = this.landingClient.diff(point);
    if (!this.isDragging && (Math.abs(x) > SELECT_BIAS || Math.abs(y) > SELECT_BIAS)) {
      // 拖拽阈值
      this.isDragging = true;
    }
    if (this.isDragging) {
      const latest = new Range({
        startX: this.landing.x,
        startY: this.landing.y,
        endX: this.landing.x + x,
        endY: this.landing.y + y,
      }).normalize();
      this.setRange(latest);
      // 获取获取与选区交叉的所有`State`节点
      const effects: string[] = [];
      this.editor.state.getDeltasMap().forEach(state => {
        if (latest.intersect(state.toRange())) effects.push(state.id);
      });
      this.editor.selection.setActiveDelta(...effects);
      // 重绘拖拽过的最大区域
      const zoomed = latest.zoom(RESIZE_OFS);
      this.dragged = this.dragged ? this.dragged.compose(zoomed) : zoomed;
      this.editor.canvas.mask.drawingEffect(this.dragged);
    }
  };
  private onMouseMoveController = throttle(this.onMouseMoveBridge, ...THE_CONFIG);

  private onMouseUpController = () => {
    this.unbindOpEvents();
    this.setRange(Range.reset());
    if (this.isDragging) {
      this.dragged && this.editor.canvas.mask.drawingEffect(this.dragged);
    }
    this.landing = null;
    this.isDragging = false;
    this.dragged = null;
    this.setRange(Range.reset());
  };

  public drawingMask = (ctx: CanvasRenderingContext2D) => {
    if (this.isDragging) {
      const { x, y, width, height } = this.range.rect();
      Shape.rect(ctx, { x, y, width, height, borderColor: BLUE_5, fillColor: BLUE_6_6 });
    }
  };
}

说到这里,在多选之外这里我们可能还需要关注一个交互,就是
Hover
的效果。如果我们是
CSS
实现的话,这个问题实际上很简单,无非是增加一个伪类的问题,然而在
Canvas
中我们需要自己实现这个效果,也就是需要借助
MouseEvent
来手动处理这个过程。当然思路是比较简单的,我们只需要维护一个
boolean

id
标识来确定当前节点是否被
Hover
,然后根据选区状态来判断是否需要绘制当前节点的
Range
即可。

// packages/core/src/canvas/dom/element.ts
export class ElementNode extends Node {
  protected onMouseEnter = () => {
    this.isHovering = true;
    if (this.editor.selection.has(this.id)) {
      return void 0;
    }
    this.editor.canvas.mask.drawingEffect(this.range);
  };

  protected onMouseLeave = () => {
    this.isHovering = false;
    if (this.editor.selection.has(this.id)) {
      return void 0;
    }
    this.editor.canvas.mask.drawingEffect(this.range);
  };

  public drawingMask = (ctx: CanvasRenderingContext2D) => {
    if (
      this.isHovering &&
      !this.editor.selection.has(this.id) &&
      !this.editor.state.get(EDITOR_STATE.MOUSE_DOWN)
    ) {
      const { x, y, width, height } = this.range.rect();
      Shape.frame(ctx, {
        x: x,
        y: y,
        width: width,
        height: height,
        borderColor: BLUE_4,
      });
    }
  };
}

而事件的调度则是由
Root
节点来实现的,这里主要是维护了一个互斥的
hoverId
来实现的,当然这里的主要目的还是模拟
OnMouseEnter
以及
OnMouseLeave
事件。基本逻辑是遍历当前的节点,如果发现需要触发相关事件的节点,则判断鼠标是否在当前节点内,如果在节点内则作为命中的节点,判断当前
Hover
的节点如果与先前不一致,则根据具体的条件来判断并且触发先前的节点
MouseLeave
与当前节点
MouseEnter
事件。

// packages/core/src/canvas/state/root.ts
export class Root extends Node {
  /** Hover 节点 */
  public hover: ElementNode | ResizeNode | null;

  private onMouseMoveBasic = (e: globalThis.MouseEvent) => {
    // 非默认状态下不执行事件
    if (!this.engine.isDefaultMode()) return void 0;
    // 按事件顺序获取节点
    const flatNode = this.getFlatNode();
    let next: ElementNode | ResizeNode | null = null;
    const point = Point.from(e, this.editor);
    for (const node of flatNode) {
      // 当前只有`ElementNode`和`ResizeNode`需要触发`Mouse Enter/Leave`事件
      const authorize = node instanceof ElementNode || node instanceof ResizeNode;
      if (authorize && node.range.include(point)) {
        next = node;
        break;
      }
    }
    // 如果命中的节点与先前 Hover 的节点不一致
    if (this.hover !== next) {
      const prev = this.hover;
      this.hover = next;
      if (prev !== null) {
        this.emit(prev, NODE_EVENT.MOUSE_LEAVE, MouseEvent.from(e, this.editor));
        if (prev instanceof ElementNode) {
          this.editor.event.trigger(EDITOR_EVENT.HOVER_LEAVE, { node: prev });
        }
      }
      if (next !== null) {
        this.emit(next, NODE_EVENT.MOUSE_ENTER, MouseEvent.from(e, this.editor));
        if (next instanceof ElementNode) {
          this.editor.event.trigger(EDITOR_EVENT.HOVER_ENTER, { node: next });
        }
      }
    }
  };
}

紧接着我们就来聊一聊选区节点的拖拽移动问题,关于这部分能力的实现我们将其作为了
SelectNode
的一部分实现。对于拖拽这件事本身来说,我们只需要关注
MouseDown
绑定事件、
MouseMove
移动、
MouseUp
取消绑定事件,那么这里我们同样也是类似的实现,只不过由于我们需要考虑节点的绘制,因此需要在其中穿插着图形的
drawing
方法调用。在这里我们采用了最方便的按需绘制方案,即所有拖拽过的区域都重新绘制,当然最好的方案还是当前事件触发区域的重绘,这样性能会更好一些,且在这里我们只绘制拖拽的边框而不是将所有节点都拖拽着绘制。此外,在这里我们还实现了交互上的优化,即只有拖拽超过一定的阈值才会触发拖拽事件,这样可以避免误操作。

// packages/core/src/canvas/dom/select.ts
export class SelectNode extends Node {

  private onMouseDownController = (e: globalThis.MouseEvent) => {
    // 非默认状态下不执行事件
    if (!this.editor.canvas.isDefaultMode()) return void 0;
    // 取消已有事件绑定
    this.unbindDragEvents();
    const selection = this.editor.selection.get();
    // 选区 & 严格点击区域判定
    if (!selection || !this.isInSelectRange(Point.from(e, this.editor), this.range)) {
      return void 0;
    }
    this.dragged = selection;
    this.landing = Point.from(e.clientX, e.clientY);
    this.bindDragEvents();
    this.refer.onMouseDownController();
  };

  private onMouseMoveBasic = (e: globalThis.MouseEvent) => {
    const selection = this.editor.selection.get();
    if (!this.landing || !selection) return void 0;
    const point = Point.from(e.clientX, e.clientY);
    const { x, y } = this.landing.diff(point);
    // 超过阈值才认为正在触发拖拽
    if (!this._isDragging && (Math.abs(x) > SELECT_BIAS || Math.abs(y) > SELECT_BIAS)) {
      this._isDragging = true;
    }
    if (this._isDragging && selection) {
      const latest = selection.move(x, y);
      const zoomed = latest.zoom(RESIZE_OFS);
      // 重绘拖拽过的最大区域
      this.dragged = this.dragged ? this.dragged.compose(zoomed) : zoomed;
      this.editor.canvas.mask.drawingEffect(this.dragged);
      const offset = this.refer.onMouseMoveController(latest);
      this.setRange(offset ? latest.move(offset.x, offset.y) : latest);
    }
  };
  private onMouseMoveController = throttle(this.onMouseMoveBasic, ...THE_CONFIG);

  private onMouseUpController = () => {
    this.unbindDragEvents();
    this.refer.onMouseUpController();
    const selection = this.editor.selection.get();
    if (this._isDragging && selection) {
      const rect = this.range;
      const { startX, startY } = selection.flat();
      const ids = [...this.editor.selection.getActiveDeltaIds()];
      this.editor.state.apply(
        new Op(OP_TYPE.MOVE, { ids, x: rect.start.x - startX, y: rect.start.y - startY })
      );
      this.editor.selection.set(rect);
      this.dragged && this.editor.canvas.mask.drawingEffect(this.dragged);
    }
    this.landing = null;
    this.dragged = null;
    this._isDragging = false;
  };
}

最后

在这里我们就依然在轻量级
DOM
的基础上,讨论了
Canvas
中描边与填充的绘制问题,以及
inside stroke
的实现方式,然后我们实现了基本的选中绘制以及拖拽多选的交互设计,并且实现了
Hover
的效果,以及拖拽节点的移动。那么在后边我们可以聊一下
fillRule
规则设计、按需绘制图形节点,也可以聊到更多的交互设计,例如
Resize
的交互设计、参考线能力的实现、富文本的绘制方案等等。