Skip to content

Instantly share code, notes, and snippets.

@mpneuried
Last active August 22, 2024 10:36
Show Gist options
  • Save mpneuried/0594963ad38e68917ef189b4e6a269db to your computer and use it in GitHub Desktop.
Save mpneuried/0594963ad38e68917ef189b4e6a269db to your computer and use it in GitHub Desktop.
Simple Makefile to build, run, tag and publish a docker containier to AWS-ECR
# Port to run the container
PORT=4000
# Until here you can define all the individual configurations for your app
# You have to define the values in {}
APP_NAME=my-super-app
DOCKER_REPO={account-nr}.dkr.ecr.{region}.amazonaws.com
# optional aws-cli options
AWS_CLI_PROFILE={aws-cli-profile}
AWS_CLI_REGION={aws-cli-region}
# import config.
# You can change the default config with `make cnf="config_special.env" build`
cnf ?= config.env
include $(cnf)
export $(shell sed 's/=.*//' $(cnf))
# import deploy config
# You can change the default deploy config with `make cnf="deploy_special.env" release`
dpl ?= deploy.env
include $(dpl)
export $(shell sed 's/=.*//' $(dpl))
# grep the version from the mix file
VERSION=$(shell ./version.sh)
# HELP
# This will output the help for each task
# thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
.PHONY: help
help: ## This help.
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
.DEFAULT_GOAL := help
# DOCKER TASKS
# Build the container
build: ## Build the container
docker build -t $(APP_NAME) .
build-nc: ## Build the container without caching
docker build --no-cache -t $(APP_NAME) .
run: ## Run container on port configured in `config.env`
docker run -i -t --rm --env-file=./config.env -p=$(PORT):$(PORT) --name="$(APP_NAME)" $(APP_NAME)
up: build run ## Run container on port configured in `config.env` (Alias to run)
stop: ## Stop and remove a running container
docker stop $(APP_NAME); docker rm $(APP_NAME)
release: build-nc publish ## Make a release by building and publishing the `{version}` ans `latest` tagged containers to ECR
# Docker publish
publish: repo-login publish-latest publish-version ## Publish the `{version}` ans `latest` tagged containers to ECR
publish-latest: tag-latest ## Publish the `latest` taged container to ECR
@echo 'publish latest to $(DOCKER_REPO)'
docker push $(DOCKER_REPO)/$(APP_NAME):latest
publish-version: tag-version ## Publish the `{version}` taged container to ECR
@echo 'publish $(VERSION) to $(DOCKER_REPO)'
docker push $(DOCKER_REPO)/$(APP_NAME):$(VERSION)
# Docker tagging
tag: tag-latest tag-version ## Generate container tags for the `{version}` ans `latest` tags
tag-latest: ## Generate container `{version}` tag
@echo 'create tag latest'
docker tag $(APP_NAME) $(DOCKER_REPO)/$(APP_NAME):latest
tag-version: ## Generate container `latest` tag
@echo 'create tag $(VERSION)'
docker tag $(APP_NAME) $(DOCKER_REPO)/$(APP_NAME):$(VERSION)
# HELPERS
# generate script to login to aws docker repo
CMD_REPOLOGIN := "eval $$\( aws ecr"
ifdef AWS_CLI_PROFILE
CMD_REPOLOGIN += " --profile $(AWS_CLI_PROFILE)"
endif
ifdef AWS_CLI_REGION
CMD_REPOLOGIN += " --region $(AWS_CLI_REGION)"
endif
CMD_REPOLOGIN += " get-login --no-include-email \)"
# login to AWS-ECR
repo-login: ## Auto login to AWS-ECR unsing aws-cli
@eval $(CMD_REPOLOGIN)
version: ## Output the current version
@echo $(VERSION)
### THIS IST THE VERSION WITH docker-compose
# import config.
# You can change the default config with `make cnf="config_special.env" build`
cnf ?= config.env
include $(cnf)
export $(shell sed 's/=.*//' $(cnf))
# import deploy config
# You can change the default deploy config with `make cnf="deploy_special.env" release`
dpl ?= deploy.env
include $(dpl)
export $(shell sed 's/=.*//' $(dpl))
# grep the version from the mix file
VERSION=$(shell ./version.sh)
# HELP
# This will output the help for each task
# thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
.PHONY: help
help: ## This help.
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
.DEFAULT_GOAL := help
# DOCKER TASKS
# Build the container
build: ## Build the release and develoment container. The development
docker-compose build --no-cache $(APP_NAME)
docker-compose run $(APP_NAME) grunt build
docker build -t $(APP_NAME) .
run: stop ## Run container on port configured in `config.env`
docker run -i -t --rm --env-file=./config.env -p=$(PORT):$(PORT) --name="$(APP_NAME)" $(APP_NAME)
dev: ## Run container in development mode
docker-compose build --no-cache $(APP_NAME) && docker-compose run $(APP_NAME)
# Build and run the container
up: ## Spin up the project
docker-compose up --build $(APP_NAME)
stop: ## Stop running containers
docker stop $(APP_NAME)
rm: stop ## Stop and remove running containers
docker rm $(APP_NAME)
clean: ## Clean the generated/compiles files
echo "nothing clean ..."
# Docker release - build, tag and push the container
release: build publish ## Make a release by building and publishing the `{version}` ans `latest` tagged containers to ECR
# Docker publish
publish: repo-login publish-latest publish-version ## publish the `{version}` ans `latest` tagged containers to ECR
publish-latest: tag-latest ## publish the `latest` taged container to ECR
@echo 'publish latest to $(DOCKER_REPO)'
docker push $(DOCKER_REPO)/$(APP_NAME):latest
publish-version: tag-version ## publish the `{version}` taged container to ECR
@echo 'publish $(VERSION) to $(DOCKER_REPO)'
docker push $(DOCKER_REPO)/$(APP_NAME):$(VERSION)
# Docker tagging
tag: tag-latest tag-version ## Generate container tags for the `{version}` ans `latest` tags
tag-latest: ## Generate container `{version}` tag
@echo 'create tag latest'
docker tag $(APP_NAME) $(DOCKER_REPO)/$(APP_NAME):latest
tag-version: ## Generate container `latest` tag
@echo 'create tag $(VERSION)'
docker tag $(APP_NAME) $(DOCKER_REPO)/$(APP_NAME):$(VERSION)
# HELPERS
# generate script to login to aws docker repo
CMD_REPOLOGIN := "aws ecr"
ifdef AWS_CLI_PROFILE
CMD_REPOLOGIN += "--profile $(AWS_CLI_PROFILE)"
endif
ifdef AWS_CLI_REGION
CMD_REPOLOGIN += "--region $(AWS_CLI_REGION)"
endif
CMD_REPOLOGIN += "get-login --no-include-email"
repo-login: ## Auto login to AWS-ECR unsing aws-cli
@eval $(CMD_REPOLOGIN)
version: ## output to version
@echo $(VERSION)
# INSTALL
# - copy the files deploy.env, config.env, version.sh and Makefile to your repo
# - replace the vars in deploy.env
# - define the version script
# Build the container
make build
# Build and publish the container
make release
# Publish a container to AWS-ECR.
# This includes the login to the repo
make publish
# Run the container
make run
# Build an run the container
make up
# Stop the running container
make stop
# Build the container with differnt config and deploy file
make cnf=another_config.env dpl=another_deploy.env build
# Example version script.
# Please choose one version or create your own
# Node.js: grep the version from a package.json file with jq
jq -rM '.version' package.json
# Elixir: grep the version from a mix file
cat mix.exs | grep version | grep '\([0-9]\+\.\?\)\{3\}' -o
@v1k0d3n
Copy link

