Last active
November 23, 2023 21:40
-
-
Save mikesparr/4dda2243ba9ee8e40dfbfe703b56392c to your computer and use it in GitHub Desktop.
Experiment migrating Vercel hosted NextJS app to Google Cloud Platform atop Cloud Run
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
##################################################################### | |
# REFERENCES | |
# - https://nextjs.org/learn/dashboard-app/getting-started | |
# - https://github.com/vercel/next.js/tree/canary/examples/with-docker | |
# - https://cloud.google.com/run/docs/quickstarts/build-and-deploy/deploy-nodejs-service | |
# - https://cloud.google.com/run/docs/configuring/services/environment-variables | |
# - https://cloud.google.com/run/docs/securing/service-identity | |
# - https://cloud.google.com/sdk/gcloud/reference/run/deploy | |
# - https://cloud.google.com/sdk/gcloud/reference/iam/service-accounts/create | |
# - https://cloud.google.com/secret-manager/docs/add-secret-version | |
# - https://cloud.google.com/secret-manager/docs/manage-access-to-secrets | |
##################################################################### | |
export PROJECT_ID=$(gcloud config get-value project) | |
export PROJECT_USER=$(gcloud config get-value core/account) # set current user | |
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)") | |
export IDNS=${PROJECT_ID}.svc.id.goog # workflow identity domain | |
export GCP_REGION="us-west1" # CHANGEME (OPT) | |
export GCP_ZONE="us-west1-b" # CHANGEME (OPT) | |
export NETWORK_NAME="default" | |
# enable apis | |
gcloud services enable compute.googleapis.com \ | |
storage.googleapis.com \ | |
run.googleapis.com \ | |
cloudbuild.googleapis.com \ | |
secretmanager.googleapis.com \ | |
artifactregistry.googleapis.com | |
# configure gcloud sdk | |
gcloud config set compute/region $GCP_REGION | |
gcloud config set compute/zone $GCP_ZONE | |
############################################################ | |
# Create NextJS 14 App | |
# - online tutorial (https://nextjs.org/learn/dashboard-app/getting-started) | |
# - build locally, setup storage and hosting on Vercel | |
############################################################ | |
export SERVICE_NAME="nextjs-dashboard" # will use later for naming service | |
export STARTER_URL="https://github.com/vercel/next-learn/tree/main/dashboard/starter-example" | |
# create app, then follow tutorial | |
npx create-next-app@latest nextjs-dashboard --use-npm --example $STARTER_URL | |
cd $SERVICE_NAME # build it here | |
############################################################ | |
# Service Account | |
# - run each application with unique service account | |
# - grant only permissions needed following least privileges | |
############################################################ | |
export SA_NAME="$SERVICE_NAME-sa" | |
export SA_EMAIL="$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" | |
# create service account to run app | |
gcloud iam service-accounts create $SA_NAME \ | |
--display-name="$SA_NAME" | |
############################################################ | |
# Secrets | |
# - replace sensitive env vars from Vercel app with secrets | |
############################################################ | |
export SECRET_PG_URL="postgres-url" | |
export SECRET_PG_PRISMA_URL="postgres-prisma-url" | |
export SECRET_PG_URL_NON_POOL="postgres-url-non-pooling" | |
export SECRET_PG_PASSWORD="postgres-password" | |
export SECRET_AUTH_SECRET="auth-secret" | |
export SECRET_VERSION="latest" | |
# source .env file but break out keys and secrets (could use --env-vars-file) | |
source .env | |
echo "POSTGRES_HOST=$POSTGRES_HOST" # verify | |
# create secrets in secret manager | |
gcloud secrets create $SECRET_PG_URL --replication-policy="automatic" | |
gcloud secrets create $SECRET_PG_PRISMA_URL --replication-policy="automatic" | |
gcloud secrets create $SECRET_PG_URL_NON_POOL --replication-policy="automatic" | |
gcloud secrets create $SECRET_PG_PASSWORD --replication-policy="automatic" | |
gcloud secrets create $SECRET_AUTH_SECRET --replication-policy="automatic" | |
# add secret versions (saving the sensitive values from local .env) | |
echo -n "$POSTGRES_URL" | \ | |
gcloud secrets versions add $SECRET_PG_URL --data-file=- | |
echo -n "$POSTGRES_PRISMA_URL" | \ | |
gcloud secrets versions add $SECRET_PG_PRISMA_URL --data-file=- | |
echo -n "$POSTGRES_URL_NON_POOLING" | \ | |
gcloud secrets versions add $SECRET_PG_URL_NON_POOL --data-file=- | |
echo -n "$POSTGRES_PASSWORD" | \ | |
gcloud secrets versions add $SECRET_PG_PASSWORD --data-file=- | |
echo -n "$AUTH_SECRET" | \ | |
gcloud secrets versions add $SECRET_AUTH_SECRET --data-file=- | |
# add IAM policy bindings to allow service account to access secrets | |
gcloud secrets add-iam-policy-binding $SECRET_PG_URL \ | |
--member="serviceAccount:$SA_EMAIL" \ | |
--role="roles/secretmanager.secretAccessor" | |
gcloud secrets add-iam-policy-binding $SECRET_PG_PRISMA_URL \ | |
--member="serviceAccount:$SA_EMAIL" \ | |
--role="roles/secretmanager.secretAccessor" | |
gcloud secrets add-iam-policy-binding $SECRET_PG_URL_NON_POOL \ | |
--member="serviceAccount:$SA_EMAIL" \ | |
--role="roles/secretmanager.secretAccessor" | |
gcloud secrets add-iam-policy-binding $SECRET_PG_PASSWORD \ | |
--member="serviceAccount:$SA_EMAIL" \ | |
--role="roles/secretmanager.secretAccessor" | |
gcloud secrets add-iam-policy-binding $SECRET_AUTH_SECRET \ | |
--member="serviceAccount:$SA_EMAIL" \ | |
--role="roles/secretmanager.secretAccessor" | |
############################################################ | |
# Containerize application | |
# - create artifact registry | |
# - create Dockerfile | |
# - build and push image to registry | |
############################################################ | |
export REPO_NAME="mike-test-repo" | |
export IMAGE_NAME="$SERVICE_NAME" | |
export TAG_NAME="v0.0.1" | |
export IMAGE_URL=${GCP_REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/${IMAGE_NAME}:${TAG_NAME} | |
# build docker image and push to registry | |
gcloud artifacts repositories create $REPO_NAME \ | |
--repository-format=docker \ | |
--location=$GCP_REGION \ | |
--description="Docker repository" | |
# configure auth | |
gcloud auth configure-docker ${GCP_REGION}-docker.pkg.dev | |
# create Dockerfile | |
cat > Dockerfile << EOF | |
FROM node:18-alpine AS base | |
# Install dependencies only when needed | |
FROM base AS deps | |
RUN apk add --no-cache --update libc6-compat python3 make g++ | |
WORKDIR /app | |
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ | |
RUN npm ci | |
# Rebuild the source code only when needed | |
FROM base AS builder | |
WORKDIR /app | |
COPY --from=deps /app/node_modules ./node_modules | |
COPY . . | |
ENV NEXT_TELEMETRY_DISABLED 1 | |
RUN npm run build | |
# Production image, copy all the files and run next | |
FROM base AS runner | |
WORKDIR /app | |
ENV NODE_ENV production | |
ENV NEXT_TELEMETRY_DISABLED 1 | |
RUN addgroup --system --gid 1001 nodejs | |
RUN adduser --system --uid 1001 nextjs | |
COPY --from=builder /app/public ./public | |
# Set the correct permission for prerender cache | |
RUN mkdir .next | |
RUN chown nextjs:nodejs .next | |
# Automatically leverage output traces to reduce image size | |
# https://nextjs.org/docs/advanced-features/output-file-tracing | |
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ | |
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static | |
USER nextjs | |
EXPOSE 3000 | |
ENV PORT 3000 | |
ENV HOSTNAME "0.0.0.0" | |
CMD ["node", "server.js"] | |
EOF | |
# create .dockerignore | |
cat > .dockerignore << EOF | |
Dockerfile | |
.dockerignore | |
node_modules | |
npm-debug.log | |
README.md | |
.next | |
.git | |
EOF | |
# build and push image to registry (platform flag for Mac compatibility) | |
docker build -t $IMAGE_URL --platform linux/amd64 . | |
docker push $IMAGE_URL | |
############################################################ | |
# Deploy service to Cloud Run | |
# - replace sensitive env vars from Vercel app with secrets | |
############################################################ | |
export SERVICE_PORT="3000" | |
export SERVICE_PLATFORM="managed" | |
# create cloud run service (with env vars and secrets) | |
gcloud run deploy $SERVICE_NAME \ | |
--region $GCP_REGION \ | |
--platform $SERVICE_PLATFORM \ | |
--image $IMAGE_URL \ | |
--port $SERVICE_PORT \ | |
--allow-unauthenticated \ | |
--session-affinity \ | |
--service-account $SA_EMAIL \ | |
--set-env-vars=POSTGRES_USER=$POSTGRES_USER \ | |
--set-env-vars=POSTGRES_HOST=$POSTGRES_HOST \ | |
--set-env-vars=POSTGRES_DATABASE=$POSTGRES_DATABASE \ | |
--set-env-vars=AUTH_URL=$AUTH_URL \ | |
--update-secrets=POSTGRES_URL=$SECRET_PG_URL:$SECRET_VERSION \ | |
--update-secrets=POSTGRES_PRISMA_URL=$SECRET_PG_PRISMA_URL:$SECRET_VERSION \ | |
--update-secrets=POSTGRES_URL_NON_POOLING=$SECRET_PG_URL_NON_POOL:$SECRET_VERSION \ | |
--update-secrets=POSTGRES_PASSWORD=$SECRET_PG_PASSWORD:$SECRET_VERSION \ | |
--update-secrets=AUTH_PASSWORD=$SECRET_AUTH_SECRET:$SECRET_VERSION | |
# get service URL and update AUTH_URL (localhost:3000/api/auth will fail) | |
export SERVICE_URL=$(gcloud run services describe $SERVICE_NAME --region $GCP_REGION --format="value(status.address.url)") | |
gcloud run services update $SERVICE_NAME \ | |
--region $GCP_REGION \ | |
--set-env-vars=AUTH_URL="$SERVICE_URL/api/auth" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment