diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml new file mode 100644 index 0000000..b27a744 --- /dev/null +++ b/.github/workflows/docker-build-push.yml @@ -0,0 +1,95 @@ +name: Docker Build + +on: + push: + branches: + - "main" + tags: + - "v*.*.*" + +env: + IMAGE_NAME: rss3/xchar + REGION_ID: us-east-1 + DEV_ACK_CLUSTER_ID: cd1d0ffc40b5242b39ddda1864e71e30d + PROD_ACK_CLUSTER_ID: cfc647c22fd6848b5a602ad4d7470632b + +jobs: + build: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.meta.outputs.version }} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: docker meta + id: meta + uses: docker/metadata-action@v3 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=sha + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Login to DockerHub + if: github.event_name != 'pull_request' + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + file: Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + deploy-dev: + runs-on: ubuntu-latest + needs: [build] + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set K8s context + uses: aliyun/ack-set-context@v1 + with: + access-key-id: "${{ secrets.ACCESS_KEY_ID }}" + access-key-secret: "${{ secrets.ACCESS_KEY_SECRET }}" + cluster-id: "${{ env.DEV_ACK_CLUSTER_ID }}" + - name: Install Tools + run: | + wget https://github.com/mikefarah/yq/releases/download/v4.25.1/yq_linux_amd64.tar.gz -O - | tar xz && mv yq_linux_amd64 /usr/local/bin/yq + curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.22.10/bin/linux/amd64/kubectl && chmod +x kubectl && mv kubectl /usr/local/bin/kubectl + - uses: sljeff/secrets2env@main + with: + secrets-json: ${{ toJson(secrets) }} + - env: + IMAGE_TAG_RELEASE: ${{ env.IMAGE_NAME }}:${{ needs.build.outputs.version }} + run: | + sh apply.sh deploy/dev/* + + deploy-prod: + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + needs: [build, deploy-dev] + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set K8s context + uses: aliyun/ack-set-context@v1 + with: + access-key-id: "${{ secrets.ACCESS_KEY_ID }}" + access-key-secret: "${{ secrets.ACCESS_KEY_SECRET }}" + cluster-id: "${{ env.PROD_ACK_CLUSTER_ID }}" + - name: Install Tools + run: | + wget https://github.com/mikefarah/yq/releases/download/v4.25.1/yq_linux_amd64.tar.gz -O - | tar xz && mv yq_linux_amd64 /usr/local/bin/yq + curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.22.10/bin/linux/amd64/kubectl && chmod +x kubectl && mv kubectl /usr/local/bin/kubectl + - uses: sljeff/secrets2env@main + with: + secrets-json: ${{ toJson(secrets) }} + - env: + IMAGE_TAG_RELEASE: ${{ env.IMAGE_NAME }}:${{ needs.build.outputs.version }} + run: | + sh apply.sh deploy/prod/* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9bedd2e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +##### BASE +FROM node:18-bullseye-slim as base + +# RUN apt-get update || : && apt-get install python3 build-essential git -y + +RUN npm i -g pnpm + +##### DEPS +FROM base as deps + +WORKDIR /app + +ADD package.json pnpm-lock.yaml* ./ + +RUN pnpm i + +##### BUILD +FROM deps as build + +WORKDIR /app + +COPY . . +COPY --from=deps /app/node_modules ./node_modules + +ENV NEXT_TELEMETRY_DISABLED 1 +ENV NODE_ENV production +ENV BUILD_STEP=1 +RUN pnpm build + +##### FINAL +FROM base + +ENV NODE_ENV=production +WORKDIR /app + +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/.next ./.next +COPY --from=build /app/public ./public +COPY --from=build /app/package.json ./package.json +COPY --from=build /app/next.config.js ./next.config.js + +CMD ["pnpm", "start"] diff --git a/apply.sh b/apply.sh new file mode 100644 index 0000000..fef4f3d --- /dev/null +++ b/apply.sh @@ -0,0 +1,27 @@ +set -e # exit on error +set -x # show command before execute + +apply_one() { + export uri="\$uri" # escape + envsubst < $1 > temp.yaml && mv temp.yaml $1 + + kubectl apply -f $1 +} + +apply() { + for filename in $@; do + apply_one $filename + done + + # rollout status / annotation + for filename in $@; do + yaml_kind=$(yq '.kind' $filename) + if [ "$yaml_kind" = "Deployment" ]; then + ns=$(yq '.metadata.namespace' $filename) + name=$(yq '.metadata.name' $filename) + kubectl -n $ns rollout status -w deploy.apps/$name + fi + done +} + +apply $@ diff --git a/deploy/dev/deploy.yaml b/deploy/dev/deploy.yaml new file mode 100644 index 0000000..6424dac --- /dev/null +++ b/deploy/dev/deploy.yaml @@ -0,0 +1,53 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: xchar + namespace: crossbell +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 5 + selector: + matchLabels: + app: xchar + tier: api + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: + app: xchar + tier: api + spec: + containers: + - image: $IMAGE_TAG_RELEASE + imagePullPolicy: Always + name: xchar + envFrom: + - secretRef: + name: xchar + ports: + - containerPort: 3000 + protocol: TCP + resources: + requests: + memory: "100Mi" + cpu: "80m" + limits: + memory: "200Mi" + cpu: "200m" + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + livenessProbe: + tcpSocket: + port: 3000 + initialDelaySeconds: 40 + periodSeconds: 20 + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 diff --git a/deploy/dev/route.yaml b/deploy/dev/route.yaml new file mode 100644 index 0000000..2e4b964 --- /dev/null +++ b/deploy/dev/route.yaml @@ -0,0 +1,14 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: xchar + namespace: crossbell +spec: + entryPoints: + - web + routes: + - kind: Rule + match: "Host(`$XCHAR_DOMAIN_DEV`)" + services: + - name: xchar + port: 3000 diff --git a/deploy/dev/secrets.yaml b/deploy/dev/secrets.yaml new file mode 100644 index 0000000..a79f618 --- /dev/null +++ b/deploy/dev/secrets.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +stringData: + REDIS_URL: $REDIS_URL_DEV +kind: Secret +metadata: + name: xchar + namespace: crossbell +type: Opaque diff --git a/deploy/dev/svc.yaml b/deploy/dev/svc.yaml new file mode 100644 index 0000000..3cd110c --- /dev/null +++ b/deploy/dev/svc.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: xchar + namespace: crossbell +spec: + type: ClusterIP + selector: + app: xchar + tier: api + ports: + - name: http + protocol: TCP + port: 3000 + targetPort: 3000 diff --git a/deploy/prod/deploy.yaml b/deploy/prod/deploy.yaml new file mode 100644 index 0000000..4f5a864 --- /dev/null +++ b/deploy/prod/deploy.yaml @@ -0,0 +1,53 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: xchar + namespace: crossbell +spec: + progressDeadlineSeconds: 600 + replicas: 3 + revisionHistoryLimit: 5 + selector: + matchLabels: + app: xchar + tier: api + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: + app: xchar + tier: api + spec: + containers: + - image: $IMAGE_TAG_RELEASE + imagePullPolicy: Always + name: xchar + envFrom: + - secretRef: + name: xchar + ports: + - containerPort: 3000 + protocol: TCP + resources: + requests: + memory: "100Mi" + cpu: "80m" + limits: + memory: "200Mi" + cpu: "200m" + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + livenessProbe: + tcpSocket: + port: 3000 + initialDelaySeconds: 40 + periodSeconds: 20 + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 diff --git a/deploy/prod/route.yaml b/deploy/prod/route.yaml new file mode 100644 index 0000000..2c3935c --- /dev/null +++ b/deploy/prod/route.yaml @@ -0,0 +1,14 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: xchar + namespace: crossbell +spec: + entryPoints: + - web + routes: + - kind: Rule + match: "Host(`$XCHAR_DOMAIN`)" + services: + - name: xchar + port: 3000 diff --git a/deploy/prod/secrets.yaml b/deploy/prod/secrets.yaml new file mode 100644 index 0000000..7f43cfb --- /dev/null +++ b/deploy/prod/secrets.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +stringData: + REDIS_URL: $REDIS_URL +kind: Secret +metadata: + name: xchar + namespace: crossbell +type: Opaque diff --git a/deploy/prod/svc.yaml b/deploy/prod/svc.yaml new file mode 100644 index 0000000..3cd110c --- /dev/null +++ b/deploy/prod/svc.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: xchar + namespace: crossbell +spec: + type: ClusterIP + selector: + app: xchar + tier: api + ports: + - name: http + protocol: TCP + port: 3000 + targetPort: 3000