Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28,226 changes: 28,226 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/components/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import MenuIcon from './icons/MenuIcon.astro'
import Search from '@/components/Search'
import TagIcon from './icons/TagIcon.astro'
import ToggleTheme from './ToggleTheme.astro'
import LanguageSwitcher from './LanguageSwitcher.astro'

// ADD YOUR SOCIAL NETWORKS HERE
const SOCIALNETWORKS = [
Expand Down Expand Up @@ -66,6 +67,7 @@ const SOCIALNETWORKS = [
<div>
<Search />
</div>
<LanguageSwitcher />
<ToggleTheme />
<button id='astro-header-drawer-button' type='button' class='md:ml-6 md:hidden'>
<MenuIcon />
Expand Down
158 changes: 158 additions & 0 deletions src/components/LanguageSwitcher.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
---
import { LANGUAGES, getTranslation } from '@/data/languages'
import { getCurrentLanguage } from '@/utils/language'

// This will need to be handled client-side since we need to detect browser language
const currentLanguage = 'en' // Default fallback for SSR
---

<div class="relative language-switcher">
<button
id="language-button"
class="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700"
aria-expanded="false"
aria-haspopup="true"
>
<span id="current-language-flag">🇺🇸</span>
<span id="current-language-name">English</span>
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>

<div
id="language-dropdown"
class="absolute right-0 z-10 mt-2 w-48 bg-white border border-gray-300 rounded-md shadow-lg opacity-0 invisible transition-all duration-200 dark:bg-gray-800 dark:border-gray-600"
>
<div class="py-1">
{LANGUAGES.map((language) => (
<button
class="language-option flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
data-language={language.code}
>
<span class="mr-3">{language.flag}</span>
{language.name}
</button>
))}
</div>
</div>
</div>

<style>
.language-switcher .language-dropdown.show {
opacity: 1;
visibility: visible;
}
</style>

<script>
// Define languages and utilities inline since we can't import in client scripts
const LANGUAGES = [
{ code: 'en', name: 'English', flag: '🇺🇸' },
{ code: 'es', name: 'Español', flag: '🇪🇸' }
]
const DEFAULT_LANGUAGE = 'en'

function getLanguageByCode(code) {
return LANGUAGES.find(lang => lang.code === code)
}

function getBrowserLanguage() {
if (typeof window === 'undefined') return DEFAULT_LANGUAGE
const browserLang = navigator.language.split('-')[0]
const supportedLanguages = LANGUAGES.map(lang => lang.code)
return supportedLanguages.includes(browserLang) ? browserLang : DEFAULT_LANGUAGE
}

function getStoredLanguage() {
if (typeof window === 'undefined') return null
return localStorage.getItem('preferredLanguage')
}

function setStoredLanguage(languageCode) {
if (typeof window === 'undefined') return
localStorage.setItem('preferredLanguage', languageCode)
}

function getCurrentLanguage() {
const stored = getStoredLanguage()
if (stored && LANGUAGES.some(lang => lang.code === stored)) {
return stored
}
return getBrowserLanguage()
}

// Initialize the language switcher
function initLanguageSwitcher() {
const button = document.getElementById('language-button')
const dropdown = document.getElementById('language-dropdown')
const currentLangFlag = document.getElementById('current-language-flag')
const currentLangName = document.getElementById('current-language-name')
const languageOptions = document.querySelectorAll('.language-option')

if (!button || !dropdown) return

// Set initial language
const currentLang = getCurrentLanguage()
const langConfig = getLanguageByCode(currentLang)
if (langConfig && currentLangFlag && currentLangName) {
currentLangFlag.textContent = langConfig.flag
currentLangName.textContent = langConfig.name
}

// Toggle dropdown
button.addEventListener('click', (e) => {
e.stopPropagation()
const isVisible = dropdown.classList.contains('opacity-100')

if (isVisible) {
dropdown.classList.remove('opacity-100', 'visible')
dropdown.classList.add('opacity-0', 'invisible')
} else {
dropdown.classList.remove('opacity-0', 'invisible')
dropdown.classList.add('opacity-100', 'visible')
}
})

// Handle language selection
languageOptions.forEach(option => {
option.addEventListener('click', (e) => {
const languageCode = e.currentTarget.dataset.language
if (languageCode) {
changeLanguage(languageCode)
}
})
})

// Close dropdown when clicking outside
document.addEventListener('click', () => {
dropdown.classList.remove('opacity-100', 'visible')
dropdown.classList.add('opacity-0', 'invisible')
})
}

function changeLanguage(languageCode) {
// Store the preference
setStoredLanguage(languageCode)

// Update UI
const langConfig = getLanguageByCode(languageCode)
const currentLangFlag = document.getElementById('current-language-flag')
const currentLangName = document.getElementById('current-language-name')

if (langConfig && currentLangFlag && currentLangName) {
currentLangFlag.textContent = langConfig.flag
currentLangName.textContent = langConfig.name
}

// For now, just reload the page
window.location.reload()
}

// Initialize when DOM is loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initLanguageSwitcher)
} else {
initLanguageSwitcher()
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
title: 'Azure Federated Identity Guide: Terraform & GitHub Actions'
description: 'Transition from authenticating via Service Principal with a client secret to using OpenID Connect'
pubDate: 'May 11 2024'
heroImage: '../../assets/images/fed-creds-github.png'
heroImage: '../../../assets/images/fed-creds-github.png'
category: 'DevOps'
tags:
- Azure
- GitHub
- Terraform
- GitHub Actions
- OIDC
language: 'en'
---

I recently needed to transition from authenticating via Service Principal with a client secret to using OpenID Connect for Terraform actions within a few GitHub Actions workflows. This post is to showcase what I needed to change as there was not a single source of this information to perform this update.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ title: >-
---

import { Image } from 'astro:assets'
import terraformError from '../../assets/images/terraform-bool-fail.png'
import pipelineTrigger from '../../assets/images/pipeline-run.png'
// import terraformError from '../../assets/images/terraform-bool-fail.png'
// import pipelineTrigger from '../../assets/images/pipeline-run.png'

This post showcases how to set up Azure pipeline parameters or variables on a pipeline and pass them to Terraform as variables. It covers string and boolean types. In most cases, a tfvars file is used, but there may be a use case when just one variable is needed, and this is where Azure pipeline parameters/variables come in handy.

Expand Down Expand Up @@ -130,7 +130,7 @@ parameters:

Since there is no default value, it is a required parameter:

<Image src={pipelineTrigger} alt='Pipeline Run.' />
{/* <Image src={pipelineTrigger} alt='Pipeline Run.' /> */}

In the plan, this commandOptions snippet using '-var' is used to pass the Azure pipelines parameter to Terraform:

Expand Down Expand Up @@ -199,7 +199,7 @@ Pass this parameter to Terraform using -var:

When triggering the updated pipeline manually, you will face the following error:

<Image src={terraformError} alt='Terraform Bool Fail Error.' />
{/* <Image src={terraformError} alt='Terraform Bool Fail Error.' /> */}

The error message **a bool is required; to convert from string, use lowercase "false"** gives us a good indication on where the error comes from. Let’s add a simple task to show what the string value of isCreateEnabled is:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ title: >-
---

import { Image } from 'astro:assets'
import websitePreview from '../../assets/images/website.png'
// import websitePreview from '../../assets/images/website.png'

This showcases the Terraform code to manage the Infrastructure to host your Streamlit application. It continues a series detailing the process of deploying a Streamlit app to Azure, broken down into the following parts:

Expand Down Expand Up @@ -249,6 +249,6 @@ Follow these steps to view your deployed website:
2. Navigate to the web app within the resource group rg-streamlit-poc.
3. In the overview section, you should see ‘Default Domain’ with a link. Click it. The URL should be similar to https://app-azure-streamlit-poc-b548.azurewebsites.net/.

<Image src={websitePreview} alt='A Streamlit website preview.' />
{/* <Image src={websitePreview} alt='A Streamlit website preview.' /> */}

In the next part, we will go through automating the infrastructure provisioning via GitHub workflows.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
title: 'Azure Federated Identity: Terraform y GitHub Actions'
description: 'Transición de autenticación vía Service Principal con client secret a usar OpenID Connect'
pubDate: 'May 11 2024'
heroImage: '../../../assets/images/fed-creds-github.png'
category: 'DevOps'
tags:
- Azure
- GitHub
- Terraform
- GitHub Actions
- OIDC
language: 'es'
---

Recientemente necesité hacer la transición de autenticación vía Service Principal con un client secret a usar OpenID Connect para acciones de Terraform dentro de algunos workflows de GitHub Actions. Este post es para mostrar lo que necesité cambiar ya que no había una sola fuente de esta información para realizar esta actualización.

## TL;DR

Con un workflow existente de Terraform en GitHub Actions, estos son los cambios clave:

1. **Configurar Federated Identity Credentials en Azure**
2. **Actualizar secretos del repositorio**
3. **Modificar el workflow de GitHub Actions**

## Configuración de Azure

Primero, necesitas configurar las credenciales de identidad federada en Azure para tu Service Principal.

```bash
# Crear la credencial de identidad federada
az ad app federated-credential create \
--id <app-id> \
--parameters '{
"name": "github-federated-credential",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:tu-usuario/tu-repositorio:ref:refs/heads/main",
"description": "Credencial federada para GitHub Actions",
"audiences": ["api://AzureADTokenExchange"]
}'
```

## Actualización del Workflow

Aquí está la diferencia principal en el workflow:

### Antes (con client secret):
```yaml
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
```

### Después (con OIDC):
```yaml
- name: Azure Login
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
```

## Ventajas de OIDC

- **Seguridad mejorada**: No hay secretos de larga duración
- **Rotación automática**: Los tokens son de corta duración
- **Auditoría mejorada**: Mejor trazabilidad de las acciones

Esta transición mejora significativamente la postura de seguridad de tus pipelines de CI/CD.
33 changes: 18 additions & 15 deletions src/content/config.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { defineCollection, z } from 'astro:content'
import { CATEGORIES } from '@/data/categories'

const blogSchema = ({ image }) =>
z.object({
title: z.string().max(60),
description: z.string().max(158),
// Transform string to Date object
pubDate: z
.string()
.or(z.date())
.transform((val) => new Date(val)),
heroImage: image(),
category: z.enum(CATEGORIES),
tags: z.array(z.string()),
draft: z.boolean().default(false),
language: z.string().optional().default('en')
})

// Keep the main blog collection for backward compatibility and all posts
const blog = defineCollection({
// Type-check frontmatter using a schema
schema: ({ image }) =>
z.object({
title: z.string().max(60),
description: z.string().max(158),
// Transform string to Date object
pubDate: z
.string()
.or(z.date())
.transform((val) => new Date(val)),
heroImage: image(),
category: z.enum(CATEGORIES),
tags: z.array(z.string()),
draft: z.boolean().default(false)
})
schema: blogSchema
})

export const collections = { blog }
Loading