Redian新闻
>
Pulumi实战 | 一款架构即代码的开源产品

Pulumi实战 | 一款架构即代码的开源产品

科技

新钛云服已累计为您分享741篇技术干货


本篇文章,主要介绍 Pulumi 是什么以及它的相关原理,并且使用它搭建一个 Nacos 和 SpringBoot 的环境!

一、Pulumi 诞生

(一)诞生原因

Pulumi 是一个架构即代码的开源产品,使用它即可在任何提供 SDK 或者 API 的云商平台,部署和使用容器、服务器以及基础架构等云资源。
它提供了多种主流的编程语言,让使用者可以通过自己最熟悉的编程语言,编写代码去控制云平台上面的各种云资源。而不是让初学者去学习那些繁琐难记的标签语言,降低了其入门的难度。

1、技术演化:

1)最初的云厂商仅仅只提供IaaS、PaaS这类云服务,并没有把它们组合起来;
IaaS: Infrastructure as a service,基础设施即服务。PaaS: Platform as a service, 平台即服务。SaaS: Software as a service, 软件即服务。
诞生一种方式:架构即代码/基础设施代码化,(Infrastructure As Code),即IaC。
结果:

亚马逊诞生了类似于云服务 CloudFormation 这类产品,这类产品可以使用一些简单的方法创建和管理一系列有关联的AWS的资源。
2)CloudFormation 只支持AWS,ROS只支持阿里云,但是用户可能会出于安全或者业务考虑,鸡蛋不想放在同一个篮子里,就会选择多个云商;

为了支持多个云商资源的创建和管理,诞生了开源产品 Terraform,该产品支持多个云商的SDK,使用标记语言的方式去创建和管理云资源。
缺点:上手难度大,用户需要额外去学习一种特定的标记语言 HCL (HashiCorp Configuration Language),对新入门同学不够友好。

2、Pulumi 登场

由此,诞生了我们的主角(Pulumi):
主要作用:
1)快速组合多类型云资源搭建用户业务;
2)满足用户多云商多区域容灾的业务需求;
3)如果使用多个云商,技术人员不需要熟练掌握两家甚至多家云厂商的技术与服务产品;
4)标记语言需要一定学习成本,技术人员只要会一种主流语言即可。

(二)Pulumi 组件
工作原理:
个人理解:
Pulumi 程序运行后,Language host 会把程序代码转换成 Pulumi 能理解的方式,然后传递给部署引擎。
部署引擎根据资源有无以及资源类型,对资源提供商发起相关操作!

1、组件(Cli端)

1)语言宿主(Language Hosts)
语言宿主负责运行一个 Pulumi 程序,并设置一个可以向部署引擎注册资源的环境。
a、语言执行器
Pulumi 用于启动程序所用语言(如Node或Python)的 Runtime (运行时),此二进制文件随Pulumi CLI一起分发。
名称类似:pulumi-language-<language-name>的二进制文件。

它是语言处理中枢,负责为您的开发语言准备好与之对应的环境。譬如:Python 3.7。


b、Runtime(运行时/语言运行器)
它会负责为您编写的程序做好运行准备,并在过程中监控程序的运行。

2)部署引擎(Deplayment  Engine)
部署引擎负责,计算将基础架构的当前状态驱动到程序表示的所需状态所需的一组操作。
当从语言宿主接收到资源注册时,引擎会查询现有状态以确定该资源之前是否已创建。
如果没有,引擎会使用资源提供者来创建它。
如果它已经存在,则引擎与资源提供者一起工作,通过将资源的旧状态与程序表示的资源的新期望状态进行比较来确定发生了什么变化(如果有的话)。
如果有更改,引擎会确定它是否可以就地更新资源,或者是否必须通过创建新版本并删除旧版本来替换它。
该决定取决于资源的哪些属性正在发生变化以及资源本身的类型。
当语言宿主与引擎通信它已完成 Pulumi 程序的执行时,引擎会查找任何它没有看到新资源注册的现有资源并安排这些资源以进行删除。
引擎已经被封装进pulumi cli,无需额外安装与部署。

