├── .gitignore ├── CI_CD.md ├── LICENSE ├── README.md ├── configureAzureWorkloadIdentity.azcli ├── configureAzureWorkloadIdentity.ps1 ├── configureAzureWorkloadIdentity.sh ├── infra.yml └── main.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea -------------------------------------------------------------------------------- /CI_CD.md: -------------------------------------------------------------------------------- 1 | # Use Pulumi in CI/CD Pipelines with GitHub Actions 2 | 3 | ## Prerequisites 4 | 5 | - Pulumi account 6 | - Azure Subscription 7 | - Azure CLI 8 | - GitHub CLI 9 | 10 | Follow the instructions on [this page](https://github.com/cli/cli#installation) to install the GitHub CLI. 11 | 12 | On Windows, you can install the GitHub CLI using PowerShell and Windows Package Manager like this: 13 | 14 | ```powershell 15 | winget install -e --id GitHub.cli 16 | ``` 17 | 18 | You should have completed the [Getting Started Provisionning Infrastructure on Azure with Pulumi](./README.md) tutorial before doing this tutorial 19 | 20 | > [!NOTE] 21 | > If you have not completed the previous tutorial, you can just create a new directory and create a new Azure Pulumi project in an `infra` subdirectory using the command `pulumi new azure-csharp`. 22 | 23 | ## Initialize a new GitHub repository 24 | 25 | - Go in the root directory for this workshop (it should be the parent of the `infra` folder), all future shell commands should be executed from this folder. 26 | - Create a `.github\workflows` folder. 27 | - Copy the `infra.yml` workflow file (located alongside these instructions) in this `.github\workflows` folder. 28 | 29 | The workflow file contains the pipeline to provision the infrastructure defined in the infra folder. It uses the [Pulumi GitHub Actions](https://github.com/pulumi/actions) that will execute the `pulumi up` command on the `dev` stack. Have a look at the `infra.yml` file to understand what it's doing. 30 | 31 | > [!NOTE] 32 | > The `Pulumi GitHub Actions` is configured in the file to work with Pulumi Cloud. If you are not using Pulumi Cloud as your backend and encryption provider, you will have to make some adjustments to the configuration, you can check these [examples](https://github.com/pulumi/actions/tree/main/examples). 33 | 34 | If you are using .NET or Go, dependencies will be automatically restored when running the `pulumi up` command in the pipeline, so you don't need to add a step before to restore the dependencies. You may do it anyway to specify a version of .NET or Go to use. Otherwise (for Python or Node.js runtimes for instance) you should modify the `infra.yml` workflow file to restore the dependencies. 35 | 36 |
37 | Steps to add for TypeScript 38 | 39 | ```yaml 40 | - name: Install pnpm 41 | uses: pnpm/action-setup@v4 42 | with: 43 | version: latest 44 | 45 | - name: Use Node.js LTS version 46 | uses: actions/setup-node@v4 47 | with: 48 | node-version: 'lts/*' 49 | cache: 'pnpm' 50 | cache-dependency-path: './infra/pnpm-lock.yaml' 51 | 52 | - name: Install dependencies 53 | run: pnpm install 54 | working-directory: 'infra' 55 | ``` 56 |
57 | 58 | - Initialize the git repository with your Pulumi project and the workflow file 59 | 60 | ```powershell 61 | git init 62 | git add . 63 | git commit -m "Initialize repository with infrastructure code" 64 | ``` 65 | 66 | - Create a new remote private GitHub repository 67 | 68 | ```powershell 69 | gh repo create pulumi-azure-workshop-lab --private --source=. --push 70 | ``` 71 | 72 | ## Create the identity in Microsoft Entra ID for the GitHub Actions workflow and register the configuration in the GitHub Secrets 73 | 74 | - Create a Pulumi [access token](https://www.pulumi.com/docs/pulumi-cloud/access-management/access-tokens/) from your Pulumi account to be able to interact with the Pulumi Cloud backend of your project from the pipeline. 75 | 76 | > [!NOTE] 77 | > You could also use OpenID Connect to authenticate to your Pulumi account instead of relying on a personal access token. You can check [this article](https://www.pulumi.com/docs/pulumi-cloud/access-management/oidc/client/github/) to see how to do that. 78 | 79 | - Copy the `configureAzureWorkloadIdentity.ps1` or the `configureAzureWorkloadIdentity.sh`script (depending on your preference) in your repository folder. 80 | 81 | - Replace 'pul-********' by your access token, and execute the `configureAzureWorkloadIdentity` script that will configure everything needed for the pipeline to provision the infrastructure in Azure: 82 | 83 |
84 | Command in PowerShell 85 | 86 | ```powershell 87 | .\configureAzureWorkloadIdentity.ps1 -PulumiToken 'pul-********' 88 | ``` 89 |
90 | 91 |
92 | Command in Bash 93 | 94 | (first, make the script executable by running `chmod +x configureAzureWorkloadIdentity.sh`) 95 | ```bash 96 | ./configureAzureWorkloadIdentity.sh --PulumiToken='pul-********' 97 | ``` 98 |
99 | 100 | > [!NOTE] 101 | > The script configures an Azure App Registration and its federated identity credentials in Microsoft Entra Id. That will allow the GitHub Actions workflow to authenticate to Azure from the main branch of this GitHub repository. The script will also register the GitHub Secrets for the federated identity that will be used by the GitHub Actions workflow. Because this does not rely on a service principal secret, it's a more secure way of authenticating to Azure from a CI/CD pipeline. Check the [documentation](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation#how-it-works) if you want to better understand how Workload Identity Federation works. 102 | 103 | ## Run the workflow to provision Azure resources 104 | 105 | - Run the infra workflow 106 | 107 | ```powershell 108 | gh workflow run infra.yml 109 | ``` 110 | 111 | - Watch the workflow progress 112 | 113 |
114 | Command in PowerShell 115 | 116 | ```powershell 117 | $runId=$(gh run list --workflow=infra.yml --json databaseId -q ".[0].databaseId"); 118 | gh run watch $runId; 119 | ``` 120 | 121 |
122 | 123 |
124 | Command in Bash 125 | 126 | ```bash 127 | runId=$(gh run list --workflow=infra.yml --json databaseId -q ".[0].databaseId"); 128 | gh run watch $runId; 129 | ``` 130 | 131 |
132 | 133 | 134 | - Open the GitHub repository in the browser 135 | 136 | ```powershell 137 | gh repo view -w 138 | ``` 139 | 140 | - Check the Azure resources have been created in the azure portal 141 | 142 | > [!NOTE] 143 | > Instead of using scripts to create and configure the GitHub repository and the workload identity federation, we could also have used Pulumi like explained in this [article](https://www.techwatching.dev/posts/azure-ready-github-repository) 144 | 145 | ## Use stack outputs 146 | 147 | - Check the [Pulumi GitHub Actions](https://github.com/pulumi/actions) documentation and add a step in the workflow to echo the `appServiceUrl` stack output. 148 | 149 |
150 | GitHub Actions workflow 151 | 152 | ```yaml 153 | - name: Provision infrastructure 154 | uses: pulumi/actions@v6 155 | id: pulumi 156 | with: 157 | command: up 158 | stack-name: dev 159 | work-dir: infra 160 | env: 161 | PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} 162 | 163 | - run: echo "App service url is ${{ steps.pulumi.outputs.appServiceUrl }}" 164 | ``` 165 | 166 |
-------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution-ShareAlike 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-ShareAlike 4.0 International Public 58 | License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-ShareAlike 4.0 International Public License ("Public 63 | License"). To the extent this Public License may be interpreted as a 64 | contract, You are granted the Licensed Rights in consideration of Your 65 | acceptance of these terms and conditions, and the Licensor grants You 66 | such rights in consideration of benefits the Licensor receives from 67 | making the Licensed Material available under these terms and 68 | conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Adapter's License means the license You apply to Your Copyright 84 | and Similar Rights in Your contributions to Adapted Material in 85 | accordance with the terms and conditions of this Public License. 86 | 87 | c. BY-SA Compatible License means a license listed at 88 | creativecommons.org/compatiblelicenses, approved by Creative 89 | Commons as essentially the equivalent of this Public License. 90 | 91 | d. Copyright and Similar Rights means copyright and/or similar rights 92 | closely related to copyright including, without limitation, 93 | performance, broadcast, sound recording, and Sui Generis Database 94 | Rights, without regard to how the rights are labeled or 95 | categorized. For purposes of this Public License, the rights 96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 97 | Rights. 98 | 99 | e. Effective Technological Measures means those measures that, in the 100 | absence of proper authority, may not be circumvented under laws 101 | fulfilling obligations under Article 11 of the WIPO Copyright 102 | Treaty adopted on December 20, 1996, and/or similar international 103 | agreements. 104 | 105 | f. Exceptions and Limitations means fair use, fair dealing, and/or 106 | any other exception or limitation to Copyright and Similar Rights 107 | that applies to Your use of the Licensed Material. 108 | 109 | g. License Elements means the license attributes listed in the name 110 | of a Creative Commons Public License. The License Elements of this 111 | Public License are Attribution and ShareAlike. 112 | 113 | h. Licensed Material means the artistic or literary work, database, 114 | or other material to which the Licensor applied this Public 115 | License. 116 | 117 | i. Licensed Rights means the rights granted to You subject to the 118 | terms and conditions of this Public License, which are limited to 119 | all Copyright and Similar Rights that apply to Your use of the 120 | Licensed Material and that the Licensor has authority to license. 121 | 122 | j. Licensor means the individual(s) or entity(ies) granting rights 123 | under this Public License. 124 | 125 | k. Share means to provide material to the public by any means or 126 | process that requires permission under the Licensed Rights, such 127 | as reproduction, public display, public performance, distribution, 128 | dissemination, communication, or importation, and to make material 129 | available to the public including in ways that members of the 130 | public may access the material from a place and at a time 131 | individually chosen by them. 132 | 133 | l. Sui Generis Database Rights means rights other than copyright 134 | resulting from Directive 96/9/EC of the European Parliament and of 135 | the Council of 11 March 1996 on the legal protection of databases, 136 | as amended and/or succeeded, as well as other essentially 137 | equivalent rights anywhere in the world. 138 | 139 | m. You means the individual or entity exercising the Licensed Rights 140 | under this Public License. Your has a corresponding meaning. 141 | 142 | 143 | Section 2 -- Scope. 144 | 145 | a. License grant. 146 | 147 | 1. Subject to the terms and conditions of this Public License, 148 | the Licensor hereby grants You a worldwide, royalty-free, 149 | non-sublicensable, non-exclusive, irrevocable license to 150 | exercise the Licensed Rights in the Licensed Material to: 151 | 152 | a. reproduce and Share the Licensed Material, in whole or 153 | in part; and 154 | 155 | b. produce, reproduce, and Share Adapted Material. 156 | 157 | 2. Exceptions and Limitations. For the avoidance of doubt, where 158 | Exceptions and Limitations apply to Your use, this Public 159 | License does not apply, and You do not need to comply with 160 | its terms and conditions. 161 | 162 | 3. Term. The term of this Public License is specified in Section 163 | 6(a). 164 | 165 | 4. Media and formats; technical modifications allowed. The 166 | Licensor authorizes You to exercise the Licensed Rights in 167 | all media and formats whether now known or hereafter created, 168 | and to make technical modifications necessary to do so. The 169 | Licensor waives and/or agrees not to assert any right or 170 | authority to forbid You from making technical modifications 171 | necessary to exercise the Licensed Rights, including 172 | technical modifications necessary to circumvent Effective 173 | Technological Measures. For purposes of this Public License, 174 | simply making modifications authorized by this Section 2(a) 175 | (4) never produces Adapted Material. 176 | 177 | 5. Downstream recipients. 178 | 179 | a. Offer from the Licensor -- Licensed Material. Every 180 | recipient of the Licensed Material automatically 181 | receives an offer from the Licensor to exercise the 182 | Licensed Rights under the terms and conditions of this 183 | Public License. 184 | 185 | b. Additional offer from the Licensor -- Adapted Material. 186 | Every recipient of Adapted Material from You 187 | automatically receives an offer from the Licensor to 188 | exercise the Licensed Rights in the Adapted Material 189 | under the conditions of the Adapter's License You apply. 190 | 191 | c. No downstream restrictions. You may not offer or impose 192 | any additional or different terms or conditions on, or 193 | apply any Effective Technological Measures to, the 194 | Licensed Material if doing so restricts exercise of the 195 | Licensed Rights by any recipient of the Licensed 196 | Material. 197 | 198 | 6. No endorsement. Nothing in this Public License constitutes or 199 | may be construed as permission to assert or imply that You 200 | are, or that Your use of the Licensed Material is, connected 201 | with, or sponsored, endorsed, or granted official status by, 202 | the Licensor or others designated to receive attribution as 203 | provided in Section 3(a)(1)(A)(i). 204 | 205 | b. Other rights. 206 | 207 | 1. Moral rights, such as the right of integrity, are not 208 | licensed under this Public License, nor are publicity, 209 | privacy, and/or other similar personality rights; however, to 210 | the extent possible, the Licensor waives and/or agrees not to 211 | assert any such rights held by the Licensor to the limited 212 | extent necessary to allow You to exercise the Licensed 213 | Rights, but not otherwise. 214 | 215 | 2. Patent and trademark rights are not licensed under this 216 | Public License. 217 | 218 | 3. To the extent possible, the Licensor waives any right to 219 | collect royalties from You for the exercise of the Licensed 220 | Rights, whether directly or through a collecting society 221 | under any voluntary or waivable statutory or compulsory 222 | licensing scheme. In all other cases the Licensor expressly 223 | reserves any right to collect such royalties. 224 | 225 | 226 | Section 3 -- License Conditions. 227 | 228 | Your exercise of the Licensed Rights is expressly made subject to the 229 | following conditions. 230 | 231 | a. Attribution. 232 | 233 | 1. If You Share the Licensed Material (including in modified 234 | form), You must: 235 | 236 | a. retain the following if it is supplied by the Licensor 237 | with the Licensed Material: 238 | 239 | i. identification of the creator(s) of the Licensed 240 | Material and any others designated to receive 241 | attribution, in any reasonable manner requested by 242 | the Licensor (including by pseudonym if 243 | designated); 244 | 245 | ii. a copyright notice; 246 | 247 | iii. a notice that refers to this Public License; 248 | 249 | iv. a notice that refers to the disclaimer of 250 | warranties; 251 | 252 | v. a URI or hyperlink to the Licensed Material to the 253 | extent reasonably practicable; 254 | 255 | b. indicate if You modified the Licensed Material and 256 | retain an indication of any previous modifications; and 257 | 258 | c. indicate the Licensed Material is licensed under this 259 | Public License, and include the text of, or the URI or 260 | hyperlink to, this Public License. 261 | 262 | 2. You may satisfy the conditions in Section 3(a)(1) in any 263 | reasonable manner based on the medium, means, and context in 264 | which You Share the Licensed Material. For example, it may be 265 | reasonable to satisfy the conditions by providing a URI or 266 | hyperlink to a resource that includes the required 267 | information. 268 | 269 | 3. If requested by the Licensor, You must remove any of the 270 | information required by Section 3(a)(1)(A) to the extent 271 | reasonably practicable. 272 | 273 | b. ShareAlike. 274 | 275 | In addition to the conditions in Section 3(a), if You Share 276 | Adapted Material You produce, the following conditions also apply. 277 | 278 | 1. The Adapter's License You apply must be a Creative Commons 279 | license with the same License Elements, this version or 280 | later, or a BY-SA Compatible License. 281 | 282 | 2. You must include the text of, or the URI or hyperlink to, the 283 | Adapter's License You apply. You may satisfy this condition 284 | in any reasonable manner based on the medium, means, and 285 | context in which You Share Adapted Material. 286 | 287 | 3. You may not offer or impose any additional or different terms 288 | or conditions on, or apply any Effective Technological 289 | Measures to, Adapted Material that restrict exercise of the 290 | rights granted under the Adapter's License You apply. 291 | 292 | 293 | Section 4 -- Sui Generis Database Rights. 294 | 295 | Where the Licensed Rights include Sui Generis Database Rights that 296 | apply to Your use of the Licensed Material: 297 | 298 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 299 | to extract, reuse, reproduce, and Share all or a substantial 300 | portion of the contents of the database; 301 | 302 | b. if You include all or a substantial portion of the database 303 | contents in a database in which You have Sui Generis Database 304 | Rights, then the database in which You have Sui Generis Database 305 | Rights (but not its individual contents) is Adapted Material, 306 | including for purposes of Section 3(b); and 307 | 308 | c. You must comply with the conditions in Section 3(a) if You Share 309 | all or a substantial portion of the contents of the database. 310 | 311 | For the avoidance of doubt, this Section 4 supplements and does not 312 | replace Your obligations under this Public License where the Licensed 313 | Rights include other Copyright and Similar Rights. 314 | 315 | 316 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 317 | 318 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 319 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 320 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 321 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 322 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 323 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 324 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 325 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 326 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 327 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 328 | 329 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 330 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 331 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 332 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 333 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 334 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 335 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 336 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 337 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 338 | 339 | c. The disclaimer of warranties and limitation of liability provided 340 | above shall be interpreted in a manner that, to the extent 341 | possible, most closely approximates an absolute disclaimer and 342 | waiver of all liability. 343 | 344 | 345 | Section 6 -- Term and Termination. 346 | 347 | a. This Public License applies for the term of the Copyright and 348 | Similar Rights licensed here. However, if You fail to comply with 349 | this Public License, then Your rights under this Public License 350 | terminate automatically. 351 | 352 | b. Where Your right to use the Licensed Material has terminated under 353 | Section 6(a), it reinstates: 354 | 355 | 1. automatically as of the date the violation is cured, provided 356 | it is cured within 30 days of Your discovery of the 357 | violation; or 358 | 359 | 2. upon express reinstatement by the Licensor. 360 | 361 | For the avoidance of doubt, this Section 6(b) does not affect any 362 | right the Licensor may have to seek remedies for Your violations 363 | of this Public License. 364 | 365 | c. For the avoidance of doubt, the Licensor may also offer the 366 | Licensed Material under separate terms or conditions or stop 367 | distributing the Licensed Material at any time; however, doing so 368 | will not terminate this Public License. 369 | 370 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 371 | License. 372 | 373 | 374 | Section 7 -- Other Terms and Conditions. 375 | 376 | a. The Licensor shall not be bound by any additional or different 377 | terms or conditions communicated by You unless expressly agreed. 378 | 379 | b. Any arrangements, understandings, or agreements regarding the 380 | Licensed Material not stated herein are separate from and 381 | independent of the terms and conditions of this Public License. 382 | 383 | 384 | Section 8 -- Interpretation. 385 | 386 | a. For the avoidance of doubt, this Public License does not, and 387 | shall not be interpreted to, reduce, limit, restrict, or impose 388 | conditions on any use of the Licensed Material that could lawfully 389 | be made without permission under this Public License. 390 | 391 | b. To the extent possible, if any provision of this Public License is 392 | deemed unenforceable, it shall be automatically reformed to the 393 | minimum extent necessary to make it enforceable. If the provision 394 | cannot be reformed, it shall be severed from this Public License 395 | without affecting the enforceability of the remaining terms and 396 | conditions. 397 | 398 | c. No term or condition of this Public License will be waived and no 399 | failure to comply consented to unless expressly agreed to by the 400 | Licensor. 401 | 402 | d. Nothing in this Public License constitutes or may be interpreted 403 | as a limitation upon, or waiver of, any privileges and immunities 404 | that apply to the Licensor or You, including from the legal 405 | processes of any jurisdiction or authority. 406 | 407 | 408 | ======================================================================= 409 | 410 | Creative Commons is not a party to its public 411 | licenses. Notwithstanding, Creative Commons may elect to apply one of 412 | its public licenses to material it publishes and in those instances 413 | will be considered the “Licensor.” The text of the Creative Commons 414 | public licenses is dedicated to the public domain under the CC0 Public 415 | Domain Dedication. Except for the limited purpose of indicating that 416 | material is shared under a Creative Commons public license or as 417 | otherwise permitted by the Creative Commons policies published at 418 | creativecommons.org/policies, Creative Commons does not authorize the 419 | use of the trademark "Creative Commons" or any other trademark or logo 420 | of Creative Commons without its prior written consent including, 421 | without limitation, in connection with any unauthorized modifications 422 | to any of its public licenses or any other arrangements, 423 | understandings, or agreements concerning use of licensed material. For 424 | the avoidance of doubt, this paragraph does not form part of the 425 | public licenses. 426 | 427 | Creative Commons may be contacted at creativecommons.org. 428 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started Provisioning Infrastructure on Azure with Pulumi 2 | 3 | ## Prerequisites 4 | 5 | ### Installations & configurations 6 | 7 | - [Azure Subscription](https://azure.microsoft.com/en-us/pricing/purchase-options/azure-account) 8 | - [Azure CLI](https://github.com/azure/azure-cli/releases) 9 | - [Pulumi CLI](https://www.pulumi.com/docs/iac/download-install/#download-install-pulumi) 10 | - Your preferred language runtime 11 | - Your favorite IDE 12 | 13 | This [page](https://www.pulumi.com/docs/clouds/azure/get-started/begin/) in the documentation covers all you need to do to set up your environment. 14 | 15 | > [!NOTE] 16 | > You can use the OS, language, and IDE you want for this workshop. Yet for the sake of simplicity, the samples in the tutorial won't cover every possible configuration. That should not prevent you from choosing the technologies and tools you are already familiar with to complete this workshop. 17 | 18 |
19 | Example on Windows with Winget 20 | 21 | On Windows for instance, you can set up your environment using PowerShell and Windows Package Manager like this: 22 | 23 | ```powershell 24 | # Install Azure CLI using winget 25 | winget install -e --id Microsoft.AzureCLI 26 | 27 | # Install Pulumi CLI using winget 28 | winget install -e --id=Pulumi.Pulumi 29 | 30 | # (Optional) Install the .NET SDK 31 | winget install Microsoft.DotNet.SDK.8 32 | ``` 33 |
34 | 35 |
36 | Example on Linux with simple script 37 | 38 | On Linux for instance, you can set up your environment using this script: 39 | 40 | ```bash 41 | # Install Azure CLI 42 | curl -L https://aka.ms/InstallAzureCli | bash 43 | 44 | # Install Pulumi CLI 45 | curl -fsSL https://get.pulumi.com | sh 46 | ``` 47 |
48 | 49 |
50 | Example on MacOS with homebrew 51 | 52 | On MacOS for instance, you can set up your environment using homebrew: 53 | 54 | ```bash 55 | # Install Azure CLI using homebrew 56 | brew install azure-cli 57 | 58 | # Install Pulumi CLI using homebrew 59 | brew install pulumi/tap/pulumi 60 | ``` 61 |
62 | 63 | Once the Azure CLI is installed: 64 |
65 | Log in to Azure and select your Azure subscription 66 | 67 | ```powershell 68 | # Log in to Azure 69 | # You can specify the -t option with your tenant identifier if you have multiple tenants 70 | az login 71 | 72 | # (Optional) List your available subscriptions and grab the identifier of the subscription you want to use 73 | az account list --query "[].{id:id, name:name}" 74 | 75 | # (Optional) Set the correct subscription identifier, "79400867-f366-4ec9-84ba-d1dca756beb5 in the example below 76 | az account set -s 79400867-f366-4ec9-84ba-d1dca756beb5 77 | az account show 78 | ``` 79 | 80 |
81 | 82 | 83 | ### Choose a backend 84 | 85 | As Pulumi is a declarative IaC solution that uses a state to manage the cloud resources, a place to store this state is needed: the "backend". An encryption provider is also needed to encrypt that will be used. You can check this [article in the documentation](https://www.pulumi.com/docs/concepts/state/#managing-state-backend-options) to see the different backends and encryption providers available. 86 | 87 | The most convenient way of doing this workshop without worrying about configuring a backend or an encryption provider is to use Pulumi Cloud which is free for individuals. You can just create an account [here](https://app.pulumi.com/signup) (or sign in using your GitHub/GitLab account) and that's it. 88 | 89 | If you don't want to use Pulumi Cloud, that's totally fine too, check the [documentation](https://www.pulumi.com/docs/iac/concepts/state-and-backends/#using-a-self-managed-backend) to use a self-managed backend and another encryption provider. You can also check this [article](https://www.techwatching.dev/posts/pulumi-azure-backend) that demonstrates how to use Pulumi with Azure Blob Storage as the backend and Azure Key Vault as the encryption provider (script to configure these resources is available at the end of the article). 90 | 91 | Log in to your backend using the [pulumi login CLI command](https://www.pulumi.com/docs/iac/cli/commands/pulumi_login/) 92 | 93 |
94 | Log in to Pulumi Cloud 95 | 96 | ```powershell 97 | pulumi login 98 | ``` 99 |
100 | 101 |
102 | Log in to local filesystem backend 103 | 104 | ```powershell 105 | pulumi login --local 106 | ``` 107 |
108 | 109 | ## Pulumi fundamentals 110 | 111 | ### Create a basic Pulumi project 112 | 113 | 1. Create a new directory for the workshop with a new `infra` directory in it. 114 | ```powershell 115 | mkdir pulumi-workshop; cd pulumi-workshop; mkdir infra; cd infra 116 | ``` 117 | 118 | 2. List the available templates 119 | ```powershell 120 | pulumi new -l 121 | ``` 122 | 123 | There are several azure templates (prefixed by azure) that are already configured to provision resources to Azure, but for the purpose of this workshop you will start a project from scratch to better understand how everything works. 124 | 125 | 3. Create a new Pulumi project using an empty template (corresponding to the language of your choice) 126 | 127 | ```powershell 128 | pulumi new typescript -n PulumiAzureWorkshop -s dev -d "Workshop to learn Pulumi with Azure fundamentals" 129 | ``` 130 | 131 | The `-s dev` option is used to initialize the project with a stack named `dev`. A [stack](https://www.pulumi.com/docs/concepts/stack/#stacks) is an independently configurable instance of a Pulumi program. Stacks are mainly use to have a different instance for each environment (dev, staging, preprod, prod ...). or for [each developer making changes to the infrastructure](https://www.pulumi.com/blog/iac-recommended-practices-developer-stacks-git-branches/#using-developer-stacks). 132 | 133 | > [!NOTE] 134 | > If you forget to log in before, you will be prompted to log in to Pulumi Cloud when running this command. Just use your GitHub/GitLab account or the credentials of the account you previously created. If you use a self-hosted backend, log in with the appropriate backend url before running the `pulumi new` command. 135 | 136 | Open the project in your favorite IDE to browse the files. 137 | 138 | ### Deploy a stack 139 | 140 | Use [`pulumi up`](https://www.pulumi.com/docs/cli/commands/pulumi_up/) to deploy the stack 141 | 142 | The command will first display a preview of the changes and then ask you whether you want to apply the changes. Select yes. 143 | 144 | As there are currently no resources in the Pulumi program, only the stack itself will be created in the state, no cloud resources will be provisioned. 145 | 146 | Depending on your template, the Pulumi program may contain an output that is displayed once the command is executed. [Outputs](https://www.pulumi.com/docs/iac/concepts/stacks/#outputs) can be used to retrieve information from a Pulumi stack like URL from provisioned cloud resources. 147 | 148 | - If there is not existing output, add an output `outputKey` with a value `outputValue`. 149 | 150 |
151 | Code in C# 152 | 153 | ```csharp 154 | return new Dictionary 155 | { 156 | ["outputKey"] = "outputValue" 157 | }; 158 | ``` 159 |
160 | 161 |
162 | Code in TypeScript 163 | 164 | ```typescript 165 | export const outputKey = "outputValue" 166 | ``` 167 |
168 | 169 |
170 | Code in Python 171 | 172 | ```typescript 173 | pulumi.export("outputKey", "outputValue") 174 | ``` 175 |
176 | 177 | ### Handle stack configuration, stack outputs, and secrets 178 | 179 | [Configuration](https://www.pulumi.com/docs/concepts/config/) allows you to configure resources with different settings depending on the stack you are using. A basic use case is to have the pricing tier of a resource in the configuration to have less expensive/powerful machines in the development environment than in production. 180 | 181 | 1. Add a setting named `AppServiceSku` with the value `F1` to the stack configuration using the command [`pulumi config set`](https://www.pulumi.com/docs/cli/commands/pulumi_config_set/) 182 | 183 |
184 | Command 185 | 186 | ```powershell 187 | pulumi config set AppServiceSku F1 188 | ``` 189 |
190 | 191 | The new setting is displayed in the dev stack configuration file: `Pulumi.dev.yaml`. 192 | 193 | 2. Modify the code to retrieve the `AppServiceSku` setting and put it in the outputs (cf. [doc](https://www.pulumi.com/docs/concepts/config/#code)). 194 | 195 |
196 | Code to retrieve the configuration in C# 197 | 198 | ```csharp 199 | var config = new Config(); 200 | var appServiceSku = config.Get("AppServiceSku"); 201 | 202 | return new Dictionary 203 | { 204 | ["outputKey"] = "outputValue", 205 | ["appServiceSku"] = appServiceSku 206 | }; 207 | ``` 208 |
209 | 210 |
211 | Code to retrieve the configuration in TypeScript 212 | 213 | ```typescript 214 | import {Config} from "@pulumi/pulumi"; 215 | 216 | const config = new Config() 217 | const appServiceSkuSetting = config.get("AppServiceSku") 218 | 219 | export const outputKey = "outputValue" 220 | export const appServiceSku = appServiceSkuSetting 221 | ``` 222 |
223 | 224 |
225 | Code to retrieve the configuration in Python 226 | 227 | ```python 228 | import pulumi 229 | from pulumi import Config 230 | 231 | 232 | config = Config() 233 | app_service_sku = config.get("AppServiceSku") 234 | 235 | pulumi.export("outputKey", "outputValue") 236 | pulumi.export("appServiceSku", app_service_sku) 237 | ``` 238 |
239 | 240 | > [!NOTE] 241 | > Run `pulumi up -y` (the `-y` option is to automatically approve the preview) to update the stack and verify your code is working as expected. This will not always be specified in the rest of the workshop. 242 | 243 | Pulumi has built-in supports for [secrets](https://www.pulumi.com/docs/concepts/secrets/#secrets-1) that are encrypted in the state. 244 | 245 | 3. Add a new secret setting `ExternalApiKey` with the value `SecretToBeKeptSecure` to the configuration and to the outputs. 246 | 247 |
248 | Command 249 | 250 | ```powershell 251 | pulumi config set --secret ExternalApiKey SecretToBeKeptSecure 252 | ``` 253 |
254 | 255 |
256 | Code in C# 257 | 258 | ```csharp 259 | var config = new Config(); 260 | var appServiceSku = config.Get("AppServiceSku"); 261 | var externalApiKey = config.RequireSecret("ExternalApiKey"); 262 | 263 | return new Dictionary 264 | { 265 | ["outputKey"] = "outputValue", 266 | ["appServiceSku"] = appServiceSku, 267 | ["apiKey"] = externalApiKey 268 | }; 269 | ``` 270 |
271 | 272 |
273 | Code in TypeScript 274 | 275 | ```typescript 276 | const config = new Config() 277 | const appServiceSkuSetting = config.get("AppServiceSku") 278 | const externalApiKey = config.requireSecret("ExternalApiKey") 279 | 280 | export const outputKey = "outputValue" 281 | export const appServiceSku = appServiceSkuSetting 282 | export const apiKey = externalApiKey 283 | ``` 284 |
285 | 286 |
287 | Code in Python 288 | 289 | ```python 290 | config = Config() 291 | app_service_sku = config.get("AppServiceSku") 292 | external_api_key = config.require_secret("ExternalApiKey") 293 | 294 | pulumi.export("outputKey", "outputValue") 295 | pulumi.export("appServiceSku", app_service_sku) 296 | pulumi.export("apiKey", external_api_key) 297 | ``` 298 |
299 | 300 | You can see that the secret is masked in the logs and that you have to use the command `pulumi stack output --show-secrets` to display it. 301 | 302 | ## Provision Azure resources 303 | 304 | ### Configure the program to use the Azure provider 305 | 306 | [Providers](https://www.pulumi.com/docs/concepts/resources/providers/) are the packages that allow you to provision resources in cloud providers or SaaS. Each resource provider is specific to a cloud provider/SaaS. 307 | 308 | 1. Add the [Azure Native Provider package](https://www.pulumi.com/registry/packages/azure-native/installation-configuration/#installation) to the project. 309 | 310 |
311 | Command for C# 312 | 313 | ```powershell 314 | dotnet add package Pulumi.AzureNative 315 | ``` 316 |
317 | 318 |
319 | Command for TypeScript 320 | 321 | ```powershell 322 | pnpm add @pulumi/azure-native 323 | ``` 324 |
325 | 326 |
327 | Command for Python 328 | 329 | ```powershell 330 | pip install pulumi-azure-native 331 | ## Or if you use poetry : 332 | ## poetry add pulumi-azure-native 333 | ``` 334 |
335 | 336 | > [!NOTE] 337 | > The package is big so it can take some time to download and install especially if you are using Node.js 338 | 339 | Azure providers allows to configure a default location for Azure resources so that you don't need to specify it each time you create a new resource. 340 | 341 | 2. Configure the [default location](https://www.pulumi.com/registry/packages/azure-native/installation-configuration/#set-configuration-using-pulumi-config) for your Azure resources. 342 | 343 |
344 | Command 345 | 346 | ```powershell 347 | pulumi config set azure-native:location westeurope 348 | ``` 349 |
350 | 351 | > [!NOTE] 352 | > All azure locations can be listed using the following command: `az account list-locations -o table` 353 | 354 | 3. Ensure you are correctly logged in the azure CLI using the `az account show` command. Otherwise, use the `az login` command. 355 | 356 | ### Work with Azure resources 357 | 358 | You can explore all Azure resources in the [documentation of the Azure API Native Provider](https://www.pulumi.com/registry/packages/azure-native/api-docs/) to find the resources you want to create. 359 | 360 | 1. Create a [resource group](https://www.pulumi.com/registry/packages/azure-native/api-docs/resources/resourcegroup/) named `rg-workshop` that will contain the resources you will create next. 361 | 362 |
363 | Code in C# 364 | 365 | ```csharp 366 | var resourceGroup = new ResourceGroup("workshop"); 367 | ``` 368 |
369 | 370 |
371 | Code in TypeScript 372 | 373 | ```typescript 374 | import {ResourceGroup} from "@pulumi/azure-native/resources"; 375 | 376 | const resourceGroup = new ResourceGroup("workshop"); 377 | ``` 378 |
379 | 380 |
381 | Code in Python 382 | 383 | ```python 384 | import pulumi_azure_native as azure_native 385 | 386 | resource_group = azure_native.resources.ResourceGroup("workshop") 387 | ``` 388 |
389 | 390 | When executing the `pulumi up` command, you will see that pulumi detects there is a new resource to create. Apply the update and verify the resource group is created. 391 | 392 | > [!NOTE] 393 | > You don't have to specify a location for the resource group, by default it will use the location you previously specified in the configuration. 394 | 395 | 2. [Configure the resource group](https://www.pulumi.com/registry/packages/azure-native/api-docs/resources/resourcegroup/#inputs) to have the tag `Type` with the value `Demo` and the tag `ProvisionedBy` with the value `Pulumi`. 396 | 397 |
398 | Code in C# 399 | 400 | ```csharp 401 | var resourceGroup = new ResourceGroup("workshop", new() 402 | { 403 | Tags = 404 | { 405 | { "Type", "Demo" }, 406 | { "ProvisionedBy", "Pulumi" } 407 | } 408 | }); 409 | ``` 410 |
411 | 412 |
413 | Code in TypeScript 414 | 415 | ```typescript 416 | const resourceGroup = new ResourceGroup("workshop", { 417 | tags: { 418 | Type: "demo", 419 | ProvisionedBy: "Pulumi" 420 | } 421 | }); 422 | ``` 423 |
424 | 425 |
426 | Code in Python 427 | 428 | ```python 429 | resource_group = azure_native.resources.ResourceGroup( 430 | "workshop", 431 | tags={ 432 | "Type": "Demo", 433 | "ProvisionedBy": "Pulumi", 434 | } 435 | ) 436 | ``` 437 |
438 | 439 | When updating the stack, you will see that pulumi detects the resource group needs to be updated. 440 | 441 | It's a good practice to follow a [naming convention](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-naming). Like the name `rg-workshop-dev` where: 442 | - `rg` is the abbreviation for the resource type "resource group" 443 | - `workshop` is the name of the application/workload 444 | - `dev` is the name of the environment/stack 445 | 446 | 3. Update the resource group name to `rg-workshop-dev` for your resource group. 447 | 448 |
449 | Code in C# 450 | 451 | ```csharp 452 | var stackName = Deployment.Instance.StackName; 453 | var resourceGroup = new ResourceGroup($"rg-workshop-{stackName}", new() 454 | { 455 | Tags = 456 | { 457 | { "Type", "Demo" }, 458 | { "ProvisionedBy", "Pulumi" } 459 | } 460 | }); 461 | ``` 462 | The stack name is directly retrieved from Pulumi to avoid hardcoding it. 463 |
464 | 465 |
466 | Code in TypeScript 467 | 468 | ```typescript 469 | const stackName = pulumi.getStack() 470 | const resourceGroup = new ResourceGroup(`rg-workshop-${stackName}`, { 471 | tags: { 472 | Type: "demo", 473 | ProvisionedBy: "Pulumi" 474 | } 475 | }); 476 | ``` 477 | The stack name is directly retrieved from Pulumi to avoid hardcoding it. 478 |
479 | 480 |
481 | Code in Python 482 | 483 | ```python 484 | stack_name = pulumi.get_stack() 485 | 486 | resource_group = azure_native.resources.ResourceGroup( 487 | f"rg-workshop-{stack_name}", 488 | tags={ 489 | "Type": "Demo", 490 | "ProvisionedBy": "Pulumi", 491 | } 492 | ) 493 | ``` 494 | The stack name is directly retrieved from Pulumi to avoid hardcoding it. 495 |
496 | 497 | 498 | When updating the stack, you will see that pulumi detects the resource group needs to be recreated (delete the one with the old name and create a new one with the new name). Indeed, when some input properties of a resource change, it triggers a replacement of the resource. The input properties concerned are always specified in the documentation of each resource. 499 | 500 | > [!NOTE] 501 | > You have seen that depending on what you do, updating the stack will result in creating, updating, or deleting resources. Instead of executing the `pulumi up` command each time you want to see the result of your changes, you can use the [`pulumi watch`](https://www.pulumi.com/docs/cli/commands/pulumi_watch/) command that will act as [hot reload for your infrastructure code](https://www.techwatching.dev/posts/pulumi-watch) (each time you make a change and save your code file, pulumi will detect it, build the code, and deploy the changes ). You can use that for the rest of the workshop or continue using `pulumi up -y` if you prefer. 502 | 503 | Sometimes it's not easy to find the correct type for the resource we want to create. You can use the [`pulumi ai web`](https://www.pulumi.com/blog/pulumi-insights-ai-cli/#pulumi-ai-in-the-cli) command to use natural-language prompts to generate Pulumi infrastructure-as-code. 504 | 505 | 4. Use pulumi ai to provision a free Web App/App Service. 506 | 507 |
508 | Command for C# 509 | 510 | ```powershell 511 | pulumi ai web -l C# "Using Azure Native Provider, create a free App Service." 512 | ``` 513 |
514 | 515 |
516 | Command for TypeScript 517 | 518 | ```powershell 519 | pulumi ai web -l typescript "Using Azure Native Provider, create a free App Service." 520 | ``` 521 |
522 | 523 |
524 | Command for Python 525 | 526 | ```powershell 527 | pulumi ai web -l python "Using Azure Native Provider, create a free App Service." 528 | ``` 529 |
530 | 531 |
532 | Code in C# 533 | 534 | ```csharp 535 | var appServicePlan = new AppServicePlan($"sp-workshop-{stackName}", new() 536 | { 537 | ResourceGroupName = resourceGroup.Name, 538 | Sku = new SkuDescriptionArgs() 539 | { 540 | Name = "F1", 541 | }, 542 | }); 543 | 544 | var appService = new WebApp($"app-workshop-{stackName}", new() 545 | { 546 | ResourceGroupName = resourceGroup.Name, 547 | ServerFarmId = appServicePlan.Id, 548 | }); 549 | ``` 550 | An [App Service Plan](https://www.pulumi.com/registry/packages/azure-native/api-docs/web/appserviceplan/) is needed to create an [App Service](https://www.pulumi.com/registry/packages/azure-native/api-docs/web/webapp/). 551 |
552 | 553 |
554 | Code in TypeScript 555 | 556 | ```typescript 557 | const appServicePlan = new AppServicePlan(`sp-workshop-${stackName}`, { 558 | resourceGroupName: resourceGroup.name, 559 | sku: { 560 | name: "F1", 561 | }, 562 | }); 563 | 564 | const appService = new WebApp(`app-workshop-${stackName}`, { 565 | resourceGroupName: resourceGroup.name, 566 | serverFarmId: appServicePlan.id, 567 | }); 568 | ``` 569 | An [App Service Plan](https://www.pulumi.com/registry/packages/azure-native/api-docs/web/appserviceplan/) is needed to create an [App Service](https://www.pulumi.com/registry/packages/azure-native/api-docs/web/webapp/). 570 |
571 | 572 |
573 | Code in Python 574 | 575 | ```python 576 | app_service_plan = azure_native.web.AppServicePlan( 577 | f"sp-workshop-{stack_name}", 578 | resource_group_name=resource_group.name 579 | sku=azure_native.web.SkuDescriptionArgs( 580 | name="F1" 581 | ) 582 | ) 583 | 584 | app_service = azure_native.web.WebApp( 585 | f"app-workshop-{stack_name}", 586 | resource_group_name=resource_group.name, 587 | server_farm_id=app_service_plan.id 588 | ) 589 | ``` 590 |
591 | 592 | > [!NOTE] 593 | > To access properties from other resources, you can just use variables. 594 | 595 | 5. Update the infrastructure to use the `AppServiceSku` setting from the configuration instead of hard coding the SKU `F1`. 596 | 597 |
598 | Code in C# 599 | 600 | ```csharp 601 | var appServiceSku = config.Require("AppServiceSku"); 602 | var appServicePlan = new AppServicePlan($"sp-workshop-{stackName}", new() 603 | { 604 | ResourceGroupName = resourceGroup.Name, 605 | Sku = new SkuDescriptionArgs() 606 | { 607 | Name = appServiceSku, 608 | }, 609 | }); 610 | ``` 611 |
612 | 613 |
614 | Code in TypeScript 615 | 616 | ```typescript 617 | const appServiceSku = config.require("AppServiceSku") 618 | const appServicePlan = new AppServicePlan("appServicePlan", { 619 | resourceGroupName: resourceGroup.name, 620 | sku: { 621 | name: appServiceSku, 622 | }, 623 | }); 624 | ``` 625 |
626 | 627 |
628 | Code in Python 629 | 630 | ```python 631 | app_service_sku = config.require("AppServiceSku") 632 | app_service_plan = azure_native.web.AppServicePlan( 633 | f"sp-workshop-{stack_name}", 634 | resource_group_name=resource_group.name, 635 | sku=azure_native.web.SkuDescriptionArgs( 636 | name=app_service_sku 637 | ) 638 | ) 639 | ``` 640 |
641 | 642 | Not only does the stack have outputs, but the resources themselves also have outputs, which are properties returned from the cloud provider. Since these values are only known once the resources have been provisioned, there are certain [considerations](https://www.pulumi.com/docs/concepts/inputs-outputs/#outputs) to keep in mind when using them in your program (particularly when performing computations based on an output). 643 | 644 | 6. Modify the program to make the stack only return one output, that is the URL of the app service. 645 | 646 |
647 | Code in C# 648 | 649 | ```csharp 650 | var appService = new WebApp($"app-workshop-{stackName}", new WebAppArgs 651 | { 652 | ResourceGroupName = resourceGroup.Name, 653 | ServerFarmId = appServicePlan.Id, 654 | }); 655 | 656 | return new Dictionary 657 | { 658 | ["AppServiceUrl"] = Output.Format($"https://{appService.DefaultHostName}") 659 | }; 660 | ``` 661 |
662 | 663 |
664 | Code in TypeScript 665 | 666 | ```typescript 667 | const appService = new WebApp("appService", { 668 | resourceGroupName: resourceGroup.name, 669 | serverFarmId: appServicePlan.id, 670 | }); 671 | 672 | export const appServiceUrl = pulumi.interpolate`https://${appService.defaultHostName}`; 673 | ``` 674 |
675 | 676 |
677 | Code in Python 678 | 679 | ```python 680 | app_service = azure_native.web.WebApp( 681 | f"app-workshop-{stack_name}", 682 | resource_group_name=resource_group.name, 683 | server_farm_id=app_service_plan.id 684 | ) 685 | 686 | pulumi.export("app_service_url", app_service.default_host_name.apply(lambda hostname: f"http://{hostname}")) 687 | ``` 688 |
689 | 690 | Sometimes, you need some data that are not available as properties of a resource. That's exactly what [provider functions](https://www.pulumi.com/docs/concepts/resources/functions/#provider-functions) are for. For instance, the [ListWebAppPublishingCredentialsOutput](https://www.pulumi.com/registry/packages/azure-native/api-docs/web/listwebapppublishingcredentials/) function can be used to retrieve the [publishing credentials](https://github.com/projectkudu/kudu/wiki/Deployment-credentials#site-credentials-aka-publish-profile-credentials) of an App Service 691 | 692 | 7. Add 2 outputs to the stack `PublishingUsername` and `PublishingUserPassword` that are secrets that can be used to deploy a zip package to the App Service. 693 | 694 |
695 | Code in C# 696 | 697 | ```csharp 698 | var publishingCredentials = ListWebAppPublishingCredentials.Invoke(new() 699 | { 700 | ResourceGroupName = resourceGroup.Name, 701 | Name = appService.Name 702 | }); 703 | 704 | return new Dictionary 705 | { 706 | ["AppServiceUrl"] = Output.Format($"https://{appService.DefaultHostName}"), 707 | ["PublishingUsername"] = Output.CreateSecret(publishingCredentials.Apply(c => c.PublishingUserName)), 708 | ["PublishingUserPassword"] = Output.CreateSecret(publishingCredentials.Apply(c => c.PublishingPassword)), 709 | }; 710 | ``` 711 | As the function outputs are not marked as secrets, you have to manually do it. 712 |
713 | 714 |
715 | Code in TypeScript 716 | 717 | ```typescript 718 | const publishingCredentials = listWebAppPublishingCredentialsOutput({ 719 | name: appService.name, 720 | resourceGroupName: resourceGroup.name 721 | }) 722 | 723 | export const appServiceUrl = pulumi.interpolate`https://${appService.defaultHostName}`; 724 | export const publishingUsername = pulumi.secret(publishingCredentials.publishingUserName) 725 | export const publishingPassword = pulumi.secret(publishingCredentials.publishingPassword) 726 | ``` 727 | As the function outputs are not marked as secrets, you have to manually do it. 728 |
729 | 730 |
731 | Code in Python 732 | 733 | ```python 734 | publishing_credentials = azure_native.web.list_web_app_publishing_credentials( 735 | resource_group_name=resource_group.name, 736 | name=app_service.name 737 | ) 738 | 739 | pulumi.export("app_service_url", app_service.default_host_name.apply(lambda hostname: f"http://{hostname}")) 740 | pulumi.export("publishing_username", Output.secret(publishing_credentials.publishing_user_name)) 741 | pulumi.export("publishing_userpassword", Output.secret(publishing_credentials.publishing_password)) 742 | ``` 743 | As the function outputs are not marked as secrets, you have to manually do it. 744 |
745 | 746 | ## Manage stacks 747 | 748 | - Use the `pulumi about` command to get some information about the current Pulumi environment. 749 | It also displays information about the current stack. 750 | 751 | ### Create a new stack 752 | 753 | - Use the [`pulumi stack init`](https://www.pulumi.com/docs/iac/cli/commands/pulumi_stack/) command to create a new `prod` stack. 754 | 755 |
756 | Command 757 | 758 | ```powershell 759 | pulumi stack init prod 760 | ``` 761 |
762 | 763 | You will be automatically switched to his new stack. You can switch back to the previous stack using the `pulumi stack select` command. 764 | 765 | - Switch back to the `dev` stack 766 | 767 |
768 | Command 769 | 770 | ```powershell 771 | pulumi stack select dev 772 | ``` 773 |
774 | 775 | - List the different stacks with the `pulumi stack ls` command. 776 | 777 | ### Provision the infrastructure for a new environment 778 | 779 | - Select the `prod` stack and try to provision the infrastructure for this stack. It should fail because some configuration is missing. 780 | 781 |
782 | Command 783 | 784 | ```powershell 785 | pulumi stack select prod 786 | pulumi up 787 | ``` 788 |
789 | 790 | - Add the missing configuration and provision the infrastructure 791 | 792 |
793 | Command 794 | 795 | ```powershell 796 | pulumi config set azure-native:location westeurope 797 | pulumi config set --secret ExternalApiKey SecretToBeKeptVerySecure 798 | pulumi config set AppServiceSku F1 799 | pulumi up 800 | ``` 801 |
802 | 803 | > [!NOTE] 804 | > You are on another environment so you don't have to set the same values. You can use another sku, another default azure location, another secret value... 805 | 806 | 807 | ### Delete resources and stack 808 | 809 | To delete all the resources in the stack you can run the command `pulumi destroy`. 810 | 811 | - Delete the resources on the `prod` environment. 812 | 813 | If you want to delete the stack itself with its configuration and deployment history you can run the command `pulumi stack rm` command. 814 | 815 | - Delete the `prod` stack 816 | 817 |
818 | Command 819 | 820 | ```powershell 821 | pulumi stack rm prod 822 | ``` 823 |
824 | 825 | ## Next 826 | 827 | To continue this lab and see more advanced features, you can check the next parts: 828 | - [Use Pulumi in CI/CD Pipelines with GitHub Actions](/CI_CD.md) 829 | - [Use existing infrastructure](/Existing%20infrastructure.md) -------------------------------------------------------------------------------- /configureAzureWorkloadIdentity.azcli: -------------------------------------------------------------------------------- 1 | # Initialize git repository with current code 2 | # Your Pulumi program should be in `infra` directory 3 | # You should have added the infra.yml workflow file in the `.github\workflows` directory 4 | git init 5 | git add . 6 | git commit -m "Initialize repository with infrastructure code" 7 | 8 | # Create a new remote private GitHub repository 9 | gh repo create pulumi-azure-workshop-lab --private --source=. --push 10 | 11 | # Retrieve the repository full name (org/repo) 12 | $repositoryFullName=$(gh repo view --json nameWithOwner -q ".nameWithOwner") 13 | 14 | # Retrieve the current subscription and current tenant identifiers 15 | $subscriptionId=$(az account show --query "id" -o tsv) 16 | $tenantId=$(az account show --query "tenantId" -o tsv) 17 | 18 | # Create an App Registration and its associated service principal 19 | $appId=$(az ad app create --display-name "GitHub Action OIDC for ${repositoryFullName}" --query "appId" -o tsv) 20 | $servicePrincipalId=$(az ad sp create --id $appId --query "id" -o tsv) 21 | 22 | # Assign the contributor role to the service principal on the subscription 23 | az role assignment create --role contributor --subscription $subscriptionId --assignee-object-id $servicePrincipalId --assignee-principal-type ServicePrincipal --scope /subscriptions/$subscriptionId 24 | 25 | # Prepare parameters for federated credentials 26 | $parametersJson = @{ 27 | name = "FederatedIdentityForWorkshop" 28 | issuer = "https://token.actions.githubusercontent.com" 29 | subject = "repo:${repositoryFullName}:ref:refs/heads/main" 30 | description = "Deployments for Pulumi workshop" 31 | audiences = @( 32 | "api://AzureADTokenExchange" 33 | ) 34 | } 35 | 36 | # Change parameters to single line string with escaped quotes to make it work with Azure CLI 37 | # https://medium.com/medialesson/use-dynamic-json-strings-with-azure-cli-commands-in-powershell-b191eccc8e9b 38 | $parameters = $($parametersJson | ConvertTo-Json -Depth 100 -Compress).Replace("`"", "\`"") 39 | 40 | # Create federated credentials 41 | az ad app federated-credential create --id $appId --parameters $parameters 42 | 43 | # Create GitHub secrets needed for the GitHub Actions 44 | gh secret set ARM_TENANT_ID --body $tenantId 45 | gh secret set ARM_SUBSCRIPTION_ID --body $subscriptionId 46 | gh secret set ARM_CLIENT_ID --body $appId 47 | 48 | # Replace by your Pulumi token 49 | $pulumiToken = "pul-******************" 50 | gh secret set PULUMI_ACCESS_TOKEN --body $pulumiToken 51 | 52 | # Run workflow 53 | gh workflow run infra.yml 54 | $runId=$(gh run list --workflow=infra.yml --json databaseId -q ".[0].databaseId") 55 | gh run watch $runId 56 | 57 | # Open the repostory in the browser 58 | gh repo view -w -------------------------------------------------------------------------------- /configureAzureWorkloadIdentity.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory=$true)] 3 | [string]$PulumiToken 4 | ) 5 | 6 | # Retrieve the repository full name (org/repo) 7 | $repositoryFullName=$(gh repo view --json nameWithOwner -q ".nameWithOwner") 8 | 9 | # Retrieve the current subscription and current tenant identifiers 10 | $subscriptionId=$(az account show --query "id" -o tsv) 11 | $tenantId=$(az account show --query "tenantId" -o tsv) 12 | 13 | # Create an App Registration and its associated service principal 14 | $appId=$(az ad app create --display-name "GitHub Action OIDC for ${repositoryFullName}" --query "appId" -o tsv) 15 | $servicePrincipalId=$(az ad sp create --id $appId --query "id" -o tsv) 16 | 17 | # Assign the contributor role to the service principal on the subscription 18 | az role assignment create --role contributor --subscription $subscriptionId --assignee-object-id $servicePrincipalId --assignee-principal-type ServicePrincipal --scope /subscriptions/$subscriptionId 19 | 20 | # Prepare parameters for federated credentials 21 | $parametersJson = @{ 22 | name = "FederatedIdentityForWorkshop" 23 | issuer = "https://token.actions.githubusercontent.com" 24 | subject = "repo:${repositoryFullName}:ref:refs/heads/main" 25 | description = "Deployments for Pulumi workshop" 26 | audiences = @( 27 | "api://AzureADTokenExchange" 28 | ) 29 | } 30 | 31 | # Change parameters to single line string with escaped quotes to make it work with Azure CLI 32 | # https://medium.com/medialesson/use-dynamic-json-strings-with-azure-cli-commands-in-powershell-b191eccc8e9b 33 | $parameters = $($parametersJson | ConvertTo-Json -Depth 100 -Compress).Replace("`"", "\`"") 34 | 35 | # Create federated credentials 36 | az ad app federated-credential create --id $appId --parameters $parameters 37 | 38 | # Create GitHub secrets needed for the GitHub Actions 39 | gh secret set ARM_TENANT_ID --body $tenantId 40 | gh secret set ARM_SUBSCRIPTION_ID --body $subscriptionId 41 | gh secret set ARM_CLIENT_ID --body $appId 42 | gh secret set PULUMI_ACCESS_TOKEN --body $PulumiToken -------------------------------------------------------------------------------- /configureAzureWorkloadIdentity.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Parse named parameters 4 | while [ $# -gt 0 ]; do 5 | case "$1" in 6 | --PulumiToken=*) 7 | PulumiToken="${1#*=}" 8 | ;; 9 | *) 10 | echo "Invalid argument: $1" 11 | exit 1 12 | esac 13 | shift 14 | done 15 | 16 | # Retrieve the repository full name (org/repo) 17 | repositoryFullName=$(gh repo view --json nameWithOwner -q ".nameWithOwner") 18 | 19 | # Retrieve the current subscription and current tenant identifiers 20 | subscriptionId=$(az account show --query "id" -o tsv) 21 | tenantId=$(az account show --query "tenantId" -o tsv) 22 | 23 | # Create an App Registration and its associated service principal 24 | appId=$(az ad app create --display-name "GitHub Action OIDC for ${repositoryFullName}" --query "appId" -o tsv) 25 | servicePrincipalId=$(az ad sp create --id $appId --query "id" -o tsv) 26 | 27 | # Assign the contributor role to the service principal on the subscription 28 | az role assignment create --role contributor --subscription $subscriptionId --assignee-object-id $servicePrincipalId --assignee-principal-type ServicePrincipal --scope /subscriptions/$subscriptionId 29 | 30 | # Prepare parameters for federated credentials 31 | parametersJson=$(cat <