[{"data":1,"prerenderedAt":819},["ShallowReactive",2],{"/en-us/blog/managing-gitlab-resources-with-pulumi":3,"navigation-en-us":41,"banner-en-us":449,"footer-en-us":459,"blog-post-authors-en-us-Josh Kodroff, Pulumi":698,"blog-related-posts-en-us-managing-gitlab-resources-with-pulumi":714,"blog-promotions-en-us":757,"next-steps-en-us":809},{"id":4,"title":5,"authorSlugs":6,"authors":8,"body":10,"category":11,"categorySlug":11,"config":12,"content":16,"date":20,"description":17,"extension":26,"externalUrl":27,"featured":14,"heroImage":19,"isFeatured":14,"meta":28,"navigation":29,"path":30,"publishedDate":20,"rawbody":31,"seo":32,"slug":13,"stem":36,"tagSlugs":37,"tags":39,"template":15,"updatedDate":27,"__hash__":40},"blogPosts/en-us/blog/managing-gitlab-resources-with-pulumi.yml","Managing GitLab resources with Pulumi",[7],"josh-kodroff-pulumi",[9],"Josh Kodroff, Pulumi","In the ever-evolving landscape of DevOps, platform engineers are increasingly seeking efficient and flexible tools to manage their GitLab resources, particularly for orchestrating continuous integration/continuous delivery (CI/CD) pipelines. [Pulumi](https://pulumi.com?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources) offers a unique approach to infrastructure as code (IaC) by allowing engineers to use familiar programming languages such as TypeScript, Python, Go, and others. This approach streamlines the automation of GitLab CI/CD workflows. Pulumi's declarative syntax, combined with its ability to treat infrastructure as software, facilitates version control, collaboration, and reproducibility, aligning seamlessly with the GitLab philosophy.\n\nLet's explore the power of using Pulumi and GitLab.\n\n## What is Pulumi?\n\nPulumi is an IaC tool that allows you to manage resources in more than 150 supported cloud or SaaS products (including AWS and GitLab, which we will be demonstrating in this post). You can express your infrastructure with Pulumi using popular general-purpose programming languages like TypeScript, Python, and Go.\n\nPulumi is declarative (just like other popular IaC tools you may be familiar with), which means that you only need to describe the desired end state of your resources and Pulumi will figure out the order of create, read, update, and delete (CRUD) operations to get from your current state to your desired state.\n\nIt might seem strange at first to use a general-purpose programming language to express your infrastructure's desired state if you're used to tools like CloudFormation or Terraform, but there are considerable advantages to Pulumi's approach, including the following:\n- **Familiar tooling.** You don't need any special tooling to use Pulumi. Code completion will work as expected in your favorite editor or IDE without any additional plugins. You can share Pulumi code using familiar packaging tools like npm, PyPI, etc.\n- **Familiar syntax.** Unlike with DSL-based IaC tools, you don't need to learn special ways of indexing an array element, or creating loops or conditionals - you can just use the normal syntax of a language you already know.\n\nThe Pulumi product has an open source component, which includes the Pulumi command line and its ecosystem of providers, which provide the integration between Pulumi and the cloud and SaaS providers it supports. Pulumi also offers a free (for individual use) and paid (for teams and organizations) SaaS service called Pulumi Cloud, which provides state file and secrets management, among many other useful features. It’s a widely-supported open-source IaC tool.\n\n## Initializing the project\n\nTo complete this example you'll need:\n\n1. [A Pulumi Cloud account](https://app.pulumi.com?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources). Pulumi Cloud is free for individual use forever and we'll never ask for your credit card. Pulumi Cloud will manage your Pulumi state file and handle any secrets encryption/decryption. Because it's free for individual use (no credit card required), we strongly recommend that you use Pulumi Cloud as your backend when learning how to use Pulumi.\n2. A GitLab account, group, and a GitLab token set to the `GITLAB_TOKEN` environment variable.\n3. An AWS account and credentials with permissions to deploy identity and access management (IAM) resources. For details on how to configure AWS credentials on your system for use with Pulumi, see [AWS Classic: Installation and Configuration](https://www.pulumi.com/registry/packages/aws/installation-configuration/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources).\n\nThis example will use two providers from the [Pulumi Registry](https://www.pulumi.com/registry/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources):\n\n1. The [GitLab Provider](https://www.pulumi.com/registry/packages/gitlab/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources) will be used to manage resources like Projects, ProjectFiles (to initialize our project repository), ProjectHooks (for the integration with Pulumi Cloud), and ProjectVariables (to hold configuration for our CI/CD pipelines).\n2. The [AWS Classic Provider](https://www.pulumi.com/registry/packages/aws/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources) will be used to manage AWS resources to create OpenID Connect (OIDC) connectivity between AWS and GitLab.\n\nYou can initialize your Pulumi project by changing into a new, empty directory, running the following command, and accepting all the default values for any subsequent prompts:\n\n```bash\npulumi new typescript\n```\n\nThis will bootstrap an empty Pulumi program. Now you can import the provider SDKs for the providers you'll need:\n\n```bash\nnpm i @pulumi/aws @pulumi/gitlab\n```\n\nYour `index.ts` file is the entry point into your Pulumi program (just as you would expect in any other Node.js program) and will be the file to which you will add your resources. Add the following imports to the top of `index.ts`:\n\n```typescript\nimport * as gitlab from \"@pulumi/gitlab\";\nimport * as aws from \"@pulumi/aws\";\n```\n\nNow you are ready to add some resources!\n\n## Adding your first resources\n\nFirst, let's define a variable that will hold the audience claim in our OIDC JWT token. Add the following code to `index.ts`:\n\n```typescript\nconst audience = \"gitlab.com\";\n```\n\nThe above code assume you're using the GitLab SaaS (\u003Chttps://gitlab.com>) If you are using a private GitLab install, your value should be the domain of your GitLab install, e.g. `gitlab.example.com`.\n\nThen, you'll use a [Pulumi function](https://www.pulumi.com/docs/concepts/resources/functions/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources) to grab an existing GitLab group by name and create a new public GitLab project in your GitLab group:\n\n```typescript\nconst group = gitlab.getGroup({\n  fullPath: \"my-gitlab-group\", // Replace the value with the name of your GL group\n});\n\nconst project = new gitlab.Project(\"pulumi-gitlab-demo\", {\n  visibilityLevel: \"public\",\n  defaultBranch: \"main\",\n  namespaceId: group.then(g => parseInt(g.id)),\n  archiveOnDestroy: false // Be sure to set this to `true` for any non-demo repos you manage with Pulumi!\n});\n```\n\n## Creating OIDC resources\n\nTo allow GitLab CI/CD to request and be granted temporary AWS credentials, you'll need to create an OIDC provider in AWS that contains the thumbprint of GitLab's certificate, and then create an AWS role that GitLab is allowed to assume.\n\nYou'll scope the assume role policy so that the role can be only be assumed by the GitLab project you declared earlier. The role that GitLab CI/CD assumed will have full administrator access so that Pulumi can create and manage any resource within AWS. (Note that it is possible to grant less than `FullAdministrator` access to Pulumi, but `FullAdministrator` is often practically required, e.g. where IAM resources, like roles, need to be created. Role creation requires `FullAdministrator`. This consideration also applies to IaC tools like Terraform.)\n\nAdd the following code to `index.ts`:\n\n```typescript\nconst GITLAB_OIDC_PROVIDER_THUMBPRINT = \"b3dd7606d2b5a8b4a13771dbecc9ee1cecafa38a\";\n\nconst gitlabOidcProvider = new aws.iam.OpenIdConnectProvider(\"gitlab-oidc-provider\", {\n  clientIdLists: [`https://${audience}`],\n  url: `https://${audience}`,\n  thumbprintLists: [GITLAB_OIDC_PROVIDER_THUMBPRINT],\n}, {\n  deleteBeforeReplace: true, // URLs are unique identifiers and cannot be auto-named, so we have to delete before replace.\n});\n\nconst gitlabAdminRole = new aws.iam.Role(\"gitlabAdminRole\", {\n  assumeRolePolicy: {\n    Version: \"2012-10-17\",\n    Statement: [\n      {\n        Effect: \"Allow\",\n        Principal: {\n          Federated: gitlabOidcProvider.arn,\n        },\n        Action: \"sts:AssumeRoleWithWebIdentity\",\n        Condition: {\n          StringLike: {\n            // Note: Square brackets around the key are what allow us to use a\n            // templated string. See:\n            // https://stackoverflow.com/questions/59791960/how-to-use-template-literal-as-key-inside-object-literal\n            [`${audience}:sub`]: pulumi.interpolate`project_path:${project.pathWithNamespace}:ref_type:branch:ref:*`\n          },\n        },\n      },\n    ],\n  },\n});\n\nnew aws.iam.RolePolicyAttachment(\"gitlabAdminRolePolicy\", {\n  policyArn: \"arn:aws:iam::aws:policy/AdministratorAccess\",\n  role: gitlabAdminRole.name,\n});\n```\n\nA few things to be aware of regarding the thumbprint:\n\n1. If you are self-hosting GitLab, you'll need to obtain the thumbprint from your private GitLab installation.\n2. If you're using GitLab SaaS, it's possible GitLab's OIDC certificate may have been rotated by the time you are reading this.\n\nIn either case, you can obtain the correct/latest thumbprint value by following AWS' instructions contained in [Obtaining the thumbprint for an OpenID Connect Identity Provider](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html) in the AWS docs.\n\nYou'll also need to add the role's ARN as a project variable so that the CI/CD process can make a request to assume the role:\n\n```typescript\nnew gitlab.ProjectVariable(\"role-arn\", {\n  project: project.id,\n  key: \"ROLE_ARN\",\n  value: gitlabAdminRole.arn,\n});\n```\n\n## Project hook (optional)\n\nPulumi features an integration with GitLab via a webhook that will post the output of the `pulumi preview` directly to a merge request as a comment. For the webhook to work, you must have a Pulumi organization set up with GitLab as its SSO source. If you don't have a Pulumi organization and would like to try the integration, you can [sign up for a free trial](https://app.pulumi.com/signup?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources) organization. The trial lasts 14 days, will give you access to all of Pulumi's paid features, and does not require a credit card. For full details on the integration, see [Pulumi CI/CD & GitLab integration](https://www.pulumi.com/docs/using-pulumi/continuous-delivery/gitlab-app/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources).\n\nTo set up the webhook, add the following to your `index.ts` file:\n\n```typescript\nnew gitlab.ProjectHook(\"project-hook\", {\n  project: project.id,\n  url: \"https://api.pulumi.com/workflow/gitlab\",\n  mergeRequestsEvents: true,\n  enableSslVerification: true,\n  token: process.env[\"PULUMI_ACCESS_TOKEN\"]!,\n  pushEvents: false,\n});\n```\n\nNote that the above resource assumes that your Pulumi access token is stored as an environment variable. You may want to instead store the token in your stack configuration file. To do this, run the following command:\n\n```bash\npulumi config set --secret pulumiAccessToken ${PULUMI_ACCESS_TOKEN}\n```\n\nThis will store the encrypted value in your Pulumi stack configuration file (`Pulumi.dev.yaml`). Because the value is encrypted, you can safely commit your stack configuration file to git. You can access its value in your Pulumi program like this:\n\n```typescript\nconst config = new pulumi.Config();\nconst pulumiAccessToken = config.requireSecret(\"pulumiAccessToken\");\n```\n\nFor more details on secrets handling in Pulumi, see [Secrets](https://www.pulumi.com/docs/concepts/secrets/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources) in the Pulumi docs.\n\n## Creating a repository and adding repository files\n\nYou'll need to create a git repository (a GitLab project) and add some files to it that will control the CI/CD process. First, create some files that you'll include in your GitLab repo:\n\n```bash\nmkdir -p repository-files/scripts\ntouch repository-files/.gitlab-ci.yml repository-files/scripts/{aws-auth.sh,pulumi-preview.sh,pulumi-up.sh}\nchmod +x repository-files/scripts/{aws-auth.sh,pulumi-preview.sh,pulumi-up.sh}\n```\n\nNext, you'll need a GitLab CI/CD YAML file to describe the pipeline: which container image it should be run in and what the steps of the pipeline are. Place the following code into `repository-files/.gitlab-ci.yml`:\n\n```yaml\ndefault:\n  image:\n    name: \"pulumi/pulumi:3.91.1\"\n    entrypoint: [\"\"]\n\nstages:\n  - infrastructure-update\n\npulumi-up:\n  stage: infrastructure-update\n  id_tokens:\n    GITLAB_OIDC_TOKEN:\n      aud: https://gitlab.com\n  before_script:\n    - chmod +x ./scripts/*.sh\n    - ./scripts/aws-auth.sh\n  script:\n    - ./scripts/pulumi-up.sh\n  only:\n    - main # i.e., the name of the default branch\n\npulumi-preview:\n  stage: infrastructure-update\n  id_tokens:\n    GITLAB_OIDC_TOKEN:\n      aud: https://gitlab.com\n  before_script:\n    - chmod +x ./scripts/*.sh\n    - ./scripts/aws-auth.sh\n  script:\n    - ./scripts/pulumi-preview.sh\n  rules:\n    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'\n\n```\n\nThe CI/CD process is fairly simple but illustrates the basic functionality needed for a production-ready pipeline (or these steps may be all your organization needs):\n\n1. Run the `pulumi preview` command when a merge request is opened or updated. This will help the reviewer gain important context. Because IaC is necessarily stateful (the state file is what enables Pulumi to be a declarative tool), when reviewing changes reviewers _must have both the code changes and the infrastructure changes to fully understand the impact of changes to the codebase_. This process constitutes continuous integration.\n2. Run the `pulumi up` command when code is merged to the default branch (called `main` by default). This process constitutes continuous delivery.\n\nNote that this example uses the [`pulumi/pulumi`](https://hub.docker.com/r/pulumi/pulumi) \"kitchen sink\" image that contains all the runtimes for all the languages Pulumi supports, along with some ancillary tools like the AWS CLI (which you'll need in order to use OIDC authentication). While the `pulumi/pulumi` image is convenient, it's also quite large (1.41 GB at the time of writing), which makes it relatively slow to initialize. If you're creating production pipelines using Pulumi, you may want to consider creating your own custom (slimmer) image that has exactly the tools you need installed, perhaps starting with one of Pulumi's language-specific images, e.g. [`pulumi/pulumi-nodejs`](https://hub.docker.com/r/pulumi/pulumi-nodejs).\n\nThen you'll need to write the script that authenticates GitLab with AWS via OIDC. Place the following code in `repository-files/scripts/aws-auth.sh`:\n\n```bash\n#!/bin/bash\n\nmkdir -p ~/.aws\necho \"${GITLAB_OIDC_TOKEN}\" > /tmp/web_identity_token\necho -e \"[profile oidc]\\nrole_arn=${ROLE_ARN}\\nweb_identity_token_file=/tmp/web_identity_token\" > ~/.aws/config\n\necho \"length of GITLAB_OIDC_TOKEN=${#GITLAB_OIDC_TOKEN}\"\necho \"ROLE_ARN=${ROLE_ARN}\"\n\nexport AWS_PROFILE=\"oidc\"\naws sts get-caller-identity\n```\n\nFor continuous integration, you'll need a script that will execute the `pulumi preview` command when a merge request is opened. Place the following code in `repository-files/scripts/pulumi-preview.sh`:\n\n```bash\n#!/bin/bash\nset -e -x\n\nexport PATH=$PATH:$HOME/.pulumi/bin\n\nyarn install\npulumi login\npulumi org set-default $PULUMI_ORG\npulumi stack select dev\nexport AWS_PROFILE=\"oidc\"\npulumi preview\n```\n\nFor continuous delivery, you'll need a similar script that will execute the `pulumi up` command when the Merge Request is merged to the default branch. Place the following code in `repository-files/scripts/pulumi-up.sh`:\n\n```bash\n#!/bin/bash\nset -e -x\n\n# Add the pulumi CLI to the PATH\nexport PATH=$PATH:$HOME/.pulumi/bin\n\nyarn install\npulumi login\npulumi org set-default $PULUMI_ORG\npulumi stack select dev\nexport AWS_PROFILE=\"oidc\"\npulumi up -y\n```\n\nFinally, you'll need to add these files to your GitLab Project. Add the following code block to your `index.ts` file:\n\n```typescript\n[\n  \"scripts/aws-auth.sh\",\n  \"scripts/pulumi-preview.sh\",\n  \"scripts/pulumi-up.sh\",\n  \".gitlab-ci.yml\",\n].forEach(file => {\n  const content = fs.readFileSync(`repository-files/${file}`, \"utf-8\");\n\n  new gitlab.RepositoryFile(file, {\n    project: project.id,\n    filePath: file,\n    branch: \"main\",\n    content: content,\n    commitMessage: `Add ${file},`,\n    encoding: \"text\",\n  });\n});\n```\n\nNote that we're able to take advantage of general-purpose programming language features: We are able to create an array and use `forEach()` to iterate through its members, and we are able to use the `fs.readFileSync()` method from the Node.js runtime to read the contents of our file. This is powerful stuff!\n\n## Project variables and stack outputs\n\nYou'll need a few more resources to complete the code. Your CI/CD process will need a Pulumi access token in order to authenticate against the Pulumi Cloud backend which holds your Pulumi state file and handles encryption and decryption of secrets. You will also need to supply name of your Pulumi organization. (If you are using Pulumi Cloud as an individual, this is your Pulumi username.) Add the following to `index.ts`:\n\n```typescript\nnew gitlab.ProjectVariable(\"pulumi-access-token\", {\n  project: project.id,\n  key: \"PULUMI_ACCESS_TOKEN\",\n  value: process.env[\"PULUMI_ACCESS_TOKEN\"]!,\n  masked: true,\n});\n\nnew gitlab.ProjectVariable(\"pulumi-org\", {\n  project: project.id,\n  key: \"PULUMI_ORG\",\n  value: pulumi.getOrganization(),\n});\n```\n\nFinally, you'll need to add a stack output so that we can run the `git clone` command to test out our pipeline. Stack outputs allow you to access values within your Pulumi program from the command line or from other Pulumi programs. For more information, see [Understanding Stack Outputs](https://www.pulumi.com/learn/building-with-pulumi/stack-outputs/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources). Add the following to `index.ts`:\n\n```typescript\nexport const gitCloneCommand = pulumi.interpolate`git clone ${project.sshUrlToRepo}`;\n```\n\n## Deploying your infrastructure and testing the pipeline\n\nTo deploy your resources, run the following command:\n\n```bash\npulumi up\n```\n\nPulumi will output a list of the resources it intends to create. Select `yes` to continue.\n\nOnce the command has completed, you can run the following command to get the git clone command for your GitLab repo:\n\n```bash\npulumi stack output gitCloneCommand\n```\n\nIn a new, empty directory, run the `git clone` command from your Pulumi stack output, e.g.:\n\n```bash\ngit clone git@gitlab.com:jkodroff/pulumi-gitlab-demo-9de2a3b.git\n```\n\nChange into the directory and create a new branch:\n\n```bash\ngit checkout -b my-first-branch\n```\n\nNow you are ready to create some sample infrastructure in our repository. You can use the `aws-typescript` to quickly generate a simple Pulumi program with AWS resources:\n\n```bash\npulumi new aws-typescript -y --force\n```\n\nThe template includes a very simple Pulumi program that you can use to prove out the pipeline:\n\n```bash\n$ cat index.ts\nimport * as pulumi from \"@pulumi/pulumi\";\nimport * as aws from \"@pulumi/aws\";\nimport * as awsx from \"@pulumi/awsx\";\n\n// Create an AWS resource (S3 Bucket)\nconst bucket = new aws.s3.Bucket(\"my-bucket\");\n\n// Export the name of the bucket\nexport const bucketName = bucket.id;\n```\n\nCommit your changes and push your branch:\n\n```bash\ngit add -A\ngit commit -m \"My first commit.\"\ngit push\n```\n\nIn the GitLab UI, create a merge request for your branch:\n\n![Screenshot demonstrating opening a GitLab Merge Request](https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683438/Blog/Content%20Images/create-merge-request.jpg)\n\nYour merge request pipeline should start running:\n\n![Screenshot demonstrating opening a GitLab Merge Request](https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683438/Blog/Content%20Images/merge-request-running.jpg)\n\nOnce the pipeline completes, you should see the output of the `pulumi preview` command in the pipeline's logs:\n\n![Screenshot of a GitLab pipeline log showing the output of the \"pulumi preview\" command](https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683438/Blog/Content%20Images/pulumi-preview.jpg)\n\nIf you installed the optional webhook, you should see the results of `pulumi preview` posted back to the merge request as a comment:\n\n![Screenshot of the GitLab Merge Request screen showing the output of the \"pulumi preview\" command as a comment](https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683438/Blog/Content%20Images/merge-request-comment.jpg)\n\nOnce the pipeline has completed running, your merge request is ready to merge:\n\n![Screenshot of the GitLab Merge Request screen showing a successfully completed pipeline](https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683438/Blog/Content%20Images/merge.jpg)\n\nMerging the merge request will trigger the main branch pipeline. (Note that in this screen you will see a failed initial run of CI/CD on the main branch toward the bottom of the screen. This is normal and is caused by the initial upload of `.gitlab-ci/yml` to the main branch without a Pulumi program being present.)\n\n![Screenshot of the GitLab pipelines screen showing a running pipeline along with a passed pipelines](https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683438/Blog/Content%20Images/piplines.jpg)\n\nIf you click into the main branch pipeline's execution, you can see your bucket has been created:\n\n![Screenshot of a GitLab pipeline log showing the output of the \"pulumi up\" command](https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683438/Blog/Content%20Images/pulumi-up.jpg)\nTo delete the bucket, run the following command in your local clone of the repository:\n\n```bash\npulumi destroy\n```\n\nAlternatively, you could create a merge request that removes the bucket from your Pulumi program and run the pipelines again. Because Pulumi is declarative, removing the bucket from your program will delete it from AWS.\n\nFinally, run the `pulumi destroy` command again in the Pulumi program with your OIDC and GitLab resources to finish cleaning up.\n\n## Next steps\n\nUsing IaC to define pipelines and other GitLab resources can greatly improve your platform team's ability to reliably and quickly manage the resources to keep application teams delivering. With Pulumi, you also get the power and expressiveness of using popular programming languages to express those resources!\n\nIf you liked what you read here, here are some ways you can enhance your CI/CD pipelines:\n\n- Add [Pulumi Policy Packs](https://www.pulumi.com/docs/using-pulumi/crossguard/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources) to your pipeline: Pulumi policy packs allow you to validate that your resources are in compliance with your organization's security and compliance policies. Pulumi's open source [Compliance Ready Policies](https://www.pulumi.com/docs/using-pulumi/crossguard/compliance-ready-policies/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources) are a great place to start on your journey. Compliance Ready Policies contain policy rules for the major cloud providers for popular compliance frameworks like PCI-DSS and ISO27001, and policy packs are easy to integrate into your pipelines.\n- Check out [Pulumi ESC (Environments, Secrets, and Configuration)](https://www.pulumi.com/product/esc/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources): Pulumi ESC makes it easy to share static secrets like GitLab tokens and can even [generate dynamic secrets like AWS OIDC credentials](https://www.pulumi.com/blog/esc-env-run-aws/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources). ESC becomes especially useful when using Pulumi at scale because it reduces the duplication of configuration and secrets that are used by multiple Pulumi programs. You don't even have to use Pulumi IaC to benefit from Pulumi ESC - [Pulumi ESC's command line](https://www.pulumi.com/docs/esc-cli/commands/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources) can be used with any CLI tool like the AWS CLI.\n","devsecops",{"slug":13,"featured":14,"template":15},"managing-gitlab-resources-with-pulumi",false,"BlogPost",{"title":5,"description":17,"authors":18,"heroImage":19,"date":20,"body":10,"category":11,"tags":21},"Learn how Pulumi's infrastructure-as-code tool helps streamline the automation of GitLab CI/CD workflows.",[9],"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683430/Blog/Hero%20Images/AdobeStock_293854129__1_.jpg","2024-01-10",[22,23,24,25],"CI/CD","DevSecOps","partners","integrations","yml",null,{},true,"/en-us/blog/managing-gitlab-resources-with-pulumi","seo:\n  title: Managing GitLab resources with Pulumi\n  description: >-\n    Learn how Pulumi's infrastructure-as-code tool helps streamline the\n    automation of GitLab CI/CD workflows.\n  ogTitle: Managing GitLab resources with Pulumi\n  ogDescription: >-\n    Learn how Pulumi's infrastructure-as-code tool helps streamline the\n    automation of GitLab CI/CD workflows.\n  noIndex: false\n  ogImage: >-\n    https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683430/Blog/Hero%20Images/AdobeStock_293854129__1_.jpg\n  ogUrl: https://about.gitlab.com/blog/managing-gitlab-resources-with-pulumi\n  ogSiteName: https://about.gitlab.com\n  ogType: article\n  canonicalUrls: https://about.gitlab.com/blog/managing-gitlab-resources-with-pulumi\ncontent:\n  title: Managing GitLab resources with Pulumi\n  description: >-\n    Learn how Pulumi's infrastructure-as-code tool helps streamline the\n    automation of GitLab CI/CD workflows.\n  authors:\n    - Josh Kodroff, Pulumi\n  heroImage: >-\n    https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683430/Blog/Hero%20Images/AdobeStock_293854129__1_.jpg\n  date: '2024-01-10'\n  body: >\n    In the ever-evolving landscape of DevOps, platform engineers are\n    increasingly seeking efficient and flexible tools to manage their GitLab\n    resources, particularly for orchestrating continuous integration/continuous\n    delivery (CI/CD) pipelines.\n    [Pulumi](https://pulumi.com?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources)\n    offers a unique approach to infrastructure as code (IaC) by allowing\n    engineers to use familiar programming languages such as TypeScript, Python,\n    Go, and others. This approach streamlines the automation of GitLab CI/CD\n    workflows. Pulumi's declarative syntax, combined with its ability to treat\n    infrastructure as software, facilitates version control, collaboration, and\n    reproducibility, aligning seamlessly with the GitLab philosophy.\n\n\n    Let's explore the power of using Pulumi and GitLab.\n\n\n    ## What is Pulumi?\n\n\n    Pulumi is an IaC tool that allows you to manage resources in more than 150\n    supported cloud or SaaS products (including AWS and GitLab, which we will be\n    demonstrating in this post). You can express your infrastructure with Pulumi\n    using popular general-purpose programming languages like TypeScript, Python,\n    and Go.\n\n\n    Pulumi is declarative (just like other popular IaC tools you may be familiar\n    with), which means that you only need to describe the desired end state of\n    your resources and Pulumi will figure out the order of create, read, update,\n    and delete (CRUD) operations to get from your current state to your desired\n    state.\n\n\n    It might seem strange at first to use a general-purpose programming language\n    to express your infrastructure's desired state if you're used to tools like\n    CloudFormation or Terraform, but there are considerable advantages to\n    Pulumi's approach, including the following:\n\n    - **Familiar tooling.** You don't need any special tooling to use Pulumi.\n    Code completion will work as expected in your favorite editor or IDE without\n    any additional plugins. You can share Pulumi code using familiar packaging\n    tools like npm, PyPI, etc.\n\n    - **Familiar syntax.** Unlike with DSL-based IaC tools, you don't need to\n    learn special ways of indexing an array element, or creating loops or\n    conditionals - you can just use the normal syntax of a language you already\n    know.\n\n\n    The Pulumi product has an open source component, which includes the Pulumi\n    command line and its ecosystem of providers, which provide the integration\n    between Pulumi and the cloud and SaaS providers it supports. Pulumi also\n    offers a free (for individual use) and paid (for teams and organizations)\n    SaaS service called Pulumi Cloud, which provides state file and secrets\n    management, among many other useful features. It’s a widely-supported\n    open-source IaC tool.\n\n\n    ## Initializing the project\n\n\n    To complete this example you'll need:\n\n\n    1. [A Pulumi Cloud\n    account](https://app.pulumi.com?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources).\n    Pulumi Cloud is free for individual use forever and we'll never ask for your\n    credit card. Pulumi Cloud will manage your Pulumi state file and handle any\n    secrets encryption/decryption. Because it's free for individual use (no\n    credit card required), we strongly recommend that you use Pulumi Cloud as\n    your backend when learning how to use Pulumi.\n\n    2. A GitLab account, group, and a GitLab token set to the `GITLAB_TOKEN`\n    environment variable.\n\n    3. An AWS account and credentials with permissions to deploy identity and\n    access management (IAM) resources. For details on how to configure AWS\n    credentials on your system for use with Pulumi, see [AWS Classic:\n    Installation and\n    Configuration](https://www.pulumi.com/registry/packages/aws/installation-configuration/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources).\n\n\n    This example will use two providers from the [Pulumi\n    Registry](https://www.pulumi.com/registry/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources):\n\n\n    1. The [GitLab\n    Provider](https://www.pulumi.com/registry/packages/gitlab/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources)\n    will be used to manage resources like Projects, ProjectFiles (to initialize\n    our project repository), ProjectHooks (for the integration with Pulumi\n    Cloud), and ProjectVariables (to hold configuration for our CI/CD\n    pipelines).\n\n    2. The [AWS Classic\n    Provider](https://www.pulumi.com/registry/packages/aws/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources)\n    will be used to manage AWS resources to create OpenID Connect (OIDC)\n    connectivity between AWS and GitLab.\n\n\n    You can initialize your Pulumi project by changing into a new, empty\n    directory, running the following command, and accepting all the default\n    values for any subsequent prompts:\n\n\n    ```bash\n\n    pulumi new typescript\n\n    ```\n\n\n    This will bootstrap an empty Pulumi program. Now you can import the provider\n    SDKs for the providers you'll need:\n\n\n    ```bash\n\n    npm i @pulumi/aws @pulumi/gitlab\n\n    ```\n\n\n    Your `index.ts` file is the entry point into your Pulumi program (just as\n    you would expect in any other Node.js program) and will be the file to which\n    you will add your resources. Add the following imports to the top of\n    `index.ts`:\n\n\n    ```typescript\n\n    import * as gitlab from \"@pulumi/gitlab\";\n\n    import * as aws from \"@pulumi/aws\";\n\n    ```\n\n\n    Now you are ready to add some resources!\n\n\n    ## Adding your first resources\n\n\n    First, let's define a variable that will hold the audience claim in our OIDC\n    JWT token. Add the following code to `index.ts`:\n\n\n    ```typescript\n\n    const audience = \"gitlab.com\";\n\n    ```\n\n\n    The above code assume you're using the GitLab SaaS (\u003Chttps://gitlab.com>) If\n    you are using a private GitLab install, your value should be the domain of\n    your GitLab install, e.g. `gitlab.example.com`.\n\n\n    Then, you'll use a [Pulumi\n    function](https://www.pulumi.com/docs/concepts/resources/functions/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources)\n    to grab an existing GitLab group by name and create a new public GitLab\n    project in your GitLab group:\n\n\n    ```typescript\n\n    const group = gitlab.getGroup({\n      fullPath: \"my-gitlab-group\", // Replace the value with the name of your GL group\n    });\n\n\n    const project = new gitlab.Project(\"pulumi-gitlab-demo\", {\n      visibilityLevel: \"public\",\n      defaultBranch: \"main\",\n      namespaceId: group.then(g => parseInt(g.id)),\n      archiveOnDestroy: false // Be sure to set this to `true` for any non-demo repos you manage with Pulumi!\n    });\n\n    ```\n\n\n    ## Creating OIDC resources\n\n\n    To allow GitLab CI/CD to request and be granted temporary AWS credentials,\n    you'll need to create an OIDC provider in AWS that contains the thumbprint\n    of GitLab's certificate, and then create an AWS role that GitLab is allowed\n    to assume.\n\n\n    You'll scope the assume role policy so that the role can be only be assumed\n    by the GitLab project you declared earlier. The role that GitLab CI/CD\n    assumed will have full administrator access so that Pulumi can create and\n    manage any resource within AWS. (Note that it is possible to grant less than\n    `FullAdministrator` access to Pulumi, but `FullAdministrator` is often\n    practically required, e.g. where IAM resources, like roles, need to be\n    created. Role creation requires `FullAdministrator`. This consideration also\n    applies to IaC tools like Terraform.)\n\n\n    Add the following code to `index.ts`:\n\n\n    ```typescript\n\n    const GITLAB_OIDC_PROVIDER_THUMBPRINT =\n    \"b3dd7606d2b5a8b4a13771dbecc9ee1cecafa38a\";\n\n\n    const gitlabOidcProvider = new\n    aws.iam.OpenIdConnectProvider(\"gitlab-oidc-provider\", {\n      clientIdLists: [`https://${audience}`],\n      url: `https://${audience}`,\n      thumbprintLists: [GITLAB_OIDC_PROVIDER_THUMBPRINT],\n    }, {\n      deleteBeforeReplace: true, // URLs are unique identifiers and cannot be auto-named, so we have to delete before replace.\n    });\n\n\n    const gitlabAdminRole = new aws.iam.Role(\"gitlabAdminRole\", {\n      assumeRolePolicy: {\n        Version: \"2012-10-17\",\n        Statement: [\n          {\n            Effect: \"Allow\",\n            Principal: {\n              Federated: gitlabOidcProvider.arn,\n            },\n            Action: \"sts:AssumeRoleWithWebIdentity\",\n            Condition: {\n              StringLike: {\n                // Note: Square brackets around the key are what allow us to use a\n                // templated string. See:\n                // https://stackoverflow.com/questions/59791960/how-to-use-template-literal-as-key-inside-object-literal\n                [`${audience}:sub`]: pulumi.interpolate`project_path:${project.pathWithNamespace}:ref_type:branch:ref:*`\n              },\n            },\n          },\n        ],\n      },\n    });\n\n\n    new aws.iam.RolePolicyAttachment(\"gitlabAdminRolePolicy\", {\n      policyArn: \"arn:aws:iam::aws:policy/AdministratorAccess\",\n      role: gitlabAdminRole.name,\n    });\n\n    ```\n\n\n    A few things to be aware of regarding the thumbprint:\n\n\n    1. If you are self-hosting GitLab, you'll need to obtain the thumbprint from\n    your private GitLab installation.\n\n    2. If you're using GitLab SaaS, it's possible GitLab's OIDC certificate may\n    have been rotated by the time you are reading this.\n\n\n    In either case, you can obtain the correct/latest thumbprint value by\n    following AWS' instructions contained in [Obtaining the thumbprint for an\n    OpenID Connect Identity\n    Provider](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html)\n    in the AWS docs.\n\n\n    You'll also need to add the role's ARN as a project variable so that the\n    CI/CD process can make a request to assume the role:\n\n\n    ```typescript\n\n    new gitlab.ProjectVariable(\"role-arn\", {\n      project: project.id,\n      key: \"ROLE_ARN\",\n      value: gitlabAdminRole.arn,\n    });\n\n    ```\n\n\n    ## Project hook (optional)\n\n\n    Pulumi features an integration with GitLab via a webhook that will post the\n    output of the `pulumi preview` directly to a merge request as a comment. For\n    the webhook to work, you must have a Pulumi organization set up with GitLab\n    as its SSO source. If you don't have a Pulumi organization and would like to\n    try the integration, you can [sign up for a free\n    trial](https://app.pulumi.com/signup?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources)\n    organization. The trial lasts 14 days, will give you access to all of\n    Pulumi's paid features, and does not require a credit card. For full details\n    on the integration, see [Pulumi CI/CD & GitLab\n    integration](https://www.pulumi.com/docs/using-pulumi/continuous-delivery/gitlab-app/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources).\n\n\n    To set up the webhook, add the following to your `index.ts` file:\n\n\n    ```typescript\n\n    new gitlab.ProjectHook(\"project-hook\", {\n      project: project.id,\n      url: \"https://api.pulumi.com/workflow/gitlab\",\n      mergeRequestsEvents: true,\n      enableSslVerification: true,\n      token: process.env[\"PULUMI_ACCESS_TOKEN\"]!,\n      pushEvents: false,\n    });\n\n    ```\n\n\n    Note that the above resource assumes that your Pulumi access token is stored\n    as an environment variable. You may want to instead store the token in your\n    stack configuration file. To do this, run the following command:\n\n\n    ```bash\n\n    pulumi config set --secret pulumiAccessToken ${PULUMI_ACCESS_TOKEN}\n\n    ```\n\n\n    This will store the encrypted value in your Pulumi stack configuration file\n    (`Pulumi.dev.yaml`). Because the value is encrypted, you can safely commit\n    your stack configuration file to git. You can access its value in your\n    Pulumi program like this:\n\n\n    ```typescript\n\n    const config = new pulumi.Config();\n\n    const pulumiAccessToken = config.requireSecret(\"pulumiAccessToken\");\n\n    ```\n\n\n    For more details on secrets handling in Pulumi, see\n    [Secrets](https://www.pulumi.com/docs/concepts/secrets/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources)\n    in the Pulumi docs.\n\n\n    ## Creating a repository and adding repository files\n\n\n    You'll need to create a git repository (a GitLab project) and add some files\n    to it that will control the CI/CD process. First, create some files that\n    you'll include in your GitLab repo:\n\n\n    ```bash\n\n    mkdir -p repository-files/scripts\n\n    touch repository-files/.gitlab-ci.yml\n    repository-files/scripts/{aws-auth.sh,pulumi-preview.sh,pulumi-up.sh}\n\n    chmod +x\n    repository-files/scripts/{aws-auth.sh,pulumi-preview.sh,pulumi-up.sh}\n\n    ```\n\n\n    Next, you'll need a GitLab CI/CD YAML file to describe the pipeline: which\n    container image it should be run in and what the steps of the pipeline are.\n    Place the following code into `repository-files/.gitlab-ci.yml`:\n\n\n    ```yaml\n\n    default:\n      image:\n        name: \"pulumi/pulumi:3.91.1\"\n        entrypoint: [\"\"]\n\n    stages:\n      - infrastructure-update\n\n    pulumi-up:\n      stage: infrastructure-update\n      id_tokens:\n        GITLAB_OIDC_TOKEN:\n          aud: https://gitlab.com\n      before_script:\n        - chmod +x ./scripts/*.sh\n        - ./scripts/aws-auth.sh\n      script:\n        - ./scripts/pulumi-up.sh\n      only:\n        - main # i.e., the name of the default branch\n\n    pulumi-preview:\n      stage: infrastructure-update\n      id_tokens:\n        GITLAB_OIDC_TOKEN:\n          aud: https://gitlab.com\n      before_script:\n        - chmod +x ./scripts/*.sh\n        - ./scripts/aws-auth.sh\n      script:\n        - ./scripts/pulumi-preview.sh\n      rules:\n        - if: $CI_PIPELINE_SOURCE == 'merge_request_event'\n\n    ```\n\n\n    The CI/CD process is fairly simple but illustrates the basic functionality\n    needed for a production-ready pipeline (or these steps may be all your\n    organization needs):\n\n\n    1. Run the `pulumi preview` command when a merge request is opened or\n    updated. This will help the reviewer gain important context. Because IaC is\n    necessarily stateful (the state file is what enables Pulumi to be a\n    declarative tool), when reviewing changes reviewers _must have both the code\n    changes and the infrastructure changes to fully understand the impact of\n    changes to the codebase_. This process constitutes continuous integration.\n\n    2. Run the `pulumi up` command when code is merged to the default branch\n    (called `main` by default). This process constitutes continuous delivery.\n\n\n    Note that this example uses the\n    [`pulumi/pulumi`](https://hub.docker.com/r/pulumi/pulumi) \"kitchen sink\"\n    image that contains all the runtimes for all the languages Pulumi supports,\n    along with some ancillary tools like the AWS CLI (which you'll need in order\n    to use OIDC authentication). While the `pulumi/pulumi` image is convenient,\n    it's also quite large (1.41 GB at the time of writing), which makes it\n    relatively slow to initialize. If you're creating production pipelines using\n    Pulumi, you may want to consider creating your own custom (slimmer) image\n    that has exactly the tools you need installed, perhaps starting with one of\n    Pulumi's language-specific images, e.g.\n    [`pulumi/pulumi-nodejs`](https://hub.docker.com/r/pulumi/pulumi-nodejs).\n\n\n    Then you'll need to write the script that authenticates GitLab with AWS via\n    OIDC. Place the following code in `repository-files/scripts/aws-auth.sh`:\n\n\n    ```bash\n\n    #!/bin/bash\n\n\n    mkdir -p ~/.aws\n\n    echo \"${GITLAB_OIDC_TOKEN}\" > /tmp/web_identity_token\n\n    echo -e \"[profile\n    oidc]\\nrole_arn=${ROLE_ARN}\\nweb_identity_token_file=/tmp/web_identity_token\"\n    > ~/.aws/config\n\n\n    echo \"length of GITLAB_OIDC_TOKEN=${#GITLAB_OIDC_TOKEN}\"\n\n    echo \"ROLE_ARN=${ROLE_ARN}\"\n\n\n    export AWS_PROFILE=\"oidc\"\n\n    aws sts get-caller-identity\n\n    ```\n\n\n    For continuous integration, you'll need a script that will execute the\n    `pulumi preview` command when a merge request is opened. Place the following\n    code in `repository-files/scripts/pulumi-preview.sh`:\n\n\n    ```bash\n\n    #!/bin/bash\n\n    set -e -x\n\n\n    export PATH=$PATH:$HOME/.pulumi/bin\n\n\n    yarn install\n\n    pulumi login\n\n    pulumi org set-default $PULUMI_ORG\n\n    pulumi stack select dev\n\n    export AWS_PROFILE=\"oidc\"\n\n    pulumi preview\n\n    ```\n\n\n    For continuous delivery, you'll need a similar script that will execute the\n    `pulumi up` command when the Merge Request is merged to the default branch.\n    Place the following code in `repository-files/scripts/pulumi-up.sh`:\n\n\n    ```bash\n\n    #!/bin/bash\n\n    set -e -x\n\n\n    # Add the pulumi CLI to the PATH\n\n    export PATH=$PATH:$HOME/.pulumi/bin\n\n\n    yarn install\n\n    pulumi login\n\n    pulumi org set-default $PULUMI_ORG\n\n    pulumi stack select dev\n\n    export AWS_PROFILE=\"oidc\"\n\n    pulumi up -y\n\n    ```\n\n\n    Finally, you'll need to add these files to your GitLab Project. Add the\n    following code block to your `index.ts` file:\n\n\n    ```typescript\n\n    [\n      \"scripts/aws-auth.sh\",\n      \"scripts/pulumi-preview.sh\",\n      \"scripts/pulumi-up.sh\",\n      \".gitlab-ci.yml\",\n    ].forEach(file => {\n      const content = fs.readFileSync(`repository-files/${file}`, \"utf-8\");\n\n      new gitlab.RepositoryFile(file, {\n        project: project.id,\n        filePath: file,\n        branch: \"main\",\n        content: content,\n        commitMessage: `Add ${file},`,\n        encoding: \"text\",\n      });\n    });\n\n    ```\n\n\n    Note that we're able to take advantage of general-purpose programming\n    language features: We are able to create an array and use `forEach()` to\n    iterate through its members, and we are able to use the `fs.readFileSync()`\n    method from the Node.js runtime to read the contents of our file. This is\n    powerful stuff!\n\n\n    ## Project variables and stack outputs\n\n\n    You'll need a few more resources to complete the code. Your CI/CD process\n    will need a Pulumi access token in order to authenticate against the Pulumi\n    Cloud backend which holds your Pulumi state file and handles encryption and\n    decryption of secrets. You will also need to supply name of your Pulumi\n    organization. (If you are using Pulumi Cloud as an individual, this is your\n    Pulumi username.) Add the following to `index.ts`:\n\n\n    ```typescript\n\n    new gitlab.ProjectVariable(\"pulumi-access-token\", {\n      project: project.id,\n      key: \"PULUMI_ACCESS_TOKEN\",\n      value: process.env[\"PULUMI_ACCESS_TOKEN\"]!,\n      masked: true,\n    });\n\n\n    new gitlab.ProjectVariable(\"pulumi-org\", {\n      project: project.id,\n      key: \"PULUMI_ORG\",\n      value: pulumi.getOrganization(),\n    });\n\n    ```\n\n\n    Finally, you'll need to add a stack output so that we can run the `git\n    clone` command to test out our pipeline. Stack outputs allow you to access\n    values within your Pulumi program from the command line or from other Pulumi\n    programs. For more information, see [Understanding Stack\n    Outputs](https://www.pulumi.com/learn/building-with-pulumi/stack-outputs/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources).\n    Add the following to `index.ts`:\n\n\n    ```typescript\n\n    export const gitCloneCommand = pulumi.interpolate`git clone\n    ${project.sshUrlToRepo}`;\n\n    ```\n\n\n    ## Deploying your infrastructure and testing the pipeline\n\n\n    To deploy your resources, run the following command:\n\n\n    ```bash\n\n    pulumi up\n\n    ```\n\n\n    Pulumi will output a list of the resources it intends to create. Select\n    `yes` to continue.\n\n\n    Once the command has completed, you can run the following command to get the\n    git clone command for your GitLab repo:\n\n\n    ```bash\n\n    pulumi stack output gitCloneCommand\n\n    ```\n\n\n    In a new, empty directory, run the `git clone` command from your Pulumi\n    stack output, e.g.:\n\n\n    ```bash\n\n    git clone git@gitlab.com:jkodroff/pulumi-gitlab-demo-9de2a3b.git\n\n    ```\n\n\n    Change into the directory and create a new branch:\n\n\n    ```bash\n\n    git checkout -b my-first-branch\n\n    ```\n\n\n    Now you are ready to create some sample infrastructure in our repository.\n    You can use the `aws-typescript` to quickly generate a simple Pulumi program\n    with AWS resources:\n\n\n    ```bash\n\n    pulumi new aws-typescript -y --force\n\n    ```\n\n\n    The template includes a very simple Pulumi program that you can use to prove\n    out the pipeline:\n\n\n    ```bash\n\n    $ cat index.ts\n\n    import * as pulumi from \"@pulumi/pulumi\";\n\n    import * as aws from \"@pulumi/aws\";\n\n    import * as awsx from \"@pulumi/awsx\";\n\n\n    // Create an AWS resource (S3 Bucket)\n\n    const bucket = new aws.s3.Bucket(\"my-bucket\");\n\n\n    // Export the name of the bucket\n\n    export const bucketName = bucket.id;\n\n    ```\n\n\n    Commit your changes and push your branch:\n\n\n    ```bash\n\n    git add -A\n\n    git commit -m \"My first commit.\"\n\n    git push\n\n    ```\n\n\n    In the GitLab UI, create a merge request for your branch:\n\n\n    ![Screenshot demonstrating opening a GitLab Merge\n    Request](https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683438/Blog/Content%20Images/create-merge-request.jpg)\n\n\n    Your merge request pipeline should start running:\n\n\n    ![Screenshot demonstrating opening a GitLab Merge\n    Request](https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683438/Blog/Content%20Images/merge-request-running.jpg)\n\n\n    Once the pipeline completes, you should see the output of the `pulumi\n    preview` command in the pipeline's logs:\n\n\n    ![Screenshot of a GitLab pipeline log showing the output of the \"pulumi\n    preview\"\n    command](https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683438/Blog/Content%20Images/pulumi-preview.jpg)\n\n\n    If you installed the optional webhook, you should see the results of `pulumi\n    preview` posted back to the merge request as a comment:\n\n\n    ![Screenshot of the GitLab Merge Request screen showing the output of the\n    \"pulumi preview\" command as a\n    comment](https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683438/Blog/Content%20Images/merge-request-comment.jpg)\n\n\n    Once the pipeline has completed running, your merge request is ready to\n    merge:\n\n\n    ![Screenshot of the GitLab Merge Request screen showing a successfully\n    completed\n    pipeline](https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683438/Blog/Content%20Images/merge.jpg)\n\n\n    Merging the merge request will trigger the main branch pipeline. (Note that\n    in this screen you will see a failed initial run of CI/CD on the main branch\n    toward the bottom of the screen. This is normal and is caused by the initial\n    upload of `.gitlab-ci/yml` to the main branch without a Pulumi program being\n    present.)\n\n\n    ![Screenshot of the GitLab pipelines screen showing a running pipeline along\n    with a passed\n    pipelines](https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683438/Blog/Content%20Images/piplines.jpg)\n\n\n    If you click into the main branch pipeline's execution, you can see your\n    bucket has been created:\n\n\n    ![Screenshot of a GitLab pipeline log showing the output of the \"pulumi up\"\n    command](https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683438/Blog/Content%20Images/pulumi-up.jpg)\n\n    To delete the bucket, run the following command in your local clone of the\n    repository:\n\n\n    ```bash\n\n    pulumi destroy\n\n    ```\n\n\n    Alternatively, you could create a merge request that removes the bucket from\n    your Pulumi program and run the pipelines again. Because Pulumi is\n    declarative, removing the bucket from your program will delete it from AWS.\n\n\n    Finally, run the `pulumi destroy` command again in the Pulumi program with\n    your OIDC and GitLab resources to finish cleaning up.\n\n\n    ## Next steps\n\n\n    Using IaC to define pipelines and other GitLab resources can greatly improve\n    your platform team's ability to reliably and quickly manage the resources to\n    keep application teams delivering. With Pulumi, you also get the power and\n    expressiveness of using popular programming languages to express those\n    resources!\n\n\n    If you liked what you read here, here are some ways you can enhance your\n    CI/CD pipelines:\n\n\n    - Add [Pulumi Policy\n    Packs](https://www.pulumi.com/docs/using-pulumi/crossguard/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources)\n    to your pipeline: Pulumi policy packs allow you to validate that your\n    resources are in compliance with your organization's security and compliance\n    policies. Pulumi's open source [Compliance Ready\n    Policies](https://www.pulumi.com/docs/using-pulumi/crossguard/compliance-ready-policies/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources)\n    are a great place to start on your journey. Compliance Ready Policies\n    contain policy rules for the major cloud providers for popular compliance\n    frameworks like PCI-DSS and ISO27001, and policy packs are easy to integrate\n    into your pipelines.\n\n    - Check out [Pulumi ESC (Environments, Secrets, and\n    Configuration)](https://www.pulumi.com/product/esc/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources):\n    Pulumi ESC makes it easy to share static secrets like GitLab tokens and can\n    even [generate dynamic secrets like AWS OIDC\n    credentials](https://www.pulumi.com/blog/esc-env-run-aws/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources).\n    ESC becomes especially useful when using Pulumi at scale because it reduces\n    the duplication of configuration and secrets that are used by multiple\n    Pulumi programs. You don't even have to use Pulumi IaC to benefit from\n    Pulumi ESC - [Pulumi ESC's command\n    line](https://www.pulumi.com/docs/esc-cli/commands/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources)\n    can be used with any CLI tool like the AWS CLI.\n  category: devsecops\n  tags:\n    - CI/CD\n    - DevSecOps\n    - partners\n    - integrations\nconfig:\n  slug: managing-gitlab-resources-with-pulumi\n  featured: false\n  template: BlogPost\n",{"title":5,"description":17,"ogTitle":5,"ogDescription":17,"noIndex":14,"ogImage":19,"ogUrl":33,"ogSiteName":34,"ogType":35,"canonicalUrls":33},"https://about.gitlab.com/blog/managing-gitlab-resources-with-pulumi","https://about.gitlab.com","article","en-us/blog/managing-gitlab-resources-with-pulumi",[38,11,24,25],"cicd",[22,23,24,25],"ooDGvpP4W3I8foXY7eJEcj1CG_gDg3a6jMTWwlrAC3A",{"data":42},{"logo":43,"freeTrial":48,"sales":53,"login":58,"items":63,"search":369,"minimal":400,"duo":419,"switchNav":428,"pricingDeployment":439},{"config":44},{"href":45,"dataGaName":46,"dataGaLocation":47},"/","gitlab logo","header",{"text":49,"config":50},"Get free trial",{"href":51,"dataGaName":52,"dataGaLocation":47},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com&glm_content=default-saas-trial/","free trial",{"text":54,"config":55},"Talk to sales",{"href":56,"dataGaName":57,"dataGaLocation":47},"/sales/","sales",{"text":59,"config":60},"Sign in",{"href":61,"dataGaName":62,"dataGaLocation":47},"https://gitlab.com/users/sign_in/","sign in",[64,91,185,190,290,350],{"text":65,"config":66,"cards":68},"Platform",{"dataNavLevelOne":67},"platform",[69,75,83],{"title":65,"description":70,"link":71},"The intelligent orchestration platform for DevSecOps",{"text":72,"config":73},"Explore our Platform",{"href":74,"dataGaName":67,"dataGaLocation":47},"/platform/",{"title":76,"description":77,"link":78},"GitLab Duo Agent Platform","Agentic AI for the entire software lifecycle",{"text":79,"config":80},"Meet GitLab Duo",{"href":81,"dataGaName":82,"dataGaLocation":47},"/gitlab-duo-agent-platform/","gitlab duo agent platform",{"title":84,"description":85,"link":86},"Why GitLab","See the top reasons enterprises choose GitLab",{"text":87,"config":88},"Learn more",{"href":89,"dataGaName":90,"dataGaLocation":47},"/why-gitlab/","why gitlab",{"text":92,"left":29,"config":93,"link":95,"lists":99,"footer":167},"Product",{"dataNavLevelOne":94},"solutions",{"text":96,"config":97},"View all Solutions",{"href":98,"dataGaName":94,"dataGaLocation":47},"/solutions/",[100,123,146],{"title":101,"description":102,"link":103,"items":108},"Automation","CI/CD and automation to accelerate deployment",{"config":104},{"icon":105,"href":106,"dataGaName":107,"dataGaLocation":47},"AutomatedCodeAlt","/solutions/delivery-automation/","automated software delivery",[109,112,115,119],{"text":22,"config":110},{"href":111,"dataGaLocation":47,"dataGaName":22},"/solutions/continuous-integration/",{"text":76,"config":113},{"href":81,"dataGaLocation":47,"dataGaName":114},"gitlab duo agent platform - product menu",{"text":116,"config":117},"Source Code Management",{"href":118,"dataGaLocation":47,"dataGaName":116},"/solutions/source-code-management/",{"text":120,"config":121},"Automated Software Delivery",{"href":106,"dataGaLocation":47,"dataGaName":122},"Automated software delivery",{"title":124,"description":125,"link":126,"items":131},"Security","Deliver code faster without compromising security",{"config":127},{"href":128,"dataGaName":129,"dataGaLocation":47,"icon":130},"/solutions/application-security-testing/","security and compliance","ShieldCheckLight",[132,136,141],{"text":133,"config":134},"Application Security Testing",{"href":128,"dataGaName":135,"dataGaLocation":47},"Application security testing",{"text":137,"config":138},"Software Supply Chain Security",{"href":139,"dataGaLocation":47,"dataGaName":140},"/solutions/supply-chain/","Software supply chain security",{"text":142,"config":143},"Software Compliance",{"href":144,"dataGaName":145,"dataGaLocation":47},"/solutions/software-compliance/","software compliance",{"title":147,"link":148,"items":153},"Measurement",{"config":149},{"icon":150,"href":151,"dataGaName":152,"dataGaLocation":47},"DigitalTransformation","/solutions/visibility-measurement/","visibility and measurement",[154,158,162],{"text":155,"config":156},"Visibility & Measurement",{"href":151,"dataGaLocation":47,"dataGaName":157},"Visibility and Measurement",{"text":159,"config":160},"Value Stream Management",{"href":161,"dataGaLocation":47,"dataGaName":159},"/solutions/value-stream-management/",{"text":163,"config":164},"Analytics & Insights",{"href":165,"dataGaLocation":47,"dataGaName":166},"/solutions/analytics-and-insights/","Analytics and insights",{"title":168,"items":169},"GitLab for",[170,175,180],{"text":171,"config":172},"Enterprise",{"href":173,"dataGaLocation":47,"dataGaName":174},"/enterprise/","enterprise",{"text":176,"config":177},"Small Business",{"href":178,"dataGaLocation":47,"dataGaName":179},"/small-business/","small business",{"text":181,"config":182},"Public Sector",{"href":183,"dataGaLocation":47,"dataGaName":184},"/solutions/public-sector/","public sector",{"text":186,"config":187},"Pricing",{"href":188,"dataGaName":189,"dataGaLocation":47,"dataNavLevelOne":189},"/pricing/","pricing",{"text":191,"config":192,"link":194,"lists":198,"feature":281},"Resources",{"dataNavLevelOne":193},"resources",{"text":195,"config":196},"View all resources",{"href":197,"dataGaName":193,"dataGaLocation":47},"/resources/",[199,231,254],{"title":200,"items":201},"Getting started",[202,207,212,217,222,227],{"text":203,"config":204},"Install",{"href":205,"dataGaName":206,"dataGaLocation":47},"/install/","install",{"text":208,"config":209},"Quick start guides",{"href":210,"dataGaName":211,"dataGaLocation":47},"/get-started/","quick setup checklists",{"text":213,"config":214},"Learn",{"href":215,"dataGaLocation":47,"dataGaName":216},"https://university.gitlab.com/","learn",{"text":218,"config":219},"Product documentation",{"href":220,"dataGaName":221,"dataGaLocation":47},"https://docs.gitlab.com/","product documentation",{"text":223,"config":224},"Best practice videos",{"href":225,"dataGaName":226,"dataGaLocation":47},"/getting-started-videos/","best practice videos",{"text":228,"config":229},"Integrations",{"href":230,"dataGaName":25,"dataGaLocation":47},"/integrations/",{"title":232,"items":233},"Discover",[234,239,244,249],{"text":235,"config":236},"Customer success stories",{"href":237,"dataGaName":238,"dataGaLocation":47},"/customers/","customer success stories",{"text":240,"config":241},"Blog",{"href":242,"dataGaName":243,"dataGaLocation":47},"/blog/","blog",{"text":245,"config":246},"The Source",{"href":247,"dataGaName":248,"dataGaLocation":47},"/the-source/","the source",{"text":250,"config":251},"Remote",{"href":252,"dataGaName":253,"dataGaLocation":47},"https://handbook.gitlab.com/handbook/company/culture/all-remote/","remote",{"title":255,"items":256},"Connect",[257,262,267,272,277],{"text":258,"config":259},"GitLab Services",{"href":260,"dataGaName":261,"dataGaLocation":47},"/services/","services",{"text":263,"config":264},"Community",{"href":265,"dataGaName":266,"dataGaLocation":47},"/community/","community",{"text":268,"config":269},"Forum",{"href":270,"dataGaName":271,"dataGaLocation":47},"https://forum.gitlab.com/","forum",{"text":273,"config":274},"Events",{"href":275,"dataGaName":276,"dataGaLocation":47},"/events/","events",{"text":278,"config":279},"Partners",{"href":280,"dataGaName":24,"dataGaLocation":47},"/partners/",{"textColor":282,"title":283,"text":284,"link":285},"#000","What’s new in GitLab","Stay updated with our latest features and improvements.",{"text":286,"config":287},"Read the latest",{"href":288,"dataGaName":289,"dataGaLocation":47},"/releases/whats-new/","whats new",{"text":291,"config":292,"lists":294},"Company",{"dataNavLevelOne":293},"company",[295],{"items":296},[297,302,308,310,315,320,325,330,335,340,345],{"text":298,"config":299},"About",{"href":300,"dataGaName":301,"dataGaLocation":47},"/company/","about",{"text":303,"config":304,"footerGa":307},"Jobs",{"href":305,"dataGaName":306,"dataGaLocation":47},"/jobs/","jobs",{"dataGaName":306},{"text":273,"config":309},{"href":275,"dataGaName":276,"dataGaLocation":47},{"text":311,"config":312},"Leadership",{"href":313,"dataGaName":314,"dataGaLocation":47},"/company/team/e-group/","leadership",{"text":316,"config":317},"Team",{"href":318,"dataGaName":319,"dataGaLocation":47},"/company/team/","team",{"text":321,"config":322},"Handbook",{"href":323,"dataGaName":324,"dataGaLocation":47},"https://handbook.gitlab.com/","handbook",{"text":326,"config":327},"Investor relations",{"href":328,"dataGaName":329,"dataGaLocation":47},"https://ir.gitlab.com/","investor relations",{"text":331,"config":332},"Trust Center",{"href":333,"dataGaName":334,"dataGaLocation":47},"/security/","trust center",{"text":336,"config":337},"AI Transparency Center",{"href":338,"dataGaName":339,"dataGaLocation":47},"/ai-transparency-center/","ai transparency center",{"text":341,"config":342},"Newsletter",{"href":343,"dataGaName":344,"dataGaLocation":47},"/company/contact/#contact-forms","newsletter",{"text":346,"config":347},"Press",{"href":348,"dataGaName":349,"dataGaLocation":47},"/press/","press",{"text":351,"config":352,"lists":353},"Contact us",{"dataNavLevelOne":293},[354],{"items":355},[356,359,364],{"text":54,"config":357},{"href":56,"dataGaName":358,"dataGaLocation":47},"talk to sales",{"text":360,"config":361},"Support portal",{"href":362,"dataGaName":363,"dataGaLocation":47},"https://support.gitlab.com","support portal",{"text":365,"config":366},"Customer portal",{"href":367,"dataGaName":368,"dataGaLocation":47},"https://customers.gitlab.com/customers/sign_in/","customer portal",{"close":370,"login":371,"suggestions":378},"Close",{"text":372,"link":373},"To search repositories and projects, login to",{"text":374,"config":375},"gitlab.com",{"href":61,"dataGaName":376,"dataGaLocation":377},"search login","search",{"text":379,"default":380},"Suggestions",[381,383,387,389,393,397],{"text":76,"config":382},{"href":81,"dataGaName":76,"dataGaLocation":377},{"text":384,"config":385},"Code Suggestions (AI)",{"href":386,"dataGaName":384,"dataGaLocation":377},"/solutions/code-suggestions/",{"text":22,"config":388},{"href":111,"dataGaName":22,"dataGaLocation":377},{"text":390,"config":391},"GitLab on AWS",{"href":392,"dataGaName":390,"dataGaLocation":377},"/partners/technology-partners/aws/",{"text":394,"config":395},"GitLab on Google Cloud",{"href":396,"dataGaName":394,"dataGaLocation":377},"/partners/technology-partners/google-cloud-platform/",{"text":398,"config":399},"Why GitLab?",{"href":89,"dataGaName":398,"dataGaLocation":377},{"freeTrial":401,"mobileIcon":406,"desktopIcon":411,"secondaryButton":414},{"text":402,"config":403},"Start free trial",{"href":404,"dataGaName":52,"dataGaLocation":405},"https://gitlab.com/-/trials/new/","nav",{"altText":407,"config":408},"Gitlab Icon",{"src":409,"dataGaName":410,"dataGaLocation":405},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203874/jypbw1jx72aexsoohd7x.svg","gitlab icon",{"altText":407,"config":412},{"src":413,"dataGaName":410,"dataGaLocation":405},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203875/gs4c8p8opsgvflgkswz9.svg",{"text":415,"config":416},"Get Started",{"href":417,"dataGaName":418,"dataGaLocation":405},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com/get-started/","get started",{"freeTrial":420,"mobileIcon":424,"desktopIcon":426},{"text":421,"config":422},"Learn more about GitLab Duo",{"href":81,"dataGaName":423,"dataGaLocation":405},"gitlab duo",{"altText":407,"config":425},{"src":409,"dataGaName":410,"dataGaLocation":405},{"altText":407,"config":427},{"src":413,"dataGaName":410,"dataGaLocation":405},{"button":429,"mobileIcon":434,"desktopIcon":436},{"text":430,"config":431},"/switch",{"href":432,"dataGaName":433,"dataGaLocation":405},"#contact","switch",{"altText":407,"config":435},{"src":409,"dataGaName":410,"dataGaLocation":405},{"altText":407,"config":437},{"src":438,"dataGaName":410,"dataGaLocation":405},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1773335277/ohhpiuoxoldryzrnhfrh.png",{"freeTrial":440,"mobileIcon":445,"desktopIcon":447},{"text":441,"config":442},"Back to pricing",{"href":188,"dataGaName":443,"dataGaLocation":405,"icon":444},"back to pricing","GoBack",{"altText":407,"config":446},{"src":409,"dataGaName":410,"dataGaLocation":405},{"altText":407,"config":448},{"src":413,"dataGaName":410,"dataGaLocation":405},{"title":450,"button":451,"config":456},"See how agentic AI transforms software delivery",{"text":452,"config":453},"Watch GitLab Transcend now",{"href":454,"dataGaName":455,"dataGaLocation":47},"/events/transcend/virtual/","transcend event",{"layout":457,"icon":458,"disabled":29},"release","AiStar",{"data":460},{"text":461,"source":462,"edit":468,"contribute":473,"config":478,"items":483,"minimal":687},"Git is a trademark of Software Freedom Conservancy and our use of 'GitLab' is under license",{"text":463,"config":464},"View page source",{"href":465,"dataGaName":466,"dataGaLocation":467},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/","page source","footer",{"text":469,"config":470},"Edit this page",{"href":471,"dataGaName":472,"dataGaLocation":467},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/content/","web ide",{"text":474,"config":475},"Please contribute",{"href":476,"dataGaName":477,"dataGaLocation":467},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/CONTRIBUTING.md/","please contribute",{"twitter":479,"facebook":480,"youtube":481,"linkedin":482},"https://twitter.com/gitlab","https://www.facebook.com/gitlab","https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg","https://www.linkedin.com/company/gitlab-com",[484,531,582,626,653],{"title":186,"links":485,"subMenu":500},[486,490,495],{"text":487,"config":488},"View plans",{"href":188,"dataGaName":489,"dataGaLocation":467},"view plans",{"text":491,"config":492},"Why Premium?",{"href":493,"dataGaName":494,"dataGaLocation":467},"/pricing/premium/","why premium",{"text":496,"config":497},"Why Ultimate?",{"href":498,"dataGaName":499,"dataGaLocation":467},"/pricing/ultimate/","why ultimate",[501],{"title":502,"links":503},"Contact Us",[504,507,509,511,516,521,526],{"text":505,"config":506},"Contact sales",{"href":56,"dataGaName":57,"dataGaLocation":467},{"text":360,"config":508},{"href":362,"dataGaName":363,"dataGaLocation":467},{"text":365,"config":510},{"href":367,"dataGaName":368,"dataGaLocation":467},{"text":512,"config":513},"Status",{"href":514,"dataGaName":515,"dataGaLocation":467},"https://status.gitlab.com/","status",{"text":517,"config":518},"Terms of use",{"href":519,"dataGaName":520,"dataGaLocation":467},"/terms/","terms of use",{"text":522,"config":523},"Privacy statement",{"href":524,"dataGaName":525,"dataGaLocation":467},"/privacy/","privacy statement",{"text":527,"config":528},"Cookie preferences",{"dataGaName":529,"dataGaLocation":467,"id":530,"isOneTrustButton":29},"cookie preferences","ot-sdk-btn",{"title":92,"links":532,"subMenu":541},[533,537],{"text":534,"config":535},"DevSecOps platform",{"href":74,"dataGaName":536,"dataGaLocation":467},"devsecops platform",{"text":538,"config":539},"AI-Assisted Development",{"href":81,"dataGaName":540,"dataGaLocation":467},"ai-assisted development",[542],{"title":543,"links":544},"Topics",[545,549,554,559,564,567,572,577],{"text":546,"config":547},"CICD",{"href":548,"dataGaName":38,"dataGaLocation":467},"/topics/ci-cd/",{"text":550,"config":551},"GitOps",{"href":552,"dataGaName":553,"dataGaLocation":467},"/topics/gitops/","gitops",{"text":555,"config":556},"DevOps",{"href":557,"dataGaName":558,"dataGaLocation":467},"/topics/devops/","devops",{"text":560,"config":561},"Version Control",{"href":562,"dataGaName":563,"dataGaLocation":467},"/topics/version-control/","version control",{"text":23,"config":565},{"href":566,"dataGaName":11,"dataGaLocation":467},"/topics/devsecops/",{"text":568,"config":569},"Cloud Native",{"href":570,"dataGaName":571,"dataGaLocation":467},"/topics/cloud-native/","cloud native",{"text":573,"config":574},"AI for Coding",{"href":575,"dataGaName":576,"dataGaLocation":467},"/topics/devops/ai-for-coding/","ai for coding",{"text":578,"config":579},"Agentic AI",{"href":580,"dataGaName":581,"dataGaLocation":467},"/topics/agentic-ai/","agentic ai",{"title":583,"links":584},"Solutions",[585,587,589,594,598,601,605,608,610,613,616,621],{"text":133,"config":586},{"href":128,"dataGaName":133,"dataGaLocation":467},{"text":122,"config":588},{"href":106,"dataGaName":107,"dataGaLocation":467},{"text":590,"config":591},"Agile development",{"href":592,"dataGaName":593,"dataGaLocation":467},"/solutions/agile-delivery/","agile delivery",{"text":595,"config":596},"SCM",{"href":118,"dataGaName":597,"dataGaLocation":467},"source code management",{"text":546,"config":599},{"href":111,"dataGaName":600,"dataGaLocation":467},"continuous integration & delivery",{"text":602,"config":603},"Value stream management",{"href":161,"dataGaName":604,"dataGaLocation":467},"value stream management",{"text":550,"config":606},{"href":607,"dataGaName":553,"dataGaLocation":467},"/solutions/gitops/",{"text":171,"config":609},{"href":173,"dataGaName":174,"dataGaLocation":467},{"text":611,"config":612},"Small business",{"href":178,"dataGaName":179,"dataGaLocation":467},{"text":614,"config":615},"Public sector",{"href":183,"dataGaName":184,"dataGaLocation":467},{"text":617,"config":618},"Education",{"href":619,"dataGaName":620,"dataGaLocation":467},"/solutions/education/","education",{"text":622,"config":623},"Financial services",{"href":624,"dataGaName":625,"dataGaLocation":467},"/solutions/finance/","financial services",{"title":191,"links":627},[628,630,632,634,637,639,641,643,645,647,649,651],{"text":203,"config":629},{"href":205,"dataGaName":206,"dataGaLocation":467},{"text":208,"config":631},{"href":210,"dataGaName":211,"dataGaLocation":467},{"text":213,"config":633},{"href":215,"dataGaName":216,"dataGaLocation":467},{"text":218,"config":635},{"href":220,"dataGaName":636,"dataGaLocation":467},"docs",{"text":240,"config":638},{"href":242,"dataGaName":243,"dataGaLocation":467},{"text":235,"config":640},{"href":237,"dataGaName":238,"dataGaLocation":467},{"text":250,"config":642},{"href":252,"dataGaName":253,"dataGaLocation":467},{"text":258,"config":644},{"href":260,"dataGaName":261,"dataGaLocation":467},{"text":263,"config":646},{"href":265,"dataGaName":266,"dataGaLocation":467},{"text":268,"config":648},{"href":270,"dataGaName":271,"dataGaLocation":467},{"text":273,"config":650},{"href":275,"dataGaName":276,"dataGaLocation":467},{"text":278,"config":652},{"href":280,"dataGaName":24,"dataGaLocation":467},{"title":291,"links":654},[655,657,659,661,663,665,667,671,676,678,680,682],{"text":298,"config":656},{"href":300,"dataGaName":293,"dataGaLocation":467},{"text":303,"config":658},{"href":305,"dataGaName":306,"dataGaLocation":467},{"text":311,"config":660},{"href":313,"dataGaName":314,"dataGaLocation":467},{"text":316,"config":662},{"href":318,"dataGaName":319,"dataGaLocation":467},{"text":321,"config":664},{"href":323,"dataGaName":324,"dataGaLocation":467},{"text":326,"config":666},{"href":328,"dataGaName":329,"dataGaLocation":467},{"text":668,"config":669},"Sustainability",{"href":670,"dataGaName":668,"dataGaLocation":467},"/sustainability/",{"text":672,"config":673},"Diversity, inclusion and belonging (DIB)",{"href":674,"dataGaName":675,"dataGaLocation":467},"/diversity-inclusion-belonging/","Diversity, inclusion and belonging",{"text":331,"config":677},{"href":333,"dataGaName":334,"dataGaLocation":467},{"text":341,"config":679},{"href":343,"dataGaName":344,"dataGaLocation":467},{"text":346,"config":681},{"href":348,"dataGaName":349,"dataGaLocation":467},{"text":683,"config":684},"Modern Slavery Transparency Statement",{"href":685,"dataGaName":686,"dataGaLocation":467},"https://handbook.gitlab.com/handbook/legal/modern-slavery-act-transparency-statement/","modern slavery transparency statement",{"items":688},[689,692,695],{"text":690,"config":691},"Terms",{"href":519,"dataGaName":520,"dataGaLocation":467},{"text":693,"config":694},"Cookies",{"dataGaName":529,"dataGaLocation":467,"id":530,"isOneTrustButton":29},{"text":696,"config":697},"Privacy",{"href":524,"dataGaName":525,"dataGaLocation":467},[699],{"id":700,"title":701,"body":27,"config":702,"content":704,"description":27,"extension":26,"meta":709,"navigation":29,"path":710,"seo":711,"stem":712,"__hash__":713},"blogAuthors/en-us/blog/authors/josh-kodroff-pulumi.yml","Josh Kodroff Pulumi",{"template":703},"BlogAuthor",{"role":705,"name":9,"config":706},"Sr. Solutions Architect, Pulumi",{"headshot":707,"ctfId":708},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683425/Blog/Author%20Headshots/joshkodroff.jpg","2GF0MF1ngEBxos4nRKt8tL",{},"/en-us/blog/authors/josh-kodroff-pulumi",{},"en-us/blog/authors/josh-kodroff-pulumi","wx26OcV-rSnQqkeErKktuYnDWb88fFYZuUGRvnsY5gw",[715,728,743],{"content":716,"config":726},{"title":717,"description":718,"authors":719,"heroImage":721,"date":722,"body":723,"category":11,"tags":724},"Teaching software development the easy way using GitLab","Learn how University of Washington lecturer Stephen G. Dame uses GitLab for Education to manage student assignments, distribute course materials, and provide inline code feedback at scale.\n",[720],"Rod Burns","https://res.cloudinary.com/about-gitlab-com/image/upload/v1749659537/Blog/Hero%20Images/display-article-image-0679-1800x945-fy26.png","2026-04-29","For instructors teaching software development, one of the biggest logistical challenges is assignment distribution and feedback at scale. How do you give large groups of students access to course materials, keep solution code private, and still deliver meaningful, contextual feedback without lots of administrative overhead?\n\nThe **[GitLab for Education program](https://about.gitlab.com/solutions/education/)** provides qualifying institutions with free access to **GitLab Ultimate**, enabling instructors to build professional-grade workflows that mirror real-world software development environments. In this article, you'll learn how Stephen G. Dame, a lecturer in the Computing and Software Systems department at the University of Washington, Bothell, uses simple workflows in GitLab to manage everything from course materials to student feedback across multiple classes.\n\n## From aerospace to academia: Bringing GitLab to the classroom\n\nDame came to academia with years of experience as a chief software engineer at Boeing Commercial Airplanes, where GitLab was used for aerospace projects. As an adjunct professor, he became an early advocate for GitLab within the university, joining the GitLab for Education program to access the full feature set needed to run structured, scalable course workflows.\n\n> **\"GitLab provides the greatest way to organize multiple classes, student assignments, lectures, and code samples through the use of Groups and Subgroups, which I found to be unique to GitLab compared to other repository platforms.\"**\n>\n> - Stephen G. Dame, University of Washington, Bothell\n\n## Set up groups: Build the right structure before writing a line of code\n\nThe foundation of an effective GitLab-based course is a well-planned group hierarchy. GitLab's **[Groups and Subgroups](https://docs.gitlab.com/tutorials/manage_user/#create-the-organization-parent-group-and-subgroups)** allow instructors to model the natural structure of a university department institution, course, and role with precise, inheritable permissions at every level.\n\nDame's structure places the university at the root (`UWTeaching`), with each course occupying its own subgroup (e.g. `css430`). Within each course sit repositories for `lecture-materials` and `code`, alongside dedicated Subgroups for `students` and `graders`. Instructor materials remain private, while student and grader subgroups are configured with controlled permissions so that assignment briefs and solutions are visible only to the right people.\n\n![Screenshot of GitLab group hierarchy — institution, course subgroup, and per-student subgroups](https://res.cloudinary.com/about-gitlab-com/image/upload/v1777463673/dpxfnitv76pdmvcqtgag.png)\n\nPermissions cascade downward through the hierarchy via **Manage > Members**, allowing Dame to add students to a course's `students` subgroup with `Reporter` access and an expiration date tied to the end of the academic quarter. Students can clone and pull from assignment repositories but cannot push — keeping solution code firmly under instructor control.\n\nStudents are guided to set up SSH keys across all their working environments (local machines, cloud shells, virtual machines) so they can clone repositories and receive weekly updates via `git pull`. They copy relevant code into their own private repositories to manage their own version history.\n\n**Tip for large classes:** For larger cohorts, adding students by hand is impractical. GitLab's REST API lets you automate subgroup creation and membership from a list of usernames. Below is a sample Python script that handles this:\n\n```python\n    import gitlab\n    from datetime import datetime\n\n    # Connect to your GitLab instance\n    gl = gitlab.Gitlab('https://gitlab.com', private_token='YOUR_PRIVATE_TOKEN')\n\n    # Target parent group ID (e.g., the ID for \"css430 > students\")\n    parent_group_id = 12345678\n\n    # Set expiration: typically the beginning of the next month after quarter end\n    expiry_date = '2025-01-01'\n\n    # List of collected student usernames\n    student_list = ['alice_css430', 'bob_css430', 'carol_css430', 'dave_css430', 'eve_css430']\n\n    for username in student_list:\n        try:\n            # 1. Create a personal subgroup for the student\n            subgroup = gl.groups.create({\n                'name': username,\n                'path': username,\n                'parent_id': parent_group_id,\n                'visibility': 'private'\n            })\n\n            # 2. Add student to the new subgroup with Expiration\n            user = gl.users.list(username=username)[0]\n            subgroup.members.create({\n                'user_id': user.id,\n                'access_level': gitlab.const.REPORTER_ACCESS,\n                'expires_at': expiry_date\n            })\n            print(f\"Success: Subgroup created and student added for {username}\")\n        except Exception as e:\n            print(f\"Error processing {username}: {e}\")\n```\nThere is also an [open source project that automates class management](https://gitlab.com/edu-docs/class-management-automation) published by GitLab that provides additional tooling for this workflow.\n## Give feedback where the work actually lives\n\nOnce the structure is in place, the feedback workflow is where GitLab's value becomes most apparent to students. Dame asks students to submit assignments by opening a **[merge request](https://docs.gitlab.com/user/project/merge_requests/)** in their repository. This gives instructors an immediate, clean diff of everything the student has written.\n![A GitLab merge request showing inline code comment function for an instructor](https://res.cloudinary.com/about-gitlab-com/image/upload/v1777467468/icclzyglbkwlvfysggbi.png)\nInstructors can click any line of code and leave an **inline comment** — not just flagging what is wrong, but explaining why, and pointing to what to look at next. Students receive this feedback in direct context with their code, which is far more actionable than a comment at the bottom of a submitted document.\n\n## Join GitLab for Education\n\nSetting up your first GitLab assignment takes some initial effort, but once the structure is in place it largely runs itself. The real payoff goes beyond organization: Students graduate having worked daily in an environment that mirrors professional software development, building habits around [version control](https://about.gitlab.com/topics/version-control/) and [code review](https://docs.gitlab.com/development/code_review/) rather than learning them as abstract concepts.\n\nIf you are just getting started, keep it simple. Begin with a single course group, one assignment template, and a basic pipeline. The structure will grow naturally alongside your confidence with the platform.\n\nMake sure to **[sign up for GitLab for Education](https://about.gitlab.com/solutions/education/join/)** so that you and your students can access all top-tier features, including unlimited reviewers on merge requests, additional compute minutes, and expanded storage.\n\n> [Apply to the GitLab for Education program today](https://about.gitlab.com/solutions/education/join/).",[620,725],"open source",{"featured":14,"template":15,"slug":727},"teaching-software-development-the-easy-way-using-gitlab",{"content":729,"config":741},{"description":730,"authors":731,"heroImage":733,"date":734,"title":735,"body":736,"category":11,"tags":737},"AI-generated code is 34% of development work. Discover how to balance productivity gains with quality, reliability, and security.",[732],"Manav Khurana","https://res.cloudinary.com/about-gitlab-com/image/upload/v1767982271/e9ogyosmuummq7j65zqg.png","2026-01-08","AI is reshaping DevSecOps: Attend GitLab Transcend to see what’s next","AI promises a step change in innovation velocity, but most software teams are hitting a wall. According to our latest [Global DevSecOps Report](https://about.gitlab.com/developer-survey/), AI-generated code now accounts for 34% of all development work. Yet 70% of DevSecOps professionals report that AI is making compliance management more difficult, and 76% say agentic AI will create unprecedented security challenges.\n\nThis is the AI paradox: AI accelerates coding, but software delivery slows down as teams struggle to test, secure, and deploy all that code.\n\n## Productivity gains meet workflow bottlenecks\nThe problem isn't AI itself. It's how software gets built today. The traditional DevSecOps lifecycle contains hundreds of small tasks that developers must navigate manually: updating tickets, running tests, requesting reviews, waiting for approvals, fixing merge conflicts, addressing security findings. These tasks drain an average of seven hours per week from every team member, according to our research.\n\nDevelopment teams are producing code faster than ever, but that code still crawls through fragmented toolchains, manual handoffs, and disconnected processes. In fact, 60% of DevSecOps teams use more than five tools for software development overall, and 49% use more than five AI tools. This fragmentation creates collaboration barriers, with 94% of DevSecOps professionals experiencing factors that limit collaboration in the software development lifecycle.\n\nThe answer isn't more tools. It's intelligent orchestration that brings software teams and their AI agents together across projects and release cycles, with enterprise-grade security, governance, and compliance built in.\n\n## Seeking deeper human-AI partnerships\nDevSecOps professionals don't want AI to take over — they want reliable partnerships. The vast majority (82%) say using agentic AI would increase their job satisfaction, and 43% envision an ideal future with a 50/50 split between human and AI contributions. They're ready to trust AI with 37% of their daily tasks without human review, particularly for documentation, test writing, and code reviews.\n\nWhat we heard resoundingly from DevSecOps professionals is that AI won't replace them; rather, it will fundamentally reshape their roles. 83% of DevSecOps professionals believe AI will significantly change their work within five years, and notably, 76% think this will create more engineering jobs, not fewer. As coding becomes easier with AI, engineers who can architect systems, ensure quality, and apply business context will be in high demand.\n\nCritically, 88% agree there are essential human qualities that AI will never fully replace, including creativity, innovation, collaboration, and strategic vision.\n\nSo how can organizations bridge the gap between AI’s promise and the reality of fragmented workflows?\n\n## Join us at GitLab Transcend: Explore how to drive real value with agentic AI\nOn February 10, 2026, GitLab will be hosting Transcend, where we'll reveal how intelligent orchestration transforms AI-powered software development. You'll get a first look at GitLab's upcoming product roadmap and learn how teams are solving real-world challenges by modernizing development workflows with AI.\n\nOrganizations winning in this new era balance AI adoption with security, compliance, and platform consolidation. AI offers genuine productivity gains when implemented thoughtfully — not by replacing human developers, but by freeing DevSecOps professionals to focus on strategic thinking and creative innovation.\n\n[Register for Transcend today](https://about.gitlab.com/events/transcend/virtual/) to secure your spot and discover how intelligent orchestration can help your software teams stay in flow.",[738,739,740],"AI/ML","DevOps platform","security",{"featured":29,"template":15,"slug":742},"ai-is-reshaping-devsecops-attend-gitlab-transcend-to-see-whats-next",{"content":744,"config":755},{"title":745,"description":746,"authors":747,"heroImage":749,"date":750,"body":751,"category":11,"tags":752},"Atlassian ending Data Center as GitLab maintains deployment choice","As Atlassian transitions Data Center customers to cloud-only, GitLab presents a menu of deployment choices that map to business needs.",[748],"Emilio Salvador","https://res.cloudinary.com/about-gitlab-com/image/upload/v1750098354/Blog/Hero%20Images/Blog/Hero%20Images/blog-image-template-1800x945%20%281%29_5XrohmuWBNuqL89BxVUzWm_1750098354056.png","2025-10-07","Change is never easy, especially when it's not your choice. Atlassian's announcement that [all Data Center products will reach end-of-life by March 28, 2029](https://www.atlassian.com/blog/announcements/atlassian-ascend), means thousands of organizations must now reconsider their DevSecOps deployment and infrastructure. But you don't have to settle for deployment options that don't fit your needs. GitLab maintains your freedom to choose — whether you need self-managed for compliance, cloud for convenience, or hybrid for flexibility — all within a single AI-powered DevSecOps platform that respects your requirements.\n\nWhile other vendors force migrations to cloud-only architectures, GitLab remains committed to supporting the deployment choices that match your business needs. Whether you're managing sensitive government data, operating in air-gapped environments, or simply prefer the control of self-managed deployments, we understand that one size doesn't fit all.\n\n## The cloud isn't the answer for everyone\n\nFor the many companies that invested millions of dollars in Data Center deployments, including those that migrated to Data Center [after its Server products were discontinued](https://about.gitlab.com/blog/atlassian-server-ending-move-to-a-single-devsecops-platform/), this announcement represents more than a product sunset. It signals a fundamental shift away from customer-centric architecture choices, forcing enterprises into difficult positions: accept a deployment model that doesn't fit their needs, or find a vendor that respects their requirements.\n\nMany of the organizations requiring self-managed deployments represent some of the world's most important organizations: healthcare systems protecting patient data, financial institutions managing trillions in assets, government agencies safeguarding national security, and defense contractors operating in air-gapped environments.\n\nThese organizations don't choose self-managed deployments for convenience; they choose them for compliance, security, and sovereignty requirements that cloud-only architectures simply cannot meet. Organizations operating in closed environments with restricted or no internet access aren't exceptions — they represent a significant portion of enterprise customers across various industries.\n\n![GitLab vs. Atlassian comparison table](https://res.cloudinary.com/about-gitlab-com/image/upload/v1759928476/ynl7wwmkh5xyqhszv46m.jpg)\n\n## The real cost of forced cloud migration goes beyond dollars\n\nWhile cloud-only vendors frame mandatory migrations as \"upgrades,\" organizations face substantial challenges beyond simple financial costs:\n\n* **Lost integration capabilities:** Years of custom integrations with legacy systems, carefully crafted workflows, and enterprise-specific automations become obsolete. Organizations with deep integrations to legacy systems often find cloud migration technically infeasible.\n\n* **Regulatory constraints:** For organizations in regulated industries, cloud migration isn't just complex — it's often not permitted. Data residency requirements, air-gapped environments, and strict regulatory frameworks don't bend to vendor preferences. The absence of single-tenant solutions in many cloud-only approaches creates insurmountable compliance barriers.\n\n* **Productivity impacts:** Cloud-only architectures often require juggling multiple products: separate tools for planning, code management, CI/CD, and documentation. Each tool means another context switch, another integration to maintain, another potential point of failure. GitLab research shows [30% of developers spend at least 50% of their job maintaining and/or integrating their DevSecOps toolchain](https://about.gitlab.com/developer-survey/). Fragmented architectures exacerbate this challenge rather than solving it.\n\n## GitLab offers choice, commitment, and consolidation\n\nEnterprise customers deserve a trustworthy technology partner. That's why we've committed to supporting a range of deployment options — whether you need on-premises for compliance, hybrid for flexibility, or cloud for convenience, the choice remains yours. That commitment continues with [GitLab Duo](https://about.gitlab.com/gitlab-duo-agent-platform/), our AI solution that supports developers at every stage of their workflow.\n\nBut we offer more than just deployment flexibility. While other vendors might force you to cobble together their products into a fragmented toolchain, GitLab provides everything in a **comprehensive AI-native DevSecOps platform**. Source code management, CI/CD, security scanning, Agile planning, and documentation are all managed within a single application and a single vendor relationship.\n\nThis isn't theoretical. When Airbus and [Iron Mountain](https://about.gitlab.com/customers/iron-mountain/) evaluated their existing fragmented toolchains, they consistently identified challenges: poor user experience, missing functionalities like built-in security scanning and review apps, and management complexity from plugin troubleshooting. **These aren't minor challenges; they're major blockers for modern software delivery.**\n\n## Your migration path: Simpler than you think\n\nWe've helped thousands of organizations migrate from other vendors, and we've built the tools and expertise to make your transition smooth:\n\n* **Automated migration tools:** Our [Bitbucket Server importer](https://docs.gitlab.com/user/import/bitbucket_server/) brings over repositories, pull requests, comments, and even Large File Storage (LFS) objects. For Jira, our [built-in importer](https://docs.gitlab.com/user/project/import/jira/) handles issues, descriptions, and labels, with professional services available for complex migrations.\n\n* **Proven at scale:** A 500 GiB repository with 13,000 pull requests, 10,000 branches, and 7,000 tags is likely to [take just 8 hours to migrate](https://docs.gitlab.com/user/import/bitbucket_server/) from Bitbucket to GitLab using parallel processing.\n\n* **Immediate ROI:** A [Forrester Consulting Total Economic Impact™ study commissioned by GitLab](https://about.gitlab.com/resources/study-forrester-tei-gitlab-ultimate/) found that investing in GitLab Ultimate confirms these benefits translate to real bottom-line impact, with a three-year 483% ROI, 5x time saved in security related activities, and 25% savings in software toolchain costs.\n\n## Start your journey to a unified DevSecOps platform\n\nForward-thinking organizations aren't waiting for vendor-mandated deadlines. They're evaluating alternatives now, while they have time to migrate thoughtfully to platforms that protect their investments and deliver on promises.\n\nOrganizations invest in self-managed deployments because they need control, compliance, and customization. When vendors deprecate these capabilities, they remove not just features but the fundamental ability to choose environments matching business requirements.\n\nModern DevSecOps platforms should offer complete functionality that respects deployment needs, consolidates toolchains, and accelerates software delivery, without forcing compromises on security or data sovereignty.\n\n[Talk to our sales team](https://about.gitlab.com/sales/) today about your migration options, or explore our [comprehensive migration resources](https://about.gitlab.com/move-to-gitlab-from-atlassian/) to see how thousands of organizations have already made the switch.\n\nYou also can [try GitLab Ultimate with GitLab Duo Enterprise](https://about.gitlab.com/free-trial/devsecops/) for free for 30 days to see what a unified DevSecOps platform can do for your organization.",[571,23,753,754],"product","features",{"featured":29,"template":15,"slug":756},"atlassian-ending-data-center-as-gitlab-maintains-deployment-choice",{"promotions":758},[759,773,784,795],{"id":760,"categories":761,"header":763,"text":764,"button":765,"image":770},"ai-modernization",[762],"ai-ml","Is AI achieving its promise at scale?","Quiz will take 5 minutes or less",{"text":766,"config":767},"Get your AI maturity score",{"href":768,"dataGaName":769,"dataGaLocation":243},"/assessments/ai-modernization-assessment/","modernization assessment",{"config":771},{"src":772},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138786/qix0m7kwnd8x2fh1zq49.png",{"id":774,"categories":775,"header":776,"text":764,"button":777,"image":781},"devops-modernization",[753,11],"Are you just managing tools or shipping innovation?",{"text":778,"config":779},"Get your DevOps maturity score",{"href":780,"dataGaName":769,"dataGaLocation":243},"/assessments/devops-modernization-assessment/",{"config":782},{"src":783},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138785/eg818fmakweyuznttgid.png",{"id":785,"categories":786,"header":787,"text":764,"button":788,"image":792},"security-modernization",[740],"Are you trading speed for security?",{"text":789,"config":790},"Get your security maturity score",{"href":791,"dataGaName":769,"dataGaLocation":243},"/assessments/security-modernization-assessment/",{"config":793},{"src":794},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138786/p4pbqd9nnjejg5ds6mdk.png",{"id":796,"paths":797,"header":800,"text":801,"button":802,"image":807},"github-azure-migration",[798,799],"migration-from-azure-devops-to-gitlab","integrating-azure-devops-scm-and-gitlab","Is your team ready for GitHub's Azure move?","GitHub is already rebuilding around Azure. Find out what it means for you.",{"text":803,"config":804},"See how GitLab compares to GitHub",{"href":805,"dataGaName":806,"dataGaLocation":243},"/compare/gitlab-vs-github/github-azure-migration/","github azure migration",{"config":808},{"src":783},{"header":810,"blurb":811,"button":812,"secondaryButton":817},"Start building faster today","See what your team can do with the intelligent orchestration platform for DevSecOps.\n",{"text":813,"config":814},"Get your free trial",{"href":815,"dataGaName":52,"dataGaLocation":816},"https://gitlab.com/-/trial_registrations/new?glm_content=default-saas-trial&glm_source=about.gitlab.com/","feature",{"text":505,"config":818},{"href":56,"dataGaName":57,"dataGaLocation":816},1777493617398]