3)资源提供商(云商)
a、资源插件
云商不同,插件不同。
b、SDK
云商相关 SDK。

2、组件(Service 端)

该组件主要保存 Pulumi 相关的 Project、Stack 等配置。
最新版 Pulumi 默认 Service 是 Pulumi 官方的 SAAS 界面:
1)官方 Service 端
https://app.pulumi.com/


2)其它存储方式充当 Service 端
当然,也可以用其它存储或者本地来保存这些配置!
如下面所示,我使用本地充当 Pulumi Service:
pulumi login file://D:\Lang-Python\Data\Pulumi-Service


二、为什么使用 Pulumi?

Pulumi 特点:多语言,多云商的服务支持。
但是,我个人觉得,这并不是我使用他的主要原因。
因为他的多语言和多云商的两个特点其实并没有让我感觉有多么便利。
多语言:各大云商也基本支持了多种主流编程语言的SDK。
多云商:由于云商支持的资源不同,其实 Pulumi 并不能做到一套代码走天下,在多个云商处复用。

我选它的因素:
它有一个资源状态的管理功能,该功能可以让你在操作资源时,省略了不少工作!
以及它的资源关联性,即一个资源的输出可以充当另一个资源的输入。
这两个特点也是与云商原生 SDK 最大的区别!
Pulumi 中处理资源之间的关联性,是通过其 output 机制实现的。

三、怎么使用Pulumi?

Pulumi 初体验

1、Pulumi 结构

2、安装 Pulumi

Pulumi支持多平台,包括Linux、Windows、MacOS等操作系统。
1)安装Pulumi需要预先安装Chocolatey包管理软件:
administrator方式打开PowerShell命令行:
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
参考:https://chocolatey.org/
2)choco安装 Pulumi
choco install pulumi

3、开始使用 Pulumi

1)创建 Pulumi 项目
pulumi new alicloud-python
如果该机器第一次使用 Pulumi 创建项目的时候,会弹出两个选项:
a)输入 Key;
b)浏览器登录 Pulumi 后端服务;
如果,选择“浏览器登录 Pulumi 后端服务”的选项,程序会触发浏览器打开 Pulumi 的 Web 在线登录界面。
我使用 GitHub 账号登录进去,里面是 Pulumi 的 Dashboard 。
该过程中,会同时让你创建 Project 以及 Stack!
2)编写自己的源代码,相关资源类型在其云商 SDK处查看



3)更新操作
pulumi up
该操作会把 stack 里面的资源进行创建或更新操作!
4)销毁操作
pulumi destroy
危险:该操作会销毁 stack 处的所有资源!


四、其它特点

1、即时性
部分云商对 Pulumi 支持度非常高,比如:Azure。
只要 Azure 上传新资源,Pulumi 基本当天就能使用该资源。
2、DevOps\CICD
Pulumi 自带 Automation API 组件,可以抛弃 CLI,使用代码包调用的方式,直接使用 Pulumi。
换言之,即程序中可以直接调用 Pulumi 程序。
3、转换器

目前支持这几种云商转换成 Pulumi 程序。

比如:Terraform 转换成 Pulumi!
https://www.pulumi.com/tf2pulumi/

五、搭建 Navos 和 SpringBoot 环境:

上面大致,讲解了 Pulumi 的基本原理和使用方式,下面给大家展示一下,通过 Pulumi 搭建一套环境的方案!

1、编写 Shell 脚本

编写脚本包括多个步骤:


如上图所示,搭建一个 Spring Boot 环境,需要许多前提依赖,比如 Java/Maven/Nacos等,相关代码,在后面。
下面是主要的配置文件,主要写明相关组件的安装路径,以及环境变量等参数。
其中的许多安装包,都是事先下载好的,都是组件官网安装包,读者请自行下载。
配置文件:
#!/bin/bash
export LC_ALL=en_US.UTF-8

# 当前目录
BASE_DIR=$(pwd)
# 环境变量保存目录
PROFILE_ENVS="/etc/profile.envs"

export BASE_DIR
export PROFILE_ENVS
# ****************************** JAVA CONFIG****************************** #
# Java 安装路径
JAVA_INSTALL="/usr/local/java11"

