Nacos 解决 laravel 多环境下配置切换

前言#

对于应用程序运行的环境来说,不同的环境有不同的配置通常是很有用的。例如,你可能希望在本地使用的缓存驱动不同于生产服务器所使用的缓存驱动。

痛点#

  1. .env 配置不能区分多环境 (开发,测试,生产)
  2. .env 配置共享太麻烦 (团队局域网环境)
  3. 配置不能实时管理,增删改配置
  4. 自动化部署配置 .env 文件过于繁琐

Nacos 简介#

Nacos 是阿里巴巴最新开源的项目,核心定位是 “一个更易于帮助构建云原生应用的动态服务发现、配置和服务管理平台”,项目地址:nacos.io/zh-cn/

应用#

这里主要使用了 Nacos 的配置管理,并没有使用到动态服务等功能。原理也很简单,通过接口直接修改 .env 文件。Nacos 服务可以直接使用使用阿里云提供的 应用配置管理,无须安装。链接如下: acmnext.console.aliyun.com/

代码#

<?php

namespace App\Console\Commands;

use GuzzleHttp\Client;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Validator;

class NacosTools extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'nacos {action?}';

    private $accessKey;
    private $secretKey;
    private $endpoint = 'acm.aliyun.com';
    private $namespace;
    private $dataId;
    private $group;
    private $port = 8080;
    private $client;

    private $serverUrl;

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Nacos 管理工具';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     * @throws \Exception
     */
    public function handle()
    {
        $this->accessKey = env('NACOS_ACCESS_KEY');
        $this->secretKey = env('NACOS_SECRET_KEY');
        $this->endpoint = env('NACOS_ENDPOINT');
        $this->namespace = env('NACOS_NAMESPACE');
        $this->port = env('NACOS_PORT', $this->port);
        $this->dataId = env('NACOS_DATA_ID');
        $this->group = env('NACOS_GROUP');

        if (!$this->validate()) {
            $this->error('请检查配置参数');

            return;
        }

        $this->client = new Client(['verify' => false]);

        $this->info('Nacos 配置工具');

        $actions = [
            '获取配置',
            '发布配置',
            '删除配置',
        ];

        if (is_null($this->argument('action'))) {
            $action = $this->choice('请选择操作',
                $actions,
                $actions[0]);
        } else {
            if (in_array($this->argument('action'), array_keys($actions))) {
                $action = $actions[$this->argument('action')];
            } else {
                $action = $this->choice('请选择操作',
                    $actions,
                    $actions[0]);
            }
        }

        $this->do($action);
    }

    public function do($action = '获取配置')
    {
        switch ($action) {
            default:
            case '获取配置':
                $config = $this->getConfig();

                if ($config) {
                    file_put_contents('.env', $config);
                    $this->info('获取配置成功');
                } else {
                    $this->error('获取配置失败');
                }

                break;
            case '发布配置':
                if ($this->publishConfig()) {
                    $this->info('发布配置成功');
                } else {
                    $this->error('发布配置失败');
                }

                break;

            case '删除配置':
                if ($this->removeConfig()) {
                    $this->info('删除配置成功');
                } else {
                    $this->error('删除配置失败');
                }

                break;
        }
    }

    /**
     * 验证配置参数
     *
     * Date: 2020/6/10
     * @return bool
     */
    private function validate()
    {
        $data = [
            'accessKey' => $this->accessKey,
            'secretKey' => $this->secretKey,
            'endpoint'  => $this->endpoint,
            'namespace' => $this->namespace,
            'dataId'    => $this->dataId,
            'group'     => $this->group,
        ];

        $rules = [
            'accessKey' => 'required',
            'secretKey' => 'required',
            'endpoint'  => 'required',
            'namespace' => 'required',
            'dataId'    => 'required',
            'group'     => 'required',
        ];

        $messages = [
            'accessKey.required' => '请填写`.env`配置 NACOS_ACCESS_KEY',
            'secretKey.required' => '请填写`.env`配置 NACOS_SECRET_KEY',
            'endpoint.required'  => '请填写`.env`配置 NACOS_ENDPOINT',
            'namespace.required' => '请填写`.env`配置 NACOS_NAMESPACE',
            'dataId.required'    => '请填写`.env`配置 NACOS_DATA_ID',
            'group.required'     => '请填写`.env`配置 NACOS_GROUP',
        ];

        $validator = Validator::make($data, $rules, $messages);

        if ($validator->fails()) {
            foreach ($validator->getMessageBag()->toArray() as $item) {
                foreach ($item as $value) {
                    $this->error($value);
                }
            }

            return false;
        }

        return true;
    }

    /**
     * 获取配置
     *
     * Date: 2020/6/10
     * @return bool
     */
    private function getConfig()
    {
        $acmHost = str_replace(['host', 'port'], [$this->getServer(), $this->port],
            'http://host:port/diamond-server/config.co');

        $query = [
            'dataId' => urlencode($this->dataId),
            'group'  => urlencode($this->group),
            'tenant' => urlencode($this->namespace),
        ];

        $headers = $this->getHeaders();

        $response = $this->client->get($acmHost, [
            'headers' => $headers,
            'query'   => $query,
        ]);

        if ($response->getReasonPhrase() == 'OK') {
            return $response->getBody()->getContents();
        } else {
            return false;
        }
    }

    /**
     * 发布配置
     *
     * Date: 2020/6/10
     * @return bool
     */
    public function publishConfig()
    {
        $acmHost = str_replace(
            ['host', 'port'],
            [$this->getServer(), $this->port],
            'http://host:port/diamond-server/basestone.do?method=syncUpdateAll');

        $headers = $this->getHeaders();

        $formParams = [
            'dataId'  => urlencode($this->dataId),
            'group'   => urlencode($this->group),
            'tenant'  => urlencode($this->namespace),
            'content' => file_get_contents('.env'),
        ];

        $response = $this->client->post($acmHost, [
            'headers'     => $headers,
            'form_params' => $formParams,
        ]);

        $result = json_decode($response->getBody()->getContents(), 1);

        return $result['message'] == 'OK';
    }

    public function removeConfig()
    {
        $acmHost = str_replace(['host', 'port'], [$this->getServer(), $this->port],
            'http://host:port/diamond-server//datum.do?method=deleteAllDatums');

        $headers = $this->getHeaders();

        $formParams = [
            'dataId' => urlencode($this->dataId),
            'group'  => urlencode($this->group),
            'tenant' => urlencode($this->namespace),
        ];

        $response = $this->client->post($acmHost, [
            'headers'     => $headers,
            'form_params' => $formParams,
        ]);

        $result = json_decode($response->getBody()->getContents(), 1);

        return $result['message'] == 'OK';
    }

    /**
     * 获取配置服务器地址
     *
     * Date: 2020/6/10
     * @return string
     */
    private function getServer()
    {
        if ($this->serverUrl) {
            return $this->serverUrl;
        }

        $serverHost = str_replace(
            ['host', 'port'],
            [$this->endpoint, $this->port],
            'http://host:port/diamond-server/diamond');

        $response = $this->client->get($serverHost);

        return $this->serverUrl = rtrim($response->getBody()->getContents(), PHP_EOL);
    }

    /**
     * 获取请求头
     *
     * Date: 2020/6/10
     * @return array
     */
    private function getHeaders()
    {
        $headers = [
            'Diamond-Client-AppName' => 'ACM-SDK-PHP',
            'Client-Version'         => '0.0.1',
            'Content-Type'           => 'application/x-www-form-urlencoded; charset=utf-8',
            'exConfigInfo'           => 'true',
            'Spas-AccessKey'         => $this->accessKey,
            'timeStamp'              => round(microtime(true) * 1000),
        ];

        $headers['Spas-Signature'] = $this->getSign($headers['timeStamp']);

        return $headers;
    }

    /**
     * 获取签名
     *
     * @param $timeStamp
     * Date: 2020/6/10
     * @return string
     */
    private function getSign($timeStamp)
    {
        $signStr = $this->namespace.'+';

        if (is_string($this->group)) {
            $signStr .= $this->group."+";
        }

        $signStr = $signStr.$timeStamp;

        return base64_encode(hash_hmac(
            'sha1',
            $signStr,
            $this->secretKey,
            true
        ));
    }
}

使用示例#

  1. 注册账号,开通服务这些就不说了
  2. .env 添加配置项 NACOS_ACCESS_KEY NACOS_SECRET_KEY
  3. php artisan nacos 0 获取配置
  4. php artisan nacos 1 发布配置
  5. php artisan nacos 2 删除配置

配置项说明#

NACOS_ENDPOINT= #nacos节点 如使用阿里云服务 即:acm.aliyun.com
NACOS_DATA_ID= #项目ID 可以填项目名
NACOS_GROUP= #分组ID 这里可以用于区分环境 建议 local production test 等值
NACOS_NAMESPACE= # 命名空间 建议用来区分服务器 server-A server-B
NACOS_ACCESS_KEY= #阿里云access_key 建议使用子账号access_key
NACOS_SECRET_KEY= #阿里云secret_key 建议使用子账号secret_key

总结#

使用 nacos 后,再也不用担心 .env.example 忘记加配置项,共享配置也不是件麻烦事了,自动部署也不需要频繁的改动配置了。

本作品采用《CC 协议》,转载必须注明作者和本文链接
Be the one you want to be.
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 12

看不明白

4年前 评论
未定义 (楼主) 4年前

其实没有觉得特别方便,现在 laravel 都可以完全 docker 开发,版本不是问题,

本地开发环境参数都是不同的 container 的,无所谓分享。

4年前 评论
未定义 (楼主) 4年前

这不就是配置中心… 硬塞就是硬塞
就目前单体应用,我推荐 Envault

4年前 评论
未定义 (楼主) 4年前

能作为注册中心吗?

3年前 评论
未定义 (楼主) 3年前

请教:那为什么不用阿里云的 ACM 呢?

3年前 评论
未定义 (楼主) 3年前

nacos 是微服务管理用的,laravel 是运行在 fpm 模式的,可能不太合适,如果用在 cli 模式下比较好,现在已经有其他框架实现了。感觉扯得有点远了。你这个其实写的很好的,只是用 laravel 的大多数用不到,另外建议可以封装成插件。

2年前 评论
leirhy

搞 php 的,应该是不会用到 nacos 的。.env 没有什么不好的,多一个依赖就多一个攻击面,Nacos 比较容易被黑,而且还多了一块运维的工作

1年前 评论