This article will show you how to run the back end, front end, and gateway using Docker containers, and ultimately DockerCompose for container orchestration.

Technology stack

The front end

  • React
  • Ant Design

The back-end

  • Go
  • Iris

The gateway

  • Nginx
  • OpenResty
  • Lua
  • Enterprise WeChat

Back-end build API

Although we’ve written EXPOSE 4182 here, which is only for testing, production environments don’t actually EXPOSE the back-end ports, but instead access each other through a network of containers, and eventually use Nginx for forwarding.

FROM Golang: 1.15.5

LABEL maintainer="K8sCat <[email protected]>"

EXPOSE 4182

ENV GOPROXY=https://goproxy.cn,direct \
    GO111MODULE=on

WORKDIR /go/src/github.com/k8scat/containerized-app/api

COPY . .

RUN go mod download && \
go build -o api main.go && \
chmod +x api

ENTRYPOINT [ "./api" ]
Copy the code

Front-end Building the Web

It is worth mentioning here that because the front end will definitely call the back end interface and the address of the interface will change according to the deployment, we use the ARG directive to set the back end interface address. Instead of changing the code, just pass –build-arg REACT_APP_BASE_URL=https://example.com/api when building the image to adjust the address of the backend interface.

One more thing that you will notice is that Entrypoint and CMD are used at the same time, so that you can adjust the port of the front end at runtime, but there is no need to do that because Nginx is ultimately used for forwarding.

FROM node:lts

LABEL maintainer="K8sCat <[email protected]>"

WORKDIR /web

COPY . .

ARG REACT_APP_BASE_URL

RUN npm config set registry https://registry.npm.taobao.org && \
npm install && \
npm run build && \
npm install -g serve

ENTRYPOINT [ "serve"."-s"."build" ]
CMD [ "-l"."3214" ]
Copy the code

Gateway Construction Gateway

Nginx configuration

Here we set the upstream of the back end and the front end respectively, and then set the location rule for forwarding.

Here are a few points to make:

  • throughset_by_luaGets the container’s environment variables, which are finally set at run timeenvironmentSetting these environment variables gives you more flexibility
  • server_nameUsing the$hostnameAt runtime, the container needs to be sethostname
  • ssl_certificatessl_certificate_keyCannot use variable Settings
  • loadinggateway.luaScript to achieve enterprise wechat gateway authentication
upstream web { server ca-web:3214; } upstream api { server ca-api:4182; } server { set_by_lua $corp_id 'return os.getenv("CORP_ID")'; set_by_lua $agent_id 'return os.getenv("AGENT_ID")'; set_by_lua $secret 'return os.getenv("SECRET")'; set_by_lua $callback_host 'return os.getenv("CALLBACK_HOST")'; set_by_lua $callback_schema 'return os.getenv("CALLBACK_SCHEMA")'; set_by_lua $callback_uri 'return os.getenv("CALLBACK_URI")'; set_by_lua $logout_uri 'return os.getenv("LOGOUT_URI")'; set_by_lua $token_expires 'return os.getenv("TOKEN_EXPIRES")'; set_by_lua $use_secure_cookie 'return os.getenv("USE_SECURE_COOKIE")'; listen 443 ssl http2; server_name $hostname; Resolver 8.8.8.8; ssl_certificate /certs/cert.crt; ssl_certificate_key /certs/cert.key; ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m; Ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers AESGCM:HIGH:! aNULL:! MD5; ssl_prefer_server_ciphers on; lua_ssl_verify_depth 2; lua_ssl_trusted_certificate /etc/pki/tls/certs/ca-bundle.crt; if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})T(\d{2})") { set $year $1; set $month $2; set $day $3; } access_log logs/access_$year$month$day.log main; error_log logs/error.log; access_by_lua_file "/usr/local/openresty/nginx/conf/gateway.lua"; location ^~ /gateway { root html; index index.html index.htm; } location ^~ /api { proxy_pass http://api; proxy_read_timeout 3600; Proxy_http_version 1.1; proxy_set_header X_FORWARDED_PROTO https; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_set_header Connection ""; } location ^~ / { proxy_pass http://web; proxy_read_timeout 3600; Proxy_http_version 1.1; proxy_set_header X_FORWARDED_PROTO https; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_set_header Connection ""; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } server { listen 80; server_name $hostname; location / { rewrite ^/(.*) https://$server_name/$1 redirect; }}Copy the code

