[{"data":1,"prerenderedAt":3370},["ShallowReactive",2],{"/blog/managing-gitlab-resources-with-pulumi":3,"navigation-en-us":2586,"banner-en-us":3011,"footer-en-us":3021,"blog-post-authors-en-us-Josh Kodroff, Pulumi":3264,"blog-related-posts-en-us-managing-gitlab-resources-with-pulumi":3281,"blog-promotions-en-us":3306,"next-steps-en-us":3360},{"id":4,"title":5,"authors":6,"body":8,"category":2564,"date":2565,"description":2566,"extension":2567,"externalUrl":2568,"faq":2568,"featured":2569,"heroImage":2570,"meta":2571,"navigation":335,"path":2572,"seo":2573,"slug":2577,"stem":2578,"tags":2579,"template":2584,"updatedDate":2568,"__hash__":2585},"blogPosts/en-us/blog/managing-gitlab-resources-with-pulumi.md","Managing GitLab resources with Pulumi",[7],"Josh Kodroff, Pulumi",{"type":9,"value":10,"toc":2553},"minimark",[11,22,25,30,33,36,39,56,59,63,66,94,103,122,125,150,153,172,181,233,236,240,245,266,277,286,427,431,434,447,452,801,804,812,821,824,869,873,891,897,978,981,1005,1012,1058,1067,1071,1074,1111,1117,1370,1373,1395,1416,1422,1545,1554,1651,1660,1752,1757,1920,1931,1935,1940,2045,2060,2095,2099,2102,2114,2121,2124,2141,2147,2163,2166,2184,2191,2211,2214,2367,2370,2405,2408,2415,2418,2423,2429,2435,2441,2447,2450,2456,2463,2469,2472,2479,2491,2494,2501,2505,2508,2511,2549],[12,13,14,15,21],"p",{},"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. ",[16,17,20],"a",{"href":18,"rel":19},"https://pulumi.com?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources",[],"Pulumi"," 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.",[12,23,24],{},"Let's explore the power of using Pulumi and GitLab.",[26,27,29],"h2",{"id":28},"what-is-pulumi","What is Pulumi?",[12,31,32],{},"Pulumi 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.",[12,34,35],{},"Pulumi 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.",[12,37,38],{},"It 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:",[40,41,42,50],"ul",{},[43,44,45,49],"li",{},[46,47,48],"strong",{},"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.",[43,51,52,55],{},[46,53,54],{},"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.",[12,57,58],{},"The 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.",[26,60,62],{"id":61},"initializing-the-project","Initializing the project",[12,64,65],{},"To complete this example you'll need:",[67,68,69,77,85],"ol",{},[43,70,71,76],{},[16,72,75],{"href":73,"rel":74},"https://app.pulumi.com?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources",[],"A Pulumi Cloud account",". 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.",[43,78,79,80,84],{},"A GitLab account, group, and a GitLab token set to the ",[81,82,83],"code",{},"GITLAB_TOKEN"," environment variable.",[43,86,87,88,93],{},"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 ",[16,89,92],{"href":90,"rel":91},"https://www.pulumi.com/registry/packages/aws/installation-configuration/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources",[],"AWS Classic: Installation and Configuration",".",[12,95,96,97,102],{},"This example will use two providers from the ",[16,98,101],{"href":99,"rel":100},"https://www.pulumi.com/registry/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources",[],"Pulumi Registry",":",[67,104,105,114],{},[43,106,107,108,113],{},"The ",[16,109,112],{"href":110,"rel":111},"https://www.pulumi.com/registry/packages/gitlab/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources",[],"GitLab Provider"," 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).",[43,115,107,116,121],{},[16,117,120],{"href":118,"rel":119},"https://www.pulumi.com/registry/packages/aws/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources",[],"AWS Classic Provider"," will be used to manage AWS resources to create OpenID Connect (OIDC) connectivity between AWS and GitLab.",[12,123,124],{},"You 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:",[126,127,132],"pre",{"className":128,"code":129,"language":130,"meta":131,"style":131},"language-bash shiki shiki-themes github-light","pulumi new typescript\n","bash","",[81,133,134],{"__ignoreMap":131},[135,136,139,143,147],"span",{"class":137,"line":138},"line",1,[135,140,142],{"class":141},"s7eDp","pulumi",[135,144,146],{"class":145},"sYBdl"," new",[135,148,149],{"class":145}," typescript\n",[12,151,152],{},"This will bootstrap an empty Pulumi program. Now you can import the provider SDKs for the providers you'll need:",[126,154,156],{"className":128,"code":155,"language":130,"meta":131,"style":131},"npm i @pulumi/aws @pulumi/gitlab\n",[81,157,158],{"__ignoreMap":131},[135,159,160,163,166,169],{"class":137,"line":138},[135,161,162],{"class":141},"npm",[135,164,165],{"class":145}," i",[135,167,168],{"class":145}," @pulumi/aws",[135,170,171],{"class":145}," @pulumi/gitlab\n",[12,173,174,175,178,179,102],{},"Your ",[81,176,177],{},"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 ",[81,180,177],{},[126,182,186],{"className":183,"code":184,"language":185,"meta":131,"style":131},"language-typescript shiki shiki-themes github-light","import * as gitlab from \"@pulumi/gitlab\";\nimport * as aws from \"@pulumi/aws\";\n","typescript",[81,187,188,214],{"__ignoreMap":131},[135,189,190,194,198,201,205,208,211],{"class":137,"line":138},[135,191,193],{"class":192},"sD7c4","import",[135,195,197],{"class":196},"sYu0t"," *",[135,199,200],{"class":192}," as",[135,202,204],{"class":203},"sgsFI"," gitlab ",[135,206,207],{"class":192},"from",[135,209,210],{"class":145}," \"@pulumi/gitlab\"",[135,212,213],{"class":203},";\n",[135,215,217,219,221,223,226,228,231],{"class":137,"line":216},2,[135,218,193],{"class":192},[135,220,197],{"class":196},[135,222,200],{"class":192},[135,224,225],{"class":203}," aws ",[135,227,207],{"class":192},[135,229,230],{"class":145}," \"@pulumi/aws\"",[135,232,213],{"class":203},[12,234,235],{},"Now you are ready to add some resources!",[26,237,239],{"id":238},"adding-your-first-resources","Adding your first resources",[12,241,242,243,102],{},"First, let's define a variable that will hold the audience claim in our OIDC JWT token. Add the following code to ",[81,244,177],{},[126,246,248],{"className":183,"code":247,"language":185,"meta":131,"style":131},"const audience = \"gitlab.com\";\n",[81,249,250],{"__ignoreMap":131},[135,251,252,255,258,261,264],{"class":137,"line":138},[135,253,254],{"class":192},"const",[135,256,257],{"class":196}," audience",[135,259,260],{"class":192}," =",[135,262,263],{"class":145}," \"gitlab.com\"",[135,265,213],{"class":203},[12,267,268,269,273,274,93],{},"The above code assume you're using the GitLab SaaS (",[16,270,271],{"href":271,"rel":272},"https://gitlab.com",[],") If you are using a private GitLab install, your value should be the domain of your GitLab install, e.g. ",[81,275,276],{},"gitlab.example.com",[12,278,279,280,285],{},"Then, you'll use a ",[16,281,284],{"href":282,"rel":283},"https://www.pulumi.com/docs/concepts/resources/functions/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources",[],"Pulumi function"," to grab an existing GitLab group by name and create a new public GitLab project in your GitLab group:",[126,287,289],{"className":183,"code":288,"language":185,"meta":131,"style":131},"const 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",[81,290,291,309,324,330,337,363,375,386,410,422],{"__ignoreMap":131},[135,292,293,295,298,300,303,306],{"class":137,"line":138},[135,294,254],{"class":192},[135,296,297],{"class":196}," group",[135,299,260],{"class":192},[135,301,302],{"class":203}," gitlab.",[135,304,305],{"class":141},"getGroup",[135,307,308],{"class":203},"({\n",[135,310,311,314,317,320],{"class":137,"line":216},[135,312,313],{"class":203},"  fullPath: ",[135,315,316],{"class":145},"\"my-gitlab-group\"",[135,318,319],{"class":203},", ",[135,321,323],{"class":322},"sAwPA","// Replace the value with the name of your GL group\n",[135,325,327],{"class":137,"line":326},3,[135,328,329],{"class":203},"});\n",[135,331,333],{"class":137,"line":332},4,[135,334,336],{"emptyLinePlaceholder":335},true,"\n",[135,338,340,342,345,347,349,351,354,357,360],{"class":137,"line":339},5,[135,341,254],{"class":192},[135,343,344],{"class":196}," project",[135,346,260],{"class":192},[135,348,146],{"class":192},[135,350,302],{"class":203},[135,352,353],{"class":141},"Project",[135,355,356],{"class":203},"(",[135,358,359],{"class":145},"\"pulumi-gitlab-demo\"",[135,361,362],{"class":203},", {\n",[135,364,366,369,372],{"class":137,"line":365},6,[135,367,368],{"class":203},"  visibilityLevel: ",[135,370,371],{"class":145},"\"public\"",[135,373,374],{"class":203},",\n",[135,376,378,381,384],{"class":137,"line":377},7,[135,379,380],{"class":203},"  defaultBranch: ",[135,382,383],{"class":145},"\"main\"",[135,385,374],{"class":203},[135,387,389,392,395,397,401,404,407],{"class":137,"line":388},8,[135,390,391],{"class":203},"  namespaceId: group.",[135,393,394],{"class":141},"then",[135,396,356],{"class":203},[135,398,400],{"class":399},"sqxcx","g",[135,402,403],{"class":192}," =>",[135,405,406],{"class":141}," parseInt",[135,408,409],{"class":203},"(g.id)),\n",[135,411,413,416,419],{"class":137,"line":412},9,[135,414,415],{"class":203},"  archiveOnDestroy: ",[135,417,418],{"class":196},"false",[135,420,421],{"class":322}," // Be sure to set this to `true` for any non-demo repos you manage with Pulumi!\n",[135,423,425],{"class":137,"line":424},10,[135,426,329],{"class":203},[26,428,430],{"id":429},"creating-oidc-resources","Creating OIDC resources",[12,432,433],{},"To 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.",[12,435,436,437,440,441,443,444,446],{},"You'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 ",[81,438,439],{},"FullAdministrator"," access to Pulumi, but ",[81,442,439],{}," is often practically required, e.g. where IAM resources, like roles, need to be created. Role creation requires ",[81,445,439],{},". This consideration also applies to IaC tools like Terraform.)",[12,448,449,450,102],{},"Add the following code to ",[81,451,177],{},[126,453,455],{"className":183,"code":454,"language":185,"meta":131,"style":131},"const 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",[81,456,457,471,475,499,516,529,539,544,557,561,565,589,595,606,612,618,629,635,641,647,658,664,670,676,682,688,722,728,733,739,745,751,756,761,779,790,796],{"__ignoreMap":131},[135,458,459,461,464,466,469],{"class":137,"line":138},[135,460,254],{"class":192},[135,462,463],{"class":196}," GITLAB_OIDC_PROVIDER_THUMBPRINT",[135,465,260],{"class":192},[135,467,468],{"class":145}," \"b3dd7606d2b5a8b4a13771dbecc9ee1cecafa38a\"",[135,470,213],{"class":203},[135,472,473],{"class":137,"line":216},[135,474,336],{"emptyLinePlaceholder":335},[135,476,477,479,482,484,486,489,492,494,497],{"class":137,"line":326},[135,478,254],{"class":192},[135,480,481],{"class":196}," gitlabOidcProvider",[135,483,260],{"class":192},[135,485,146],{"class":192},[135,487,488],{"class":203}," aws.iam.",[135,490,491],{"class":141},"OpenIdConnectProvider",[135,493,356],{"class":203},[135,495,496],{"class":145},"\"gitlab-oidc-provider\"",[135,498,362],{"class":203},[135,500,501,504,507,510,513],{"class":137,"line":332},[135,502,503],{"class":203},"  clientIdLists: [",[135,505,506],{"class":145},"`https://${",[135,508,509],{"class":203},"audience",[135,511,512],{"class":145},"}`",[135,514,515],{"class":203},"],\n",[135,517,518,521,523,525,527],{"class":137,"line":339},[135,519,520],{"class":203},"  url: ",[135,522,506],{"class":145},[135,524,509],{"class":203},[135,526,512],{"class":145},[135,528,374],{"class":203},[135,530,531,534,537],{"class":137,"line":365},[135,532,533],{"class":203},"  thumbprintLists: [",[135,535,536],{"class":196},"GITLAB_OIDC_PROVIDER_THUMBPRINT",[135,538,515],{"class":203},[135,540,541],{"class":137,"line":377},[135,542,543],{"class":203},"}, {\n",[135,545,546,549,552,554],{"class":137,"line":388},[135,547,548],{"class":203},"  deleteBeforeReplace: ",[135,550,551],{"class":196},"true",[135,553,319],{"class":203},[135,555,556],{"class":322},"// URLs are unique identifiers and cannot be auto-named, so we have to delete before replace.\n",[135,558,559],{"class":137,"line":412},[135,560,329],{"class":203},[135,562,563],{"class":137,"line":424},[135,564,336],{"emptyLinePlaceholder":335},[135,566,568,570,573,575,577,579,582,584,587],{"class":137,"line":567},11,[135,569,254],{"class":192},[135,571,572],{"class":196}," gitlabAdminRole",[135,574,260],{"class":192},[135,576,146],{"class":192},[135,578,488],{"class":203},[135,580,581],{"class":141},"Role",[135,583,356],{"class":203},[135,585,586],{"class":145},"\"gitlabAdminRole\"",[135,588,362],{"class":203},[135,590,592],{"class":137,"line":591},12,[135,593,594],{"class":203},"  assumeRolePolicy: {\n",[135,596,598,601,604],{"class":137,"line":597},13,[135,599,600],{"class":203},"    Version: ",[135,602,603],{"class":145},"\"2012-10-17\"",[135,605,374],{"class":203},[135,607,609],{"class":137,"line":608},14,[135,610,611],{"class":203},"    Statement: [\n",[135,613,615],{"class":137,"line":614},15,[135,616,617],{"class":203},"      {\n",[135,619,621,624,627],{"class":137,"line":620},16,[135,622,623],{"class":203},"        Effect: ",[135,625,626],{"class":145},"\"Allow\"",[135,628,374],{"class":203},[135,630,632],{"class":137,"line":631},17,[135,633,634],{"class":203},"        Principal: {\n",[135,636,638],{"class":137,"line":637},18,[135,639,640],{"class":203},"          Federated: gitlabOidcProvider.arn,\n",[135,642,644],{"class":137,"line":643},19,[135,645,646],{"class":203},"        },\n",[135,648,650,653,656],{"class":137,"line":649},20,[135,651,652],{"class":203},"        Action: ",[135,654,655],{"class":145},"\"sts:AssumeRoleWithWebIdentity\"",[135,657,374],{"class":203},[135,659,661],{"class":137,"line":660},21,[135,662,663],{"class":203},"        Condition: {\n",[135,665,667],{"class":137,"line":666},22,[135,668,669],{"class":203},"          StringLike: {\n",[135,671,673],{"class":137,"line":672},23,[135,674,675],{"class":322},"            // Note: Square brackets around the key are what allow us to use a\n",[135,677,679],{"class":137,"line":678},24,[135,680,681],{"class":322},"            // templated string. See:\n",[135,683,685],{"class":137,"line":684},25,[135,686,687],{"class":322},"            // https://stackoverflow.com/questions/59791960/how-to-use-template-literal-as-key-inside-object-literal\n",[135,689,691,694,697,699,702,705,708,711,714,716,719],{"class":137,"line":690},26,[135,692,693],{"class":203},"            [",[135,695,696],{"class":145},"`${",[135,698,509],{"class":203},[135,700,701],{"class":145},"}:sub`",[135,703,704],{"class":203},"]: pulumi.",[135,706,707],{"class":141},"interpolate",[135,709,710],{"class":145},"`project_path:${",[135,712,713],{"class":203},"project",[135,715,93],{"class":145},[135,717,718],{"class":203},"pathWithNamespace",[135,720,721],{"class":145},"}:ref_type:branch:ref:*`\n",[135,723,725],{"class":137,"line":724},27,[135,726,727],{"class":203},"          },\n",[135,729,731],{"class":137,"line":730},28,[135,732,646],{"class":203},[135,734,736],{"class":137,"line":735},29,[135,737,738],{"class":203},"      },\n",[135,740,742],{"class":137,"line":741},30,[135,743,744],{"class":203},"    ],\n",[135,746,748],{"class":137,"line":747},31,[135,749,750],{"class":203},"  },\n",[135,752,754],{"class":137,"line":753},32,[135,755,329],{"class":203},[135,757,759],{"class":137,"line":758},33,[135,760,336],{"emptyLinePlaceholder":335},[135,762,764,767,769,772,774,777],{"class":137,"line":763},34,[135,765,766],{"class":192},"new",[135,768,488],{"class":203},[135,770,771],{"class":141},"RolePolicyAttachment",[135,773,356],{"class":203},[135,775,776],{"class":145},"\"gitlabAdminRolePolicy\"",[135,778,362],{"class":203},[135,780,782,785,788],{"class":137,"line":781},35,[135,783,784],{"class":203},"  policyArn: ",[135,786,787],{"class":145},"\"arn:aws:iam::aws:policy/AdministratorAccess\"",[135,789,374],{"class":203},[135,791,793],{"class":137,"line":792},36,[135,794,795],{"class":203},"  role: gitlabAdminRole.name,\n",[135,797,799],{"class":137,"line":798},37,[135,800,329],{"class":203},[12,802,803],{},"A few things to be aware of regarding the thumbprint:",[67,805,806,809],{},[43,807,808],{},"If you are self-hosting GitLab, you'll need to obtain the thumbprint from your private GitLab installation.",[43,810,811],{},"If you're using GitLab SaaS, it's possible GitLab's OIDC certificate may have been rotated by the time you are reading this.",[12,813,814,815,820],{},"In either case, you can obtain the correct/latest thumbprint value by following AWS' instructions contained in ",[16,816,819],{"href":817,"rel":818},"https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html",[],"Obtaining the thumbprint for an OpenID Connect Identity Provider"," in the AWS docs.",[12,822,823],{},"You'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:",[126,825,827],{"className":183,"code":826,"language":185,"meta":131,"style":131},"new gitlab.ProjectVariable(\"role-arn\", {\n  project: project.id,\n  key: \"ROLE_ARN\",\n  value: gitlabAdminRole.arn,\n});\n",[81,828,829,845,850,860,865],{"__ignoreMap":131},[135,830,831,833,835,838,840,843],{"class":137,"line":138},[135,832,766],{"class":192},[135,834,302],{"class":203},[135,836,837],{"class":141},"ProjectVariable",[135,839,356],{"class":203},[135,841,842],{"class":145},"\"role-arn\"",[135,844,362],{"class":203},[135,846,847],{"class":137,"line":216},[135,848,849],{"class":203},"  project: project.id,\n",[135,851,852,855,858],{"class":137,"line":326},[135,853,854],{"class":203},"  key: ",[135,856,857],{"class":145},"\"ROLE_ARN\"",[135,859,374],{"class":203},[135,861,862],{"class":137,"line":332},[135,863,864],{"class":203},"  value: gitlabAdminRole.arn,\n",[135,866,867],{"class":137,"line":339},[135,868,329],{"class":203},[26,870,872],{"id":871},"project-hook-optional","Project hook (optional)",[12,874,875,876,879,880,885,886,93],{},"Pulumi features an integration with GitLab via a webhook that will post the output of the ",[81,877,878],{},"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 ",[16,881,884],{"href":882,"rel":883},"https://app.pulumi.com/signup?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources",[],"sign up for a free trial"," 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 ",[16,887,890],{"href":888,"rel":889},"https://www.pulumi.com/docs/using-pulumi/continuous-delivery/gitlab-app/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources",[],"Pulumi CI/CD & GitLab integration",[12,892,893,894,896],{},"To set up the webhook, add the following to your ",[81,895,177],{}," file:",[126,898,900],{"className":183,"code":899,"language":185,"meta":131,"style":131},"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",[81,901,902,918,922,931,940,949,965,974],{"__ignoreMap":131},[135,903,904,906,908,911,913,916],{"class":137,"line":138},[135,905,766],{"class":192},[135,907,302],{"class":203},[135,909,910],{"class":141},"ProjectHook",[135,912,356],{"class":203},[135,914,915],{"class":145},"\"project-hook\"",[135,917,362],{"class":203},[135,919,920],{"class":137,"line":216},[135,921,849],{"class":203},[135,923,924,926,929],{"class":137,"line":326},[135,925,520],{"class":203},[135,927,928],{"class":145},"\"https://api.pulumi.com/workflow/gitlab\"",[135,930,374],{"class":203},[135,932,933,936,938],{"class":137,"line":332},[135,934,935],{"class":203},"  mergeRequestsEvents: ",[135,937,551],{"class":196},[135,939,374],{"class":203},[135,941,942,945,947],{"class":137,"line":339},[135,943,944],{"class":203},"  enableSslVerification: ",[135,946,551],{"class":196},[135,948,374],{"class":203},[135,950,951,954,957,960,963],{"class":137,"line":365},[135,952,953],{"class":203},"  token: process.env[",[135,955,956],{"class":145},"\"PULUMI_ACCESS_TOKEN\"",[135,958,959],{"class":203},"]",[135,961,962],{"class":192},"!",[135,964,374],{"class":203},[135,966,967,970,972],{"class":137,"line":377},[135,968,969],{"class":203},"  pushEvents: ",[135,971,418],{"class":196},[135,973,374],{"class":203},[135,975,976],{"class":137,"line":388},[135,977,329],{"class":203},[12,979,980],{},"Note 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:",[126,982,984],{"className":128,"code":983,"language":130,"meta":131,"style":131},"pulumi config set --secret pulumiAccessToken ${PULUMI_ACCESS_TOKEN}\n",[81,985,986],{"__ignoreMap":131},[135,987,988,990,993,996,999,1002],{"class":137,"line":138},[135,989,142],{"class":141},[135,991,992],{"class":145}," config",[135,994,995],{"class":145}," set",[135,997,998],{"class":196}," --secret",[135,1000,1001],{"class":145}," pulumiAccessToken",[135,1003,1004],{"class":203}," ${PULUMI_ACCESS_TOKEN}\n",[12,1006,1007,1008,1011],{},"This will store the encrypted value in your Pulumi stack configuration file (",[81,1009,1010],{},"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:",[126,1013,1015],{"className":183,"code":1014,"language":185,"meta":131,"style":131},"const config = new pulumi.Config();\nconst pulumiAccessToken = config.requireSecret(\"pulumiAccessToken\");\n",[81,1016,1017,1036],{"__ignoreMap":131},[135,1018,1019,1021,1023,1025,1027,1030,1033],{"class":137,"line":138},[135,1020,254],{"class":192},[135,1022,992],{"class":196},[135,1024,260],{"class":192},[135,1026,146],{"class":192},[135,1028,1029],{"class":203}," pulumi.",[135,1031,1032],{"class":141},"Config",[135,1034,1035],{"class":203},"();\n",[135,1037,1038,1040,1042,1044,1047,1050,1052,1055],{"class":137,"line":216},[135,1039,254],{"class":192},[135,1041,1001],{"class":196},[135,1043,260],{"class":192},[135,1045,1046],{"class":203}," config.",[135,1048,1049],{"class":141},"requireSecret",[135,1051,356],{"class":203},[135,1053,1054],{"class":145},"\"pulumiAccessToken\"",[135,1056,1057],{"class":203},");\n",[12,1059,1060,1061,1066],{},"For more details on secrets handling in Pulumi, see ",[16,1062,1065],{"href":1063,"rel":1064},"https://www.pulumi.com/docs/concepts/secrets/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources",[],"Secrets"," in the Pulumi docs.",[26,1068,1070],{"id":1069},"creating-a-repository-and-adding-repository-files","Creating a repository and adding repository files",[12,1072,1073],{},"You'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:",[126,1075,1077],{"className":128,"code":1076,"language":130,"meta":131,"style":131},"mkdir -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",[81,1078,1079,1090,1101],{"__ignoreMap":131},[135,1080,1081,1084,1087],{"class":137,"line":138},[135,1082,1083],{"class":141},"mkdir",[135,1085,1086],{"class":196}," -p",[135,1088,1089],{"class":145}," repository-files/scripts\n",[135,1091,1092,1095,1098],{"class":137,"line":216},[135,1093,1094],{"class":141},"touch",[135,1096,1097],{"class":145}," repository-files/.gitlab-ci.yml",[135,1099,1100],{"class":145}," repository-files/scripts/{aws-auth.sh,pulumi-preview.sh,pulumi-up.sh}\n",[135,1102,1103,1106,1109],{"class":137,"line":326},[135,1104,1105],{"class":141},"chmod",[135,1107,1108],{"class":145}," +x",[135,1110,1100],{"class":145},[12,1112,1113,1114,102],{},"Next, 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 ",[81,1115,1116],{},"repository-files/.gitlab-ci.yml",[126,1118,1122],{"className":1119,"code":1120,"language":1121,"meta":131,"style":131},"language-yaml shiki shiki-themes github-light","default:\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","yaml",[81,1123,1124,1133,1140,1151,1165,1169,1176,1184,1188,1195,1204,1211,1218,1228,1235,1243,1250,1257,1264,1271,1281,1285,1292,1300,1306,1312,1320,1326,1332,1338,1344,1351,1358],{"__ignoreMap":131},[135,1125,1126,1130],{"class":137,"line":138},[135,1127,1129],{"class":1128},"shJU0","default",[135,1131,1132],{"class":203},":\n",[135,1134,1135,1138],{"class":137,"line":216},[135,1136,1137],{"class":1128},"  image",[135,1139,1132],{"class":203},[135,1141,1142,1145,1148],{"class":137,"line":326},[135,1143,1144],{"class":1128},"    name",[135,1146,1147],{"class":203},": ",[135,1149,1150],{"class":145},"\"pulumi/pulumi:3.91.1\"\n",[135,1152,1153,1156,1159,1162],{"class":137,"line":332},[135,1154,1155],{"class":1128},"    entrypoint",[135,1157,1158],{"class":203},": [",[135,1160,1161],{"class":145},"\"\"",[135,1163,1164],{"class":203},"]\n",[135,1166,1167],{"class":137,"line":339},[135,1168,336],{"emptyLinePlaceholder":335},[135,1170,1171,1174],{"class":137,"line":365},[135,1172,1173],{"class":1128},"stages",[135,1175,1132],{"class":203},[135,1177,1178,1181],{"class":137,"line":377},[135,1179,1180],{"class":203},"  - ",[135,1182,1183],{"class":145},"infrastructure-update\n",[135,1185,1186],{"class":137,"line":388},[135,1187,336],{"emptyLinePlaceholder":335},[135,1189,1190,1193],{"class":137,"line":412},[135,1191,1192],{"class":1128},"pulumi-up",[135,1194,1132],{"class":203},[135,1196,1197,1200,1202],{"class":137,"line":424},[135,1198,1199],{"class":1128},"  stage",[135,1201,1147],{"class":203},[135,1203,1183],{"class":145},[135,1205,1206,1209],{"class":137,"line":567},[135,1207,1208],{"class":1128},"  id_tokens",[135,1210,1132],{"class":203},[135,1212,1213,1216],{"class":137,"line":591},[135,1214,1215],{"class":1128},"    GITLAB_OIDC_TOKEN",[135,1217,1132],{"class":203},[135,1219,1220,1223,1225],{"class":137,"line":597},[135,1221,1222],{"class":1128},"      aud",[135,1224,1147],{"class":203},[135,1226,1227],{"class":145},"https://gitlab.com\n",[135,1229,1230,1233],{"class":137,"line":608},[135,1231,1232],{"class":1128},"  before_script",[135,1234,1132],{"class":203},[135,1236,1237,1240],{"class":137,"line":614},[135,1238,1239],{"class":203},"    - ",[135,1241,1242],{"class":145},"chmod +x ./scripts/*.sh\n",[135,1244,1245,1247],{"class":137,"line":620},[135,1246,1239],{"class":203},[135,1248,1249],{"class":145},"./scripts/aws-auth.sh\n",[135,1251,1252,1255],{"class":137,"line":631},[135,1253,1254],{"class":1128},"  script",[135,1256,1132],{"class":203},[135,1258,1259,1261],{"class":137,"line":637},[135,1260,1239],{"class":203},[135,1262,1263],{"class":145},"./scripts/pulumi-up.sh\n",[135,1265,1266,1269],{"class":137,"line":643},[135,1267,1268],{"class":1128},"  only",[135,1270,1132],{"class":203},[135,1272,1273,1275,1278],{"class":137,"line":649},[135,1274,1239],{"class":203},[135,1276,1277],{"class":145},"main",[135,1279,1280],{"class":322}," # i.e., the name of the default branch\n",[135,1282,1283],{"class":137,"line":660},[135,1284,336],{"emptyLinePlaceholder":335},[135,1286,1287,1290],{"class":137,"line":666},[135,1288,1289],{"class":1128},"pulumi-preview",[135,1291,1132],{"class":203},[135,1293,1294,1296,1298],{"class":137,"line":672},[135,1295,1199],{"class":1128},[135,1297,1147],{"class":203},[135,1299,1183],{"class":145},[135,1301,1302,1304],{"class":137,"line":678},[135,1303,1208],{"class":1128},[135,1305,1132],{"class":203},[135,1307,1308,1310],{"class":137,"line":684},[135,1309,1215],{"class":1128},[135,1311,1132],{"class":203},[135,1313,1314,1316,1318],{"class":137,"line":690},[135,1315,1222],{"class":1128},[135,1317,1147],{"class":203},[135,1319,1227],{"class":145},[135,1321,1322,1324],{"class":137,"line":724},[135,1323,1232],{"class":1128},[135,1325,1132],{"class":203},[135,1327,1328,1330],{"class":137,"line":730},[135,1329,1239],{"class":203},[135,1331,1242],{"class":145},[135,1333,1334,1336],{"class":137,"line":735},[135,1335,1239],{"class":203},[135,1337,1249],{"class":145},[135,1339,1340,1342],{"class":137,"line":741},[135,1341,1254],{"class":1128},[135,1343,1132],{"class":203},[135,1345,1346,1348],{"class":137,"line":747},[135,1347,1239],{"class":203},[135,1349,1350],{"class":145},"./scripts/pulumi-preview.sh\n",[135,1352,1353,1356],{"class":137,"line":753},[135,1354,1355],{"class":1128},"  rules",[135,1357,1132],{"class":203},[135,1359,1360,1362,1365,1367],{"class":137,"line":758},[135,1361,1239],{"class":203},[135,1363,1364],{"class":1128},"if",[135,1366,1147],{"class":203},[135,1368,1369],{"class":145},"$CI_PIPELINE_SOURCE == 'merge_request_event'\n",[12,1371,1372],{},"The 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):",[67,1374,1375,1386],{},[43,1376,1377,1378,1380,1381,1385],{},"Run the ",[81,1379,878],{}," 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 ",[1382,1383,1384],"em",{},"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.",[43,1387,1377,1388,1391,1392,1394],{},[81,1389,1390],{},"pulumi up"," command when code is merged to the default branch (called ",[81,1393,1277],{}," by default). This process constitutes continuous delivery.",[12,1396,1397,1398,1405,1406,1408,1409,93],{},"Note that this example uses the ",[16,1399,1402],{"href":1400,"rel":1401},"https://hub.docker.com/r/pulumi/pulumi",[],[81,1403,1404],{},"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 ",[81,1407,1404],{}," 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. ",[16,1410,1413],{"href":1411,"rel":1412},"https://hub.docker.com/r/pulumi/pulumi-nodejs",[],[81,1414,1415],{},"pulumi/pulumi-nodejs",[12,1417,1418,1419,102],{},"Then you'll need to write the script that authenticates GitLab with AWS via OIDC. Place the following code in ",[81,1420,1421],{},"repository-files/scripts/aws-auth.sh",[126,1423,1425],{"className":128,"code":1424,"language":130,"meta":131,"style":131},"#!/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",[81,1426,1427,1432,1436,1445,1465,1486,1490,1505,1516,1520,1534],{"__ignoreMap":131},[135,1428,1429],{"class":137,"line":138},[135,1430,1431],{"class":322},"#!/bin/bash\n",[135,1433,1434],{"class":137,"line":216},[135,1435,336],{"emptyLinePlaceholder":335},[135,1437,1438,1440,1442],{"class":137,"line":326},[135,1439,1083],{"class":141},[135,1441,1086],{"class":196},[135,1443,1444],{"class":145}," ~/.aws\n",[135,1446,1447,1450,1453,1456,1459,1462],{"class":137,"line":332},[135,1448,1449],{"class":196},"echo",[135,1451,1452],{"class":145}," \"${",[135,1454,1455],{"class":203},"GITLAB_OIDC_TOKEN",[135,1457,1458],{"class":145},"}\"",[135,1460,1461],{"class":192}," >",[135,1463,1464],{"class":145}," /tmp/web_identity_token\n",[135,1466,1467,1469,1472,1475,1478,1481,1483],{"class":137,"line":339},[135,1468,1449],{"class":196},[135,1470,1471],{"class":196}," -e",[135,1473,1474],{"class":145}," \"[profile oidc]\\nrole_arn=${",[135,1476,1477],{"class":203},"ROLE_ARN",[135,1479,1480],{"class":145},"}\\nweb_identity_token_file=/tmp/web_identity_token\"",[135,1482,1461],{"class":192},[135,1484,1485],{"class":145}," ~/.aws/config\n",[135,1487,1488],{"class":137,"line":365},[135,1489,336],{"emptyLinePlaceholder":335},[135,1491,1492,1494,1497,1500,1502],{"class":137,"line":377},[135,1493,1449],{"class":196},[135,1495,1496],{"class":145}," \"length of GITLAB_OIDC_TOKEN=${",[135,1498,1499],{"class":192},"#",[135,1501,1455],{"class":203},[135,1503,1504],{"class":145},"}\"\n",[135,1506,1507,1509,1512,1514],{"class":137,"line":388},[135,1508,1449],{"class":196},[135,1510,1511],{"class":145}," \"ROLE_ARN=${",[135,1513,1477],{"class":203},[135,1515,1504],{"class":145},[135,1517,1518],{"class":137,"line":412},[135,1519,336],{"emptyLinePlaceholder":335},[135,1521,1522,1525,1528,1531],{"class":137,"line":424},[135,1523,1524],{"class":192},"export",[135,1526,1527],{"class":203}," AWS_PROFILE",[135,1529,1530],{"class":192},"=",[135,1532,1533],{"class":145},"\"oidc\"\n",[135,1535,1536,1539,1542],{"class":137,"line":567},[135,1537,1538],{"class":141},"aws",[135,1540,1541],{"class":145}," sts",[135,1543,1544],{"class":145}," get-caller-identity\n",[12,1546,1547,1548,1550,1551,102],{},"For continuous integration, you'll need a script that will execute the ",[81,1549,878],{}," command when a merge request is opened. Place the following code in ",[81,1552,1553],{},"repository-files/scripts/pulumi-preview.sh",[126,1555,1557],{"className":128,"code":1556,"language":130,"meta":131,"style":131},"#!/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",[81,1558,1559,1563,1573,1577,1589,1593,1601,1608,1621,1634,1644],{"__ignoreMap":131},[135,1560,1561],{"class":137,"line":138},[135,1562,1431],{"class":322},[135,1564,1565,1568,1570],{"class":137,"line":216},[135,1566,1567],{"class":196},"set",[135,1569,1471],{"class":196},[135,1571,1572],{"class":196}," -x\n",[135,1574,1575],{"class":137,"line":326},[135,1576,336],{"emptyLinePlaceholder":335},[135,1578,1579,1581,1584,1586],{"class":137,"line":332},[135,1580,1524],{"class":192},[135,1582,1583],{"class":203}," PATH",[135,1585,1530],{"class":192},[135,1587,1588],{"class":203},"$PATH:$HOME/.pulumi/bin\n",[135,1590,1591],{"class":137,"line":339},[135,1592,336],{"emptyLinePlaceholder":335},[135,1594,1595,1598],{"class":137,"line":365},[135,1596,1597],{"class":141},"yarn",[135,1599,1600],{"class":145}," install\n",[135,1602,1603,1605],{"class":137,"line":377},[135,1604,142],{"class":141},[135,1606,1607],{"class":145}," login\n",[135,1609,1610,1612,1615,1618],{"class":137,"line":388},[135,1611,142],{"class":141},[135,1613,1614],{"class":145}," org",[135,1616,1617],{"class":145}," set-default",[135,1619,1620],{"class":203}," $PULUMI_ORG\n",[135,1622,1623,1625,1628,1631],{"class":137,"line":412},[135,1624,142],{"class":141},[135,1626,1627],{"class":145}," stack",[135,1629,1630],{"class":145}," select",[135,1632,1633],{"class":145}," dev\n",[135,1635,1636,1638,1640,1642],{"class":137,"line":424},[135,1637,1524],{"class":192},[135,1639,1527],{"class":203},[135,1641,1530],{"class":192},[135,1643,1533],{"class":145},[135,1645,1646,1648],{"class":137,"line":567},[135,1647,142],{"class":141},[135,1649,1650],{"class":145}," preview\n",[12,1652,1653,1654,1656,1657,102],{},"For continuous delivery, you'll need a similar script that will execute the ",[81,1655,1390],{}," command when the Merge Request is merged to the default branch. Place the following code in ",[81,1658,1659],{},"repository-files/scripts/pulumi-up.sh",[126,1661,1663],{"className":128,"code":1662,"language":130,"meta":131,"style":131},"#!/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",[81,1664,1665,1669,1677,1681,1686,1696,1700,1706,1712,1722,1732,1742],{"__ignoreMap":131},[135,1666,1667],{"class":137,"line":138},[135,1668,1431],{"class":322},[135,1670,1671,1673,1675],{"class":137,"line":216},[135,1672,1567],{"class":196},[135,1674,1471],{"class":196},[135,1676,1572],{"class":196},[135,1678,1679],{"class":137,"line":326},[135,1680,336],{"emptyLinePlaceholder":335},[135,1682,1683],{"class":137,"line":332},[135,1684,1685],{"class":322},"# Add the pulumi CLI to the PATH\n",[135,1687,1688,1690,1692,1694],{"class":137,"line":339},[135,1689,1524],{"class":192},[135,1691,1583],{"class":203},[135,1693,1530],{"class":192},[135,1695,1588],{"class":203},[135,1697,1698],{"class":137,"line":365},[135,1699,336],{"emptyLinePlaceholder":335},[135,1701,1702,1704],{"class":137,"line":377},[135,1703,1597],{"class":141},[135,1705,1600],{"class":145},[135,1707,1708,1710],{"class":137,"line":388},[135,1709,142],{"class":141},[135,1711,1607],{"class":145},[135,1713,1714,1716,1718,1720],{"class":137,"line":412},[135,1715,142],{"class":141},[135,1717,1614],{"class":145},[135,1719,1617],{"class":145},[135,1721,1620],{"class":203},[135,1723,1724,1726,1728,1730],{"class":137,"line":424},[135,1725,142],{"class":141},[135,1727,1627],{"class":145},[135,1729,1630],{"class":145},[135,1731,1633],{"class":145},[135,1733,1734,1736,1738,1740],{"class":137,"line":567},[135,1735,1524],{"class":192},[135,1737,1527],{"class":203},[135,1739,1530],{"class":192},[135,1741,1533],{"class":145},[135,1743,1744,1746,1749],{"class":137,"line":591},[135,1745,142],{"class":141},[135,1747,1748],{"class":145}," up",[135,1750,1751],{"class":196}," -y\n",[12,1753,1754,1755,896],{},"Finally, you'll need to add these files to your GitLab Project. Add the following code block to your ",[81,1756,177],{},[126,1758,1760],{"className":183,"code":1759,"language":185,"meta":131,"style":131},"[\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",[81,1761,1762,1767,1774,1781,1788,1795,1813,1845,1849,1862,1867,1872,1881,1886,1901,1911,1916],{"__ignoreMap":131},[135,1763,1764],{"class":137,"line":138},[135,1765,1766],{"class":203},"[\n",[135,1768,1769,1772],{"class":137,"line":216},[135,1770,1771],{"class":145},"  \"scripts/aws-auth.sh\"",[135,1773,374],{"class":203},[135,1775,1776,1779],{"class":137,"line":326},[135,1777,1778],{"class":145},"  \"scripts/pulumi-preview.sh\"",[135,1780,374],{"class":203},[135,1782,1783,1786],{"class":137,"line":332},[135,1784,1785],{"class":145},"  \"scripts/pulumi-up.sh\"",[135,1787,374],{"class":203},[135,1789,1790,1793],{"class":137,"line":339},[135,1791,1792],{"class":145},"  \".gitlab-ci.yml\"",[135,1794,374],{"class":203},[135,1796,1797,1800,1803,1805,1808,1810],{"class":137,"line":365},[135,1798,1799],{"class":203},"].",[135,1801,1802],{"class":141},"forEach",[135,1804,356],{"class":203},[135,1806,1807],{"class":399},"file",[135,1809,403],{"class":192},[135,1811,1812],{"class":203}," {\n",[135,1814,1815,1818,1821,1823,1826,1829,1831,1834,1836,1838,1840,1843],{"class":137,"line":377},[135,1816,1817],{"class":192},"  const",[135,1819,1820],{"class":196}," content",[135,1822,260],{"class":192},[135,1824,1825],{"class":203}," fs.",[135,1827,1828],{"class":141},"readFileSync",[135,1830,356],{"class":203},[135,1832,1833],{"class":145},"`repository-files/${",[135,1835,1807],{"class":203},[135,1837,512],{"class":145},[135,1839,319],{"class":203},[135,1841,1842],{"class":145},"\"utf-8\"",[135,1844,1057],{"class":203},[135,1846,1847],{"class":137,"line":388},[135,1848,336],{"emptyLinePlaceholder":335},[135,1850,1851,1854,1856,1859],{"class":137,"line":412},[135,1852,1853],{"class":192},"  new",[135,1855,302],{"class":203},[135,1857,1858],{"class":141},"RepositoryFile",[135,1860,1861],{"class":203},"(file, {\n",[135,1863,1864],{"class":137,"line":424},[135,1865,1866],{"class":203},"    project: project.id,\n",[135,1868,1869],{"class":137,"line":567},[135,1870,1871],{"class":203},"    filePath: file,\n",[135,1873,1874,1877,1879],{"class":137,"line":591},[135,1875,1876],{"class":203},"    branch: ",[135,1878,383],{"class":145},[135,1880,374],{"class":203},[135,1882,1883],{"class":137,"line":597},[135,1884,1885],{"class":203},"    content: content,\n",[135,1887,1888,1891,1894,1896,1899],{"class":137,"line":608},[135,1889,1890],{"class":203},"    commitMessage: ",[135,1892,1893],{"class":145},"`Add ${",[135,1895,1807],{"class":203},[135,1897,1898],{"class":145},"},`",[135,1900,374],{"class":203},[135,1902,1903,1906,1909],{"class":137,"line":614},[135,1904,1905],{"class":203},"    encoding: ",[135,1907,1908],{"class":145},"\"text\"",[135,1910,374],{"class":203},[135,1912,1913],{"class":137,"line":620},[135,1914,1915],{"class":203},"  });\n",[135,1917,1918],{"class":137,"line":631},[135,1919,329],{"class":203},[12,1921,1922,1923,1926,1927,1930],{},"Note that we're able to take advantage of general-purpose programming language features: We are able to create an array and use ",[81,1924,1925],{},"forEach()"," to iterate through its members, and we are able to use the ",[81,1928,1929],{},"fs.readFileSync()"," method from the Node.js runtime to read the contents of our file. This is powerful stuff!",[26,1932,1934],{"id":1933},"project-variables-and-stack-outputs","Project variables and stack outputs",[12,1936,1937,1938,102],{},"You'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 ",[81,1939,177],{},[126,1941,1943],{"className":183,"code":1942,"language":185,"meta":131,"style":131},"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\nnew gitlab.ProjectVariable(\"pulumi-org\", {\n  project: project.id,\n  key: \"PULUMI_ORG\",\n  value: pulumi.getOrganization(),\n});\n",[81,1944,1945,1960,1964,1972,1985,1994,1998,2002,2017,2021,2030,2041],{"__ignoreMap":131},[135,1946,1947,1949,1951,1953,1955,1958],{"class":137,"line":138},[135,1948,766],{"class":192},[135,1950,302],{"class":203},[135,1952,837],{"class":141},[135,1954,356],{"class":203},[135,1956,1957],{"class":145},"\"pulumi-access-token\"",[135,1959,362],{"class":203},[135,1961,1962],{"class":137,"line":216},[135,1963,849],{"class":203},[135,1965,1966,1968,1970],{"class":137,"line":326},[135,1967,854],{"class":203},[135,1969,956],{"class":145},[135,1971,374],{"class":203},[135,1973,1974,1977,1979,1981,1983],{"class":137,"line":332},[135,1975,1976],{"class":203},"  value: process.env[",[135,1978,956],{"class":145},[135,1980,959],{"class":203},[135,1982,962],{"class":192},[135,1984,374],{"class":203},[135,1986,1987,1990,1992],{"class":137,"line":339},[135,1988,1989],{"class":203},"  masked: ",[135,1991,551],{"class":196},[135,1993,374],{"class":203},[135,1995,1996],{"class":137,"line":365},[135,1997,329],{"class":203},[135,1999,2000],{"class":137,"line":377},[135,2001,336],{"emptyLinePlaceholder":335},[135,2003,2004,2006,2008,2010,2012,2015],{"class":137,"line":388},[135,2005,766],{"class":192},[135,2007,302],{"class":203},[135,2009,837],{"class":141},[135,2011,356],{"class":203},[135,2013,2014],{"class":145},"\"pulumi-org\"",[135,2016,362],{"class":203},[135,2018,2019],{"class":137,"line":412},[135,2020,849],{"class":203},[135,2022,2023,2025,2028],{"class":137,"line":424},[135,2024,854],{"class":203},[135,2026,2027],{"class":145},"\"PULUMI_ORG\"",[135,2029,374],{"class":203},[135,2031,2032,2035,2038],{"class":137,"line":567},[135,2033,2034],{"class":203},"  value: pulumi.",[135,2036,2037],{"class":141},"getOrganization",[135,2039,2040],{"class":203},"(),\n",[135,2042,2043],{"class":137,"line":591},[135,2044,329],{"class":203},[12,2046,2047,2048,2051,2052,2057,2058,102],{},"Finally, you'll need to add a stack output so that we can run the ",[81,2049,2050],{},"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 ",[16,2053,2056],{"href":2054,"rel":2055},"https://www.pulumi.com/learn/building-with-pulumi/stack-outputs/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources",[],"Understanding Stack Outputs",". Add the following to ",[81,2059,177],{},[126,2061,2063],{"className":183,"code":2062,"language":185,"meta":131,"style":131},"export const gitCloneCommand = pulumi.interpolate`git clone ${project.sshUrlToRepo}`;\n",[81,2064,2065],{"__ignoreMap":131},[135,2066,2067,2069,2072,2075,2077,2079,2081,2084,2086,2088,2091,2093],{"class":137,"line":138},[135,2068,1524],{"class":192},[135,2070,2071],{"class":192}," const",[135,2073,2074],{"class":196}," gitCloneCommand",[135,2076,260],{"class":192},[135,2078,1029],{"class":203},[135,2080,707],{"class":141},[135,2082,2083],{"class":145},"`git clone ${",[135,2085,713],{"class":203},[135,2087,93],{"class":145},[135,2089,2090],{"class":203},"sshUrlToRepo",[135,2092,512],{"class":145},[135,2094,213],{"class":203},[26,2096,2098],{"id":2097},"deploying-your-infrastructure-and-testing-the-pipeline","Deploying your infrastructure and testing the pipeline",[12,2100,2101],{},"To deploy your resources, run the following command:",[126,2103,2105],{"className":128,"code":2104,"language":130,"meta":131,"style":131},"pulumi up\n",[81,2106,2107],{"__ignoreMap":131},[135,2108,2109,2111],{"class":137,"line":138},[135,2110,142],{"class":141},[135,2112,2113],{"class":145}," up\n",[12,2115,2116,2117,2120],{},"Pulumi will output a list of the resources it intends to create. Select ",[81,2118,2119],{},"yes"," to continue.",[12,2122,2123],{},"Once the command has completed, you can run the following command to get the git clone command for your GitLab repo:",[126,2125,2127],{"className":128,"code":2126,"language":130,"meta":131,"style":131},"pulumi stack output gitCloneCommand\n",[81,2128,2129],{"__ignoreMap":131},[135,2130,2131,2133,2135,2138],{"class":137,"line":138},[135,2132,142],{"class":141},[135,2134,1627],{"class":145},[135,2136,2137],{"class":145}," output",[135,2139,2140],{"class":145}," gitCloneCommand\n",[12,2142,2143,2144,2146],{},"In a new, empty directory, run the ",[81,2145,2050],{}," command from your Pulumi stack output, e.g.:",[126,2148,2150],{"className":128,"code":2149,"language":130,"meta":131,"style":131},"git clone git@gitlab.com:jkodroff/pulumi-gitlab-demo-9de2a3b.git\n",[81,2151,2152],{"__ignoreMap":131},[135,2153,2154,2157,2160],{"class":137,"line":138},[135,2155,2156],{"class":141},"git",[135,2158,2159],{"class":145}," clone",[135,2161,2162],{"class":145}," git@gitlab.com:jkodroff/pulumi-gitlab-demo-9de2a3b.git\n",[12,2164,2165],{},"Change into the directory and create a new branch:",[126,2167,2169],{"className":128,"code":2168,"language":130,"meta":131,"style":131},"git checkout -b my-first-branch\n",[81,2170,2171],{"__ignoreMap":131},[135,2172,2173,2175,2178,2181],{"class":137,"line":138},[135,2174,2156],{"class":141},[135,2176,2177],{"class":145}," checkout",[135,2179,2180],{"class":196}," -b",[135,2182,2183],{"class":145}," my-first-branch\n",[12,2185,2186,2187,2190],{},"Now you are ready to create some sample infrastructure in our repository. You can use the ",[81,2188,2189],{},"aws-typescript"," to quickly generate a simple Pulumi program with AWS resources:",[126,2192,2194],{"className":128,"code":2193,"language":130,"meta":131,"style":131},"pulumi new aws-typescript -y --force\n",[81,2195,2196],{"__ignoreMap":131},[135,2197,2198,2200,2202,2205,2208],{"class":137,"line":138},[135,2199,142],{"class":141},[135,2201,146],{"class":145},[135,2203,2204],{"class":145}," aws-typescript",[135,2206,2207],{"class":196}," -y",[135,2209,2210],{"class":196}," --force\n",[12,2212,2213],{},"The template includes a very simple Pulumi program that you can use to prove out the pipeline:",[126,2215,2217],{"className":128,"code":2216,"language":130,"meta":131,"style":131},"$ 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",[81,2218,2219,2230,2249,2266,2284,2288,2314,2335,2339,2360],{"__ignoreMap":131},[135,2220,2221,2224,2227],{"class":137,"line":138},[135,2222,2223],{"class":141},"$",[135,2225,2226],{"class":145}," cat",[135,2228,2229],{"class":145}," index.ts\n",[135,2231,2232,2234,2236,2238,2241,2244,2247],{"class":137,"line":216},[135,2233,193],{"class":141},[135,2235,197],{"class":196},[135,2237,200],{"class":145},[135,2239,2240],{"class":145}," pulumi",[135,2242,2243],{"class":145}," from",[135,2245,2246],{"class":145}," \"@pulumi/pulumi\"",[135,2248,213],{"class":203},[135,2250,2251,2253,2255,2257,2260,2262,2264],{"class":137,"line":326},[135,2252,193],{"class":141},[135,2254,197],{"class":196},[135,2256,200],{"class":145},[135,2258,2259],{"class":145}," aws",[135,2261,2243],{"class":145},[135,2263,230],{"class":145},[135,2265,213],{"class":203},[135,2267,2268,2270,2272,2274,2277,2279,2282],{"class":137,"line":332},[135,2269,193],{"class":141},[135,2271,197],{"class":196},[135,2273,200],{"class":145},[135,2275,2276],{"class":145}," awsx",[135,2278,2243],{"class":145},[135,2280,2281],{"class":145}," \"@pulumi/awsx\"",[135,2283,213],{"class":203},[135,2285,2286],{"class":137,"line":339},[135,2287,336],{"emptyLinePlaceholder":335},[135,2289,2290,2293,2296,2299,2302,2305,2308,2311],{"class":137,"line":365},[135,2291,2292],{"class":141},"//",[135,2294,2295],{"class":145}," Create",[135,2297,2298],{"class":145}," an",[135,2300,2301],{"class":145}," AWS",[135,2303,2304],{"class":145}," resource",[135,2306,2307],{"class":203}," (S3 ",[135,2309,2310],{"class":145},"Bucket",[135,2312,2313],{"class":203},")\n",[135,2315,2316,2318,2321,2323,2325,2328,2330,2333],{"class":137,"line":377},[135,2317,254],{"class":141},[135,2319,2320],{"class":145}," bucket",[135,2322,260],{"class":145},[135,2324,146],{"class":145},[135,2326,2327],{"class":145}," aws.s3.Bucket",[135,2329,356],{"class":203},[135,2331,2332],{"class":141},"\"my-bucket\"",[135,2334,1057],{"class":203},[135,2336,2337],{"class":137,"line":388},[135,2338,336],{"emptyLinePlaceholder":335},[135,2340,2341,2343,2346,2349,2352,2355,2357],{"class":137,"line":412},[135,2342,2292],{"class":141},[135,2344,2345],{"class":145}," Export",[135,2347,2348],{"class":145}," the",[135,2350,2351],{"class":145}," name",[135,2353,2354],{"class":145}," of",[135,2356,2348],{"class":145},[135,2358,2359],{"class":145}," bucket\n",[135,2361,2362,2364],{"class":137,"line":424},[135,2363,1524],{"class":192},[135,2365,2366],{"class":203}," const bucketName = bucket.id;\n",[12,2368,2369],{},"Commit your changes and push your branch:",[126,2371,2373],{"className":128,"code":2372,"language":130,"meta":131,"style":131},"git add -A\ngit commit -m \"My first commit.\"\ngit push\n",[81,2374,2375,2385,2398],{"__ignoreMap":131},[135,2376,2377,2379,2382],{"class":137,"line":138},[135,2378,2156],{"class":141},[135,2380,2381],{"class":145}," add",[135,2383,2384],{"class":196}," -A\n",[135,2386,2387,2389,2392,2395],{"class":137,"line":216},[135,2388,2156],{"class":141},[135,2390,2391],{"class":145}," commit",[135,2393,2394],{"class":196}," -m",[135,2396,2397],{"class":145}," \"My first commit.\"\n",[135,2399,2400,2402],{"class":137,"line":326},[135,2401,2156],{"class":141},[135,2403,2404],{"class":145}," push\n",[12,2406,2407],{},"In the GitLab UI, create a merge request for your branch:",[12,2409,2410],{},[2411,2412],"img",{"alt":2413,"src":2414},"Screenshot demonstrating opening a GitLab Merge Request","https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683438/Blog/Content%20Images/create-merge-request.jpg",[12,2416,2417],{},"Your merge request pipeline should start running:",[12,2419,2420],{},[2411,2421],{"alt":2413,"src":2422},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683438/Blog/Content%20Images/merge-request-running.jpg",[12,2424,2425,2426,2428],{},"Once the pipeline completes, you should see the output of the ",[81,2427,878],{}," command in the pipeline's logs:",[12,2430,2431],{},[2411,2432],{"alt":2433,"src":2434},"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",[12,2436,2437,2438,2440],{},"If you installed the optional webhook, you should see the results of ",[81,2439,878],{}," posted back to the merge request as a comment:",[12,2442,2443],{},[2411,2444],{"alt":2445,"src":2446},"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",[12,2448,2449],{},"Once the pipeline has completed running, your merge request is ready to merge:",[12,2451,2452],{},[2411,2453],{"alt":2454,"src":2455},"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",[12,2457,2458,2459,2462],{},"Merging 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 ",[81,2460,2461],{},".gitlab-ci/yml"," to the main branch without a Pulumi program being present.)",[12,2464,2465],{},[2411,2466],{"alt":2467,"src":2468},"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",[12,2470,2471],{},"If you click into the main branch pipeline's execution, you can see your bucket has been created:",[12,2473,2474,2478],{},[2411,2475],{"alt":2476,"src":2477},"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:",[126,2480,2482],{"className":128,"code":2481,"language":130,"meta":131,"style":131},"pulumi destroy\n",[81,2483,2484],{"__ignoreMap":131},[135,2485,2486,2488],{"class":137,"line":138},[135,2487,142],{"class":141},[135,2489,2490],{"class":145}," destroy\n",[12,2492,2493],{},"Alternatively, 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.",[12,2495,2496,2497,2500],{},"Finally, run the ",[81,2498,2499],{},"pulumi destroy"," command again in the Pulumi program with your OIDC and GitLab resources to finish cleaning up.",[26,2502,2504],{"id":2503},"next-steps","Next steps",[12,2506,2507],{},"Using 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!",[12,2509,2510],{},"If you liked what you read here, here are some ways you can enhance your CI/CD pipelines:",[40,2512,2513,2528],{},[43,2514,2515,2516,2521,2522,2527],{},"Add ",[16,2517,2520],{"href":2518,"rel":2519},"https://www.pulumi.com/docs/using-pulumi/crossguard/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources",[],"Pulumi Policy Packs"," 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 ",[16,2523,2526],{"href":2524,"rel":2525},"https://www.pulumi.com/docs/using-pulumi/crossguard/compliance-ready-policies/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources",[],"Compliance Ready Policies"," 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.",[43,2529,2530,2531,2536,2537,2542,2543,2548],{},"Check out ",[16,2532,2535],{"href":2533,"rel":2534},"https://www.pulumi.com/product/esc/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources",[],"Pulumi ESC (Environments, Secrets, and Configuration)",": Pulumi ESC makes it easy to share static secrets like GitLab tokens and can even ",[16,2538,2541],{"href":2539,"rel":2540},"https://www.pulumi.com/blog/esc-env-run-aws/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources",[],"generate dynamic secrets like AWS OIDC credentials",". 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 - ",[16,2544,2547],{"href":2545,"rel":2546},"https://www.pulumi.com/docs/esc-cli/commands/?utm_source=GitLab&utm_medium=Referral&utm_campaign=Managing-GitLab-Resources",[],"Pulumi ESC's command line"," can be used with any CLI tool like the AWS CLI.",[2550,2551,2552],"style",{},"html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}html pre.shiki code .shJU0, html code.shiki .shJU0{--shiki-default:#22863A}",{"title":131,"searchDepth":216,"depth":216,"links":2554},[2555,2556,2557,2558,2559,2560,2561,2562,2563],{"id":28,"depth":216,"text":29},{"id":61,"depth":216,"text":62},{"id":238,"depth":216,"text":239},{"id":429,"depth":216,"text":430},{"id":871,"depth":216,"text":872},{"id":1069,"depth":216,"text":1070},{"id":1933,"depth":216,"text":1934},{"id":2097,"depth":216,"text":2098},{"id":2503,"depth":216,"text":2504},"devsecops","2024-01-10","Learn how Pulumi's infrastructure-as-code tool helps streamline the automation of GitLab CI/CD workflows.","md",null,false,"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683430/Blog/Hero%20Images/AdobeStock_293854129__1_.jpg",{},"/en-us/blog/managing-gitlab-resources-with-pulumi",{"title":5,"description":2566,"ogTitle":5,"ogDescription":2566,"noIndex":2569,"ogImage":2570,"ogUrl":2574,"ogSiteName":2575,"ogType":2576,"canonicalUrls":2574},"https://about.gitlab.com/blog/managing-gitlab-resources-with-pulumi","https://about.gitlab.com","article","managing-gitlab-resources-with-pulumi","en-us/blog/managing-gitlab-resources-with-pulumi",[2580,2581,2582,2583],"CI/CD","DevSecOps","partners","integrations","BlogPost","wpuwRfFkUNyKJ3feGBf0EAuEMyghXC6P990SxuiyUbg",{"logo":2587,"freeTrial":2592,"sales":2597,"login":2602,"items":2607,"search":2931,"minimal":2962,"duo":2981,"switchNav":2990,"pricingDeployment":3001},{"config":2588},{"href":2589,"dataGaName":2590,"dataGaLocation":2591},"/","gitlab logo","header",{"text":2593,"config":2594},"Get free trial",{"href":2595,"dataGaName":2596,"dataGaLocation":2591},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com&glm_content=default-saas-trial/","free trial",{"text":2598,"config":2599},"Request a demo",{"href":2600,"dataGaName":2601,"dataGaLocation":2591},"/sales/?contact-topic=request-demo","sales",{"text":2603,"config":2604},"Sign in",{"href":2605,"dataGaName":2606,"dataGaLocation":2591},"https://gitlab.com/users/sign_in/","sign in",[2608,2637,2736,2741,2853,2909],{"text":2609,"config":2610,"menu":2612},"Platform",{"dataNavLevelOne":2611},"platform",{"type":2613,"columns":2614},"cards",[2615,2621,2629],{"title":2609,"description":2616,"link":2617},"The intelligent orchestration platform for DevSecOps",{"text":2618,"config":2619},"Explore our Platform",{"href":2620,"dataGaName":2611,"dataGaLocation":2591},"/platform/",{"title":2622,"description":2623,"link":2624},"GitLab Duo Agent Platform","Agentic AI for the entire software lifecycle",{"text":2625,"config":2626},"Meet GitLab Duo",{"href":2627,"dataGaName":2628,"dataGaLocation":2591},"/gitlab-duo-agent-platform/","gitlab duo agent platform",{"title":2630,"description":2631,"link":2632},"Why GitLab","See the top reasons enterprises choose GitLab",{"text":2633,"config":2634},"Learn more",{"href":2635,"dataGaName":2636,"dataGaLocation":2591},"/why-gitlab/","why gitlab",{"text":2638,"left":335,"config":2639,"menu":2641},"Product",{"dataNavLevelOne":2640},"solutions",{"type":2642,"link":2643,"columns":2647,"feature":2715},"lists",{"text":2644,"config":2645},"View all Solutions",{"href":2646,"dataGaName":2640,"dataGaLocation":2591},"/solutions/",[2648,2671,2694],{"title":2649,"description":2650,"link":2651,"items":2656},"Automation","CI/CD and automation to accelerate deployment",{"config":2652},{"icon":2653,"href":2654,"dataGaName":2655,"dataGaLocation":2591},"AutomatedCodeAlt","/solutions/delivery-automation/","automated software delivery",[2657,2660,2663,2667],{"text":2580,"config":2658},{"href":2659,"dataGaLocation":2591,"dataGaName":2580},"/solutions/continuous-integration/",{"text":2622,"config":2661},{"href":2627,"dataGaLocation":2591,"dataGaName":2662},"gitlab duo agent platform - product menu",{"text":2664,"config":2665},"Source Code Management",{"href":2666,"dataGaLocation":2591,"dataGaName":2664},"/solutions/source-code-management/",{"text":2668,"config":2669},"Automated Software Delivery",{"href":2654,"dataGaLocation":2591,"dataGaName":2670},"Automated software delivery",{"title":2672,"description":2673,"link":2674,"items":2679},"Security","Deliver code faster without compromising security",{"config":2675},{"href":2676,"dataGaName":2677,"dataGaLocation":2591,"icon":2678},"/solutions/application-security-testing/","security and compliance","ShieldCheckLight",[2680,2684,2689],{"text":2681,"config":2682},"Application Security Testing",{"href":2676,"dataGaName":2683,"dataGaLocation":2591},"Application security testing",{"text":2685,"config":2686},"Software Supply Chain Security",{"href":2687,"dataGaLocation":2591,"dataGaName":2688},"/solutions/supply-chain/","Software supply chain security",{"text":2690,"config":2691},"Software Compliance",{"href":2692,"dataGaName":2693,"dataGaLocation":2591},"/solutions/software-compliance/","software compliance",{"title":2695,"link":2696,"items":2701},"Measurement",{"config":2697},{"icon":2698,"href":2699,"dataGaName":2700,"dataGaLocation":2591},"DigitalTransformation","/solutions/visibility-measurement/","visibility and measurement",[2702,2706,2710],{"text":2703,"config":2704},"Visibility & Measurement",{"href":2699,"dataGaLocation":2591,"dataGaName":2705},"Visibility and Measurement",{"text":2707,"config":2708},"Value Stream Management",{"href":2709,"dataGaLocation":2591,"dataGaName":2707},"/solutions/value-stream-management/",{"text":2711,"config":2712},"Analytics & Insights",{"href":2713,"dataGaLocation":2591,"dataGaName":2714},"/solutions/analytics-and-insights/","Analytics and insights",{"title":2716,"type":2642,"items":2717},"GitLab for",[2718,2724,2730],{"text":2719,"config":2720},"Enterprise",{"icon":2721,"href":2722,"dataGaLocation":2591,"dataGaName":2723},"Building","/enterprise/","enterprise",{"text":2725,"config":2726},"Small Business",{"icon":2727,"href":2728,"dataGaLocation":2591,"dataGaName":2729},"Work","/small-business/","small business",{"text":2731,"config":2732},"Public Sector",{"icon":2733,"href":2734,"dataGaLocation":2591,"dataGaName":2735},"Organization","/solutions/public-sector/","public sector",{"text":2737,"config":2738},"Pricing",{"href":2739,"dataGaName":2740,"dataGaLocation":2591,"dataNavLevelOne":2740},"/pricing/","pricing",{"text":2742,"config":2743,"menu":2745},"Resources",{"dataNavLevelOne":2744},"resources",{"type":2642,"link":2746,"columns":2750,"feature":2842},{"text":2747,"config":2748},"View all resources",{"href":2749,"dataGaName":2744,"dataGaLocation":2591},"/resources/",[2751,2783,2810],{"title":2752,"items":2753},"Getting started",[2754,2759,2764,2769,2774,2779],{"text":2755,"config":2756},"Install",{"href":2757,"dataGaName":2758,"dataGaLocation":2591},"/install/","install",{"text":2760,"config":2761},"Quick start guides",{"href":2762,"dataGaName":2763,"dataGaLocation":2591},"/get-started/","quick setup checklists",{"text":2765,"config":2766},"Learn",{"href":2767,"dataGaLocation":2591,"dataGaName":2768},"https://university.gitlab.com/","learn",{"text":2770,"config":2771},"Product documentation",{"href":2772,"dataGaName":2773,"dataGaLocation":2591},"https://docs.gitlab.com/","product documentation",{"text":2775,"config":2776},"Best practice videos",{"href":2777,"dataGaName":2778,"dataGaLocation":2591},"/getting-started-videos/","best practice videos",{"text":2780,"config":2781},"Integrations",{"href":2782,"dataGaName":2583,"dataGaLocation":2591},"/integrations/",{"title":2784,"items":2785},"Discover",[2786,2791,2796,2801,2805],{"text":2787,"config":2788},"Customer success stories",{"href":2789,"dataGaName":2790,"dataGaLocation":2591},"/customers/","customer success stories",{"text":2792,"config":2793},"Blog",{"href":2794,"dataGaName":2795,"dataGaLocation":2591},"/blog/","blog",{"text":2797,"config":2798},"Demo Hub",{"href":2799,"dataGaName":2800,"dataGaLocation":2591},"/demo-hub/","demo hub",{"text":2802,"config":2803},"The Source",{"href":2804,"dataGaName":2795,"dataGaLocation":2591},"/the-source/",{"text":2806,"config":2807},"Remote",{"href":2808,"dataGaName":2809,"dataGaLocation":2591},"https://handbook.gitlab.com/handbook/company/culture/all-remote/","remote",{"title":2811,"items":2812},"Connect",[2813,2818,2823,2828,2833,2838],{"text":2814,"config":2815},"GitLab Services",{"href":2816,"dataGaName":2817,"dataGaLocation":2591},"/services/","services",{"text":2819,"config":2820},"Contribute",{"href":2821,"dataGaName":2822,"dataGaLocation":2591},"https://contributors.gitlab.com","contribute",{"text":2824,"config":2825},"Community",{"href":2826,"dataGaName":2827,"dataGaLocation":2591},"/community/","community",{"text":2829,"config":2830},"Forum",{"href":2831,"dataGaName":2832,"dataGaLocation":2591},"https://forum.gitlab.com/","forum",{"text":2834,"config":2835},"Events",{"href":2836,"dataGaName":2837,"dataGaLocation":2591},"/events/","events",{"text":2839,"config":2840},"Partners",{"href":2841,"dataGaName":2582,"dataGaLocation":2591},"/partners/",{"config":2843,"title":2846,"text":2847,"link":2848},{"background":2844,"textColor":2845},"url('https://res.cloudinary.com/about-gitlab-com/image/upload/v1777322348/qpq8yrgn8knii57omj0c.png')","#000","What’s new in GitLab","Stay updated with our latest features and improvements.",{"text":2849,"config":2850},"Read the latest",{"href":2851,"dataGaName":2852,"dataGaLocation":2591},"/whats-new/","whats new",{"text":2854,"config":2855,"menu":2857},"Company",{"dataNavLevelOne":2856},"company",{"type":2642,"columns":2858},[2859],{"items":2860},[2861,2866,2872,2874,2879,2884,2889,2894,2899,2904],{"text":2862,"config":2863},"About",{"href":2864,"dataGaName":2865,"dataGaLocation":2591},"/company/","about",{"text":2867,"config":2868,"footerGa":2871},"Jobs",{"href":2869,"dataGaName":2870,"dataGaLocation":2591},"/jobs/","jobs",{"dataGaName":2870},{"text":2834,"config":2873},{"href":2836,"dataGaName":2837,"dataGaLocation":2591},{"text":2875,"config":2876},"Leadership",{"href":2877,"dataGaName":2878,"dataGaLocation":2591},"/company/team/e-group/","leadership",{"text":2880,"config":2881},"Handbook",{"href":2882,"dataGaName":2883,"dataGaLocation":2591},"https://handbook.gitlab.com/","handbook",{"text":2885,"config":2886},"Investor relations",{"href":2887,"dataGaName":2888,"dataGaLocation":2591},"https://ir.gitlab.com/overview/default.aspx","investor relations",{"text":2890,"config":2891},"Trust Center",{"href":2892,"dataGaName":2893,"dataGaLocation":2591},"/security/","trust center",{"text":2895,"config":2896},"AI Transparency Center",{"href":2897,"dataGaName":2898,"dataGaLocation":2591},"/ai-transparency-center/","ai transparency center",{"text":2900,"config":2901},"Newsletter",{"href":2902,"dataGaName":2903,"dataGaLocation":2591},"/company/contact/#contact-forms","newsletter",{"text":2905,"config":2906},"Press",{"href":2907,"dataGaName":2908,"dataGaLocation":2591},"/press/","press",{"text":2910,"config":2911,"menu":2912},"Contact us",{"dataNavLevelOne":2856},{"type":2642,"columns":2913},[2914],{"items":2915},[2916,2921,2926],{"text":2917,"config":2918},"Talk to sales",{"href":2919,"dataGaName":2920,"dataGaLocation":2591},"/sales/","talk to sales",{"text":2922,"config":2923},"Support portal",{"href":2924,"dataGaName":2925,"dataGaLocation":2591},"https://support.gitlab.com/hc/en-us","support portal",{"text":2927,"config":2928},"Customer portal",{"href":2929,"dataGaName":2930,"dataGaLocation":2591},"https://customers.gitlab.com/customers/sign_in/","customer portal",{"close":2932,"login":2933,"suggestions":2940},"Close",{"text":2934,"link":2935},"To search repositories and projects, login to",{"text":2936,"config":2937},"gitlab.com",{"href":2605,"dataGaName":2938,"dataGaLocation":2939},"search login","search",{"text":2941,"default":2942},"Suggestions",[2943,2945,2949,2951,2955,2959],{"text":2622,"config":2944},{"href":2627,"dataGaName":2622,"dataGaLocation":2939},{"text":2946,"config":2947},"Code Suggestions (AI)",{"href":2948,"dataGaName":2946,"dataGaLocation":2939},"/solutions/code-suggestions/",{"text":2580,"config":2950},{"href":2659,"dataGaName":2580,"dataGaLocation":2939},{"text":2952,"config":2953},"GitLab on AWS",{"href":2954,"dataGaName":2952,"dataGaLocation":2939},"/partners/technology-partners/aws/",{"text":2956,"config":2957},"GitLab on Google Cloud",{"href":2958,"dataGaName":2956,"dataGaLocation":2939},"/partners/technology-partners/google-cloud-platform/",{"text":2960,"config":2961},"Why GitLab?",{"href":2635,"dataGaName":2960,"dataGaLocation":2939},{"freeTrial":2963,"mobileIcon":2968,"desktopIcon":2973,"secondaryButton":2976},{"text":2964,"config":2965},"Start free trial",{"href":2966,"dataGaName":2596,"dataGaLocation":2967},"https://gitlab.com/-/trials/new/","nav",{"altText":2969,"config":2970},"Gitlab Icon",{"src":2971,"dataGaName":2972,"dataGaLocation":2967},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203874/jypbw1jx72aexsoohd7x.svg","gitlab icon",{"altText":2969,"config":2974},{"src":2975,"dataGaName":2972,"dataGaLocation":2967},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1758203875/gs4c8p8opsgvflgkswz9.svg",{"text":2977,"config":2978},"Get Started",{"href":2979,"dataGaName":2980,"dataGaLocation":2967},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com/get-started/","get started",{"freeTrial":2982,"mobileIcon":2986,"desktopIcon":2988},{"text":2983,"config":2984},"Learn more about GitLab Duo",{"href":2627,"dataGaName":2985,"dataGaLocation":2967},"gitlab duo",{"altText":2969,"config":2987},{"src":2971,"dataGaName":2972,"dataGaLocation":2967},{"altText":2969,"config":2989},{"src":2975,"dataGaName":2972,"dataGaLocation":2967},{"button":2991,"mobileIcon":2996,"desktopIcon":2998},{"text":2992,"config":2993},"/switch",{"href":2994,"dataGaName":2995,"dataGaLocation":2967},"#contact","switch",{"altText":2969,"config":2997},{"src":2971,"dataGaName":2972,"dataGaLocation":2967},{"altText":2969,"config":2999},{"src":3000,"dataGaName":2972,"dataGaLocation":2967},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1773335277/ohhpiuoxoldryzrnhfrh.png",{"freeTrial":3002,"mobileIcon":3007,"desktopIcon":3009},{"text":3003,"config":3004},"Back to pricing",{"href":2739,"dataGaName":3005,"dataGaLocation":2967,"icon":3006},"back to pricing","GoBack",{"altText":2969,"config":3008},{"src":2971,"dataGaName":2972,"dataGaLocation":2967},{"altText":2969,"config":3010},{"src":2975,"dataGaName":2972,"dataGaLocation":2967},{"title":3012,"titleMobile":3013,"button":3014,"config":3019},"Duo Agent Platform delivers 400% ROI, per new Forrester Consulting study.","400% ROI: Forrester TEI for GitLab Duo",{"text":2633,"config":3015},{"href":3016,"dataGaName":3017,"dataGaLocation":3018},"https://about.gitlab.com/blog/gitlab-duo-agent-platform-delivers-400-percent-roi/","forrester-tei-dap-banner","global-banner",{"layout":3020,"disabled":2569},"release",{"data":3022},{"text":3023,"source":3024,"edit":3030,"contribute":3035,"config":3040,"items":3045,"minimal":3253},"Git is a trademark of Software Freedom Conservancy and our use of 'GitLab' is under license",{"text":3025,"config":3026},"View page source",{"href":3027,"dataGaName":3028,"dataGaLocation":3029},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/","page source","footer",{"text":3031,"config":3032},"Edit this page",{"href":3033,"dataGaName":3034,"dataGaLocation":3029},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/content/","web ide",{"text":3036,"config":3037},"Please contribute",{"href":3038,"dataGaName":3039,"dataGaLocation":3029},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/CONTRIBUTING.md/","please contribute",{"twitter":3041,"facebook":3042,"youtube":3043,"linkedin":3044},"https://twitter.com/gitlab","https://www.facebook.com/gitlab","https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg","https://www.linkedin.com/company/gitlab-com",[3046,3093,3145,3189,3221],{"title":2737,"links":3047,"subMenu":3062},[3048,3052,3057],{"text":3049,"config":3050},"View plans",{"href":2739,"dataGaName":3051,"dataGaLocation":3029},"view plans",{"text":3053,"config":3054},"Why Premium?",{"href":3055,"dataGaName":3056,"dataGaLocation":3029},"/pricing/premium/","why premium",{"text":3058,"config":3059},"Why Ultimate?",{"href":3060,"dataGaName":3061,"dataGaLocation":3029},"/pricing/ultimate/","why ultimate",[3063],{"title":3064,"links":3065},"Contact Us",[3066,3069,3071,3073,3078,3083,3088],{"text":3067,"config":3068},"Contact sales",{"href":2919,"dataGaName":2601,"dataGaLocation":3029},{"text":2922,"config":3070},{"href":2924,"dataGaName":2925,"dataGaLocation":3029},{"text":2927,"config":3072},{"href":2929,"dataGaName":2930,"dataGaLocation":3029},{"text":3074,"config":3075},"Status",{"href":3076,"dataGaName":3077,"dataGaLocation":3029},"https://status.gitlab.com/","status",{"text":3079,"config":3080},"Terms of use",{"href":3081,"dataGaName":3082,"dataGaLocation":3029},"/terms/","terms of use",{"text":3084,"config":3085},"Privacy statement",{"href":3086,"dataGaName":3087,"dataGaLocation":3029},"/privacy/","privacy statement",{"text":3089,"config":3090},"Cookie preferences",{"dataGaName":3091,"dataGaLocation":3029,"id":3092,"isOneTrustButton":335},"cookie preferences","ot-sdk-btn",{"title":2638,"links":3094,"subMenu":3103},[3095,3099],{"text":3096,"config":3097},"DevSecOps platform",{"href":2620,"dataGaName":3098,"dataGaLocation":3029},"devsecops platform",{"text":3100,"config":3101},"AI-Assisted Development",{"href":2627,"dataGaName":3102,"dataGaLocation":3029},"ai-assisted development",[3104],{"title":3105,"links":3106},"Topics",[3107,3112,3117,3122,3127,3130,3135,3140],{"text":3108,"config":3109},"CICD",{"href":3110,"dataGaName":3111,"dataGaLocation":3029},"/topics/ci-cd/","cicd",{"text":3113,"config":3114},"GitOps",{"href":3115,"dataGaName":3116,"dataGaLocation":3029},"/topics/gitops/","gitops",{"text":3118,"config":3119},"DevOps",{"href":3120,"dataGaName":3121,"dataGaLocation":3029},"/topics/devops/","devops",{"text":3123,"config":3124},"Version Control",{"href":3125,"dataGaName":3126,"dataGaLocation":3029},"/topics/version-control/","version control",{"text":2581,"config":3128},{"href":3129,"dataGaName":2564,"dataGaLocation":3029},"/topics/devsecops/",{"text":3131,"config":3132},"Cloud Native",{"href":3133,"dataGaName":3134,"dataGaLocation":3029},"/topics/cloud-native/","cloud native",{"text":3136,"config":3137},"AI for Coding",{"href":3138,"dataGaName":3139,"dataGaLocation":3029},"/topics/devops/ai-for-coding/","ai for coding",{"text":3141,"config":3142},"Agentic AI",{"href":3143,"dataGaName":3144,"dataGaLocation":3029},"/topics/agentic-ai/","agentic ai",{"title":3146,"links":3147},"Solutions",[3148,3150,3152,3157,3161,3164,3168,3171,3173,3176,3179,3184],{"text":2681,"config":3149},{"href":2676,"dataGaName":2681,"dataGaLocation":3029},{"text":2670,"config":3151},{"href":2654,"dataGaName":2655,"dataGaLocation":3029},{"text":3153,"config":3154},"Agile development",{"href":3155,"dataGaName":3156,"dataGaLocation":3029},"/solutions/agile-delivery/","agile delivery",{"text":3158,"config":3159},"SCM",{"href":2666,"dataGaName":3160,"dataGaLocation":3029},"source code management",{"text":3108,"config":3162},{"href":2659,"dataGaName":3163,"dataGaLocation":3029},"continuous integration & delivery",{"text":3165,"config":3166},"Value stream management",{"href":2709,"dataGaName":3167,"dataGaLocation":3029},"value stream management",{"text":3113,"config":3169},{"href":3170,"dataGaName":3116,"dataGaLocation":3029},"/solutions/gitops/",{"text":2719,"config":3172},{"href":2722,"dataGaName":2723,"dataGaLocation":3029},{"text":3174,"config":3175},"Small business",{"href":2728,"dataGaName":2729,"dataGaLocation":3029},{"text":3177,"config":3178},"Public sector",{"href":2734,"dataGaName":2735,"dataGaLocation":3029},{"text":3180,"config":3181},"Education",{"href":3182,"dataGaName":3183,"dataGaLocation":3029},"/solutions/education/","education",{"text":3185,"config":3186},"Financial services",{"href":3187,"dataGaName":3188,"dataGaLocation":3029},"/solutions/finance/","financial services",{"title":2742,"links":3190},[3191,3193,3195,3197,3200,3202,3205,3207,3209,3211,3213,3215,3217,3219],{"text":2755,"config":3192},{"href":2757,"dataGaName":2758,"dataGaLocation":3029},{"text":2760,"config":3194},{"href":2762,"dataGaName":2763,"dataGaLocation":3029},{"text":2765,"config":3196},{"href":2767,"dataGaName":2768,"dataGaLocation":3029},{"text":2770,"config":3198},{"href":2772,"dataGaName":3199,"dataGaLocation":3029},"docs",{"text":2792,"config":3201},{"href":2794,"dataGaName":2795,"dataGaLocation":3029},{"text":3203,"config":3204},"What's new",{"href":2851,"dataGaName":2852,"dataGaLocation":3029},{"text":2787,"config":3206},{"href":2789,"dataGaName":2790,"dataGaLocation":3029},{"text":2806,"config":3208},{"href":2808,"dataGaName":2809,"dataGaLocation":3029},{"text":2814,"config":3210},{"href":2816,"dataGaName":2817,"dataGaLocation":3029},{"text":2819,"config":3212},{"href":2821,"dataGaName":2822,"dataGaLocation":3029},{"text":2824,"config":3214},{"href":2826,"dataGaName":2827,"dataGaLocation":3029},{"text":2829,"config":3216},{"href":2831,"dataGaName":2832,"dataGaLocation":3029},{"text":2834,"config":3218},{"href":2836,"dataGaName":2837,"dataGaLocation":3029},{"text":2839,"config":3220},{"href":2841,"dataGaName":2582,"dataGaLocation":3029},{"title":2854,"links":3222},[3223,3225,3227,3229,3231,3233,3237,3242,3244,3246,3248],{"text":2862,"config":3224},{"href":2864,"dataGaName":2856,"dataGaLocation":3029},{"text":2867,"config":3226},{"href":2869,"dataGaName":2870,"dataGaLocation":3029},{"text":2875,"config":3228},{"href":2877,"dataGaName":2878,"dataGaLocation":3029},{"text":2880,"config":3230},{"href":2882,"dataGaName":2883,"dataGaLocation":3029},{"text":2885,"config":3232},{"href":2887,"dataGaName":2888,"dataGaLocation":3029},{"text":3234,"config":3235},"Sustainability",{"href":3236,"dataGaName":3234,"dataGaLocation":3029},"/sustainability/",{"text":3238,"config":3239},"Diversity, inclusion and belonging (DIB)",{"href":3240,"dataGaName":3241,"dataGaLocation":3029},"/diversity-inclusion-belonging/","Diversity, inclusion and belonging",{"text":2890,"config":3243},{"href":2892,"dataGaName":2893,"dataGaLocation":3029},{"text":2900,"config":3245},{"href":2902,"dataGaName":2903,"dataGaLocation":3029},{"text":2905,"config":3247},{"href":2907,"dataGaName":2908,"dataGaLocation":3029},{"text":3249,"config":3250},"Modern Slavery Transparency Statement",{"href":3251,"dataGaName":3252,"dataGaLocation":3029},"https://handbook.gitlab.com/handbook/legal/modern-slavery-act-transparency-statement/","modern slavery transparency statement",{"items":3254},[3255,3258,3261],{"text":3256,"config":3257},"Terms",{"href":3081,"dataGaName":3082,"dataGaLocation":3029},{"text":3259,"config":3260},"Cookies",{"dataGaName":3091,"dataGaLocation":3029,"id":3092,"isOneTrustButton":335},{"text":3262,"config":3263},"Privacy",{"href":3086,"dataGaName":3087,"dataGaLocation":3029},[3265],{"id":3266,"title":3267,"body":2568,"config":3268,"content":3270,"description":2568,"extension":3275,"meta":3276,"navigation":335,"path":3277,"seo":3278,"stem":3279,"__hash__":3280},"blogAuthors/en-us/blog/authors/josh-kodroff-pulumi.yml","Josh Kodroff Pulumi",{"template":3269},"BlogAuthor",{"role":3271,"name":7,"config":3272},"Sr. Solutions Architect, Pulumi",{"headshot":3273,"ctfId":3274},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749683425/Blog/Author%20Headshots/joshkodroff.jpg","2GF0MF1ngEBxos4nRKt8tL","yml",{},"/en-us/blog/authors/josh-kodroff-pulumi",{},"en-us/blog/authors/josh-kodroff-pulumi","wx26OcV-rSnQqkeErKktuYnDWb88fFYZuUGRvnsY5gw",[3282,3290,3298],{"title":3283,"description":3284,"heroImage":3285,"category":2564,"date":3286,"authors":3287,"slug":3289,"externalUrl":2568},"Consolidate your GitLab stack with Gitaly on Kubernetes","Discover how to install Gitaly on Kubernetes, which is now generally available.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1756500636/wmey6kqzzuhirk88w2de.png","2026-05-07",[3288],"Olivier Campeau","gitaly-on-kubernetes-generally-available",{"title":3291,"description":3292,"heroImage":3293,"category":2564,"date":3294,"authors":3295,"slug":3297,"externalUrl":2568},"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","https://res.cloudinary.com/about-gitlab-com/image/upload/v1749659537/Blog/Hero%20Images/display-article-image-0679-1800x945-fy26.png","2026-04-29",[3296],"Rod Burns","teaching-software-development-the-easy-way-using-gitlab",{"title":3299,"description":3300,"heroImage":3301,"category":2564,"date":3302,"authors":3303,"slug":3305,"externalUrl":2568},"AI is reshaping DevSecOps: Attend GitLab Transcend to see what’s next","AI-generated code is 34% of development work. Discover how to balance productivity gains with quality, reliability, and security.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1767982271/e9ogyosmuummq7j65zqg.png","2026-01-08",[3304],"Manav Khurana","ai-is-reshaping-devsecops-attend-gitlab-transcend-to-see-whats-next",{"promotions":3307},[3308,3322,3334,3346],{"id":3309,"categories":3310,"header":3312,"text":3313,"button":3314,"image":3319},"ai-modernization",[3311],"ai","Is AI achieving its promise at scale?","Quiz will take 5 minutes or less",{"text":3315,"config":3316},"Get your AI maturity score",{"href":3317,"dataGaName":3318,"dataGaLocation":2795},"/assessments/ai-modernization-assessment/","modernization assessment",{"config":3320},{"src":3321},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138786/qix0m7kwnd8x2fh1zq49.png",{"id":3323,"categories":3324,"header":3326,"text":3313,"button":3327,"image":3331},"devops-modernization",[3325,2564],"product","Are you just managing tools or shipping innovation?",{"text":3328,"config":3329},"Get your DevOps maturity score",{"href":3330,"dataGaName":3318,"dataGaLocation":2795},"/assessments/devops-modernization-assessment/",{"config":3332},{"src":3333},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138785/eg818fmakweyuznttgid.png",{"id":3335,"categories":3336,"header":3338,"text":3313,"button":3339,"image":3343},"security-modernization",[3337],"security","Are you trading speed for security?",{"text":3340,"config":3341},"Get your security maturity score",{"href":3342,"dataGaName":3318,"dataGaLocation":2795},"/assessments/security-modernization-assessment/",{"config":3344},{"src":3345},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1772138786/p4pbqd9nnjejg5ds6mdk.png",{"id":3347,"paths":3348,"header":3351,"text":3352,"button":3353,"image":3358},"github-azure-migration",[3349,3350],"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":3354,"config":3355},"See how GitLab compares to GitHub",{"href":3356,"dataGaName":3357,"dataGaLocation":2795},"/compare/gitlab-vs-github/github-azure-migration/","github azure migration",{"config":3359},{"src":3333},{"header":3361,"blurb":3362,"button":3363,"secondaryButton":3368},"Start building faster today","See what your team can do with the intelligent orchestration platform for DevSecOps.\n",{"text":3364,"config":3365},"Get your free trial",{"href":3366,"dataGaName":2596,"dataGaLocation":3367},"https://gitlab.com/-/trial_registrations/new?glm_content=default-saas-trial&glm_source=about.gitlab.com/","feature",{"text":3067,"config":3369},{"href":2919,"dataGaName":2601,"dataGaLocation":3367},1786803753266]