# Java 安装包
JAVA_FOLDER_NAME="jdk-11.0.16"
JAVA_PACKAGE="${JAVA_FOLDER_NAME}_linux-x64_bin.tar.gz"

# JAVA_HOME_PATH
JAVA_HOME_PATH="${JAVA_INSTALL}/${JAVA_FOLDER_NAME}"

export JAVA_INSTALL
export JAVA_FOLDER_NAME
export JAVA_PACKAGE
export JAVA_HOME_PATH

# ****************************** Maven CONFIG****************************** #
# Maven 安装路径
MAVEN_INSTALL="/usr/local/maven386"

# Maven 安装包
MAVEN_FOLDER_NAME="apache-maven-3.8.6"
MAVEN_PACKAGE="${MAVEN_FOLDER_NAME}-bin.tar.gz"

# MAVEN_HOME_PATH
MAVEN_HOME_PATH="${MAVEN_INSTALL}/${MAVEN_FOLDER_NAME}"

export MAVEN_INSTALL
export MAVEN_FOLDER_NAME
export MAVEN_PACKAGE
export MAVEN_HOME_PATH

# ****************************** Nacos CONFIG****************************** #
# Nacos 安装路径
NACOS_INSTALL="/usr/local/nacos211"

# Nacos 安装包
NACOS_FOLDER_NAME="nacos"
NACOS_PACKAGE="nacos-server-2.1.1.tar.gz"

# NACOS_HOME_PATH
NACOS_HOME_PATH="${NACOS_INSTALL}/${NACOS_FOLDER_NAME}"

export NACOS_INSTALL
export NACOS_FOLDER_NAME
export NACOS_PACKAGE
export NACOS_HOME_PATH

# ****************************** SpringBoot Boot CONFIG****************************** #
# SpringBoot Boot 工作台
SPRING_BOOT_WORKFLOW="/opt/boot_workflow"
SPRING_BOOT_GROUP_ID="com.gavin"
SPRING_BOOT_ARTIFACT_ID="na-boot"
SPRING_BOOT_VERSION="0.0.1-snapshot"

export SPRING_BOOT_WORKFLOW
export SPRING_BOOT_GROUP_ID
export SPRING_BOOT_ARTIFACT_ID
export SPRING_BOOT_VERSION
方法库:
add_dir() {
   dirs=$*
  log "mkdir -p ${dirs}"
   mkdir -p "${dirs}" >/dev/null 2>&1
   LAST_INFO=$?
   if [[ ${LAST_INFO} -eq 0 ]]; then
      ok "Folder \"${dirs}\" Create Success"
   else
      err "Folder \"${dirs}\" Create Failed"
   fi
}

1)安装 Java

function install_java() {
   cd "${BASE_DIR}" || return 1

   # 添加安装目录
  add_dir "${JAVA_INSTALL}"

   # 解压缩到安装目录
  tar -zxf "packages/${JAVA_PACKAGE}" -C "${JAVA_INSTALL}/"

   # 添加环境变量
   cat >"${PROFILE_ENVS}/java.sh" <<EOF
export JAVA_HOME=${JAVA_HOME_PATH}
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export PATH=${JAVA_HOME}/bin:$PATH
EOF
   echo "source ${PROFILE_ENVS}/java.sh" >>/etc/profile

   source /etc/profile

   # 添加软链接
   ln -s "${JAVA_HOME_PATH}/bin/java" /usr/bin/java
   ln -s "${JAVA_HOME_PATH}/bin/javac" /usr/bin/javac
}

2)安装 Maven