v1k0d3n commented Jan 14, 2019

@shapeofarchitect there are a bunch of articles on the benefits of makefiles or some build system like Bazel, so I won't go into a lot of detail; mainly for the sake of time, since I'm only passing through the interwebs and happened to run into this gist.

so I think what you're saying is that services like Dockerhub have auto-build/web-hook methods of providing artifacts and publishing these artifacts (am I correct in assuming this is what you're asking)? if so, consider this:

not all docker registries have auto-build/webhook methods that can take advantage of the --build-args feature in docker. for example, Quay, which has been our preference, continually says that they're releasing the feature, but I think that acquisition after acquisition has pushed down the priorities over the last couple of years, unfortunately.

so let's say that your docker registry of choice does not take advantage of the --build-args flags, as is our case. also, let's say that you have some dockerfile that looks like this (a very simple golang-based project):

FROM golang:1.11

ARG GOOS=linux
ARG GOARCH=amd64

ENV GOOS=$GOOS
ENV GOARCH=$GOARCH

WORKDIR /go/src/github.com/v1k0d3n/myapp
COPY . .

RUN go get -d -v ./...

RUN CGO_ENABLED=0 go build -ldflags '-w -s' -a -installsuffix cgo -o /myapp

FROM scratch AS build
COPY --from=0 /myapp /myapp

VOLUME /data

ENTRYPOINT [ "/myapp" ]
CMD [ "--help" ]

the first thing you'll probably recognize is that we're leveraging docker's multi-stage build process. you likely already know this, but for others landing on this gist who may not, it allows us to build a golang application from a private repo, use our ssh keys to pull down other private go-lang based dependencies we may require (our example application does) and then throw away the build environment/keys, leaving only the application binary.

there's really no good way to do this well with a build-system like Dockerhub (i could be wrong about this since I haven't checked in a while). golaang makes things a big tricky, since we need to build the version information into the application using -ldflags option at buildtime. if I were to do this all via bash, say build, package, and push that application to Quay, i could possibly do something like:

export VERSION="v0.2.4"
export COMMIT="$(git rev-parse --short HEAD)"
export REGISTRY="quay.io"
export NAMESPACE="v1k0d3n"

if [ $(git tag -l "${VERSION}") ]; then
  echo "Git tag ${VERSION} already exists. Continuing..."
else
  echo "Git tag \"${VERSION}\" not found! Creating tag based on commit \"${COMMIT}\"."
  git tag ${VERSION}
  git push origin ${VERSION}
fi

docker build --build-arg COMMIT="${COMMIT}" --build-arg VERSION="${VERSION}" -t "${REGISTRY}/${NAMESPACE}/myapp:${VERSION}" .
docker build --build-arg COMMIT="${COMMIT}" --build-arg VERSION="${VERSION}" -t "${REGISTRY}/${NAMESPACE}/myapp:${VERSION}-${COMMIT}" .
docker push "${REGISTRY}/${NAMESPACE}/myapp:${VERSION}"
docker push "${REGISTRY}/${NAMESPACE}/myapp:${VERSION}-${COMMIT}"

but makefiles have an additional logic that is a bit more build-oriented than bash in the fact that it can track which targets require rebuilding, etc. overall, my point is that with simple dockerfiles; sure you have an extremely valid point. once you start getting into more complex application builds using docker, the solution isn't as simple or straightforward. the person can create the dockerfile, if they have access to the repo, sure, but what if we want to (in this case) create a docker image and also build an artifact for the system that user may be on? in our case example above this application needs to run on MacOS, and Linux. I want a single build file to handle all of this logic based on a command structure.

make build
make test
make push

And a Makefile is the best way that users and CI platforms can accomplish this.

@fd98279
Copy link

fd98279 commented Apr 24, 2019

Do you get this error for make run?

run:
@echo "Run docker image $(APP_NAME) at port $(PORT)"
docker run -d -i -t --rm --env-file=./config.env -p=$(PORT):$(PORT) --name="$(APP_NAME)" $(APP_NAME)

vagrant@linux:$ make run
make: Circular 8888 <- 8888 dependency dropped.

-p argument PORT:PORT is causing make to treat that line a circular dependency

@fd98279
Copy link

fd98279 commented Apr 24, 2019

Escaping colon like this worked: $(PORT):$(PORT)

@SpyPower
Copy link

SpyPower commented Jul 8, 2019

Escaping colon like this worked: $(PORT):$(PORT)

You mentioned on https://gist.github.com/mpneuried/0594963ad38e68917ef189b4e6a269db#gistcomment-2896932 that you used -p $(PORT):$(PORT) and it caused make to treat that line as a circular dependency and then you mentioned on the quoted text exactly the same as a solution. Can you please clarify?

@SamyCoenen
Copy link

Isn't it a best practice to specify all as your default target instead of help https://www.gnu.org/prep/standards/html_node/Standard-Targets.html#Standard-Targets

@cbrunnkvist
Copy link

@SpyPower that error must have been due to @fd98279 forgetting to tab-indent the line properly. Unless indented, make would think the whole line is a target: dependencies kind of line and get confused by the colon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment