From d95055ea737d11b50a45c58dd310e51080e20537 Mon Sep 17 00:00:00 2001 From: Ben Meier Date: Thu, 20 Jun 2024 17:24:23 +0100 Subject: [PATCH 1/4] feat: added documentation for ci/cd pipeline Signed-off-by: Ben Meier --- content/en/docs/How to/github.md | 4 + .../en/docs/How to/score-cicd-pipelines.md | 193 ++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 content/en/docs/How to/score-cicd-pipelines.md diff --git a/content/en/docs/How to/github.md b/content/en/docs/How to/github.md index 4e6d3f60..e0a7c8cf 100644 --- a/content/en/docs/How to/github.md +++ b/content/en/docs/How to/github.md @@ -26,6 +26,7 @@ steps: with: file: version: '' + token: ${{ secrets.GITHUB_TOKEN }} - run: score-compose --version ``` @@ -37,6 +38,8 @@ The action accepts the following inputs: - `version` - The version of the CLI to install. You can retrieve the latest version from the release page of the respective CLI tool you're working with. For example [score-compose releases](https://github.com/score-spec/score-compose/releases) or [score-helm releases](https://github.com/score-spec/score-helm/releases). +- `token` - The Github Actions Token in the environment (`${{ secrets.GITHUB_TOKEN }}`). + ## Example Here is a complete example workflow: @@ -55,6 +58,7 @@ jobs: with: file: score-compose version: '0.13.0' + token: ${{ secrets.GITHUB_TOKEN }} - run: score-compose --version ``` diff --git a/content/en/docs/How to/score-cicd-pipelines.md b/content/en/docs/How to/score-cicd-pipelines.md new file mode 100644 index 00000000..c4302650 --- /dev/null +++ b/content/en/docs/How to/score-cicd-pipelines.md @@ -0,0 +1,193 @@ +--- +title: "CI/CD pipelines with Score" +subtitle: "How to combine multiple Score implementations into a CI pipeline for testing and deployment" +linkTitle: CI/CD pipelines with Score +weight: 2 +draft: false +description: > + How to combine multiple Score implementations into a CI pipeline for testing and deployment +--- + +A big benefit of the Score workload specification is that it allows the same workload (or workloads) to be deployed in different contexts on different container runtimes. Often this is used to support local development, but it is equally valuable within the Continuous Integration (CI) pipeline and production deployment too. This guide illustrates how to combine the two reference Score implementations into a Github Actions CI/CD pipeline that uses [`score-compose`]({{< relref "/docs/score implementation/score-compose.md" >}}) for testing within the CI Pipeline, followed by [`score-k8s`]({{< relref "/docs/score implementation/score-k8s.md" >}}) for production deployment. + +The instructions below are shown for Github Actions but can be used as inspiration for a similar process in any other CI tool. + +## The Score file + +Any valid Score file could be used for this guide as long as it uses resource types supported by `score-compose` and `score-k8s` CLIs or which are provided by custom provisioner files. In the example below, there is a simple container with a `dns` and `route` resource. + +```yaml +apiVersion: score.dev/v1b1 +metadata: + name: hello-world + +containers: + web: + image: . + +service: + ports: + web: + port: 8080 + targetPort: 80 + +resources: + example-dns: + type: dns + example-route: + type: route + params: + port: 8080 + host: ${resources.example-dns.dns.host} + path: / +``` + +Notice that the image is "." since the tag is not yet known until the build executes. The image is provided by a Docker file: + +```Dockerfile +FROM nginx:latest +RUN echo "Score Example" > /usr/share/nginx/html/index.html +``` + +## Setting up a Github Actions Pipeline + +In the source repository, the `.github/workflows/ci.yaml` file will contain the workflow definition. + +### Triggers + +The file starts with the definition of how the workflow is triggered. In this case, it will run on any pull requests and merges into the main branch. The pull requests must only use `score-compose` while the production release will use `score-k8s`. + +```yaml +name: CI +on: + pull_request: + branches: [ "*" ] + push: + branches: [ "main" ] +``` + +### Building the image + +The first job in the workflow will build and tag the image locally with a semantic version. This is not Score-specific and can be changed completely for the target project. + +```yaml +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: paulhatch/semantic-version@v5.0.3 + id: semver + - uses: docker/build-push-action@v5 + with: + load: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.semver.outputs.version }} + cache-from: type=gha + cache-to: type=gha,mode=max +``` + +### Testing with `score-compose` + +The next set of steps will focus on testing with `score-compose`. This provides value because it: + +1. Tests that the `score.yaml` file is valid. +2. Tests that the resource provisioning and outputs work as expected. +3. Launches the container with all dependencies and runs a basic test to check that the web server works as expected. In reality, this can be replaced with an arbitrarily complex test suite for code coverage. + +This helps to maximize the chance that the "release" step to production will succeed and result in a working application. + +```yaml + - uses: score-spec/setup-score@v2 + with: + file: score-compose + version: 0.15.6 + token: ${{ secrets.GITHUB_TOKEN }} + - run: score-compose init + - run: score-compose generate score.yaml --image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.semver.outputs.version }} + - run: docker compose up -d + # Integration tests here + - run: curl http://$(score-compose resources get-outputs 'dns.default#hello-world.example-dns' --format '{{.host}}'):8080 -v | tee | grep 'Score Example' + outputs: + version: ${{ steps.semver.outputs.version }} +``` + +### Publish the image and deploy the Kubernetes manifests + +The second job in the workflow is the `release` job. This job pushes the image up to a remote container registry, converts the Score manifest into Kubernetes manifests, and then deploys those manifests to a target cluster. + +First, the registry login, and image build, followed by the push. + +```yaml +jobs: + ... + release: + runs-on: ubuntu-latest + needs: build-and-test + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/setup-buildx-action@v3 + - uses: docker/build-push-action@v6 + with: + platforms: linux/amd64 + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-test.outputs.version }} + cache-from: type=gha + cache-to: type=gha,mode=max +``` + +And finally, the steps to convert to Kubernetes manifests and deploy them. Notice that the `generate` call is setting the image to the tag that was just uploaded in the previous steps. The `azure/` actions are maintained by Azure, but are not Azure-specific and can deploy to any generic Kubernetes cluster as needed. Notice that this requires a `KUBECONFIG` secret variable set in the Github Actions workflow to authenticate with the target cluster. + +```yaml + - uses: score-spec/setup-score@v2 + with: + file: score-k8s + version: 0.1.5 + token: ${{ secrets.GITHUB_TOKEN }} + - uses: azure/k8s-set-context@v2 + with: + method: kubeconfig + kubeconfig: ${{ secrets.KUBECONFIG }} + - run: score-k8s init + - run: score-k8s generate score.yaml --image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-test.outputs.version }} + - uses: azure/k8s-deploy@v1 + with: + namespace: default + manifests: ./manifests +``` + +This workflow can now test, push, and deploy a Score application. However, there is a problem that remains: the `.score-k8s/state.yaml` file. + +### Maintaining `score-k8s` state + +CI Workflows typically start with a clean slate every time they execute. No state is stored on disk between runs. However, `score-k8s` _does_ store unique data, random seeds, and non-hermetic attributes in a `.score-k8s/state.yaml` file. For best results, this file should be restored before running `score-k8s` generate. + +In this example, the file is stored as a secret in the target Kubernetes cluster. First, it is downloaded before running init or generate: + +```yaml + - run: kubectl get secret -n default score-k8s-state-yaml -o json | jq '.data.content' > .score-k8s/state.yaml +``` + +And then it can be uploaded again after the deployment: + +```yaml + - run: kubectl create secret generic -n default score-k8s-state-yaml --from-file=content=.score-k8s/state.yaml +``` + +It's a good idea to restrict the concurrency of this job so that concurrent jobs do not overwrite files incorrectly: + + +```yaml +jobs: + release: + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +``` From 88da9420a8bf98768bb47b07cfe12c7bfbf217ed Mon Sep 17 00:00:00 2001 From: Ben Meier Date: Thu, 20 Jun 2024 17:30:28 +0100 Subject: [PATCH 2/4] fix: review issues Signed-off-by: Ben Meier --- .../en/docs/How to/score-cicd-pipelines.md | 77 +++++++++---------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/content/en/docs/How to/score-cicd-pipelines.md b/content/en/docs/How to/score-cicd-pipelines.md index c4302650..3be66f70 100644 --- a/content/en/docs/How to/score-cicd-pipelines.md +++ b/content/en/docs/How to/score-cicd-pipelines.md @@ -8,7 +8,7 @@ description: > How to combine multiple Score implementations into a CI pipeline for testing and deployment --- -A big benefit of the Score workload specification is that it allows the same workload (or workloads) to be deployed in different contexts on different container runtimes. Often this is used to support local development, but it is equally valuable within the Continuous Integration (CI) pipeline and production deployment too. This guide illustrates how to combine the two reference Score implementations into a Github Actions CI/CD pipeline that uses [`score-compose`]({{< relref "/docs/score implementation/score-compose.md" >}}) for testing within the CI Pipeline, followed by [`score-k8s`]({{< relref "/docs/score implementation/score-k8s.md" >}}) for production deployment. +A big benefit of the Score workload specification is that it allows the same workload, or workloads, to be deployed in different contexts on different container runtimes. Often this is used to support local development, but it's equally valuable within the Continuous Integration (CI) pipeline and production deployment too. This guide illustrates how to combine the two reference Score implementations into a Github Actions CI/CD pipeline that uses [`score-compose`]({{< relref "/docs/score implementation/score-compose.md" >}}) for testing within the CI Pipeline, followed by [`score-k8s`]({{< relref "/docs/score implementation/score-k8s.md" >}}) for production deployment. The instructions below are shown for Github Actions but can be used as inspiration for a similar process in any other CI tool. @@ -42,7 +42,7 @@ resources: path: / ``` -Notice that the image is "." since the tag is not yet known until the build executes. The image is provided by a Docker file: +Notice that the image is "." since the tag isn't yet known until the build executes. The image is provided by a Docker file: ```Dockerfile FROM nginx:latest @@ -51,11 +51,11 @@ RUN echo "Score Example" > /usr/share/nginx/html/index.html ## Setting up a Github Actions Pipeline -In the source repository, the `.github/workflows/ci.yaml` file will contain the workflow definition. +In the source repository, the `.github/workflows/ci.yaml` file contains the workflow definition. ### Triggers -The file starts with the definition of how the workflow is triggered. In this case, it will run on any pull requests and merges into the main branch. The pull requests must only use `score-compose` while the production release will use `score-k8s`. +The file starts with the definition of how the workflow is triggered. In this case, it runs on any pull requests and merges into the main branch. The pull requests must only use `score-compose` while the production release uses `score-k8s`. ```yaml name: CI @@ -68,7 +68,7 @@ on: ### Building the image -The first job in the workflow will build and tag the image locally with a semantic version. This is not Score-specific and can be changed completely for the target project. +The first job in the workflow builds and tags the image locally with a semantic version. This isn't Score-specific and can be changed completely for the target project. ```yaml jobs: @@ -89,27 +89,27 @@ jobs: ### Testing with `score-compose` -The next set of steps will focus on testing with `score-compose`. This provides value because it: +The next set of steps focuses on testing with `score-compose`. This provides value because it: 1. Tests that the `score.yaml` file is valid. 2. Tests that the resource provisioning and outputs work as expected. 3. Launches the container with all dependencies and runs a basic test to check that the web server works as expected. In reality, this can be replaced with an arbitrarily complex test suite for code coverage. -This helps to maximize the chance that the "release" step to production will succeed and result in a working application. +This helps to maximize the chance that the "release" step to production succeeds and results in a working application. ```yaml - - uses: score-spec/setup-score@v2 - with: - file: score-compose - version: 0.15.6 - token: ${{ secrets.GITHUB_TOKEN }} - - run: score-compose init - - run: score-compose generate score.yaml --image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.semver.outputs.version }} - - run: docker compose up -d - # Integration tests here - - run: curl http://$(score-compose resources get-outputs 'dns.default#hello-world.example-dns' --format '{{.host}}'):8080 -v | tee | grep 'Score Example' - outputs: - version: ${{ steps.semver.outputs.version }} + - uses: score-spec/setup-score@v2 + with: + file: score-compose + version: 0.15.6 + token: ${{ secrets.GITHUB_TOKEN }} + - run: score-compose init + - run: score-compose generate score.yaml --image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.semver.outputs.version }} + - run: docker compose up -d + # Integration tests here + - run: curl http://$(score-compose resources get-outputs 'dns.default#hello-world.example-dns' --format '{{.host}}'):8080 -v | tee | grep 'Score Example' +outputs: + version: ${{ steps.semver.outputs.version }} ``` ### Publish the image and deploy the Kubernetes manifests @@ -147,21 +147,21 @@ jobs: And finally, the steps to convert to Kubernetes manifests and deploy them. Notice that the `generate` call is setting the image to the tag that was just uploaded in the previous steps. The `azure/` actions are maintained by Azure, but are not Azure-specific and can deploy to any generic Kubernetes cluster as needed. Notice that this requires a `KUBECONFIG` secret variable set in the Github Actions workflow to authenticate with the target cluster. ```yaml - - uses: score-spec/setup-score@v2 - with: - file: score-k8s - version: 0.1.5 - token: ${{ secrets.GITHUB_TOKEN }} - - uses: azure/k8s-set-context@v2 - with: - method: kubeconfig - kubeconfig: ${{ secrets.KUBECONFIG }} - - run: score-k8s init - - run: score-k8s generate score.yaml --image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-test.outputs.version }} - - uses: azure/k8s-deploy@v1 - with: - namespace: default - manifests: ./manifests +- uses: score-spec/setup-score@v2 + with: + file: score-k8s + version: 0.1.5 + token: ${{ secrets.GITHUB_TOKEN }} +- uses: azure/k8s-set-context@v2 + with: + method: kubeconfig + kubeconfig: ${{ secrets.KUBECONFIG }} +- run: score-k8s init +- run: score-k8s generate score.yaml --image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-test.outputs.version }} +- uses: azure/k8s-deploy@v1 + with: + namespace: default + manifests: ./manifests ``` This workflow can now test, push, and deploy a Score application. However, there is a problem that remains: the `.score-k8s/state.yaml` file. @@ -170,20 +170,19 @@ This workflow can now test, push, and deploy a Score application. However, there CI Workflows typically start with a clean slate every time they execute. No state is stored on disk between runs. However, `score-k8s` _does_ store unique data, random seeds, and non-hermetic attributes in a `.score-k8s/state.yaml` file. For best results, this file should be restored before running `score-k8s` generate. -In this example, the file is stored as a secret in the target Kubernetes cluster. First, it is downloaded before running init or generate: +In this example, the file is stored as a secret in the target Kubernetes cluster. First, it's downloaded before running init or generate: ```yaml - - run: kubectl get secret -n default score-k8s-state-yaml -o json | jq '.data.content' > .score-k8s/state.yaml +- run: kubectl get secret -n default score-k8s-state-yaml -o json | jq '.data.content' > .score-k8s/state.yaml ``` And then it can be uploaded again after the deployment: ```yaml - - run: kubectl create secret generic -n default score-k8s-state-yaml --from-file=content=.score-k8s/state.yaml +- run: kubectl create secret generic -n default score-k8s-state-yaml --from-file=content=.score-k8s/state.yaml ``` -It's a good idea to restrict the concurrency of this job so that concurrent jobs do not overwrite files incorrectly: - +It's a good idea to restrict the concurrency of this job so that concurrent jobs don't overwrite files incorrectly: ```yaml jobs: From da8a321fd97af146ac2af1dd1df9e8829decd722 Mon Sep 17 00:00:00 2001 From: Ben Meier <1651305+astromechza@users.noreply.github.com> Date: Fri, 21 Jun 2024 00:05:25 +0100 Subject: [PATCH 3/4] fix: review typo Co-authored-by: Mathieu Benoit Signed-off-by: Ben Meier <1651305+astromechza@users.noreply.github.com> --- content/en/docs/How to/score-cicd-pipelines.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/en/docs/How to/score-cicd-pipelines.md b/content/en/docs/How to/score-cicd-pipelines.md index 3be66f70..b6d1b2ba 100644 --- a/content/en/docs/How to/score-cicd-pipelines.md +++ b/content/en/docs/How to/score-cicd-pipelines.md @@ -49,7 +49,7 @@ FROM nginx:latest RUN echo "Score Example" > /usr/share/nginx/html/index.html ``` -## Setting up a Github Actions Pipeline +## Setting up a GitHub Actions Pipeline In the source repository, the `.github/workflows/ci.yaml` file contains the workflow definition. From d9382e748c6ec6f079bfd7528950885287253e86 Mon Sep 17 00:00:00 2001 From: Ben Meier Date: Fri, 21 Jun 2024 08:59:21 +0100 Subject: [PATCH 4/4] chore: added managed score note Signed-off-by: Ben Meier --- content/en/docs/How to/score-cicd-pipelines.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/content/en/docs/How to/score-cicd-pipelines.md b/content/en/docs/How to/score-cicd-pipelines.md index b6d1b2ba..5ddcec0d 100644 --- a/content/en/docs/How to/score-cicd-pipelines.md +++ b/content/en/docs/How to/score-cicd-pipelines.md @@ -164,6 +164,12 @@ And finally, the steps to convert to Kubernetes manifests and deploy them. Notic manifests: ./manifests ``` +With the Score file in this example, the generated `manifests.yaml` file contains: + +- `apps/v1/Deployment` - the app itself +- `v1/Service` - the internal service +- `gateway.networking.k8s.io/v1/HTTPRoute` - the HTTP route from the ingress DNS name to the service + This workflow can now test, push, and deploy a Score application. However, there is a problem that remains: the `.score-k8s/state.yaml` file. ### Maintaining `score-k8s` state @@ -190,3 +196,9 @@ jobs: concurrency: group: ${{ github.workflow }}-${{ github.ref }} ``` + +### Moving to a managed Score implementation + +Managing the local `state.yaml` file is a risk. It may cause issues when concurrent deployments occur or if a workflow fails unexpectedly. Managed Score implementations and services are responsible for storing and converting any related resource state on behalf of the deployment and ensuring it is correctly shared between deployments. + +When using one of these managed services, the Github Actions workflow can be modified to replace the `score-k8s` and `kubectl` invocations.