function install_maven() {
   cd "${BASE_DIR}" || return 1

   # 添加安装目录
  add_dir "${MAVEN_INSTALL}"

   # 解压缩到安装目录
  tar -zxf "packages/${MAVEN_PACKAGE}" -C "${MAVEN_INSTALL}/"

   # 新建仓库目录
  add_dir "${MAVEN_HOME_PATH}/repository"

   # 更换阿里云镜像并设置maven仓库位置
   cat >"${MAVEN_HOME_PATH}/conf/settings.xml" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
  <pluginGroups>
  </pluginGroups>
  <proxies>
  </proxies>
  <servers>
  </servers>
  <mirrors>
      <mirror>
          <id>alimaven</id>
          <name>aliyun maven</name>
          <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
          <mirrorOf>central</mirrorOf>
      </mirror>
  </mirrors>
  <localRepository>${MAVEN_HOME_PATH}/repository</localRepository>
  <profiles>
  </profiles>
</settings>
EOF

   # 添加环境变量
   cat >"${PROFILE_ENVS}/maven.sh" <<EOF
export MAVEN_HOME=${MAVEN_HOME_PATH}
export PATH=$PATH:$MAVEN_HOME/bin
EOF
   echo "source ${PROFILE_ENVS}/maven.sh" >>/etc/profile

   source /etc/profile

   # 添加软链接
   ln -s "${MAVEN_HOME_PATH}/bin/mvn" /usr/bin/mvn
}

3)安装 Navos

function start_nacos() {
   # 单机模式运行 nacos
   bash "${NACOS_HOME_PATH}/bin/startup.sh" -m standalone
}

function install_nacos() {
   cd "${BASE_DIR}" || return 1

   # 添加安装目录
  add_dir "${NACOS_INSTALL}"

   # 解压缩到安装目录
  tar -zxf "packages/${NACOS_PACKAGE}" -C "${NACOS_INSTALL}/"

   # 启动 nacos
  start_nacos
}

4)创建 Spring Boot

function create_spring_boot() {
   # 添加项目目录
  add_dir "${SPRING_BOOT_WORKFLOW}"

   # 进入项目目录
   cd "${SPRING_BOOT_WORKFLOW}" || return 1

   # maven 创建项目
   echo "y" | mvn archetype:generate -DgroupId="${SPRING_BOOT_GROUP_ID}" \
       -DartifactId="${SPRING_BOOT_ARTIFACT_ID}" \
       -DarchetypeArtifactId=maven-archetype-quickstart \
       -Dversion="${SPRING_BOOT_VERSION}"

   # 编译 SpringBoot Boot
  compile_spring_boot

   # 启动 SpringBoot Boot
  start_spring_boot
}

function compile_spring_boot() {
   cd "${SPRING_BOOT_WORKFLOW}/${SPRING_BOOT_ARTIFACT_ID}/" || return 1

  mvn compile
}

function start_spring_boot() {
   cd "${SPRING_BOOT_WORKFLOW}/${SPRING_BOOT_ARTIFACT_ID}/" || return 1

   # 启动
  nohup mvn spring-boot:run -Dspring-boot.run.profiles=prod &
}

5)主执行函数

#!/bin/bash
export LC_ALL=en_US.UTF-8
source ./config.sh
source ./functions.sh
source ./scripts/java.sh
source ./scripts/maven.sh
source ./scripts/nacos.sh
source ./scripts/spring_boot.sh

# 前置处理
pre_deal() {
  yum_install_pkg "rsync"
  yum_install_pkg "tree"
  yum_install_pkg "lsof"
  yum_install_pkg "lrzsz"
}

# 前置处理
pre_deal

# 添加 profile 环境文件夹
add_dir "${PROFILE_ENVS}"

# 安装 Java
install_java

# 安装 Maven
install_maven

# 安装 Nacos
install_nacos

# 创建 SpringBoot Boot 项目
create_spring_boot
6)替换 Java 文件
为了,测试说明,替换 Maven 生成的 Spring Boot 初始代码!
App.java
package com.gavin;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;

@EnableAutoConfiguration // 作用: 开启自动配置 初始化spring环境 springmvc环境
@ComponentScan // 作用: 用来扫描相关注解 扫描范围 当前入口类所在的包及子包(com.gavin及其子包)
public class App {
   public static void main(String[] args) {
       // springApplication: spring应用类   作用: 用来启动springboot应用
       // 参数1: 传入入口类 类对象   参数2: main函数的参数
       SpringApplication.run(App.class, args);
  }
}
创建 controller 文件夹:
helloController.java
package com.gavin.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class helloController {
   @GetMapping("/hello")
   public String hello() {
       System.out.println("Hello SpringBoot!!!");
       return "Hello SpringBoot";
  }
}

2、生成阿里云镜像


1)Packer介绍

传统模式下,我们制作镜像,都是在本地或者公司服务器上,安装好相应的软件,然后打成镜像文件,比较麻烦。
现在介绍一个直接在云商打包镜像的利器:Packer,它是与 Terrform 同一个公司的产品。
官网地址:
Packer by HashiCorp
它利用相关脚本,即可轻松制作线上镜像。

2)安装 Packer

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 packer
Windows
https://releases.hashicorp.com/packer/1.8.3/packer_1.8.3_windows_386.zip
https://releases.hashicorp.com/packer/1.8.3/packer_1.8.3_windows_amd64.zip

3)打包镜像脚本

把之前写的脚本压缩成一个文件,方便 Packer 执行!
SpringBoot.zip

4)创建打包镜像配置

spring_boot.json
{
 "variables": {
   "access_key": "ACCESS_KEY",
   "secret_key": "SECRET_KEY"
},
 "builders": [{
   "type":"alicloud-ecs",
   "access_key":"ACCESS_KEY",
   "secret_key":"SECRET_KEY",
   "region":"cn-hangzhou",
   "image_name":"packer_spring_boot_image",
   "source_image":"centos_7_03_64_20G_alibase_20170818.vhd",
   "ssh_username":"root",
   "instance_type":"ecs.n2.small",
   "internet_charge_type":"PayByTraffic",
   "io_optimized":"true"
}],
 "provisioners": [{
   "type": "file",
   "source": "SpringBoot.zip",
   "destination": "/tmp/"
  },{
   "type": "shell",
   "inline": [
     "sleep 30",
     "cd /tmp",
     "yum install -y unzip",
     "unzip SpringBoot.zip",
     "cd SpringBoot",
     "sudo chmod 755 main.sh",
     "./main.sh"
  ]
}]
}

5)执行镜像打包(Windows)

F:\PackerSoftware\packer\packer.exe validate spring_boot.json
F:\PackerSoftware\packer\packer.exe build spring_boot.json

3、编写 Pulumi 代码(Python)

下面就是真正用到 Pulumi 的地方了,我们编写,一系列 Python 代码,实现 Spring Boot 项目的完成。

1)创建 VPC

import pulumi_alicloud as alicloud

def create_network(pre_name):
   # VPC
   vpc_name = "{}_vpc".format(pre_name)
   vpc = alicloud.vpc.Network(vpc_name, cidr_block="172.16.0.0/12")

   return vpc

2)创建 Switch

import pulumi_alicloud as alicloud

def create_switch(pre_name, az, vpc):
   # 交换机
   vswitch_name = "{}_vswitch".format(pre_name)
   vswitch = alicloud.vpc.Switch(vswitch_name, zone_id=az, cidr_block="172.16.1.0/24", vpc_id=vpc.id)

   return vswitch

3)创建安全组以及安全规则

需要开放的安全组规则端口为:22、80。22 为 SSH 端口,80 为 Sprint Boot 写的简单 Demo 需要放开的 HTTP 端口号。
import pulumi
import pulumi_alicloud as alicloud

def create_security_group(pre_name, vpc):
   # 安全组
   sg_name = "{}_sg".format(pre_name)
   sg_description = "{} security groups".format(pre_name)
   sg = alicloud.ecs.SecurityGroup(sg_name, description=sg_description, vpc_id=vpc.id)

   return sg

def create_security_group_rule(pre_name, sg, port_range):
   # 安全组规则
   sg_rule_name = "{}_sg_rule".format(pre_name)
   sg_rule = alicloud.ecs.SecurityGroupRule(
       sg_rule_name,
       security_group_id=sg.id,
       ip_protocol="tcp",
       type="ingress",
       nic_type="intranet",
       port_range=port_range,
       cidr_ip="0.0.0.0/0"
  )

   return sg_rule

4)创建 ECS

镜像ID为上一步骤(生成阿里云镜像),生成的镜像的ID。
import pulumi
import pulumi_alicloud as alicloud

