Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

expands platform lifecycle tutorial #769

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This information is used during the `export` phase in order to avoid re-uploadin
Starting from `Platform API 0.7`, the `analyze` phase runs before the `detect` phase in order to validate registry access for all images that are used during the `build` as early as possible. In this way it provides faster failures for end users. The other responsibilities of the `analyzer` were moved to the `restorer`.\
For more information, please see [this migration guide][platform-api-06-07-migration].

The `lifecycle` should attempt to locate a reference to the latest `OCI image` from a previous build that is readable and was created by the `lifecycle` using the same application source code. If no such reference is found, the `analysis` is skipped.\
The `lifecycle` should attempt to locate a reference to the latest `OCI image` from a previous build that is readable and was created by the `lifecycle` using the same application source code. If no such reference is found, the `analysis` is skipped.

The `lifecycle` must write [analysis metadata][analyzedtoml-toml] to `<analyzed>`, where:

Expand Down
303 changes: 302 additions & 1 deletion content/docs/for-platform-operators/tutorials/lifecycle/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,305 @@ expand=true
include_summaries=true
+++

Coming soon! In the meantime check out this excellent [blog post](https://medium.com/buildpacks/unpacking-cloud-native-buildpacks-ff51b5a767bf) from our community.
A `platform` orchestrates builds by invoking the [lifecycle][lifecycle] binary together with buildpacks and application source code to produce a runnable `OCI image`.

<!--more-->

The majority of Buildpack users use platforms, such as [pack][pack] and [kpack][kpack], to run Buildpacks and create `OCI images`. However this might not be desireable especially for users maintaining their own platforms and seeking more control over how the underlying Buildpack `lifecycle phases` are executed.
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

> This tutorial is derived from a [blog post][blog post] contributed by one of our community.
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

In this step-by-step tutorial, you will build a `Bash` application without using any `platform` tooling like `pack` or `kpack`. You will also leverage the individual `lifecycle phases` to produce a runnable application image.
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

## Prerequisites

You'll need to clone a local copy of the following to get started:
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

* The `lifecycle`
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

```text
git clone https://github.com/buildpacks/lifecycle
```

* The official `Buildpack.io` samples repo

```text
git clone https://github.com/buildpacks/samples
```

As previously mentioned, the `lifecycle` orchestrates `Buildpacks` then assembles the resulting artifacts into an `OCI image`. The `lifecycle` is composed of a series of distinct `phases` that need to be executed to have the final image built and exported.

## Overview

Now that you’re set up, let’s build our `Bash` application and dive deeper into the `lifecycle` and its phases.

### Build the lifecycle

As a starting step, you need to build the `lifecycle` in order to use its phases. This could be done by navigating to the `lifecycle` directory and executing one of the following commands, depending on your system architecture.

* `make build` for `AMD64` architectures (for Linux users)
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
* `make build-darwin-arm64` for `ARM64` architectures (for Mac users)

> Please note that the entire process is most easily followed on Linux systems
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

### Set environment variables

In order to execute the various `lifecycle phases` correctly, you first need to set the values of few important environment variables by running the following commands in the terminal:

```text
export CNB_USER_ID=$(id -u) CNB_GROUP_ID=$(id -g) CNB_PLATFORM_API=0.14
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
export CNB_SAMPLES_PATH="/<your-path>/samples"
export CNB_LIFECYCLE_PATH="/<your-path/lifecycle/out/<your-arch>/lifecycle"
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
```

Where

* `CNB_USER_ID` and `CNB_GROUP_ID` are arbitrary values that need to be consistent. This example re-uses our user id and group id for the `CNB` user. In a production system, these are commonly set to `1000`.
* `CNB_PLATFORM_API` or the `Platform API` version, varies depending on the use case. This tutorial uses `v0.14`, which is the latest [Platform API][Platform API] version.
* `CNB_SAMPLES_PATH` represents the path of our local copy of the `samples` directory.
* `CNB_LIFECYCLE_PATH` represents the path of our local compiled `lifecycle` directory.

### Examine lifecycle phases

A single app image build consists of the following phases:

1. [Analysis](#analyze)
2. [Detection](#detect)
3. [Cache Restoration](#restore)
4. [Build](#build)
5. [Export](#export)

> Note that a `platform` executes the phases above either by invoking phase-specific lifecycle binaries in order or by executing `/cnb/lifecycle/creator`.

Let's expand each `lifecycle` phase to explain how the `lifecycle` orchestrates buildpacks:

#### Analyze

The `analyze` phase runs before the `detect` phase in order to validate registry access for all images used during the `build` as early as possible. In this way it provides faster failures for end users.

Prior to executing `/cnb/lifecycle/analyzer`, you need to create two directories in the `root` directory as follows:
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

```text
mkdir -p apps/bash-script
mkdir -p layers
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"root" typically means /, but in my case, running as a non-root user, I didn't have permission to make a directory there. I suggest we tell the user to make a "build root" directory and then update the commands to look something like:

cd <build root>
mkdir -p apps/bash-script
mkdir -p layers

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used /tmp/tutorial as the root for my run-through.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is an simple solution to create a tmp dir for all this work?

TUTORIAL_ROOT=$(mktemp -d)
cd ${TUTORIAL_ROOT}
...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added code suggestions


* `apps` directory that contains a `bash-script` directory
* `layers` directory that contains subdirectories representing each layer created by the Buildpack in the final image or build cache.

Next, you need to copy the `bash-script` samples into our `apps/bash-script` directory, which will host our app's source code.

```text
cp -r "${CNB_SAMPLES_PATH}/apps/bash-script/*" ./apps/bash-script
AidanDelaney marked this conversation as resolved.
Show resolved Hide resolved
```

Now, you can invoke the `analyzer` for `AMD64` architecture

```text
${CNB_LIFECYCLE_PATH}/analyzer -log-level debug -daemon -layers="./layers" -run-image cnbs/sample-stack-run:jammy apps/bash-script
```

Or if you are on an `ARM64` platform

```text
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
${CNB_LIFECYCLE_PATH}/analyzer -log-level debug -daemon -layers="./layers" -run-image arm64v8/ubuntu:latest apps/bash-script
```

The commands above run the `analyzer` with:

* A `debug` logging level
* Pointing to the local `Docker daemon`
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
* Pointing to the `layers` directory, which is the main `lifecycle` working directory
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
* Running the specified image
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
* The path to the app that you are analyzing
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

Now the `analyzer`:

* Checks a registry for previous images called `apps/bash-script`.
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
* Resolves the image metadata making it available to the subsequent `restore` phase.
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
* Verifies that you have write access to the registry to create or update the image called `apps/bash-script`.
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

In this tutorial, there is no previous `apps/bash-script` image, and the output produced should be similar to the following:

```text
sample-stack-run:jammy apps/bash-script
Starting analyzer...
Parsing inputs...
Ensuring privileges...
Executing command...
Timer: Analyzer started at 2024-09-30T07:38:14Z
Image with name "apps/bash-script" not found
Image with name "cnbs/sample-stack-run:jammy" not found
Timer: Analyzer ran for 41.92µs and ended at 2024-09-30T07:38:14Z
Run image info in analyzed metadata is:
{"Reference":"","Image":"cnbs/sample-stack-run:jammy","Extend":false,"target":{"os":"linux","arch":"amd64"}}
```

Now if you check the `layers` directory, you should have a `analyzed.toml` file with a few null entries.
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

#### Detect

In this phase, the `detector` looks for an ordered group of buildpacks that will be used during the `build` phase. The `detector` requires an `order.toml` file being present in the `root` directory, which you could derive from `builder.toml` in the `samples` directory while removing the deprecated `stack` section as follows:
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

```text
cat "${CNB_SAMPLES_PATH}/builders/jammy/builder.toml" | grep -v -i "stack" | sed 's/\.\.\/\.\./\./' > order.toml

```

`order.toml` files contain a list of groups with each group containing a list of buildpacks. The `detector` reads `order.toml` and looks for the first group that passes the detection process.
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

##### Set buildpacks layout directory

Before running the `detector`, you need to:

1. Create a `buildpacks` directory in the `root` directory

```text
mkdir -p buildpacks
```

2. Then you must populate the `buildpacks` directory with your buildpacks of interest.

> You have to follow the [directory layout][directory layout] defined in the buildpack spec, where each top-level directory is a `buildpack ID` and each second-level directory is a `buildpack version`.

hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
We will use [`dasel`](http://github.com/tomwright/dasel/) to help us parse toml files.

```command
Copy link
Member

@natalieparellano natalieparellano Oct 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a little bit heavy and kind of hides what is happening under the scenes. Can we just tell the end user what the directory tree should look like? (Or maybe we could do both)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't run the command so can't really comment on this.
@AidanDelaney could you take a look at this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic for loop creates a directory structure that looks like the following listing. In short, it takes our sample buildpacks from the source directory and puts them into the directory structure that lifecycle expects:

$ tree buildpacks/
buildpacks/
├── samples_dotnet-framework
│   └── 0.0.1
│       └── dotnet-framework
│           ├── bin
│           │   ├── build.bat
│           │   └── detect.bat
│           ├── buildpack.toml
│           └── package.toml
├── samples_hello-extensions
│   └── 0.0.1
│       └── hello-extensions
│           ├── bin
│           │   ├── build
│           │   └── detect
│           └── buildpack.toml
├── samples_hello-moon
│   └── 0.0.1
│       └── hello-moon
│           ├── bin
│           │   ├── build
│           │   └── detect
│           ├── buildpack.toml
│           └── README.md
├── samples_hello-moon-windows
│   └── 0.0.1
│       └── hello-moon-windows
│           ├── bin
│           │   ├── build.bat
│           │   └── detect.bat
│           ├── buildpack.toml
│           └── README.md
├── samples_hello-processes
│   └── 0.0.1
│       └── hello-processes
│           ├── bin
│           │   ├── build
│           │   └── detect
│           ├── buildpack.toml
│           └── README.md
├── samples_hello-universe
│   └── 0.0.1
│       └── hello-universe
│           ├── buildpack.toml
│           ├── package.toml
│           └── README.md
├── samples_hello-universe-windows
│   └── 0.0.1
│       └── hello-universe-windows
│           ├── buildpack.toml
│           ├── package.toml
│           └── README.md
├── samples_hello-world
│   └── 0.0.1
│       └── hello-world
│           ├── bin
│           │   ├── build
│           │   └── detect
│           ├── buildpack.toml
│           ├── package.toml
│           └── README.md
├── samples_hello-world-windows
│   └── 0.0.1
│       └── hello-world-windows
│           ├── bin
│           │   ├── build.bat
│           │   └── detect.bat
│           ├── buildpack.toml
│           ├── package.toml
│           └── README.md
├── samples_java-maven
│   └── 0.0.2
│       └── java-maven
│           ├── bin
│           │   ├── build
│           │   └── detect
│           ├── buildpack.toml
│           └── README.md
├── samples_kotlin-gradle
│   └── 0.0.2
│       └── kotlin-gradle
│           ├── bin
│           │   ├── build
│           │   └── detect
│           ├── buildpack.toml
│           └── README.md
└── samples_ruby-bundler
    └── 0.0.1
        └── ruby-bundler
            ├── bin
            │   ├── build
            │   └── detect
            ├── buildpack.toml
            └── README.md

46 directories, 47 files

@natalieparellano Do yuu think that explanation and the directory listing is sufficient? We can include the example along with the difficult to understand shell loop to generate the structure.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good - let's show the tree for just one buildpack to keep things short. Maybe before / after would explain it best

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about

Before running this command, the samples/buildpacks directory has the following structure:

├── hello-world
│   ├── README.md
│   ├── bin
│   │   ├── build
│   │   └── detect
│   ├── buildpack.toml
│   └── package.toml
... more buildpacks ...

After, it has the following structure:

├── samples_hello-world 
│ └── 0.0.1
│  └── hello-world
│  ├── bin
│  │ ├── build
│  │ └── detect
│  ├── buildpack.toml
│  ├── package.toml
│  └── README.md
... more buildpacks ...

$ go install github.com/tomwright/dasel/v2/cmd/dasel@master

Let’s do that for every buildpack in the `samples/buildpacks` directory:

```text
for f in $(ls --color=no ${CNB_SAMPLES_PATH}/buildpacks | grep -v README)
do
bp_version=$(cat $f | dasel -r toml "${CNB_SAMPLES_PATH}/buildpacks/${f}/buildpack.version" | sed s/\'//g)
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
mkdir -p ./buildpacks/samples_"${f}"/${bp_version}
cp -r "$CNB_SAMPLES_PATH/buildpacks/${f}/" ./buildpacks/samples_"${f}"/${bp_version}/
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
done
```

Now, you can run the `detector` binary:

```text
${CNB_LIFECYCLE_PATH}/detector -log-level debug -layers="./layers" -order="./order.toml" -buildpacks="./buildpacks" -app apps/bash-script
```

The output of the above command should have `group.toml` and `plan.toml` output files (i.e., the groups that have passed the detection have been written into the `group.toml` file writing its build plan into the `plan.toml` file)
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

```text
OUTPUT PLACEHOLDER
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
```

You can view more details about the [order](https://buildpacks.io/docs/for-platform-operators/concepts/lifecycle/detect/#ordertoml), [group](https://buildpacks.io/docs/for-platform-operators/concepts/lifecycle/detect/#grouptoml) and [plan](https://buildpacks.io/docs/concepts/components/lifecycle/detect/#plantoml) toml files in the platform documentation.
Copy link
Member

@natalieparellano natalieparellano Oct 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can add some minimal explanatory text here, similar to what we have for the analyzer - I'll leave a code suggestion hopefully "soon"

Just links would be fine

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we link to https://buildpacks.io/docs/for-platform-operators/concepts/lifecycle/analyze/ for the explanation of what analyzer does?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works for me - in general, we could add those links for every phase


#### Restore

The `restorer` retrieves cache contents, if it contains any, into the build container. During this phase, the `restorer` looks for layers that could be reused or should be replaced while building the application image.
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

First, you need to create the `cache` directory, and then run the `restorer` binary as added below:
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

```text
mkdir cache
```

```text
${CNB_LIFECYCLE_PATH}/restorer -log-level debug -layers="./layers" -group="./layers/group.toml" -cache-dir="./cache" -analyzed="./layers/analyzed.toml"
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
```

The `cache` directory should now be populated by two sub-directories, `committed` and `staging` as shown in the output below:

```text
OUTPUT PLACEHOLDER
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
```

#### Build

The `builder` transforms application source code into runnable artifacts that can be packaged into a container.
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

Before running the `builder`, the following steps are required:

1. Create two directories:
* `platform` directory to store configurations and environment variables
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
* `workspace` directory to store application source code and where you build it
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

```text
mkdir -p platform
mkdir -p workspace
```

2. Copy the source code from the `app` directory to the `workspace` directory

```text
cp -r apps/bash-script/* ./workspace
```

3. Create a `launcher` file with instructions to run your application
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I don't think we should create this file - it is the buildpack that does that during a normal build. We should probably just update our hello world buildpack to not be a complete no-op and actually contribute a process (cc @AidanDelaney)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack. Can we keep this right now and create an issue for me to update the samples?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline, we'll create an issue and add a note here that we're faking the work of the buildpack

hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

```text
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
cat << EOF > ./layers/samples_hello-moon/launch.toml
[[processes]]
type = "shell"
command = ["./app.sh"]
EOF
```

Now you're ready to run the `builder` as follows:

```text
${CNB_LIFECYCLE_PATH}/builder -log-level debug -layers="./layers" -group="./layers/group.toml" -analyzed="./layers/analyzed.toml" -plan="./layers/plan.toml" -buildpacks="./buildpacks" -app="./workspace" -platform="./platform"
```

Taking a deep look at the following output shows that you have built the two buildpacks that we need in order to run our `bash-script` application. In addition, checking the `layers` directory should show other directories like the two from the buildpacks, a `config` and a `sbom` ones.
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

```text
OUTPUT PLACEHOLDER
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
```

#### Export

The purpose of the `export` phase is to create a new `OCI` image using a combination of remote layers, local `<layers>/<layer>` layers, and the processed `app` directory.
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

To export the artifacts built by the `builder`, you first need to specify the path of the `launcher` that your image is going to run:
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

* For AMD64 architectures

```text
export {CNB_LINUX_LAUNCHER_PATH}=/<your-path>/lifecycle/out/linux-amd64/lifecycle/launcher
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
```

* For ARM64 Architectures

```text
export {CNB_LINUX_LAUNCHER_PATH}=/<your-path>/lifecycle/out/linux-arm64/lifecycle/launcher
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
```

Now you can run the `exporter`:

```text
${CNB_LIFECYCLE_PATH}/exporter --log-level debug -launch-cache "./cache" -daemon -cache-dir "./cache" -analyzed "./layers/analyzed.toml" -group "./layers/group.toml" -layers="./layers" -app "./workspace" -launcher="${CNB_LINUX_LAUNCHER_PATH}" -process-type="shell" apps/bash-script
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
```

You can verify that the image was successfully exported by running the `docker images` command.

### Run the app image

Finally, you can run the exported image as follows:

```text
docker run -it apps/bash-script ./app.sh
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved
```

```text
OUTPUT PLACEHOLDER
```

## Wrapping up

At the end of this tutorial, we hope that you now have a better understanding on how you could use `Buildpacks` to create container images. You are now ready to explore this technology further and customize it to fit your application development and deployment needs.
hyounes4560 marked this conversation as resolved.
Show resolved Hide resolved

[pack]: https://buildpacks.io/docs/for-platform-operators/how-to/integrate-ci/pack/
[kpack]: https://buildpacks.io/docs/for-platform-operators/how-to/integrate-ci/kpack/
[lifecycle]: https://buildpacks.io/docs/for-platform-operators/concepts/lifecycle/
[directory layout]: https://github.com/buildpacks/spec/blob/main/platform.md#buildpacks-directory-layout
[Platform API]: https://github.com/buildpacks/spec/releases?q=platform
[blog post]: https://medium.com/buildpacks/unpacking-cloud-native-buildpacks-ff51b5a767bf
Loading