SaltyCrane: devopshttps://www.saltycrane.com/blog/2021-04-13T13:06:02-07:00Buildtime vs runtime environment variables with Next.js and Docker
2021-04-13T13:06:02-07:00https://www.saltycrane.com/blog/2021/04/buildtime-vs-runtime-environment-variables-nextjs-docker/<p>
For a Next.js app, <strong>buildtime</strong> environment variables are
variables that are used when the <code>next build</code> command runs.
<strong>Runtime</strong> variables are variables used when the
<code>next start</code> command runs.
</p>
<p>
Below are ways to set buildtime and rutime environment variables with Docker
and ways to use buildtime and runtime environment variables with Next.js. Note
the <code>Dockerfile</code> is written for simplicity to illustrate the
examples. For a more optimized Next.js Docker build see my
<a href="/blog/2021/04/nextjs-gitlab-cicd-docker-multi-stage-example/"
>Docker multi-stage CI example</a
>.
</p>
<h4 id="methods-for-setting-environment-variables-with-docker">
Methods for setting environment variables with Docker
</h4>
<table>
<thead>
<tr>
<th align="left">Method</th>
<th align="center">Available at buildtime</th>
<th align="center">Available at runtime</th>
<th align="center">Value passed to <code>docker build</code></th>
<th align="center">Value passed to <code>docker run</code></th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><code>ARG</code></td>
<td align="center">✔</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="left">
<a
href="#setting-static-environment-variables-for-buildtime-and-runtime"
><code>ENV</code></a
>
</td>
<td align="center">✔</td>
<td align="center">✔</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="left">
<a href="#setting-dynamic-buildtime-environment-variables"
><code>ARG</code> + <code>docker build --build-arg</code></a
>
</td>
<td align="center">✔</td>
<td align="center"></td>
<td align="center">✔</td>
<td align="center"></td>
</tr>
<tr>
<td align="left">
<a
href="#setting-dynamic-buildtime-environment-variables-that-are-available-at-runtime-also"
><code>ARG</code> + <code>ENV</code> +
<code>docker build --build-arg</code></a
>
</td>
<td align="center">✔</td>
<td align="center">✔</td>
<td align="center">✔</td>
<td align="center"></td>
</tr>
<tr>
<td align="left">
<a href="#setting-dynamic-runtime-environment-variables"
><code>docker run --env</code></a
>
</td>
<td align="center"></td>
<td align="center">✔</td>
<td align="center"></td>
<td align="center">✔</td>
</tr>
</tbody>
</table>
<h4 id="methods-for-using-environment-variables-in-nextjs">
Methods for using environment variables in Next.js
</h4>
<table>
<thead>
<tr>
<th align="left">Method</th>
<th align="left">Set at</th>
<th align="center">
Available in Next.js client side rendered code (browser)
</th>
<th align="center">Available in Next.js server side rendered code</th>
<th align="center">Available in Node.js</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">
<a href="https://nextjs.org/docs/basic-features/environment-variables"
><code>.env</code> files</a
>
</td>
<td align="left">?both?</td>
<td align="center"></td>
<td align="center">✔</td>
<td align="center"></td>
<td>
<code>process.env</code> cannot be destructured or accessed with dynamic
properties
</td>
</tr>
<tr>
<td align="left">
<a
href="https://nextjs.org/docs/basic-features/environment-variables#exposing-environment-variables-to-the-browser"
><code>NEXT_PUBLIC_</code> prefixed vars in <code>.env</code> files</a
>
</td>
<td align="left">buildtime</td>
<td align="center">✔</td>
<td align="center">✔</td>
<td align="center"></td>
<td>
<code>process.env</code> cannot be destructured or accessed with dynamic
properties
</td>
</tr>
<tr>
<td align="left">
<a href="#using-buildtime-environment-variables"
><code>env</code> in <code>next.config.js</code></a
>
</td>
<td align="left">buildtime</td>
<td align="center">✔</td>
<td align="center">✔</td>
<td align="center"></td>
<td>
<code>process.env</code> cannot be destructured or accessed with dynamic
properties
</td>
</tr>
<tr>
<td align="left">
<a
href="#using-runtime-environment-variables-client-side-or-server-side"
><code>publicRuntimeConfig</code></a
>
</td>
<td align="left">runtime</td>
<td align="center">✔</td>
<td align="center">✔</td>
<td align="center"></td>
<td>Requires page uses SSR</td>
</tr>
<tr>
<td align="left">
<a href="#using-runtime-environment-variables-server-side-only"
><code>serverRuntimeConfig</code></a
>
</td>
<td align="left">runtime</td>
<td align="center"></td>
<td align="center">✔</td>
<td align="center"></td>
<td></td>
</tr>
<tr>
<td align="left">
<a
href="#using-runtime-environment-variables-server-side-not-processed-by-nextjs"
><code>process.env</code></a
>
</td>
<td align="left">runtime</td>
<td align="center"></td>
<td align="center"></td>
<td align="center">✔</td>
<td></td>
</tr>
</tbody>
</table>
<h4 id="assume-this-packagejson-for-the-examples-below">
Assume this <code>package.json</code> for the examples below
</h4>
<pre><code class="language-json">{
"scripts": {
"build": "next build",
"dev": "next",
"start": "next start"
},
"dependencies": {
"next": "^10.0.9",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}
</code></pre>
<h4 id="setting-static-environment-variables-for-buildtime-and-runtime">
Setting static environment variables for buildtime and runtime
</h4>
<p>
Environment variables can be specified with the <code>ENV</code> instruction
in a <code>Dockerfile</code>. Below <code>MY_VAR</code> will be available to
both <code>next build</code> and <code>next start</code>. For more information
see
<a href="https://docs.docker.com/engine/reference/builder/#env"
>https://docs.docker.com/engine/reference/builder/#env</a
>
</p>
<p>
<strong><code>Dockerfile</code></strong>
</p>
<pre><code class="language-Dockerfile">FROM node:14-alpine
ENV MY_VAR=cake
WORKDIR /app
COPY . ./
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
</code></pre>
<p><strong>Docker build</strong></p>
<pre><code class="language-sh">docker build -t mytag .
</code></pre>
<p><strong>Docker run</strong></p>
<pre><code class="language-sh">docker run mytag
</code></pre>
<h4 id="setting-dynamic-buildtime-environment-variables">
Setting dynamic buildtime environment variables
</h4>
<p>
Dynamic environment variables can be passed to the
<code>docker build</code> command using <code>--build-arg</code> and used in
the <code>Dockerfile</code> with the <code>ARG</code> statement. Below
<code>MY_VAR</code> is an environment variable available to
<code>next build</code>.
</p>
<p>
Note that <code>MY_VAR</code> is not available to <code>next start</code>.
<code>ARG</code> statements act like <code>ENV</code> statements in that they
are treated like environment variables during <code>docker build</code>, but
they are not persisted in the image. To make them available during
<code>docker run</code> (and <code>next start</code>) set the value using
<code>ENV</code> (see the next example).
</p>
<p>
For more information see
<a href="https://docs.docker.com/engine/reference/builder/#arg"
>https://docs.docker.com/engine/reference/builder/#arg</a
>
</p>
<p>
<strong><code>Dockerfile</code></strong>
</p>
<pre><code>FROM node:14-alpine
ARG MY_VAR
WORKDIR /app
COPY . ./
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
</code></pre>
<p><strong>Docker build</strong></p>
<pre><code>docker build --build-arg MY_VAR=cake -t mytag .
</code></pre>
<p><strong>Docker run</strong></p>
<pre><code>docker run mytag
</code></pre>
<h4
id="setting-dynamic-buildtime-environment-variables-that-are-available-at-runtime-also"
>
Setting dynamic buildtime environment variables that are available at runtime
also
</h4>
<p>
The variable in the previous example, set using <code>ARG</code>, is not
persisted in the Docker image so it is not available at runtime. To make it
available at runtime, copy the value from <code>ARG</code> to
<code>ENV</code>.
</p>
<p>
<strong><code>Dockerfile</code></strong>
</p>
<pre><code>FROM node:14-alpine
ARG MY_VAR
ENV MY_VAR=$MYVAR
WORKDIR /app
COPY . ./
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
</code></pre>
<p><strong>Docker build</strong></p>
<pre><code>docker build --build-arg MY_VAR=cake -t mytag .
</code></pre>
<p><strong>Docker run</strong></p>
<pre><code>docker run mytag
</code></pre>
<h4 id="setting-dynamic-runtime-environment-variables">
Setting dynamic runtime environment variables
</h4>
<p>
Dynamic environment variables can be passed to <code>docker run</code> using
the <code>--env</code> flag. These will not be available to
<code>next build</code> but they will be available to <code>next start</code>.
For more information see
<a
href="https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file"
>https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file</a
>
</p>
<p>
<strong><code>Dockerfile</code></strong>
</p>
<pre><code>FROM node:14-alpine
WORKDIR /app
COPY . ./
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
</code></pre>
<p><strong>Docker build</strong></p>
<pre><code>docker build -t mytag .
</code></pre>
<p><strong>Docker run</strong></p>
<pre><code>docker run --env MY_VAR=cake mytag
</code></pre>
<h4 id="using-buildtime-environment-variables">
Using buildtime environment variables
</h4>
<p>
To use buildtime environment variables in Next.js code, set them using
<code>env</code> in <code>next.config.js</code>. Then access them via
<code>process.env</code> in your app code. NOTE:
<code>process.env</code> cannot be destructured or used with dynamic property
access. Next.js does a string substituion at build time using the webpack
<a href="https://webpack.js.org/plugins/define-plugin/">DefinePlugin</a>. For
more information see
<a
href="https://nextjs.org/docs/api-reference/next.config.js/environment-variables"
>https://nextjs.org/docs/api-reference/next.config.js/environment-variables</a
>
</p>
<p>
<strong><code>next.config.js</code></strong>
</p>
<pre><code>module.exports = {
env: {
MY_VAR: process.env.MY_VAR
}
}
</code></pre>
<p>
<strong><code>my-app-file.js</code></strong>
</p>
<pre><code>console.log(process.env.MY_VAR)
</code></pre>
<h4 id="using-runtime-environment-variables-client-side-or-server-side">
Using runtime environment variables (client-side or server-side)
</h4>
<p>
To use runtime environment variables (client-side or server-side), set them
using <code>publicRuntimeConfig</code> in <code>next.config.js</code>. Then
access them using <code>getConfig</code> from <code>next/config</code>. NOTE:
this only works for Next.js pages where server-side rendering (SSR) is used.
i.e. the page must use
<a
href="https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering"
><code>getServerSideProps</code></a
>
or
<a href="https://nextjs.org/docs/api-reference/data-fetching/getInitialProps"
><code>getInitialProps</code></a
>. For more information see
<a
href="https://nextjs.org/docs/api-reference/next.config.js/runtime-configuration"
>https://nextjs.org/docs/api-reference/next.config.js/runtime-configuration</a
>
</p>
<p>
<strong><code>next.config.js</code></strong>
</p>
<pre><code>module.exports = {
publicRuntimeConfig: {
MY_VAR: process.env.MY_VAR
}
}
</code></pre>
<p>
<strong><code>my-app-file.js</code></strong>
</p>
<pre><code>import getConfig from "next/config";
const { publicRuntimeConfig } = getConfig();
console.log(publicRuntimeConfig.MY_VAR)
</code></pre>
<h4 id="using-runtime-environment-variables-server-side-only">
Using runtime environment variables (server-side only)
</h4>
<p>
To use runtime environment variables (server-side only), set them using
<code>serverRuntimeConfig</code> in <code>next.config.js</code>. Then access
them using <code>getConfig</code> from <code>next/config</code>. For more
information see
<a
href="https://nextjs.org/docs/api-reference/next.config.js/runtime-configuration"
>https://nextjs.org/docs/api-reference/next.config.js/runtime-configuration</a
>
</p>
<p>
NOTE: this applies to to files Next.js "builds". Server run files
not processed by Next.js can use <code>process.env</code> to access
environment variables. See below.
</p>
<p>
<strong><code>next.config.js</code></strong>
</p>
<pre><code>module.exports = {
serverRuntimeConfig: {
MY_VAR: process.env.MY_VAR
}
}
</code></pre>
<p>
<strong><code>my-app-file.js</code></strong>
</p>
<pre><code>import getConfig from "next/config";
const { serverRuntimeConfig } = getConfig();
console.log(serverRuntimeConfig.MY_VAR)
</code></pre>
<h4
id="using-runtime-environment-variables-server-side-not-processed-by-nextjs"
>
Using runtime environment variables server-side (not processed by Next.js)
</h4>
<p>
For files not processed by Next.js (<code>next build</code>) (e.g. a
<code>server.js</code> file run by <code>node</code>), runtime environment
variables can be accessed on the server via <code>process.env</code>. NOTE:
"runtime" variables here means variables used when the Node.js
process runs. For more information see
<a
href="https://nodejs.org/docs/latest-v14.x/api/process.html#process_process_env"
>https://nodejs.org/docs/latest-v14.x/api/process.html#process_process_env</a
>
</p>
<p>
<strong><code>server.js</code></strong>
</p>
<pre><code>console.log(process.env.MY_VAR)
</code></pre>
<h4 id="nextjs-assetprefix">Next.js <code>assetPrefix</code></h4>
<p>
If the
<a
href="https://nextjs.org/docs/api-reference/next.config.js/cdn-support-with-asset-prefix"
>Next.js <code>assetPrefix</code></a
>
is set in <code>next.config.js</code> using an environment variable, the
environment variable should be set at buildtime for
<a
href="https://nextjs.org/docs/advanced-features/automatic-static-optimization"
>Next.js static pages</a
>
but set at runtime for
<a href="https://nextjs.org/docs/basic-features/pages#server-side-rendering"
>server rendered pages</a
>.
</p>
<p>
<strong><code>next.config.js</code></strong>
</p>
<pre><code>module.exports = {
assetPrefix: process.env.MY_ASSET_PREFIX
}
</code></pre>
Next.js GitLab CI/CD Docker multi-stage example
2021-04-06T09:59:46-07:00https://www.saltycrane.com/blog/2021/04/nextjs-gitlab-cicd-docker-multi-stage-example/<p>
This describes an example <a href="https://nextjs.org/">Next.js</a> project
with a <a href="https://docs.gitlab.com/ee/ci/">GitLab CI/CD</a> pipeline that
does the following:
</p>
<ul>
<li>installs npm packages and builds static assets</li>
<li>runs ESLint, TypeScript, and Cypress</li>
<li>builds a Docker image for deployment</li>
<li>
pushes the Docker image to the
<a href="https://docs.gitlab.com/ee/user/packages/container_registry/"
>GitLab Container Registry</a
>
</li>
</ul>
<p>
This example prepares a Docker image for deployment but doesn't actually
deploy it. See
<a
href="/blog/2021/03/example-nextjs-gitlab-cicd-amazon-ecr-and-ecs-deploy-pipeline/"
>an example CI/CD pipeline that deploys to Amazon ECS</a
>.
</p>
<p>
To increase speed and reduce image size, it uses
<a href="https://docs.docker.com/develop/develop-images/multistage-build/"
>Docker multi-stage builds</a
>.
</p>
<ul>
<li>
GitLab repo:
<a
href="https://gitlab.com/saltycrane/next-docker-multi-stage-gitlab-ci-cd-example"
>https://gitlab.com/saltycrane/next-docker-multi-stage-gitlab-ci-cd-example</a
>
</li>
<li>
CI/CD Pipelines:
<a
href="https://gitlab.com/saltycrane/next-docker-multi-stage-gitlab-ci-cd-example/-/pipelines"
>https://gitlab.com/saltycrane/next-docker-multi-stage-gitlab-ci-cd-example/-/pipelines</a
>
</li>
</ul>
<h4 id="dockerfile"><code>Dockerfile</code></h4>
<p>The Dockerfile defines 3 stages:</p>
<ul>
<li>
the "builder" stage installs npm packages and builds static
assets. It produces artifacts (<code>/app</code> and
<code>/root/.cache</code>) that are used by the cypress and deploy stages.
It is also used to build an image used to run ESLint and TypeScript.
</li>
<li>
the "cypress" stage uses a different base image from the
"builder" stage and is used to run cypress tests
</li>
<li>
the final deploy stage copies the <code>/app</code> directory from the
"builder" stage and sets <code>NODE_ENV</code> to
"production" and exposes port 3000
</li>
</ul>
<pre><code>ARG BASE_IMAGE=node:14.16-alpine
# ================================================================
# builder stage
# ================================================================
FROM $BASE_IMAGE as builder
ENV NODE_ENV=test
ENV NEXT_TELEMETRY_DISABLED=1
RUN apk add --no-cache bash git
WORKDIR /app
COPY ./package.json ./
COPY ./package-lock.json ./
RUN CI=true npm ci
COPY . ./
RUN NODE_ENV=production npm run build
# ================================================================
# cypress stage
# ================================================================
FROM cypress/base:14.16.0 as cypress
WORKDIR /app
# copy cypress from the builder image
COPY --from=builder /root/.cache /root/.cache/
COPY --from=builder /app ./
ENV NODE_ENV=test
ENV NEXT_TELEMETRY_DISABLED=1
# ================================================================
# final deploy stage
# ================================================================
FROM $BASE_IMAGE
WORKDIR /app
COPY --from=builder /app ./
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
EXPOSE 3000
CMD ["npm", "start"]
</code></pre>
<h4 id="gitlab-ciyml"><code>.gitlab-ci.yml</code></h4>
<ul>
<li>
3 images are built: <code>test</code>, <code>cypress</code>, and
<code>deploy</code>. The <code>test</code> image is used for running ESLint
and TypeScript and is needed for <code>cypress</code> and
<code>deploy</code>. The <code>cypress</code> image is used for running
Cypress.
</li>
<li>
it uses
<a href="https://docs.docker.com/develop/develop-images/build_enhancements/"
>Docker BuildKit</a
>
to make caching easier. (With BuildKit, cached layers will be automatically
pulled when needed. Without BuildKit, images used for caching need to be
explicitly pulled.) For comparison, see
<a
href="https://gitlab.com/saltycrane/next-docker-multi-stage-gitlab-ci-cd-example/-/commit/6b238a34a8bdeb310d17fcbaa83227ba2eb78d92"
>this diff adding BuildKit</a
>. Note <code>DOCKER_BUILDKIT</code> is set to <code>1</code> to enable
BuildKit.
</li>
</ul>
<pre><code>variables:
# enable docker buildkit. Used with `BUILDKIT_INLINE_CACHE=1` below
DOCKER_BUILDKIT: 1
DOCKER_TLS_CERTDIR: "/certs"
IMAGE_TEST: $CI_REGISTRY_IMAGE/test:latest
IMAGE_CYPRESS: $CI_REGISTRY_IMAGE/cypress:latest
IMAGE_DEPLOY: $CI_REGISTRY_IMAGE/deploy:latest
stages:
- build
- misc
- deploy
.base:
image: docker:latest
services:
- docker:dind
before_script:
- docker --version
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
build:builder:
extends: .base
stage: build
script:
- docker build --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from "$IMAGE_TEST" --target builder -t "$IMAGE_TEST" .
- docker push "$IMAGE_TEST"
build:deployimage:
extends: .base
stage: misc
needs: ["build:builder"]
script:
- docker build --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from "$IMAGE_DEPLOY" --cache-from "$IMAGE_TEST" --cache-from "$IMAGE_CYPRESS" -t "$IMAGE_DEPLOY" .
- docker push "$IMAGE_DEPLOY"
test:cypress:
extends: .base
stage: misc
needs: ["build:builder"]
script:
- docker build --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from "$IMAGE_CYPRESS" --cache-from "$IMAGE_TEST" --target cypress -t "$IMAGE_CYPRESS" .
- docker push "$IMAGE_CYPRESS"
- docker run "$IMAGE_CYPRESS" npm run cy:citest
test:eslint:
extends: .base
stage: misc
needs: ["build:builder"]
script:
- docker run "$IMAGE_TEST" npm run eslint
test:typescript:
extends: .base
stage: misc
needs: ["build:builder"]
script:
- docker run "$IMAGE_TEST" npm run tsc
deploy:
stage: deploy
needs: ["build:deployimage", "test:cypress", "test:eslint", "test:typescript"]
script:
- echo "deploy here"
</code></pre>
<h4 id="dockerignore"><code>.dockerignore</code></h4>
<p>
Adding the <code>.git</code> directory to <code>.dockerignore</code> prevented
cache invalidation for the <code>COPY . ./</code> command in the
<code>Dockerfile</code>.
</p>
<pre><code>.git
</code></pre>
<h4 id="references">References</h4>
<ul>
<li>
<a href="https://docs.docker.com/develop/develop-images/multistage-build/"
>https://docs.docker.com/develop/develop-images/multistage-build/</a
>
</li>
<li>
<a
href="https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#making-docker-in-docker-builds-faster-with-docker-layer-caching"
>https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#making-docker-in-docker-builds-faster-with-docker-layer-caching</a
>
</li>
<li>
<a href="https://docs.gitlab.com/ee/ci/directed_acyclic_graph/index.html"
>https://docs.gitlab.com/ee/ci/directed_acyclic_graph/index.html</a
>
</li>
<li>
<a href="https://docs.gitlab.com/ee/ci/yaml/README.html#needs"
>https://docs.gitlab.com/ee/ci/yaml/README.html#needs</a
>
</li>
<li>
<a href="https://testdriven.io/blog/faster-ci-builds-with-docker-cache/"
>https://testdriven.io/blog/faster-ci-builds-with-docker-cache/</a
>
</li>
<li>
<a
href="https://docs.docker.com/engine/reference/commandline/build/#specifying-external-cache-sources"
>https://docs.docker.com/engine/reference/commandline/build/#specifying-external-cache-sources</a
>
</li>
<li>
<a href="https://docs.docker.com/develop/develop-images/build_enhancements/"
>https://docs.docker.com/develop/develop-images/build_enhancements/</a
>
</li>
</ul>
How to run Docker in Docker on Mac
2021-04-06T09:48:14-07:00https://www.saltycrane.com/blog/2021/04/how-run-docker-docker-mac/<p>
<a href="https://hub.docker.com/_/docker">Docker in Docker</a> can be used in
<a href="https://docs.gitlab.com/ee/ci/docker/using_docker_build.html"
>GitLab CI/CD to build Docker images</a
>. This is how to run Docker in Docker on Mac.
</p>
<ul>
<li>
<p>create directory</p>
<pre><code>mkdir /tmp/my-project
cd /tmp/my-project
</code></pre>
</li>
<li>
<p>create <code>docker-compose.yml</code> file:</p>
<pre><code>version: "3"
services:
docker-daemon:
container_name: "my-docker-daemon"
environment:
DOCKER_TLS_CERTDIR: ""
image: "docker:dind"
networks:
"my-network":
aliases:
- "docker"
privileged: true
docker-client:
command: sh -c 'while [ 1 ]; do sleep 1000; done'
container_name: "my-docker-client"
depends_on:
- "docker-daemon"
environment:
DOCKER_HOST: "tcp://docker:2375"
image: "docker:latest"
networks:
"my-network": {}
networks:
"my-network":
name: "my-network"
</code></pre>
</li>
<li>
<p>run the docker daemon and client containers</p>
<pre><code>docker-compose up -d
</code></pre>
</li>
<li>
<p>run a shell in the client container</p>
<pre><code>docker exec -it my-docker-client sh
</code></pre>
</li>
<li>
<p>run a docker command in the docker client container</p>
<pre><code>/ # docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
</code></pre>
</li>
</ul>
<h4 id="references">References</h4>
<ul>
<li>
<a href="https://hub.docker.com/_/docker"
>https://hub.docker.com/_/docker</a
>
</li>
<li>
<a href="https://www.caktusgroup.com/blog/2020/02/25/docker-image/"
>https://www.caktusgroup.com/blog/2020/02/25/docker-image/</a
>
</li>
</ul>
Next.js Cypress GitLab CI example
2021-03-25T16:12:37-07:00https://www.saltycrane.com/blog/2021/03/nextjs-cypress-gitlab-ci-example/<p>
This is an example <a href="https://nextjs.org/">Next.js</a> project that runs
a <a href="https://www.cypress.io/">Cypress</a> test in Docker using a
<a href="https://docs.gitlab.com/ee/ci/">GitLab CI</a> pipeline. It also uses
the
<a href="https://docs.gitlab.com/ee/user/packages/container_registry/"
>GitLab Container Registry</a
>
for caching purposes.
</p>
<ul>
<li>
GitLab repo:
<a href="https://gitlab.com/saltycrane/next-cypress-gitlab-ci-example"
>https://gitlab.com/saltycrane/next-cypress-gitlab-ci-example</a
>
</li>
<li>
CI Pipelines:
<a
href="https://gitlab.com/saltycrane/next-cypress-gitlab-ci-example/-/pipelines"
>https://gitlab.com/saltycrane/next-cypress-gitlab-ci-example/-/pipelines</a
>
</li>
</ul>
<h4 id="gitlab-ciyml"><code>.gitlab-ci.yml</code></h4>
<pre><code>variables:
DOCKER_TLS_CERTDIR: "/certs"
stages:
- test
test-cypress:
stage: test
image: docker:latest
services:
- docker:dind
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:latest
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker pull $IMAGE_TAG || true
- docker build --cache-from $IMAGE_TAG -t $IMAGE_TAG .
- docker push $IMAGE_TAG
- docker run $IMAGE_TAG npm run cy:citest
</code></pre>
<h4 id="dockerfile"><code>Dockerfile</code></h4>
<p>
This uses the official Cypress Docker image (<a
href="https://github.com/cypress-io/cypress-docker-images/blob/master/base/14.16.0/Dockerfile"
><code>Dockerfile</code></a
>).
</p>
<pre><code>FROM cypress/base:14.16.0
WORKDIR /app
# run npm install before adding app code for better Docker caching
# https://semaphoreci.com/docs/docker/docker-layer-caching.html
COPY ./package.json /app
COPY ./package-lock.json /app
# CI=true suppresses Cypress progress log spam
RUN CI=true npm ci
COPY . /app
RUN npm run build
</code></pre>
<h4 id="packagejson"><code>package.json</code></h4>
<pre><code>{
"scripts": {
"build": "next build",
"cy:citest": "start-server-and-test start http://localhost:3000 cy:run",
"cy:run": "cypress run",
"dev": "next",
"start": "next start"
},
"dependencies": {
"next": "^10.0.9",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"cypress": "^6.8.0",
"start-server-and-test": "^1.12.1"
}
}
</code></pre>
<h4 id="cypressintegrationindex_specjs">
<code>cypress/integration/index_spec.js</code>
</h4>
<pre><code>describe("index page", () => {
it("loads successfully", () => {
cy.visit("http://localhost:3000");
cy.contains("Index");
});
});
</code></pre>
<h4 id="references">References</h4>
<ul>
<li>
<a
href="https://docs.cypress.io/guides/continuous-integration/introduction#Setting-up-CI"
>https://docs.cypress.io/guides/continuous-integration/introduction#Setting-up-CI</a
>
</li>
<li>
<a
href="https://docs.gitlab.com/ee/user/packages/container_registry/#build-and-push-by-using-gitlab-cicd"
>https://docs.gitlab.com/ee/user/packages/container_registry/#build-and-push-by-using-gitlab-cicd</a
>
</li>
</ul>
Example Next.js GitLab CI/CD Amazon ECR and ECS deploy pipeline
2021-03-25T10:29:45-07:00https://www.saltycrane.com/blog/2021/03/example-nextjs-gitlab-cicd-amazon-ecr-and-ecs-deploy-pipeline/<p>
I've created an example <a href="https://nextjs.org/">Next.js</a> project
with a <a href="https://docs.gitlab.com/ee/ci/">GitLab CI/CD</a> pipeline that
builds a Docker image, pushes it to
<a href="https://aws.amazon.com/ecr/">Amazon ECR</a>, deploys it to an
<a href="https://aws.amazon.com/ecs/">Amazon ECS</a>
<a href="https://aws.amazon.com/fargate/">Fargate</a> cluster, and uploads
static assets (JS, CSS, etc.) to
<a href="https://aws.amazon.com/s3/">Amazon S3</a>. The example GitLab repo is
here:
<a href="https://gitlab.com/saltycrane/next-aws-ecr-ecs-gitlab-ci-cd-example"
>https://gitlab.com/saltycrane/next-aws-ecr-ecs-gitlab-ci-cd-example</a
>
</p>
<h4 id="interesting-files">Interesting files</h4>
<p>
Here are the interesting parts of some of the files. See the full source code
in the
<a href="https://gitlab.com/saltycrane/next-aws-ecr-ecs-gitlab-ci-cd-example"
>GitLab repo</a
>.
</p>
<ul>
<li>
<p>
<code>.gitlab-ci.yml</code> (<a
href="https://gitlab.com/saltycrane/next-aws-ecr-ecs-gitlab-ci-cd-example/-/blob/main/.gitlab-ci.yml"
>view at gitlab</a
>)
</p>
<ul>
<li>
the variables <code>AWS_ACCESS_KEY_ID</code>,
<code>AWS_SECRET_ACCESS_KEY</code>, and <code>ECR_HOST</code> are set in
the GitLab UI under "Settings" > "CI/CD" >
"Variables"
</li>
<li>
this uses the
<a href="https://hub.docker.com/r/saltycrane/aws-cli-and-docker"
>saltycrane/aws-cli-and-docker</a
>
Docker image which provides the <code>aws</code> v2 command line tools
and <code>docker</code> in a single image. It is based on
<a href="https://hub.docker.com/r/amazon/aws-cli">amazon/aws-cli</a> and
installs bc, curl, docker, jq, and tar. This idea is from
<a href="https://www.youtube.com/watch?v=jg9sUceyGaQ"
>Valentin's tutorial</a
>.
</li>
</ul>
<pre><code>variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
AWS_DEFAULT_REGION: "us-east-1"
CI_APPLICATION_REPOSITORY: "$ECR_HOST/next-aws-ecr-ecs-gitlab-ci-cd-example"
CI_APPLICATION_TAG: "$CI_PIPELINE_IID"
CI_AWS_S3_BUCKET: "next-aws-ecr-ecs-gitlab-ci-cd-example"
CI_AWS_ECS_CLUSTER: "next-aws-ecr-ecs-gitlab-ci-cd-example"
CI_AWS_ECS_SERVICE: "next-aws-ecr-ecs-gitlab-ci-cd-example"
CI_AWS_ECS_TASK_DEFINITION: "next-aws-ecr-ecs-gitlab-ci-cd-example"
NEXT_JS_ASSET_URL: "https://$CI_AWS_S3_BUCKET.s3.amazonaws.com"
stages:
- build
- deploy
build:
stage: build
image: saltycrane/aws-cli-and-docker
services:
- docker:dind
script:
- ./bin/build-and-push-image-to-ecr
- ./bin/upload-assets-to-s3
deploy:
stage: deploy
image: saltycrane/aws-cli-and-docker
services:
- docker:dind
script:
- ./bin/ecs update-task-definition
</code></pre>
</li>
<li>
<p>
<code>Dockerfile</code> (<a
href="https://gitlab.com/saltycrane/next-aws-ecr-ecs-gitlab-ci-cd-example/-/blob/main/Dockerfile"
>view at gitlab</a
>)
</p>
<p>
The value of <code>NEXT_JS_ASSET_URL</code> is passed in using the
<code>--build-arg</code> option of the <code>docker build</code> command
run in <code>bin/build-and-push-image-to-ecr</code>. It is used like an
environment variable in the <code>RUN npm run build</code> command below.
In this project it is assigned to <code>assetPrefix</code> in
<code>next.config.js</code>.
</p>
<pre><code>FROM node:14.16-alpine
ARG NEXT_JS_ASSET_URL
ENV NODE_ENV=production
WORKDIR /app
COPY ./package.json ./
COPY ./package-lock.json ./
RUN npm ci
COPY . ./
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
</code></pre>
</li>
<li>
<p>
<code>bin/build-and-push-image-to-ecr</code> (<a
href="https://gitlab.com/saltycrane/next-aws-ecr-ecs-gitlab-ci-cd-example/-/blob/main/bin/build-and-push-image-to-ecr"
>view at gitlab</a
>)
</p>
<pre><code># log in to the amazon ecr docker registry
aws ecr get-login-password | docker login --username AWS --password-stdin "$ECR_HOST"
# build docker image
docker pull "$CI_APPLICATION_REPOSITORY:latest" || true
docker build --build-arg "NEXT_JS_ASSET_URL=$NEXT_JS_ASSET_URL" --cache-from "$CI_APPLICATION_REPOSITORY:latest" -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" -t "$CI_APPLICATION_REPOSITORY:latest" .
# push image to amazon ecr
docker push "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG"
docker push "$CI_APPLICATION_REPOSITORY:latest"
</code></pre>
</li>
<li>
<p>
<code>bin/upload-assets-to-s3</code> (<a
href="https://gitlab.com/saltycrane/next-aws-ecr-ecs-gitlab-ci-cd-example/-/blob/main/bin/upload-assets-to-s3"
>view at gitlab</a
>)
</p>
<pre><code>LOCAL_ASSET_PATH=/tmp/upload-assets
mkdir $LOCAL_ASSET_PATH
# copy the generated assets out of the docker image
docker run --rm --entrypoint tar "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" cf - .next | tar xf - -C $LOCAL_ASSET_PATH
# rename .next to _next
mv "$LOCAL_ASSET_PATH/.next" "$LOCAL_ASSET_PATH/_next"
# remove directories that should not be uploaded to S3
rm -rf "$LOCAL_ASSET_PATH/_next/cache"
rm -rf "$LOCAL_ASSET_PATH/_next/server"
# gzip files
find $LOCAL_ASSET_PATH -regex ".*\.\(css\|svg\|js\)$" -exec gzip {} \;
# strip .gz extension off of gzipped files
find $LOCAL_ASSET_PATH -name "*.gz" -exec sh -c 'mv $1 `echo $1 | sed "s/.gz$//"`' - {} \;
# upload gzipped js, css, and svg assets
aws s3 sync --no-progress $LOCAL_ASSET_PATH "s3://$CI_AWS_S3_BUCKET" --cache-control max-age=31536000 --content-encoding gzip --exclude "*" --include "*.js" --include "*.css" --include "*.svg"
# upload non-gzipped assets
aws s3 sync --no-progress $LOCAL_ASSET_PATH "s3://$CI_AWS_S3_BUCKET" --cache-control max-age=31536000 --exclude "*.js" --exclude "*.css" --exclude "*.svg" --exclude "*.map"
</code></pre>
</li>
<li>
<p>
<code>bin/ecs</code> (<a
href="https://gitlab.com/saltycrane/next-aws-ecr-ecs-gitlab-ci-cd-example/-/blob/main/bin/ecs"
>view full file</a
>) (This file was copied from the
<a href=""><code>gitlab-org</code> repo</a>)
</p>
<pre><code>#!/bin/bash -e
update_task_definition() {
local -A register_task_def_args=( \
['task-role-arn']='taskRoleArn' \
['execution-role-arn']='executionRoleArn' \
['network-mode']='networkMode' \
['cpu']='cpu' \
['memory']='memory' \
['pid-mode']='pidMode' \
['ipc-mode']='ipcMode' \
['proxy-configuration']='proxyConfiguration' \
['volumes']='volumes' \
['placement-constraints']='placementConstraints' \
['requires-compatibilities']='requiresCompatibilities' \
['inference-accelerators']='inferenceAccelerators' \
)
image_repository=$CI_APPLICATION_REPOSITORY
image_tag=$CI_APPLICATION_TAG
new_image_name="${image_repository}:${image_tag}"
register_task_definition_from_remote
new_task_definition=$(aws ecs register-task-definition "${args[@]}")
new_task_revision=$(read_task "$new_task_definition" 'revision')
new_task_definition_family=$(read_task "$new_task_definition" 'family')
# Making sure that we at least have one running task (even if desiredCount gets updated again with new task definition below)
service_task_count=$(aws ecs describe-services --cluster "$CI_AWS_ECS_CLUSTER" --services "$CI_AWS_ECS_SERVICE" --query "services[0].desiredCount")
if [[ $service_task_count == 0 ]]; then
aws ecs update-service --cluster "$CI_AWS_ECS_CLUSTER" --service "$CI_AWS_ECS_SERVICE" --desired-count 1
fi
# Update ECS service with newly created task defintion revision.
aws ecs update-service \
--cluster "$CI_AWS_ECS_CLUSTER" \
--service "$CI_AWS_ECS_SERVICE" \
--task-definition "$new_task_definition_family":"$new_task_revision"
return 0
}
read_task() {
val=$(echo "$1" | jq -r ".taskDefinition.$2")
if [ "$val" == "null" ];then
val=$(echo "$1" | jq -r ".$2")
fi
if [ "$val" != "null" ];then
echo -n "${val}"
fi
}
register_task_definition_from_remote() {
task=$(aws ecs describe-task-definition --task-definition "$CI_AWS_ECS_TASK_DEFINITION")
current_container_definitions=$(read_task "$task" 'containerDefinitions')
new_container_definitions=$(echo "$current_container_definitions" | jq --arg val "$new_image_name" '.[0].image = $val')
args+=("--family" "${CI_AWS_ECS_TASK_DEFINITION}")
args+=("--container-definitions" "${new_container_definitions}")
for option in "${!register_task_def_args[@]}"; do
value=$(read_task "$task" "${register_task_def_args[$option]}")
if [ -n "$value" ];then
args+=("--${option}" "${value}")
fi
done
}
update_task_definition
</code></pre>
</li>
</ul>
<h4 id="usage---set-up-aws-resources">Usage - set up AWS resources</h4>
<p>
Below are the minimum steps I needed to create the required AWS services for
my example. I use the AWS region <code>"us-east-1"</code>. For info
about creating some of these services via the command line, see my
<a href="/blog/2021/03/amazon-ecs-notes/">Amazon ECS notes</a>.
</p>
<p><strong>Create an ECR repository</strong></p>
<ul>
<li>
create a private ECR repository here:
<a href="https://console.aws.amazon.com/ecr/repositories?region=us-east-1"
>https://console.aws.amazon.com/ecr/repositories?region=us-east-1</a
>
</li>
<li>name the repository "next-aws-ecr-ecs-gitlab-ci-cd-example"</li>
<li>
leave "Tag immutability" disabled to allow the "latest"
tag to be overwritten
</li>
</ul>
<p><strong>Create an ECS Fargate cluster</strong></p>
<ul>
<li>
click "Create Cluster" here:
<a
href="https://us-east-1.console.aws.amazon.com/ecs/home?region=us-east-1#/clusters"
>https://us-east-1.console.aws.amazon.com/ecs/home?region=us-east-1#/clusters</a
>
</li>
<li>choose "Networking only" (Fargate)</li>
<li>name the cluster "next-aws-ecr-ecs-gitlab-ci-cd-example"</li>
<li>check the "Create VPC" checkbox</li>
<li>click "Create"</li>
</ul>
<p><strong>Create an ECS task definition</strong></p>
<ul>
<li>
click "Create new Task Definition" here:
<a
href="https://console.aws.amazon.com/ecs/home?region=us-east-1#/taskDefinitions"
>https://console.aws.amazon.com/ecs/home?region=us-east-1#/taskDefinitions</a
>
</li>
<li>select "FARGATE" and click "Next step"</li>
<li>
configure task
<ul>
<li>
for "Task Definition Name" enter
"next-aws-ecr-ecs-gitlab-ci-cd-example"
</li>
<li>for "Task Role" select "None"</li>
<li>
for "Task execution role" select "Create new role"
</li>
<li>for "Task memory" select "0.5GB"</li>
<li>for "Task CPU" select "0.25 vCPU"</li>
<li>
click "Add container"
<ul>
<li>
for "Container Name" enter
"next-aws-ecr-ecs-gitlab-ci-cd-example"
</li>
<li>
for "Image" enter "asdf" (this will be updated
by the gitlab ci/cd job)
</li>
<li>leave "Private repository authentication" unchecked</li>
<li>for "Port mappings" enter "3000"</li>
<li>click "Add"</li>
</ul>
</li>
<li>click "Create"</li>
</ul>
</li>
</ul>
<p><strong>Create an ECS service</strong></p>
<ul>
<li>
click "Create" here:
<a
href="https://us-east-1.console.aws.amazon.com/ecs/home?region=us-east-1#/clusters/next-aws-ecr-ecs-gitlab-ci-cd-example/services"
>https://us-east-1.console.aws.amazon.com/ecs/home?region=us-east-1#/clusters/next-aws-ecr-ecs-gitlab-ci-cd-example/services</a
>
</li>
<li>
configure service
<ul>
<li>for "Launch type" select "FARGATE"</li>
<li>
for "Task Definition" enter
"next-aws-ecr-ecs-gitlab-ci-cd-example"
</li>
<li>
for "Cluster" select
"next-aws-ecr-ecs-gitlab-ci-cd-example"
</li>
<li>
for "Service name" enter
"next-aws-ecr-ecs-gitlab-ci-cd-example"
</li>
<li>for "Number of tasks" enter 1</li>
<li>for "Deployment type" select "Rolling update"</li>
<li>click "Next step"</li>
</ul>
</li>
<li>
configure network
<ul>
<li>
select the appropriate "Cluster VPC" and two
"Subnets"
</li>
<li>click "Next step"</li>
</ul>
</li>
<li>
set Auto Scaling
<ul>
<li>click "Next step"</li>
</ul>
</li>
<li>
review
<ul>
<li>click "Create Service"</li>
</ul>
</li>
</ul>
<p><strong>Open port 3000</strong></p>
<ul>
<li>
on the ECS service page
<a
href="https://us-east-1.console.aws.amazon.com/ecs/home?region=us-east-1#/clusters/next-aws-ecr-ecs-gitlab-ci-cd-example/services/next-aws-ecr-ecs-gitlab-ci-cd-example/details"
>https://us-east-1.console.aws.amazon.com/ecs/home?region=us-east-1#/clusters/next-aws-ecr-ecs-gitlab-ci-cd-example/services/next-aws-ecr-ecs-gitlab-ci-cd-example/details</a
>
under "Network Access", next to "Security groups", click
the link to the security group
</li>
<li>click "Actions" then click "Edit inbound rules"</li>
<li>click "Add rule"</li>
<li>for "Port range" enter "3000"</li>
<li>for "Source" select "0.0.0.0/0"</li>
<li>click "Save rules"</li>
</ul>
<p><strong>Create a S3 bucket</strong></p>
<ul>
<li>
click "Create bucket" here:
<a href="https://s3.console.aws.amazon.com/s3/home?region=us-east-1"
>https://s3.console.aws.amazon.com/s3/home?region=us-east-1</a
>
</li>
<li>
for "Bucket name" enter
"next-aws-ecr-ecs-gitlab-ci-cd-example"
</li>
<li>uncheck "Block all public access"</li>
<li>
check the "I acknowledge that the current settings might result in this
bucket and the objects within becoming public" checkbox
</li>
<li>click "Create bucket"</li>
</ul>
<p><strong>Update permissions for S3 bucket</strong></p>
<ul>
<li>
go to Permissions (<a
href="https://s3.console.aws.amazon.com/s3/buckets/next-aws-ecr-ecs-gitlab-ci-cd-example?region=us-east-1&tab=permissions"
>https://s3.console.aws.amazon.com/s3/buckets/next-aws-ecr-ecs-gitlab-ci-cd-example?region=us-east-1&tab=permissions</a
>) and under "Bucket policy", click "Edit"
</li>
<li>
enter:
<pre><code>{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::next-aws-ecr-ecs-gitlab-ci-cd-example/*"
}
]
}
</code></pre>
</li>
<li>click "Save changes"</li>
</ul>
<p><strong>Create an IAM user</strong></p>
<ul>
<li>
<a
href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/get-set-up-for-amazon-ecs.html#create-an-iam-user"
>create an IAM user</a
>. The user must have at least ECR, ECS, and S3 permissions.
</li>
<li>
take note of the <code>AWS_ACCESS_KEY_ID</code> and
<code>AWS_SECRET_ACCESS_KEY</code>
</li>
</ul>
<h4 id="usage---run-the-cicd-pipeline">Usage - run the CI/CD pipeline</h4>
<p>
<strong>Fork the example gitlab repo and configure CI/CD variables</strong>
</p>
<ul>
<li>
fork
<a
href="https://gitlab.com/saltycrane/next-aws-ecr-ecs-gitlab-ci-cd-example"
>https://gitlab.com/saltycrane/next-aws-ecr-ecs-gitlab-ci-cd-example</a
>
</li>
<li>
go to "Settings" > "CI/CD" > "Variables"
and add the following variables. You can choose to "protect" and
"mask" all of them.
<ul>
<li><code>AWS_ACCESS_KEY_ID</code></li>
<li><code>AWS_SECRET_ACCESS_KEY</code></li>
<li>
<code>ECR_HOST</code> (This is the part of the ECR repository URI before
the <code>/</code>. It looks something like
<code>XXXXXXXXXXXX.dkr.ecr.us-east-1.amazonaws.com</code>)
</li>
</ul>
</li>
</ul>
<p>
<strong>Edit variables in <code>.gitlab-ci.yml</code></strong>
</p>
<p>
If you used names other than
"next-aws-ecr-ecs-gitlab-ci-cd-example", edit the variables in
<code>.gitlab-ci.yml</code>.
</p>
<p><strong>Test it</strong></p>
<ul>
<li>clone the repo and push a commit</li>
<li>
see the pipeline running under "CI/CD" > "Pipelines"
</li>
<li>
go to the cluster tasks page:
<a
href="https://us-east-1.console.aws.amazon.com/ecs/home?region=us-east-1#/clusters/next-aws-ecr-ecs-gitlab-ci-cd-example/tasks"
>https://us-east-1.console.aws.amazon.com/ecs/home?region=us-east-1#/clusters/next-aws-ecr-ecs-gitlab-ci-cd-example/tasks</a
>
</li>
<li>click on the task and copy the "Public IP"</li>
<li>
enter the public IP followed by <code>:3000</code> in the browser (Note: the
IP address changes for every <code>git push</code>. A
<a href="https://aws.amazon.com/elasticloadbalancing/">load balancer</a>
should probably be used, but I didn't do that.)
</li>
</ul>
<h4 id="references-buildpush">References (build/push)</h4>
<ul>
<li>
<a href="https://www.youtube.com/watch?v=jg9sUceyGaQ"
>https://www.youtube.com/watch?v=jg9sUceyGaQ</a
>
</li>
<li>
<a
href="https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#docker"
>https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#docker</a
>
</li>
<li>
<a
href="https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#using-docker-caching"
>https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#using-docker-caching</a
>
</li>
</ul>
<h4 id="references-deploy">References (deploy)</h4>
<ul>
<li>
<a
href="https://docs.gitlab.com/ee/ci/cloud_deployment/#deploy-your-application-to-the-aws-elastic-container-service-ecs"
>https://docs.gitlab.com/ee/ci/cloud_deployment/#deploy-your-application-to-the-aws-elastic-container-service-ecs</a
>
</li>
<li>
<a
href="https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/AWS/Deploy-ECS.gitlab-ci.yml"
>https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/AWS/Deploy-ECS.gitlab-ci.yml</a
>
</li>
<li>
<a
href="https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml"
>https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml</a
>
</li>
<li>
<a
href="https://gitlab.com/gitlab-org/cloud-deploy/-/blob/master/aws/ecs/Dockerfile"
>https://gitlab.com/gitlab-org/cloud-deploy/-/blob/master/aws/ecs/Dockerfile</a
>
</li>
<li>
<a
href="https://gitlab.com/gitlab-org/cloud-deploy/-/blob/master/aws/src/bin/ecs"
>https://gitlab.com/gitlab-org/cloud-deploy/-/blob/master/aws/src/bin/ecs</a
>
</li>
</ul>
GitLab CI/CD hello world examples
2021-03-25T09:06:02-07:00https://www.saltycrane.com/blog/2021/03/gitlab-cicd-hello-world-examples/<p>
These are my notes for creating "hello world" and "docker hello
world"
<a href="https://docs.gitlab.com/ee/ci/README.html">GitLab CI/CD</a>
pipelines.
</p>
<h4 id="gitlab-cicd-hello-world-gitlab-repo">
GitLab CI/CD hello world
<small
>(<a href="https://gitlab.com/saltycrane/gitlab-ci-cd-hello-world"
>gitlab repo</a
>)</small
>
</h4>
<ul>
<li>
<p>
create git repo, push it to gitlab, and set origin. Replace
<code>saltycrane</code> with your username.
</p>
<pre><code>mkdir gitlab-ci-cd-hello-world
cd gitlab-ci-cd-hello-world
git init
touch .gitignore
git add .
git commit -m 'first commit'
git push --set-upstream git@gitlab.com:saltycrane/gitlab-ci-cd-hello-world.git --all
git remote add origin git@gitlab.com:saltycrane/gitlab-ci-cd-hello-world.git
</code></pre>
</li>
<li>
<p>add a <code>.gitlab-ci.yml</code> file</p>
<pre><code>build-hello:
script:
- echo "hello world"
</code></pre>
</li>
<li>
<p>commit and push</p>
<pre><code>git add .
git commit -m 'add ci/cd config'
git push origin
</code></pre>
</li>
<li>
<p>
see the pipeline run (replace <code>saltycrane</code> with your username):
<a
href="https://gitlab.com/saltycrane/gitlab-ci-cd-hello-world/-/pipelines"
>https://gitlab.com/saltycrane/gitlab-ci-cd-hello-world/-/pipelines</a
>
</p>
</li>
</ul>
<h4 id="gitlab-cicd-docker-hello-world-gitlab-repo">
GitLab CI/CD Docker hello world
<small
>(<a href="https://gitlab.com/saltycrane/gitlab-ci-cd-docker-hello-world"
>gitlab repo</a
>)</small
>
</h4>
<ul>
<li>
<p>
create git repo, push it to gitlab, and set origin. Replace
<code>saltycrane</code> with your username.
</p>
<pre><code>mkdir gitlab-ci-cd-docker-hello-world
cd gitlab-ci-cd-docker-hello-world
git init
touch .gitignore
git add .
git commit -m 'first commit'
git push --set-upstream git@gitlab.com:saltycrane/gitlab-ci-cd-docker-hello-world.git --all
git remote add origin git@gitlab.com:saltycrane/gitlab-ci-cd-docker-hello-world.git
</code></pre>
</li>
<li>
<p>add a <code>Dockerfile</code> file</p>
<pre><code>FROM alpine
RUN echo "hello"
</code></pre>
</li>
<li>
<p>add a <code>.gitlab-ci.yml</code> file</p>
<pre><code>variables:
DOCKER_TLS_CERTDIR: "/certs"
build-docker:
image: docker:latest
services:
- docker:dind
script:
- docker build -t hello .
</code></pre>
</li>
<li>
<p>commit and push</p>
<pre><code>git add .
git commit -m 'add Dockerfile and ci/cd config'
git push origin
</code></pre>
</li>
<li>
<p>
see the pipeline run (replace <code>saltycrane</code> with your username):
<a
href="https://gitlab.com/saltycrane/gitlab-ci-cd-docker-hello-world/-/pipelines"
>https://gitlab.com/saltycrane/gitlab-ci-cd-docker-hello-world/-/pipelines</a
>
</p>
</li>
</ul>
Amazon ECS notes
2021-03-19T10:04:07-07:00https://www.saltycrane.com/blog/2021/03/amazon-ecs-notes/<p>
These are my notes for creating a Docker image, pushing it to
<a href="https://aws.amazon.com/ecr/">Amazon ECR</a> (Elastic Container
Registry), and deploying it to
<a href="https://aws.amazon.com/ecs/">Amazon ECS</a> (Elastic Container
Service) using
<a href="https://aws.amazon.com/fargate/">AWS Fargate</a> (serverless for
containers) using command line tools.
</p>
<h4 id="create-docker-image-on-local-machine">
Create docker image on local machine
</h4>
<ul>
<li>
<p>install docker (macOS)</p>
<pre><code>brew install homebrew/cask/docker
</code></pre>
</li>
<li>
<p>create directory</p>
<pre><code>mkdir /tmp/my-project
cd /tmp/my-project
</code></pre>
</li>
<li>
<p>create <code>/tmp/my-project/Dockerfile</code>:</p>
<pre><code>FROM python:3.9-alpine3.13
WORKDIR /app
RUN echo 'Hello' > ./index.html
EXPOSE 80
CMD ["python", "-m", "http.server", "80"]
</code></pre>
</li>
<li>
<p>create Docker image</p>
<pre><code>docker build -t my-image .
</code></pre>
</li>
<li>
<p>test running the Docker image locally</p>
<pre><code>docker run -p 80:80 my-image
</code></pre>
</li>
<li>
<p>
go to <a href="http://localhost">http://localhost</a> in the browser and
see the text "Hello"
</p>
</li>
</ul>
<h4 id="install-and-configure-aws-command-line-tools">
Install and configure AWS command line tools
</h4>
<ul>
<li>
<p>install AWS command line tools</p>
<pre><code>brew install awscli
</code></pre>
</li>
<li>
<p>
<a
href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/get-set-up-for-amazon-ecs.html#create-an-iam-user"
>create an IAM user</a
>
</p>
</li>
<li>
<p>
run
<a
href="https://docs.aws.amazon.com/cli/latest/reference/configure/index.html"
><code>aws configure</code></a
>
and enter:
</p>
<ul>
<li>AWS Access Key ID</li>
<li>AWS Secret Access Key</li>
</ul>
<p>This creates the file <code>~/.aws/credentials</code></p>
</li>
</ul>
<h4 id="create-ecr-repository-and-push-image-to-it">
Create ECR repository and push image to it
</h4>
<p>
From
<a
href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html#use-ecr"
>https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html#use-ecr</a
>
</p>
<ul>
<li>
<p>
create an Amazon ECR repository using
<a
href="https://docs.aws.amazon.com/cli/latest/reference/ecr/create-repository.html"
><code>aws ecr create-repository</code></a
>
</p>
<pre><code>aws ecr create-repository --repository-name my-repository --region us-east-1
</code></pre>
<p>output:</p>
<pre><code>{
"repository": {
"repositoryArn": "arn:aws:ecr:us-east-1:AAAAAAAAAAAA:repository/my-repository",
"registryId": "AAAAAAAAAAAA",
"repositoryName": "my-repository",
"repositoryUri": "AAAAAAAAAAAA.dkr.ecr.us-east-1.amazonaws.com/my-repository",
"createdAt": "2021-03-17T10:48:18-07:00",
"imageTagMutability": "MUTABLE",
"imageScanningConfiguration": {
"scanOnPush": false
},
"encryptionConfiguration": {
"encryptionType": "AES256"
}
}
}
</code></pre>
<p>
Take note of the "registryId" and use it in place of
"AAAAAAAAAAAA" below.
</p>
</li>
</ul>
<ul>
<li>
<p>tag the docker image with the <code>repositoryUri</code></p>
<pre><code>docker tag my-image AAAAAAAAAAAA.dkr.ecr.us-east-1.amazonaws.com/my-repository
</code></pre>
</li>
<li>
<p>
log in to the Amazon ECR registry using
<a
href="https://docs.aws.amazon.com/cli/latest/reference/ecr/get-login-password.html"
><code>aws ecr get-login-password</code></a
>
</p>
<pre><code>aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin AAAAAAAAAAAA.dkr.ecr.us-east-1.amazonaws.com
</code></pre>
</li>
<li>
<p>push the docker image to the Amazon ECR repository</p>
<pre><code>docker push AAAAAAAAAAAA.dkr.ecr.us-east-1.amazonaws.com/my-repository
</code></pre>
</li>
<li>
<p>
see the image in AWS console
<a href="https://console.aws.amazon.com/ecr/repositories?region=us-east-1"
>https://console.aws.amazon.com/ecr/repositories?region=us-east-1</a
>
</p>
</li>
</ul>
<h4 id="install-ecs-command-line-tools">Install ECS command line tools</h4>
<ul>
<li>
install <code>ecs-cli</code>. Note there is <code>ecs-cli</code> in addition
to <code>aws ecs</code> tools. The reason is probably similar to why some
services are named
<a href="https://docs.aws.amazon.com/"
>"Amazon Service" and some are named "AWS Service"</a
>. (It seems like <code>ecs-cli</code> provides higher level commands.)
<pre><code>brew install amazon-ecs-cli
</code></pre>
</li>
</ul>
<h4 id="create-amazon-ecs-fargate-cluster">
Create Amazon ECS Fargate cluster
</h4>
<p>
From
<a
href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-cli-tutorial-fargate.html"
>https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-cli-tutorial-fargate.html</a
>
</p>
<ul>
<li>
create a cluster using
<a
href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cmd-ecs-cli-up.html"
><code>ecs-cli up</code></a
>
<pre><code>ecs-cli up --cluster my-cluster --launch-type FARGATE --region us-east-1
</code></pre>
output:
<pre><code>INFO[0001] Created cluster cluster=my-cluster region=us-east-1
INFO[0002] Waiting for your cluster resources to be created...
INFO[0002] Cloudformation stack status stackStatus=CREATE_IN_PROGRESS
VPC created: vpc-BBBBBBBBBBBBBBBBB
Subnet created: subnet-CCCCCCCCCCCCCCCCC
Subnet created: subnet-DDDDDDDDDDDDDDDDD
Cluster creation succeeded.
</code></pre>
Take note of the VPC (virtual private cloud), and two subnet IDs to use
later. See the cluster in the AWS console UI:
<a href="https://console.aws.amazon.com/ecs/home?region=us-east-1#/clusters"
>https://console.aws.amazon.com/ecs/home?region=us-east-1#/clusters</a
>
</li>
</ul>
<h4 id="gather-parameters-required-to-deploy-to-ecs-cluster">
Gather parameters required to deploy to ECS cluster
</h4>
<p>
From
<a
href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-cli-tutorial-fargate.html"
>https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-cli-tutorial-fargate.html</a
>
</p>
<h5 id="create-task-execution-iam-role">Create task execution IAM role</h5>
<ul>
<li>
<p>
create a file <code>/tmp/my-project/task-execution-assume-role.json</code>
</p>
<pre><code>{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
</code></pre>
</li>
<li>
<p>
create the task execution role using
<a
href="https://docs.aws.amazon.com/cli/latest/reference/iam/create-role.html"
><code>aws iam create-role</code></a
>
</p>
<pre><code>aws iam create-role --role-name my-task-execution-role --assume-role-policy-document file:///tmp/my-project/task-execution-assume-role.json --region us-east-1
</code></pre>
</li>
<li>
<p>
attach the task execution role policy using
<a
href="https://docs.aws.amazon.com/cli/latest/reference/iam/attach-role-policy.html"
><code>aws iam attach-role-policy</code></a
>
</p>
<pre><code>aws iam attach-role-policy --role-name my-task-execution-role --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy --region us-east-1
</code></pre>
</li>
</ul>
<h5 id="get-security-group-id">Get security group ID</h5>
<ul>
<li>
<p>
get the default security group ID for the virtual private cloud (VPC)
created when creating the ECS cluster using
<a
href="https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-security-groups.html"
><code>aws ec2 describe-security-groups</code></a
>. Replace "vpc-BBBBBBBBBBBBBBBBB" with your VPC ID
</p>
<pre><code>aws ec2 describe-security-groups --filters Name=vpc-id,Values=vpc-BBBBBBBBBBBBBBBBB --region us-east-1
</code></pre>
<p>output:</p>
<pre><code>{
"SecurityGroups": [
{
"Description": "default VPC security group",
"GroupName": "default",
"IpPermissions": [
{
"IpProtocol": "-1",
"IpRanges": [],
"Ipv6Ranges": [],
"PrefixListIds": [],
"UserIdGroupPairs": [
{
"GroupId": "sg-EEEEEEEEEEEEEEEEE",
"UserId": "AAAAAAAAAAAA"
}
]
}
],
"OwnerId": "AAAAAAAAAAAA",
"GroupId": "sg-EEEEEEEEEEEEEEEEE",
"IpPermissionsEgress": [
{
"IpProtocol": "-1",
"IpRanges": [
{
"CidrIp": "0.0.0.0/0"
}
],
"Ipv6Ranges": [],
"PrefixListIds": [],
"UserIdGroupPairs": []
}
],
"VpcId": "vpc-BBBBBBBBBBBBBBBBB"
}
]
}
</code></pre>
<p>Take note of the "GroupId" to be used later</p>
</li>
</ul>
<h4 id="deploy-to-amazon-ecs-cluster">Deploy to Amazon ECS cluster</h4>
<p>
From
<a
href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-cli-tutorial-fargate.html"
>https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-cli-tutorial-fargate.html</a
>
</p>
<ul>
<li>
<p>
create <code>/tmp/my-project/ecs-params.yml</code> replacing
"subnet-CCCCCCCCCCCCCCCCC",
"subnet-DDDDDDDDDDDDDDDDD", and "sg-EEEEEEEEEEEEEEEEE"
with appropriate IDs from above.
<a
href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cmd-ecs-cli-compose-ecsparams.html"
>ECS Parameters docs</a
>
</p>
<pre><code>version: 1
task_definition:
task_execution_role: my-task-execution-role
ecs_network_mode: awsvpc
task_size:
mem_limit: 0.5GB
cpu_limit: 256
run_params:
network_configuration:
awsvpc_configuration:
subnets:
- "subnet-CCCCCCCCCCCCCCCCC"
- "subnet-DDDDDDDDDDDDDDDDD"
security_groups:
- "sg-EEEEEEEEEEEEEEEEE"
assign_public_ip: ENABLED
</code></pre>
</li>
<li>
<p>
create <code>/tmp/my-project/docker-compose.yml</code> replacing
AAAAAAAAAAAA with the registryId:
</p>
<pre><code>version: '3'
services:
web:
image: 'AAAAAAAAAAAA.dkr.ecr.us-east-1.amazonaws.com/my-repository'
ports:
- '80:80'
</code></pre>
</li>
<li>
<p>
deploy to the ECS cluster using
<a
href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cmd-ecs-cli-compose-service-up.html"
><code>ecs-cli compose service up</code></a
>. This creates a task definition and service. This uses the
<code>docker-compose.yml</code> file in the current directory.
</p>
<pre><code>ecs-cli compose --cluster my-cluster --project-name my-project --ecs-params ecs-params.yml --region us-east-1 service up --launch-type FARGATE
</code></pre>
<p>
see the service in the web UI:
<a
href="https://console.aws.amazon.com/ecs/home?region=us-east-1#/clusters/my-cluster/services"
>https://console.aws.amazon.com/ecs/home?region=us-east-1#/clusters/my-cluster/services</a
>
</p>
</li>
</ul>
<h4 id="hit-the-server-in-the-browser">Hit the server in the browser</h4>
<ul>
<li>
<p>
configure security group to allow inbound access on port 80 using
<a
href="https://docs.aws.amazon.com/cli/latest/reference/ec2/authorize-security-group-ingress.html"
><code>aws ec2 authorize-security-group-ingress</code></a
>
</p>
<pre><code>aws ec2 authorize-security-group-ingress --group-id sg-EEEEEEEEEEEEEEEEE --protocol tcp --port 80 --cidr 0.0.0.0/0 --region us-east-1
</code></pre>
</li>
<li>
<p>
get the IP address using
<a
href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cmd-ecs-cli-compose-service-ps.html"
><code>ecs-cli compose service ps</code></a
>
</p>
<pre><code>ecs-cli compose --cluster my-cluster --project-name my-project --region us-east-1 service ps
</code></pre>
<p>output:</p>
<pre><code>Name State Ports TaskDefinition Health
my-cluster/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/web RUNNING FF.FF.FF.FF:80->80/tcp my-project:1 UNKNOWN
</code></pre>
<p>Take note of the IP address under "Ports"</p>
</li>
<li>
<p>
visit in the browser:
<a href="http://FF.FF.FF.FF">http://FF.FF.FF.FF</a> replacing
"FF.FF.FF.FF" with your IP address
</p>
</li>
</ul>
<h4 id="destroy">Destroy</h4>
<ul>
<li>
<p>
delete the ECS service using
<a
href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cmd-ecs-cli-compose-service-rm.html"
><code>ecs-cli compose service down</code></a
>
</p>
<pre><code>ecs-cli compose --cluster my-cluster --project-name my-project --region us-east-1 service down
</code></pre>
</li>
<li>
<p>
delete the ECS cluster using
<a
href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cmd-ecs-cli-down.html"
><code>ecs-cli down</code></a
>
</p>
<pre><code>ecs-cli down --force --cluster my-cluster --region us-east-1
</code></pre>
</li>
<li>
<p>
delete the ECR repository using
<a
href="https://docs.aws.amazon.com/cli/latest/reference/ecr/delete-repository.html"
><code>aws ecr delete-repository</code></a
>
</p>
<pre><code>aws ecr delete-repository --repository-name my-repository --region us-east-1 --force
</code></pre>
</li>
</ul>
<h4 id="references">References</h4>
<ul>
<li>
<a
href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html"
>https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html</a
>
</li>
<li>
<a
href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-cli-tutorial-fargate.html"
>https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-cli-tutorial-fargate.html</a
>
</li>
</ul>
Notes on Fabric 2 and Python 3
2021-02-07T19:49:30-08:00https://www.saltycrane.com/blog/2021/02/notes-fabric-2-and-python-3/<p>
<a href="https://docs.fabfile.org/en/2.5/index.html">Fabric 2</a> is a Python
package used for running commands on remote machines via SSH. Fabric 2
supports Python 3 and is a rewrite of the Fabric I used
<a href="/blog/2009/10/notes-python-fabric-09b1/">years ago</a>. Here are my
notes on using Fabric 2 and Python 3.
</p>
<h4>Set up SSH config and SSH agent</h4>
<ul>
<li>
Create or edit your <code>~/.ssh/config</code> file to contain your remote
host parameters
<pre>
Host myhost
User myusername
HostName myhost.com
IdentityFile ~/.ssh/id_rsa
</pre
>
</li>
<li>
Add your private key to your SSH agent
<pre class="console">
$ ssh-add ~/.ssh/id_rsa
</pre
>
</li>
</ul>
<h4>Create a project, create a virtualenv, and install fabric2</h4>
<pre class="console">
$ mkdir -p /tmp/my-project
$ cd /tmp/my-project
$ python3 -m venv venv
$ source venv/bin/activate
$ pip install fabric2
</pre>
<h4>Create a fabfile.py script</h4>
<p>
Create a file <code>/tmp/my-project/fabfile.py</code> with the following
contents. Note: "myhost" is the same name used in
<code>~/.ssh/config</code> described above.
</p>
<pre class="python">
from fabric2 import task
hosts = ["myhost"]
@task(hosts=hosts)
def mytask(c):
print("Starting mytask...")
with c.cd("/var"):
c.run("ls -l")
print("Done.")
</pre>
<h4>Run the fabric script</h4>
<p>
In <code>/tmp/my-project</code>, with the virtualenv activated, run the fabric
task to list the contents of <code>/var</code> on the remote host.
</p>
<pre class="console">$ fab2 mytask </pre>
<p>Output:</p>
<pre>
Starting mytask...
total 48
drwxr-xr-x 2 root root 4096 backups
drwxr-xr-x 9 root root 4096 cache
drwxrwxrwt 2 root root 4096 crash
drwxr-xr-x 38 root root 4096 lib
drwxrwsr-x 2 root root 4096 local
drwxrwxrwt 2 root root 4096 lock
drwxrwxr-x 14 root root 4096 log
drwxrwsr-x 2 root root 4096 mail
drwxr-xr-x 2 root root 4096 opt
drwxr-xr-x 5 root root 4096 spool
drwxrwxrwt 2 root root 4096 tmp
drwxr-xr-x 3 root root 4096 www
Done.
</pre>
<h4>See also / References</h4>
<ul>
<li>
<a href="http://www.fabfile.org/upgrading.html#the-whole-thing"
>Example fabfile (Upgrading from 1.x docs)</a
>
</li>
<li>
<a
href="http://docs.paramiko.org/en/stable/api/client.html#paramiko.client.SSHClient"
>
paramiko SSHClient API reference
</a>
</li>
</ul>