def create_instance(
       pre_name,
       availability_zone=None,
       vswitch=None,
       sg=None,
       password=None,
       user_data=None,
       instance_type=None,
       image_id=None
):
   # 实例
   sg_ids = [sg.id]
   instance_name = "{}-instance".format(pre_name)
   instance = alicloud.ecs.Instance(
       instance_name,
       availability_zone=availability_zone,
       instance_type=instance_type,
       security_groups=sg_ids,
       image_id=image_id,
       instance_name=instance_name,
       vswitch_id=vswitch.id,
       internet_max_bandwidth_out=10,
       password=password,
       user_data=user_data
  )

   pulumi.export("{}-IP".format(instance_name), instance.public_ip)

   return instance

4、创建实例

1) pulumi up

pulumi up

2)查看结果

打开浏览器,输入“实例IP/test/hello”,即可看到 Spring Boot 返回的内容!

3) pulumi destroy

如果该实例不想用了,直接销毁即可!

5、总结



六、后记

综上,本文简单介绍了 Pulumi 的基本原理以及简单用法。主要是为了起抛砖引玉的作用,个人认为 Pulumi 的好处和用法还有待探索。希望这篇文章能给读者一定的帮助,谢谢!


    推荐阅读   



    了解新钛云服   


微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
【5.24折扣】Lulumemon 6折!Myvitamins半价!理肤泉买3赠1!【本周折扣汇总】Unineed/资生堂/Lululemon半价!Selfridges 2折起!Myprotein 3折!Lululemon老板进不了前8!加拿大最富有的人排行!竟有2个是华人!是如何赚钱的?中产女性人手一条?瑜伽裤卖疯了,动辄上千元!lululemon中国营收3年翻番!CEO:花30亿收购这个公司是“罕见的失误”Lululemon折扣区持续火爆上新!日常必备,人人都是帕梅拉!如何用DCF+DDM模型给Lululemon估值? 在线等, 挺急!就是牛!Lululemon被指定为BC省重点项目! 招聘员工直接拿身份了?【周末得吃好的】卤牛腱+芝麻烧饼+酸辣汤+杏仁蛋糕音乐人朱婧汐Akini Jing 分享新歌《Pump Up》加入88rising 全新单曲Pump Up活力上线奥斯卡上被扇耳光的红人狂喷Lululemon!网友:$100瑜伽裤,鄙视穷人罢了加拿大政府给Lululemon开小灶?拿Offer就给工作签证?lululemon的折扣款瑜伽裤都替你找全了!买就趁现在!戴森和lululemon,在中国遇到了狠人Prompt 即代码:设计和管理 AI 编程的最佳实践云计算开源产业联盟:2023年中国企业开源治理全景观察报告【雨水】心雨细无声· 写在老爸“七七”之日IRS代码 826、846、570 是什么?2022 年纳税记录中常用代码的含义lululemon、adidas都5折起!速冲!“山寨”围剿LululemonLululemon的千元瑜伽裤,到底是不是智商税“中央公园五人”冤狱事件,那是川普对美国的第一次尖叫早报 | lululemon热度下滑 业绩增长或放缓,董洁小红书直播GMV破3000万;欧莱雅否认欧珑在中国撤柜传闻开源产品测评之SQL上线能力官宣! Lululemon指定为BC省重点项目! 外籍员工直接拿身份?冬季周末的一大享受新款加入!lululemon精选时尚运动服饰3.9折起+包邮!上千元的Lululemon,是中产鄙视链顶端?草书《兰亭序》真的!穿旧Lululemon也能退钱!加拿大小哥教你怎么做!lululemon崛起后,怎么没人尊重维秘了【3.16折扣】哈利波特魔法音乐会全英巡演!Lululemon精选半价!营销吸引大量黄牛被斥“low爆” lululemon仍将在中国大举开店坐拥多个TOP级开源项目,不搞“竞争性开源”,蚂蚁在玩一种很新的开源海澜之家官宣张颂文;钟薛高推出3.5元雪糕;lululemonQ4净营收同比增长30%…| 刀法品牌热讯太上头了!7款Lululemon“高仿”平替,你要的爆款Amazon都有!
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。