From 59c5b683ee501db8905a56e13757c846120558a9 Mon Sep 17 00:00:00 2001 From: Stella Yang Date: Fri, 19 Jul 2024 11:30:50 -0700 Subject: [PATCH] add ruby tutorial --- docs/src/ruby/download.md | 22 ++++++++++++ docs/src/ruby/image_structure.md | 34 ++++++++++++++++++ docs/src/ruby/intro.md | 5 +++ docs/src/ruby/multiple_langs.md | 43 +++++++++++++++++++++++ docs/src/ruby/ruby_tutorial.md | 47 +++++++++++++++++++++++++ docs/src/ruby/what_is_pack_build.md | 20 +++++++++++ docs/src/shared/call_to_action.md | 11 ++++++ docs/src/shared/configure_builder.md | 23 ++++++++++++ docs/src/shared/install_pack.md | 13 +++++++ docs/src/shared/pack_build.md | 20 +++++++++++ docs/src/shared/procfile.md | 13 +++++++ docs/src/shared/use_the_image.md | 52 ++++++++++++++++++++++++++++ docs/src/shared/what_is_a_builder.md | 18 ++++++++++ 13 files changed, 321 insertions(+) create mode 100644 docs/src/ruby/download.md create mode 100644 docs/src/ruby/image_structure.md create mode 100644 docs/src/ruby/intro.md create mode 100644 docs/src/ruby/multiple_langs.md create mode 100644 docs/src/ruby/ruby_tutorial.md create mode 100644 docs/src/ruby/what_is_pack_build.md create mode 100644 docs/src/shared/call_to_action.md create mode 100644 docs/src/shared/configure_builder.md create mode 100644 docs/src/shared/install_pack.md create mode 100644 docs/src/shared/pack_build.md create mode 100644 docs/src/shared/procfile.md create mode 100644 docs/src/shared/use_the_image.md create mode 100644 docs/src/shared/what_is_a_builder.md diff --git a/docs/src/ruby/download.md b/docs/src/ruby/download.md new file mode 100644 index 0000000..f1907c0 --- /dev/null +++ b/docs/src/ruby/download.md @@ -0,0 +1,22 @@ +## Download an example Ruby on Rails application + +How do you configure a CNB? Give them an application. While Dockerfile is procedural, buildpacks, are declarative. A buildpack will determine what your application needs to function by inspecting the code on disk. + +For this example, we're using a pre-built Ruby on Rails application. Download it now: + +``` +:::>- $ git clone https://github.com/heroku/ruby-getting-started +:::>- $ cd ruby-getting-started +``` + +Verify you're in the correct directory: + +``` +:::>> $ ls +``` + +This tutorial was built using the following commit SHA: + +``` +:::>> $ git log --oneline | head -n1 +``` diff --git a/docs/src/ruby/image_structure.md b/docs/src/ruby/image_structure.md new file mode 100644 index 0000000..760c3db --- /dev/null +++ b/docs/src/ruby/image_structure.md @@ -0,0 +1,34 @@ +## Image structure under the hood + +> [!NOTE] +> Skip this section if you want to try building your application with CNBs and learn about container structure later. + +If you’re an advanced `Dockerfile` user you might be interested in learning more about the internal structure of the image on disk. You can access the image disk interactively by using the `bash` docker command above. + +If you view the root directory `/` you’ll see there is a `layers` folder. Every buildpack that executes gets a unique folder. For example: + +``` +:::>> $ docker run --rm my-image-name "ls /layers | grep ruby" +``` + +Individual buildpacks can compose multiple layers from their buildpack directory. For example you can see that `ruby` binary is present within that ruby buildpack directory: + +``` +:::>> $ docker run --rm my-image-name "which ruby" +``` + +OCI images are represented as sequential modifications to disk. By scoping buildpack disk modifications to their own directory, the CNB API guarantees that changes to a layer in one buildpack will not affect the contents of disk to another layer. This means that OCI images produced by CNBs are rebaseable by default, while those produced by Dockerfile are not. + +We saw before how the image booted a web server by default. This is accomplished using an entrypoint. In another terminal outside of the running container you can view that entrypoint: + +``` +:::>> $ docker inspect my-image-name | grep '"Entrypoint": \[' -A2 +``` + +From within the image, you can see that file on disk: + +``` +:::>> $ docker run --rm my-image-name "ls /cnb/process/" +``` + +While you might not need this level of detail to build and run an application with Cloud Native Buildpacks, it is useful to understand how they’re structured if you ever want to write your own buildpack. diff --git a/docs/src/ruby/intro.md b/docs/src/ruby/intro.md new file mode 100644 index 0000000..4b322c4 --- /dev/null +++ b/docs/src/ruby/intro.md @@ -0,0 +1,5 @@ +# Heroku Ruby Cloud Native Buildpack (CNB) Tutorial + +Build a Ruby on Rails application image in 5 minutes, no Dockerfile required. + +At the end of this tutorial, you'll have a working [OCI image](https://opencontainers.org/) of a Ruby on Rails application that can run locally. You will learn about the Cloud Native Buildpack (CNB) ecosystem, and how to utilize the [pack CLI](https://buildpacks.io/docs/for-platform-operators/how-to/integrate-ci/pack/) to build images without the need to write or maintain a Dockerfile. diff --git a/docs/src/ruby/multiple_langs.md b/docs/src/ruby/multiple_langs.md new file mode 100644 index 0000000..0a0db30 --- /dev/null +++ b/docs/src/ruby/multiple_langs.md @@ -0,0 +1,43 @@ +## Configuring multiple languages + +Language support is provided by individual buildpacks that are shipped with the builder. The above example uses the `heroku/ruby` buildpack which is [visible on GitHub](https://github.com/heroku/buildpacks-ruby). When you execute `pack build` with a builder, every buildpack has the opportunity to "detect" if it should execute against that project. The `heroku/ruby` buildpack looks for a `Gemfile.lock` in the root of the project and if found, knows how to detect a Ruby version and install dependencies. + +In addition to this auto-detection behavior, you can specify buildpacks through the `--buildpack` flag with the `pack` CLI or through a [project.toml](https://buildpacks.io/docs/for-app-developers/how-to/build-inputs/specify-buildpacks/) file at the root of your application. + +For example, if you wanted to install both Ruby, NodeJS and Python you could create a `project.toml` file in the root of your application and specify those buildpacks. + +```toml +:::>> file.write project.toml +[_] +schema-version = "0.2" +id = "sample.ruby+python.app" +name = "Sample Ruby & Python App" +version = "1.0.0" + +[[io.buildpacks.group]] +uri = "heroku/python" + +[[io.buildpacks.group]] +uri = "heroku/nodejs" + +[[io.buildpacks.group]] +uri = "heroku/ruby" + +[[io.buildpacks.group]] +uri = "heroku/procfile" +``` + +Ensure that a `requirements.txt` file, a `package.json` file and a `Gemfile.lock` file all exist and then build your application: + +``` +:::>> $ touch requirements.txt +:::>> $ pack build my-image-name --path . +``` + +You can run the image and inspect everything is installed as expected: + +``` +$ docker run -it --rm my-image-name bash +$ which python +:::-> $ docker run --rm my-image-name "which python" +``` diff --git a/docs/src/ruby/ruby_tutorial.md b/docs/src/ruby/ruby_tutorial.md new file mode 100644 index 0000000..6025e2e --- /dev/null +++ b/docs/src/ruby/ruby_tutorial.md @@ -0,0 +1,47 @@ +``` +:::>> rundoc.require "./intro.md" +``` + +``` +:::>> rundoc.require "../shared/install_pack.md" +``` + +``` +:::>> rundoc.require "../shared/configure_builder.md" +``` + +``` +:::>> rundoc.require "../shared/what_is_a_builder.md" +``` + +``` +:::>> rundoc.require "./download.md" +``` + +``` +:::>> rundoc.require "../shared/pack_build.md" +``` + +``` +:::>> rundoc.require "./what_is_pack_build.md" +``` + +``` +:::>> rundoc.require "../shared/use_the_image.md" +``` + +``` +:::>> rundoc.require "./image_structure.md" +``` + +``` +:::>> rundoc.require "../shared/call_to_action.md" +``` + +``` +:::>> rundoc.require "./multiple_langs.md" +``` + +``` +:::>> rundoc.require "../shared/procfile.md" +``` diff --git a/docs/src/ruby/what_is_pack_build.md b/docs/src/ruby/what_is_pack_build.md new file mode 100644 index 0000000..56634ba --- /dev/null +++ b/docs/src/ruby/what_is_pack_build.md @@ -0,0 +1,20 @@ +## What does `pack build` do? + +> [!NOTE] +> Skip ahead if you want to run the application first and get into the details later. + +When you run `pack build` with a builder, each buildpack runs a detection script to determine if it should be eligible to build the application. In our case the `heroku/ruby` buildpack found a `Gemfile.lock` file and `heroku/nodejs-engine` buildpack found a `package.json` file on disk. As a result, both buildpacks have enough information to install Ruby and Node dependencies. You can view a list of the buildpacks used in the output above: + + +``` +:::-> $ grep DETECTING -A5 ./build_output.txt +``` + +After the detect phase, each buildpack will execute. Buildpacks can inspect your project, install files to disk, run commands, write environment variables, [and more](https://buildpacks.io/docs/for-buildpack-authors/). You can see some examples of that in the output above. For example, the Ruby buildpack installs dependencies from the `Gemfile` automatically: + +``` +:::-> $ grep "bundle install" -m1 -A10 ./build_output.txt +``` + +If you’re familiar with Dockerfile you might know that [many commands in a Dockerfile will create a layer](https://dockerlabs.collabnix.com/beginners/dockerfile/Layering-Dockerfile.html). Buildpacks also use layers, but the CNB buildpack API provides for fine grained control over what exactly is in these layers and how they’re composed. Unlike Dockerfile, all images produced by CNBs [can be rebased](https://tag-env-sustainability.cncf.io/blog/2023-12-reduce-reuse-rebase-buildpacks/#reduce-reuserebase). The CNB api also improves on many of the pitfalls outlined in the satirical article [Write a Good Dockerfile in 19 'Easy' Steps](https://jkutner.github.io/2021/04/26/write-good-dockerfile.html). + diff --git a/docs/src/shared/call_to_action.md b/docs/src/shared/call_to_action.md new file mode 100644 index 0000000..119eba8 --- /dev/null +++ b/docs/src/shared/call_to_action.md @@ -0,0 +1,11 @@ +## Try CNBs out on your application + +So far we've learned that CNBs are a declarative interface for producing OCI images (like docker). They aim to be no to low configuration and once built, you can interact with them like any other image. + +For the next step, we encourage you to try running `pack` with the Heroku builder against your application and let us know how it went. We encourage you to share your experience by [opening a discussion](https://github.com/heroku/buildpacks/discussions) and walking us through what happened: + +- What went well? +- What could be better? +- Do you have any questions? + +We are actively working on our Cloud Native Buildpacks and want to hear about your experience. The documentation below covers some intermediate-level topics that you might find helpful. diff --git a/docs/src/shared/configure_builder.md b/docs/src/shared/configure_builder.md new file mode 100644 index 0000000..e96260b --- /dev/null +++ b/docs/src/shared/configure_builder.md @@ -0,0 +1,23 @@ +## Configure the default pack builder + +Once `pack` is installed, the only configuration you'll need for this tutorial is to set a default builder: + +``` +:::>> $ pack config default-builder heroku/builder:22 +``` + +You can view your default builder at any time: + +``` +:::>> $ pack config default-builder +``` + +The following tutorial is built on amd64 architecture (also known as x86). If you are building on a machine with different architecture (such as arm64/aarch64 for a Mac) you will need to tell Docker to use `linux/amd64` architecture. You can do this via a `--platform linux/amd64` flag or by exporting an environment variable: + +``` +$ export DOCKER_DEFAULT_PLATFORM=linux/amd64 +:::-- rundoc.configure +# Needed because all `$` commands are run as separate isolated processes + +ENV["DOCKER_DEFAULT_PLATFORM"] = "linux/amd64" +``` diff --git a/docs/src/shared/install_pack.md b/docs/src/shared/install_pack.md new file mode 100644 index 0000000..6c53e1a --- /dev/null +++ b/docs/src/shared/install_pack.md @@ -0,0 +1,13 @@ +## Install the pack CLI + +We assume you have [docker installed](https://docs.docker.com/engine/install/) and a working copy [of git](https://github.com/git-guides/install-git). Next, you will need to install the CLI tool for building CNBs, [pack CLI](https://buildpacks.io/docs/for-platform-operators/how-to/integrate-ci/pack/). If you're on a Mac you can install it via Homebrew: + +``` +$ brew install buildpacks/tap/pack +``` + +Ensure that `pack` is installed correctly: + +``` +:::>> $ pack --version +``` diff --git a/docs/src/shared/pack_build.md b/docs/src/shared/pack_build.md new file mode 100644 index 0000000..e230eea --- /dev/null +++ b/docs/src/shared/pack_build.md @@ -0,0 +1,20 @@ +## Build the application image with the pack CLI + +Now build an image named `my-image-name` by executing the heroku builder against the application by running the +`pack build` command: + +``` +$ pack build my-image-name --path . +:::-- $ docker rmi -f my-image-name +:::-- $ pack build my-image-name --path . 2>&1 | ruby -pe 'print Time.now.strftime("[%Y-%m-%d %H:%M:%S] ")' | tee build_output.txt +:::-> $ cat build_output.txt +``` + +> [!NOTE] +> Your output may differ. + +Verify that you see “Successfully built image my-image-name” at the end of the output. And verify that the image is present locally: + +``` +:::>> $ docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}" | grep my-image-name +``` diff --git a/docs/src/shared/procfile.md b/docs/src/shared/procfile.md new file mode 100644 index 0000000..9f1950a --- /dev/null +++ b/docs/src/shared/procfile.md @@ -0,0 +1,13 @@ +## Configuring your web process with the Procfile + +Most buildpacks rely on existing community standards to allow you to configure your application declaratively. They can also implement custom logic based on file contents on disk or environment variables present at build time. + +The `Procfile` is a configuration file format that was [introduced by Heroku in 2011](https://devcenter.heroku.com/articles/procfile), you can now use this behavior on your CNB-powered application via the `heroku/procfile`, which like the rest of the buildpacks in our builder [is open source](https://github.com/heroku/buildpacks-procfile). The `heroku/procfile` buildpack allows you to configure your web startup process. + +This is the `Procfile` of the getting started guide: + +``` +:::-> $ cat Procfile +``` + +By including this file and using `heroku/procfile` buildpack, your application will receive a default web process. You can configure this behavior by changing the contents of that file. diff --git a/docs/src/shared/use_the_image.md b/docs/src/shared/use_the_image.md new file mode 100644 index 0000000..a2202e3 --- /dev/null +++ b/docs/src/shared/use_the_image.md @@ -0,0 +1,52 @@ +## Use the image + +Even though we used `pack` and CNBs to build our image, it can be run with your favorite tools like any other OCI image. We will be using the `docker` command line to run our image. + +By default, images will be booted into a web server configuration. You can launch the app we just built by running: + +``` +$ docker run -it --rm --env PORT=9292 -p 9292:9292 my-image-name +:::-> background.start("docker run --rm --env PORT=9292 -p 9292:9292 my-image-name", name: "docker_server") +:::-- $ sleep 10 +:::-> background.log.read(name: "docker_server") +``` + +Now when you visit [http://localhost:9292](http://localhost:9292) you should see a working web application: + +``` +:::>> website.visit(name: "localhost", url: "http://localhost:9292") +:::>> website.screenshot(name: "localhost") +``` + +Don't forget to stop the docker container when you're done. + +``` +:::-- $ docker stop $(docker ps -q --filter ancestor=my-image-name ) +:::-- background.stop(name: "docker_server") +``` + +Here's a quick breakdown of that command we just ran: + +- `docker run` Create and run a new container from an image. +- `-it` Makes the container interactive and allocates a TTY. +- `--rm` Automatically remove the container when it exits. +- `--env PORT=9292` Creates an environment variable named `PORT` and sets it to `9292` this is needed so the application inside the container knows what port to bind the web server. +- `-p 9292:9292` Publishes a container's port(s) to the host. This is what allows requests from your machine to be received by the container. +- `my-image-name` The name of the image you want to use for the application. + +So far, we've downloaded an application via git and run a single command `pack build` to generate an image, and then we can use that image as if it was generated via a Dockerfile via the `docker run` command. + +In addition to running the image as a web server, you can access the container's terminal interactively. In a new terminal window try running this command: + +``` +$ docker run -it --rm my-image-name bash +``` + +Now you can inspect the container interactively. For example, you can see the files on disk with `ls`: + +``` +$ ls +:::-> $ docker run --rm my-image-name ls +``` + +And anything else you would typically do via an interactive container session. diff --git a/docs/src/shared/what_is_a_builder.md b/docs/src/shared/what_is_a_builder.md new file mode 100644 index 0000000..6182584 --- /dev/null +++ b/docs/src/shared/what_is_a_builder.md @@ -0,0 +1,18 @@ +## What is a builder? + +> [!NOTE] +> Skip ahead if you want to build the application first and get into the details later. You won't need to +> know about builders for the rest of this tutorial. + +In short, a builder is a delivery mechanism for buildpacks. A builder contains references to base images and individual buildpacks. A base image contains the operating system and system dependencies. Buildpacks are the components that will configure an image to run your application, that’s where the bulk of the logic lives and why the project is called “Cloud Native Buildpacks” and not “Cloud Native Builders.” + +You can view the contents of a builder via the command `pack builder inspect`. For example: + +``` +:::>> $ pack builder inspect heroku/builder:22 | grep Buildpacks: -m1 -A10 +``` + +> [!NOTE] +> Your output version numbers may differ. + +This output shows the various buildpacks that represent the different languages that are supported by this builder such as `heroku/go` and `heroku/nodejs-engine`.