Dockerfile

FROM Openresty/openresty: 1.19.3.1 - centos

LABEL maintainer="K8sCat <[email protected]>"

COPY gateway.conf /etc/nginx/conf.d/gateway.conf
COPY gateway.lua /usr/local/openresty/nginx/conf/gateway.lua
COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf

# Install lua-resty-http
RUN /usr/local/openresty/luajit/bin/luarocks install lua-resty-http
Copy the code

Lua implements gateway authentication based on enterprise wechat

Some of these configuration parameters are set by fetching Nginx variables.

local json = require("cjson")
local http = require("resty.http")

local uri = ngx.var.uri
local uri_args = ngx.req.get_uri_args()
local scheme = ngx.var.scheme

local corp_id = ngx.var.corp_id
local agent_id = ngx.var.agent_id
local secret = ngx.var.secret
local callback_scheme = ngx.var.callback_scheme or scheme
local callback_host = ngx.var.callback_host
local callback_uri = ngx.var.callback_uri
local use_secure_cookie = ngx.var.use_secure_cookie == "true" or false
local callback_url = callback_scheme .. ": / /". callback_host .. callback_urilocal redirect_url = callback_scheme .. ": / /". callback_host .. ngx.var.request_urilocal logout_uri = ngx.var.logout_uri or "/logout"
local token_expires = ngx.var.token_expires or "7200"
token_expires = tonumber(token_expires)

local function request_access_token(code)
    local request = http.new()
    request:set_timeout(7000)
    local res, err = request:request_uri("https://qyapi.weixin.qq.com/cgi-bin/gettoken", {
        method = "GET",
        query = {
            corpid = corp_id,
            corpsecret = secret,
        },
        ssl_verify = true,})if not res then
        return nil, (err or "access token request failed: ". (error "unknown reason"))
    end
    if res.status~ =200 then
        return nil."received ". res.status." from https://qyapi.weixin.qq.com/cgi-bin/gettoken: ". res.bodyend
    local data = json.decode(res.body)
    if data["errcode"] ~ =0 then
        return nil, data["errmsg"]
    else
        return data["access_token"]
    end
end

local function request_user(access_token, code)
    local request = http.new()
    request:set_timeout(7000)
    local res, err = request:request_uri("https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo", {
        method = "GET",
        query = {
            access_token = access_token,
            code = code,
        },
        ssl_verify = true,})if not res then
        return nil."get profile request failed: ". (error "unknown reason")
    end
    if res.status~ =200 then
        return nil."received ". res.status." from https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo"
    end
    local userinfo = json.decode(res.body)
    if userinfo["errcode"] = =0 then
        if userinfo["UserId"] then
            res, err = request:request_uri("https://qyapi.weixin.qq.com/cgi-bin/user/get", {
                method = "GET",
                query = {
                    access_token = access_token,
                    userid = userinfo["UserId"],
                },
                ssl_verify = true,})if not res then
                return nil."get user request failed: ". (error "unknown reason")
            end
            if res.status~ =200 then
                return nil."received ". res.status." from https://qyapi.weixin.qq.com/cgi-bin/user/get"
            end
            local user = json.decode(res.body)
            if user["errcode"] = =0 then
                return user
            else
                return nil, user["errmsg"]
            end
        else
            return nil."UserId not exists"
        end
    else
        return nil, userinfo["errmsg"]
    end
end

local function is_authorized(a)
    local headers = ngx.req.get_headers()
    local expires = tonumber(ngx.var.cookie_OauthExpires) or 0
    local user_id = ngx.unescape_uri(ngx.var.cookie_OauthUserID or "")
    local token = ngx.var.cookie_OauthAccessToken or ""
    if expires == 0 and headers["OauthExpires"] then
        expires = tonumber(headers["OauthExpires"])
    end
    if user_id:len() = =0 and headers["OauthUserID"] then
        user_id = headers["OauthUserID"]
    end
    if token:len() = =0 and headers["OauthAccessToken"] then
        token = headers["OauthAccessToken"]
    end
    local expect_token = callback_host .. user_id .. expires
    if token == expect_token and expires then
        if expires > ngx.time(a)then
            return true
        else
            return false
        end
    else
        return false
    end
end

local function redirect_to_auth(a)
    return ngx.redirect("https://open.work.weixin.qq.com/wwopen/sso/qrConnect?". ngx.encode_args({ appid = corp_id, agentid = agent_id, redirect_uri = callback_url, state = redirect_url }))end

local function authorize(a)
    if uri ~= callback_uri then
        return redirect_to_auth()
    end
    local code = uri_args["code"]
    if not code then
        ngx.log(ngx.ERR, "not received code from https://open.work.weixin.qq.com/wwopen/sso/qrConnect")
        return ngx.exit(ngx.HTTP_FORBIDDEN)
    end

    local access_token, request_access_token_err = request_access_token(code)
    if not access_token then
        ngx.log(ngx.ERR, "got error during access token request: ". request_access_token_err)return ngx.exit(ngx.HTTP_FORBIDDEN)
    end

    local user, request_user_err = request_user(access_token, code)
    if not user then
        ngx.log(ngx.ERR, "got error during profile request: ". request_user_err)return ngx.exit(ngx.HTTP_FORBIDDEN)
    end
    ngx.log(ngx.ERR, "user id: ". user["userid"])

    local expires = ngx.time() + token_expires
    local cookie_tail = "; version=1; path=/; Max-Age=". expiresif use_secure_cookie then
        cookie_tail = cookie_tail .. "; secure"
    end

    local user_id = user["userid"]
    local user_token = callback_host .. user_id .. expires

    ngx.header["Set-Cookie"] = {
        "OauthUserID=". ngx.escape_uri(user_id) .. cookie_tail,"OauthAccessToken=". ngx.escape_uri(user_token) .. cookie_tail,"OauthExpires=". expires .. cookie_tail, }return ngx.redirect(uri_args["state"])
end

local function handle_logout(a)
    if uri == logout_uri then
        ngx.header["Set-Cookie"] = "OauthAccessToken==deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
        --return ngx.redirect("/")
    end
end

handle_logout()
if (not is_authorized()) then
    authorize()
end
Copy the code

Use DockerCompose for container orchestration

Here are a few points:

  • Set front-endargsThe back-end interface address can be passed in at front-end build time
  • Gateway settinghostnameThe gateway container can be sethostname
  • Gateway settingenvironmentRelevant configurations can be passed in
  • Ultimately, only the gateway layer exposes ports at runtime
version: "3.8"

services:
  api:
    build: ./api
    image: ca-api:latest
    container_name: ca-api

  web:
    build:
      context: ./web
      args:
        REACT_APP_BASE_URL: https://example.com/api
    image: ca-web:latest
    container_name: ca-web
    
  gateway:
    build: ./gateway
    image: ca-gateway:latest
    hostname: example.com
    volumes:
      - ./gateway/certs/fullchain.pem:/certs/cert.crt
      - ./gateway/certs/privkey.pem:/certs/cert.key
    ports:
      - 80: 80
      - 443: 443
    environment:
      - CORP_ID=
      - AGENT_ID=
      - SECRET=
      - CALLBACK_HOST=example.com
      - CALLBACK_SCHEMA=https
      - CALLBACK_URI=/gateway/oauth_wechat
      - LOGOUT_URI=/gateway/oauth_logout
      - TOKEN_EXPIRES=7200
      - USE_SECURE_COOKIE=true
    container_name: ca-gateway
Copy the code

Open source code

  • GitHub
  • Gitee

Original link: k8scat.com/posts/conta…