diff --git a/.commitlintrc.json b/.commitlintrc.json new file mode 100644 index 0000000..7a4907e --- /dev/null +++ b/.commitlintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["@commitlint/config-conventional"] +} \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d2cbd74..a79d760 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,11 +55,10 @@ jobs: echo "✅ All links working!" pkill -f "mkdocs serve" || true - # Solo para main: guardar el sitio construido + # Upload build artifact for validation/review - name: Upload build artifact - if: github.ref == 'refs/heads/main' uses: actions/upload-artifact@v4 with: name: site path: site/ - retention-days: 1 + retention-days: 3 diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml deleted file mode 100644 index f65287e..0000000 --- a/.github/workflows/ci-cd.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: CI/CD Pipeline - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -permissions: - contents: read - pages: write - id-token: write - -jobs: - build-and-validate: - name: Build and Validate - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.x' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - - name: Build MkDocs site - run: mkdocs build --strict --use-directory-urls - - - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v3 - with: - path: site/ - - deploy: - name: Deploy to GitHub Pages - if: github.ref == 'refs/heads/main' - needs: build-and-validate - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/deploy-aws.yml b/.github/workflows/deploy-aws.yml new file mode 100644 index 0000000..6b2bd5d --- /dev/null +++ b/.github/workflows/deploy-aws.yml @@ -0,0 +1,98 @@ +name: Deploy to AWS S3 + CloudFront + +on: + push: + branches: [ main ] + +permissions: + contents: read + id-token: write # Required for AWS OIDC authentication + + +jobs: + commit_lint: + name: Validate Commit Messages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate PR Title + uses: wagoid/commitlint-github-action@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + configFile: .commitlintrc.json + + build-and-deploy: + needs: commit_lint + name: Build and Deploy to AWS + runs-on: ubuntu-latest + environment: + name: aws-prod + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Build MkDocs site + run: mkdocs build --strict --use-directory-urls + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Sync to S3 + run: | + aws s3 sync site/ s3://${{ secrets.AWS_S3_BUCKET }}/ \ + --delete \ + --cache-control "public, max-age=3600" \ + --exclude "*.html" \ + --exclude "sitemap.xml" + + # Upload HTML files with shorter cache + aws s3 sync site/ s3://${{ secrets.AWS_S3_BUCKET }}/ \ + --cache-control "public, max-age=600, must-revalidate" \ + --content-type "text/html; charset=utf-8" \ + --exclude "*" \ + --include "*.html" + + # Upload sitemap with no cache + aws s3 sync site/ s3://${{ secrets.AWS_S3_BUCKET }}/ \ + --cache-control "public, max-age=0, must-revalidate" \ + --exclude "*" \ + --include "sitemap.xml" + + cleanup-staging: + name: Stop Staging Site + needs: build-and-deploy + runs-on: ubuntu-latest + environment: + name: aws-stag + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Stop Staging Site + run: | + aws s3 rm s3://${{ secrets.AWS_S3_BUCKET }}/staging/ --recursive \ No newline at end of file diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml new file mode 100644 index 0000000..866919a --- /dev/null +++ b/.github/workflows/deploy-staging.yml @@ -0,0 +1,91 @@ +name: Deploy to Staging (AWS S3 + CloudFront) + +on: + push: + branches: [ staging ] + +permissions: + contents: read + id-token: write # Required for AWS OIDC authentication + + +jobs: + + commit_lint: + name: Validate Commit Messages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate PR Title + uses: wagoid/commitlint-github-action@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + configFile: .commitlintrc.json + build-and-deploy-staging: + name: Build and Deploy to Staging + needs: commit_lint + runs-on: ubuntu-latest + environment: + name: aws-stag + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Build MkDocs site + run: mkdocs build --strict --use-directory-urls + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Add staging banner to site + run: | + # Add a staging environment banner to all HTML files + find site -name "*.html" -type f -exec sed -i.bak '/\ + 🚧 STAGING ENVIRONMENT - NOT FOR PRODUCTION USE 🚧\ + ' {} \; + # Clean up backup files + find site -name "*.bak" -type f -delete + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Sync to S3 (Staging) + run: | + aws s3 sync site/ s3://${{ secrets.AWS_S3_BUCKET }}/ \ + --delete \ + --cache-control "public, max-age=300" \ + --exclude "*.html" \ + --exclude "sitemap.xml" + + # Upload HTML files with shorter cache for staging + aws s3 sync site/ s3://${{ secrets.AWS_S3_BUCKET }}/ \ + --cache-control "public, max-age=60, must-revalidate" \ + --content-type "text/html; charset=utf-8" \ + --exclude "*" \ + --include "*.html" + + # Upload sitemap with no cache + aws s3 sync site/ s3://${{ secrets.AWS_S3_BUCKET }}/ \ + --cache-control "public, max-age=0, must-revalidate" \ + --exclude "*" \ + --include "sitemap.xml" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 0baa5b3..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Deploy to GitHub Pages - -on: - workflow_run: - workflows: ["Build and Validate"] - types: - - completed - branches: [ main ] - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - deploy: - name: Deploy to GitHub Pages - if: github.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: Download build artifact - uses: actions/download-artifact@v4 - with: - name: site - path: site/ - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ github.event.workflow_run.id }} - - - name: Setup Pages - uses: actions/configure-pages@v4 - - - name: Upload to GitHub Pages - uses: actions/upload-pages-artifact@v3 - with: - path: site/ - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 5403a8a..9305398 100644 --- a/.gitignore +++ b/.gitignore @@ -271,3 +271,6 @@ Thumbs.db # Temporary files *.tmp *.temp + +# Generated files +broken_links.json diff --git a/broken_links.json b/broken_links.json index bf87eb7..64bd6bb 100644 --- a/broken_links.json +++ b/broken_links.json @@ -1,35 +1,36 @@ { "summary": { - "total_files_scanned": 35, - "working_links": 79, - "broken_links": 0, + "total_files_scanned": 45, + "working_links": 80, + "broken_links": 2, "base_url": "http://127.0.0.1:8000" }, - "broken_links": [], - "working_links": [ + "broken_links": [ { - "file": "about.md", - "text": "guía para ponentes", - "url": "comunidad/ponentes/", - "full_url": "http://127.0.0.1:8000/comunidad/ponentes/", - "status": "200 OK", - "line": 42, + "file": "blog/media/README.md", + "text": "alt text", + "url": "media/nombre-del-articulo/imagen.png", + "full_url": "http://127.0.0.1:8000/blog/media/media/nombre-del-articulo/imagen.png", + "status": "404 Not Found", + "line": 55, "link_type": "markdown" }, { - "file": "about.md", - "text": "nuestra página de voluntarios", - "url": "/comunidad/voluntarios/", - "full_url": "http://127.0.0.1:8000/comunidad/voluntarios/", - "status": "200 OK", - "line": 46, + "file": "comunidad/como-contribuir.md", + "text": "formas de contribuir", + "url": "#formas-de-contribuir", + "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir.html#formas-de-contribuir", + "status": "404 Not Found", + "line": 63, "link_type": "markdown" - }, + } + ], + "working_links": [ { "file": "index.md", "text": "Explorar charlas", - "url": "/meetups/", - "full_url": "http://127.0.0.1:8000/meetups/", + "url": "/meetups/index.md", + "full_url": "http://127.0.0.1:8000/meetups/index.html", "status": "200 OK", "line": 8, "link_type": "html" @@ -82,370 +83,352 @@ { "file": "index.md", "text": "Ver Eventos", - "url": "/meetups/", - "full_url": "http://127.0.0.1:8000/meetups/", + "url": "/meetups/index.md", + "full_url": "http://127.0.0.1:8000/meetups/index.html", "status": "200 OK", "line": 49, "link_type": "html" }, { - "file": "meetups/2023/index.md", - "text": "Ver detalles", - "url": "202311-noviembre", - "full_url": "http://127.0.0.1:8000/202311-noviembre", - "status": "200 OK", - "line": 15, - "link_type": "markdown" - }, - { - "file": "meetups/2023/index.md", - "text": "Ver detalles", - "url": "202311-noviembre", - "full_url": "http://127.0.0.1:8000/202311-noviembre", - "status": "200 OK", - "line": 16, - "link_type": "markdown" - }, - { - "file": "meetups/2023/index.md", - "text": "Ver detalles", - "url": "202310-octubre", - "full_url": "http://127.0.0.1:8000/202310-octubre", - "status": "200 OK", - "line": 17, - "link_type": "markdown" - }, - { - "file": "meetups/2023/index.md", - "text": "Ver detalles", - "url": "202309-septiembre", - "full_url": "http://127.0.0.1:8000/202309-septiembre", - "status": "200 OK", - "line": 18, - "link_type": "markdown" - }, - { - "file": "meetups/2023/index.md", - "text": "Ver detalles", - "url": "202309-septiembre", - "full_url": "http://127.0.0.1:8000/202309-septiembre", + "file": "about.md", + "text": "guía para ponentes", + "url": "comunidad/ponentes/", + "full_url": "http://127.0.0.1:8000/comunidad/ponentes/", "status": "200 OK", - "line": 19, + "line": 42, "link_type": "markdown" }, { - "file": "meetups/2023/index.md", - "text": "ponentes y voluntarios reconocidos", - "url": "/comunidad/como-contribuir/", - "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir/", + "file": "about.md", + "text": "nuestra página de voluntarios", + "url": "comunidad/voluntarios/", + "full_url": "http://127.0.0.1:8000/comunidad/voluntarios/", "status": "200 OK", - "line": 59, + "line": 46, "link_type": "markdown" }, { - "file": "meetups/2024/index.md", + "file": "meetups/2025/index.md", "text": "Ver detalles", - "url": "202411-noviembre", - "full_url": "http://127.0.0.1:8000/202411-noviembre", + "url": "202508-agosto/", + "full_url": "http://127.0.0.1:8000/meetups/2025/202508-agosto", "status": "200 OK", "line": 15, "link_type": "markdown" }, { - "file": "meetups/2024/index.md", + "file": "meetups/2025/index.md", "text": "Ver detalles", - "url": "202411-noviembre", - "full_url": "http://127.0.0.1:8000/202411-noviembre", + "url": "202507-julio/", + "full_url": "http://127.0.0.1:8000/meetups/2025/202507-julio", "status": "200 OK", "line": 16, "link_type": "markdown" }, { - "file": "meetups/2024/index.md", + "file": "meetups/2025/index.md", "text": "Ver detalles", - "url": "202410-octubre", - "full_url": "http://127.0.0.1:8000/202410-octubre", + "url": "202506-junio/", + "full_url": "http://127.0.0.1:8000/meetups/2025/202506-junio", "status": "200 OK", "line": 17, "link_type": "markdown" }, { - "file": "meetups/2024/index.md", + "file": "meetups/2025/index.md", "text": "Ver detalles", - "url": "202409-septiembre", - "full_url": "http://127.0.0.1:8000/202409-septiembre", + "url": "202505-mayo/", + "full_url": "http://127.0.0.1:8000/meetups/2025/202505-mayo", "status": "200 OK", "line": 18, "link_type": "markdown" }, { - "file": "meetups/2024/index.md", + "file": "meetups/2025/index.md", "text": "Ver detalles", - "url": "202409-septiembre", - "full_url": "http://127.0.0.1:8000/202409-septiembre", + "url": "202504-unam/", + "full_url": "http://127.0.0.1:8000/meetups/2025/202504-unam", "status": "200 OK", "line": 19, "link_type": "markdown" }, { - "file": "meetups/2024/index.md", + "file": "meetups/2025/index.md", "text": "Ver detalles", - "url": "202408-agosto", - "full_url": "http://127.0.0.1:8000/202408-agosto", + "url": "202504-unam/", + "full_url": "http://127.0.0.1:8000/meetups/2025/202504-unam", "status": "200 OK", "line": 20, "link_type": "markdown" }, { - "file": "meetups/2024/index.md", + "file": "meetups/2025/index.md", "text": "Ver detalles", - "url": "202408-agosto", - "full_url": "http://127.0.0.1:8000/202408-agosto", + "url": "202504-abril/", + "full_url": "http://127.0.0.1:8000/meetups/2025/202504-abril", "status": "200 OK", "line": 21, "link_type": "markdown" }, { - "file": "meetups/2024/index.md", + "file": "meetups/2025/index.md", "text": "Ver detalles", - "url": "202407-julio", - "full_url": "http://127.0.0.1:8000/202407-julio", + "url": "202503-marzo/", + "full_url": "http://127.0.0.1:8000/meetups/2025/202503-marzo", "status": "200 OK", "line": 22, "link_type": "markdown" }, { - "file": "meetups/2024/index.md", + "file": "meetups/2025/index.md", "text": "Ver detalles", - "url": "202407-julio", - "full_url": "http://127.0.0.1:8000/202407-julio", + "url": "202503-marzo/", + "full_url": "http://127.0.0.1:8000/meetups/2025/202503-marzo", "status": "200 OK", "line": 23, "link_type": "markdown" }, { - "file": "meetups/2024/index.md", + "file": "meetups/2025/index.md", "text": "Ver detalles", - "url": "202406-junio", - "full_url": "http://127.0.0.1:8000/202406-junio", + "url": "202502-febrero/", + "full_url": "http://127.0.0.1:8000/meetups/2025/202502-febrero", "status": "200 OK", "line": 24, "link_type": "markdown" }, { - "file": "meetups/2024/index.md", + "file": "meetups/2025/index.md", "text": "Ver detalles", - "url": "202405-mayo", - "full_url": "http://127.0.0.1:8000/202405-mayo", + "url": "202502-febrero/", + "full_url": "http://127.0.0.1:8000/meetups/2025/202502-febrero", "status": "200 OK", "line": 25, "link_type": "markdown" }, { - "file": "meetups/2024/index.md", + "file": "meetups/2025/index.md", "text": "Ver detalles", - "url": "202404-abril", - "full_url": "http://127.0.0.1:8000/202404-abril", + "url": "202501-enero/", + "full_url": "http://127.0.0.1:8000/meetups/2025/202501-enero", "status": "200 OK", "line": 26, "link_type": "markdown" }, { - "file": "meetups/2024/index.md", + "file": "meetups/2025/index.md", "text": "Ver detalles", - "url": "202403-marzo", - "full_url": "http://127.0.0.1:8000/202403-marzo", + "url": "202501-enero/", + "full_url": "http://127.0.0.1:8000/meetups/2025/202501-enero", "status": "200 OK", "line": 27, "link_type": "markdown" }, { - "file": "meetups/2024/index.md", - "text": "Ver detalles", - "url": "202402-febrero", - "full_url": "http://127.0.0.1:8000/202402-febrero", - "status": "200 OK", - "line": 28, - "link_type": "markdown" - }, - { - "file": "meetups/2024/index.md", - "text": "Ver detalles", - "url": "202401-enero", - "full_url": "http://127.0.0.1:8000/202401-enero", - "status": "200 OK", - "line": 29, - "link_type": "markdown" - }, - { - "file": "meetups/2024/index.md", + "file": "meetups/2025/index.md", "text": "ponentes y voluntarios reconocidos", - "url": "/comunidad/como-contribuir/", - "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir/", + "url": "../../comunidad/como-contribuir/", + "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir", "status": "200 OK", - "line": 69, + "line": 68, "link_type": "markdown" }, { - "file": "meetups/2025/index.md", + "file": "meetups/2024/index.md", "text": "Ver detalles", - "url": "202507-julio", - "full_url": "http://127.0.0.1:8000/202507-julio", + "url": "202411-noviembre/", + "full_url": "http://127.0.0.1:8000/meetups/2024/202411-noviembre", "status": "200 OK", "line": 15, "link_type": "markdown" }, { - "file": "meetups/2025/index.md", + "file": "meetups/2024/index.md", "text": "Ver detalles", - "url": "202506-junio", - "full_url": "http://127.0.0.1:8000/202506-junio", + "url": "202411-noviembre/", + "full_url": "http://127.0.0.1:8000/meetups/2024/202411-noviembre", "status": "200 OK", "line": 16, "link_type": "markdown" }, { - "file": "meetups/2025/index.md", + "file": "meetups/2024/index.md", "text": "Ver detalles", - "url": "202505-mayo", - "full_url": "http://127.0.0.1:8000/202505-mayo", + "url": "202410-octubre/", + "full_url": "http://127.0.0.1:8000/meetups/2024/202410-octubre", "status": "200 OK", "line": 17, "link_type": "markdown" }, { - "file": "meetups/2025/index.md", + "file": "meetups/2024/index.md", "text": "Ver detalles", - "url": "202504-abril", - "full_url": "http://127.0.0.1:8000/202504-abril", + "url": "202409-septiembre/", + "full_url": "http://127.0.0.1:8000/meetups/2024/202409-septiembre", "status": "200 OK", "line": 18, "link_type": "markdown" }, { - "file": "meetups/2025/index.md", + "file": "meetups/2024/index.md", "text": "Ver detalles", - "url": "202504-abril", - "full_url": "http://127.0.0.1:8000/202504-abril", + "url": "202409-septiembre/", + "full_url": "http://127.0.0.1:8000/meetups/2024/202409-septiembre", "status": "200 OK", "line": 19, "link_type": "markdown" }, { - "file": "meetups/2025/index.md", + "file": "meetups/2024/index.md", "text": "Ver detalles", - "url": "202504-abril", - "full_url": "http://127.0.0.1:8000/202504-abril", + "url": "202408-agosto/", + "full_url": "http://127.0.0.1:8000/meetups/2024/202408-agosto", "status": "200 OK", "line": 20, "link_type": "markdown" }, { - "file": "meetups/2025/index.md", + "file": "meetups/2024/index.md", "text": "Ver detalles", - "url": "202503-marzo", - "full_url": "http://127.0.0.1:8000/202503-marzo", + "url": "202408-agosto/", + "full_url": "http://127.0.0.1:8000/meetups/2024/202408-agosto", "status": "200 OK", "line": 21, "link_type": "markdown" }, { - "file": "meetups/2025/index.md", + "file": "meetups/2024/index.md", "text": "Ver detalles", - "url": "202503-marzo", - "full_url": "http://127.0.0.1:8000/202503-marzo", + "url": "202407-julio/", + "full_url": "http://127.0.0.1:8000/meetups/2024/202407-julio", "status": "200 OK", "line": 22, "link_type": "markdown" }, { - "file": "meetups/2025/index.md", + "file": "meetups/2024/index.md", "text": "Ver detalles", - "url": "202502-febrero", - "full_url": "http://127.0.0.1:8000/202502-febrero", + "url": "202407-julio/", + "full_url": "http://127.0.0.1:8000/meetups/2024/202407-julio", "status": "200 OK", "line": 23, "link_type": "markdown" }, { - "file": "meetups/2025/index.md", + "file": "meetups/2024/index.md", "text": "Ver detalles", - "url": "202502-febrero", - "full_url": "http://127.0.0.1:8000/202502-febrero", + "url": "202406-junio/", + "full_url": "http://127.0.0.1:8000/meetups/2024/202406-junio", "status": "200 OK", "line": 24, "link_type": "markdown" }, { - "file": "meetups/2025/index.md", + "file": "meetups/2024/index.md", "text": "Ver detalles", - "url": "202501-enero", - "full_url": "http://127.0.0.1:8000/202501-enero", + "url": "202405-mayo/", + "full_url": "http://127.0.0.1:8000/meetups/2024/202405-mayo", "status": "200 OK", "line": 25, "link_type": "markdown" }, { - "file": "meetups/2025/index.md", + "file": "meetups/2024/index.md", "text": "Ver detalles", - "url": "202501-enero", - "full_url": "http://127.0.0.1:8000/202501-enero", + "url": "202404-abril/", + "full_url": "http://127.0.0.1:8000/meetups/2024/202404-abril", "status": "200 OK", "line": 26, "link_type": "markdown" }, { - "file": "meetups/2025/index.md", + "file": "meetups/2024/index.md", + "text": "Ver detalles", + "url": "202403-marzo/", + "full_url": "http://127.0.0.1:8000/meetups/2024/202403-marzo", + "status": "200 OK", + "line": 27, + "link_type": "markdown" + }, + { + "file": "meetups/2024/index.md", + "text": "Ver detalles", + "url": "202402-febrero/", + "full_url": "http://127.0.0.1:8000/meetups/2024/202402-febrero", + "status": "200 OK", + "line": 28, + "link_type": "markdown" + }, + { + "file": "meetups/2024/index.md", + "text": "Ver detalles", + "url": "202401-enero/", + "full_url": "http://127.0.0.1:8000/meetups/2024/202401-enero", + "status": "200 OK", + "line": 29, + "link_type": "markdown" + }, + { + "file": "meetups/2024/index.md", "text": "ponentes y voluntarios reconocidos", - "url": "/comunidad/como-contribuir/", - "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir/", + "url": "../../comunidad/como-contribuir/", + "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir", "status": "200 OK", - "line": 67, + "line": 69, "link_type": "markdown" }, { - "file": "comunidad/como-contribuir.md", - "text": "Ser Ponente", - "url": "ponentes/", - "full_url": "http://127.0.0.1:8000/ponentes/", + "file": "meetups/2023/index.md", + "text": "Ver detalles", + "url": "202311-noviembre/", + "full_url": "http://127.0.0.1:8000/meetups/2023/202311-noviembre", "status": "200 OK", - "line": 31, + "line": 15, "link_type": "markdown" }, { - "file": "comunidad/como-contribuir.md", - "text": "Ser Voluntario", - "url": "voluntarios/", - "full_url": "http://127.0.0.1:8000/voluntarios/", + "file": "meetups/2023/index.md", + "text": "Ver detalles", + "url": "202311-noviembre/", + "full_url": "http://127.0.0.1:8000/meetups/2023/202311-noviembre", "status": "200 OK", - "line": 37, + "line": 16, "link_type": "markdown" }, { - "file": "comunidad/como-contribuir.md", - "text": "Alianzas", - "url": "alianzas/", - "full_url": "http://127.0.0.1:8000/alianzas/", + "file": "meetups/2023/index.md", + "text": "Ver detalles", + "url": "202310-octubre/", + "full_url": "http://127.0.0.1:8000/meetups/2023/202310-octubre", "status": "200 OK", - "line": 55, + "line": 17, "link_type": "markdown" }, { - "file": "comunidad/como-contribuir.md", - "text": "formas de contribuir", - "url": "#formas-de-contribuir", - "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir/#formas-de-contribuir", + "file": "meetups/2023/index.md", + "text": "Ver detalles", + "url": "202309-septiembre/", + "full_url": "http://127.0.0.1:8000/meetups/2023/202309-septiembre", "status": "200 OK", - "line": 63, + "line": 18, "link_type": "markdown" }, { - "file": "comunidad/como-contribuir.md", - "text": "nuestros meetups", - "url": "../meetups/", - "full_url": "http://127.0.0.1:8000/meetups/", + "file": "meetups/2023/index.md", + "text": "Ver detalles", + "url": "202309-septiembre/", + "full_url": "http://127.0.0.1:8000/meetups/2023/202309-septiembre", "status": "200 OK", - "line": 70, + "line": 19, + "link_type": "markdown" + }, + { + "file": "meetups/2023/index.md", + "text": "ponentes y voluntarios reconocidos", + "url": "../../comunidad/como-contribuir/", + "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir", + "status": "200 OK", + "line": 59, "link_type": "markdown" }, { @@ -713,10 +696,55 @@ "file": "comunidad/voluntarios.md", "text": "Más Información", "url": "como-contribuir/", - "full_url": "http://127.0.0.1:8000/como-contribuir/", + "full_url": "http://127.0.0.1:8000/comunidad/como-contribuir", "status": "200 OK", "line": 87, "link_type": "html" + }, + { + "file": "comunidad/sedes_faq.md", + "text": "Ser Ponente", + "url": "ponentes/#por-que-ser-ponente", + "full_url": "http://127.0.0.1:8000/comunidad/ponentes/#por-que-ser-ponente", + "status": "200 OK", + "line": 109, + "link_type": "markdown" + }, + { + "file": "comunidad/como-contribuir.md", + "text": "Ser Ponente", + "url": "ponentes/", + "full_url": "http://127.0.0.1:8000/comunidad/ponentes", + "status": "200 OK", + "line": 31, + "link_type": "markdown" + }, + { + "file": "comunidad/como-contribuir.md", + "text": "Ser Voluntario", + "url": "voluntarios/", + "full_url": "http://127.0.0.1:8000/comunidad/voluntarios", + "status": "200 OK", + "line": 37, + "link_type": "markdown" + }, + { + "file": "comunidad/como-contribuir.md", + "text": "Alianzas", + "url": "alianzas/", + "full_url": "http://127.0.0.1:8000/comunidad/alianzas", + "status": "200 OK", + "line": 55, + "link_type": "markdown" + }, + { + "file": "comunidad/como-contribuir.md", + "text": "nuestros meetups", + "url": "../meetups/index.md", + "full_url": "http://127.0.0.1:8000/meetups/index.html", + "status": "200 OK", + "line": 70, + "link_type": "markdown" } ] -} +} \ No newline at end of file diff --git a/docs/about.md b/docs/about.md index c50b71d..507aeb0 100644 --- a/docs/about.md +++ b/docs/about.md @@ -39,11 +39,11 @@ Creemos que el verdadero crecimiento profesional viene de la colaboración y el ### Ser Ponente -Comparte tu experiencia y conocimiento con la comunidad. Consulta nuestra [guía para ponentes](comunidad/ponentes.md) para más información. +Comparte tu experiencia y conocimiento con la comunidad. Consulta nuestra [guía para ponentes](comunidad/ponentes/) para más información. ### Ser Voluntario -Ayuda a organizar eventos, gestionar redes sociales o contribuir con el desarrollo del sitio web. Conoce más en [nuestra página de voluntarios](comunidad/voluntarios.md). +Ayuda a organizar eventos, gestionar redes sociales o contribuir con el desarrollo del sitio web. Conoce más en [nuestra página de voluntarios](comunidad/voluntarios/). ### Asistir a Meetups diff --git a/docs/comunidad/como-contribuir.md b/docs/comunidad/como-contribuir.md index dd4ebc5..12d897b 100644 --- a/docs/comunidad/como-contribuir.md +++ b/docs/comunidad/como-contribuir.md @@ -28,13 +28,13 @@ Contribuir a Python CDMX te permite: Comparte tu conocimiento con la comunidad a través de charlas técnicas, casos de uso o introducciones a nuevas tecnologías. -**Más información:** [Ser Ponente](ponentes.md) +**Más información:** [Ser Ponente](ponentes/) ### Ser Voluntario Ayuda a organizar eventos, gestionar redes sociales, o contribuir con el desarrollo del sitio web. -**Más información:** [Ser Voluntario](voluntarios.md) +**Más información:** [Ser Voluntario](voluntarios/) ### Desarrollo Web @@ -52,7 +52,7 @@ Escribe artículos, documentación, o crea contenido para nuestras redes sociale Ayuda a establecer y mantener relaciones con otras comunidades y empresas. -**Más información:** [Alianzas](alianzas.md) +**Más información:** [Alianzas](alianzas/) --- diff --git a/docs/comunidad/ponentes.md b/docs/comunidad/ponentes.md index c7a050f..40031cc 100644 --- a/docs/comunidad/ponentes.md +++ b/docs/comunidad/ponentes.md @@ -122,8 +122,8 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra Desarrollo de APIs, Frameworks Web, Buenas Prácticas Charlas recientes: @@ -155,7 +155,7 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra OpenSource, Automatización, Hardening de Infraestructura Charlas recientes: @@ -187,7 +187,7 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra Inteligencia Artificial, Tecnologías de Código Abierto Charlas recientes: @@ -219,8 +219,8 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra Kubernetes, MLOps, GitOps, Cloud Native Charlas recientes: @@ -252,8 +252,8 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra Validación de Datos, Mejores Prácticas, Nuevas Tecnologías Charlas recientes: @@ -285,7 +285,7 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra Desarrollo Web, Portafolios, Python Charlas recientes: @@ -317,10 +317,10 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra DevOps, Docker, Kubernetes, CI/CD, AWS Charlas recientes: @@ -352,7 +352,7 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra NLP, Lenguas Indígenas, MLOps, Cultura Libre Charlas recientes: @@ -384,7 +384,7 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra Agentes de IA, Machine Learning, LangGraph, LLMs Charlas recientes: @@ -416,7 +416,7 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra Open Source, Ruby, Contribución Comunitaria Charlas recientes: @@ -448,7 +448,7 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra Machine Learning, Análisis de Datos, Algoritmos Estadísticos Charlas recientes: @@ -480,10 +480,10 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra Desarrollo Python, Análisis de Datos, Metaprogramación Charlas recientes: @@ -515,8 +515,8 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra Ingeniería de Datos, Procesamiento ETL, Big Data Charlas recientes: @@ -548,7 +548,7 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra FinTech, Performance, DevOps, Software Libre Charlas recientes: @@ -580,7 +580,7 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra Inteligencia Artificial, Machine Learning, Embeddings, Google Developer Expert Charlas recientes: @@ -612,7 +612,7 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra Core Python, Optimización, Concurrencia, GIL Charlas recientes: @@ -644,7 +644,7 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra Ingeniería de Software, Algoritmos, Resolución de Problemas Charlas recientes: @@ -676,7 +676,7 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra Extensiones LibreOffice, Automatización de Oficina Charlas recientes: @@ -708,7 +708,7 @@ Conoce a algunos de los ponentes que han compartido su conocimiento con nuestra Seguridad Informática, PCI DSS, Cumplimiento Normativo Charlas recientes: diff --git a/docs/comunidad/sedes_faq.md b/docs/comunidad/sedes_faq.md index 2fc5796..40a17c8 100644 --- a/docs/comunidad/sedes_faq.md +++ b/docs/comunidad/sedes_faq.md @@ -106,7 +106,7 @@ Lo coordinamos con gusto durante la planeación. **¡Sí!** Solo pedimos que sigan el mismo proceso que cualquier miembro de la comunidad: -[Ser Ponente](ponentes.md#por-que-ser-ponente) +[Ser Ponente](ponentes/#por-que-ser-ponente) **Registrar su propuesta** en [python-cdmx-charlas/issues](https://github.com/PythonMexico/python-cdmx-charlas/issues) para mantener el espíritu abierto y curado del contenido. diff --git a/docs/comunidad/voluntarios.md b/docs/comunidad/voluntarios.md index d786d0f..096a5f0 100644 --- a/docs/comunidad/voluntarios.md +++ b/docs/comunidad/voluntarios.md @@ -84,7 +84,7 @@ Ser voluntario en Python CDMX te permite: Ver Voluntarios - Más Información + Más Información diff --git a/docs/images/favicon.ico b/docs/images/favicon.ico new file mode 100644 index 0000000..159307f Binary files /dev/null and b/docs/images/favicon.ico differ diff --git a/docs/images/logo.jpg b/docs/images/logo.jpg deleted file mode 100644 index f52d277..0000000 Binary files a/docs/images/logo.jpg and /dev/null differ diff --git a/docs/index.md b/docs/index.md index 23b5fca..9e8ef2e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,7 +6,7 @@

Explorar charlas - Participa + Participa
@@ -18,8 +18,8 @@

Conoce a quienes hacen posible
Python CDMX!

- Voluntarios - Ponentes + Voluntarios + Ponentes
@@ -36,12 +36,12 @@

Ser Ponente

Comparte tu conocimiento con la comunidad. Charlas técnicas, casos de uso, mejores prácticas y más.

- Conoce Más + Conoce Más

Ser Voluntario

Ayuda a organizar eventos, gestionar redes sociales, o contribuir con el desarrollo del sitio web.

- Únete + Únete

Asistir

diff --git a/docs/js/custom.js b/docs/js/custom.js index 43c71e6..fec0ecd 100644 --- a/docs/js/custom.js +++ b/docs/js/custom.js @@ -18,75 +18,7 @@ document.addEventListener('DOMContentLoaded', function() { imageObserver.observe(img); }); - // Búsqueda avanzada - const searchInput = document.querySelector('.search-input'); - const searchFilters = document.querySelectorAll('.filter-chip'); - const searchableCards = document.querySelectorAll('.speaker-card, .volunteer-card, .meetup-card'); - - if (searchInput) { - searchInput.addEventListener('input', function() { - const searchTerm = this.value.toLowerCase(); - filterCards(searchTerm, getActiveFilters()); - }); - } - - searchFilters.forEach(filter => { - filter.addEventListener('click', function() { - this.classList.toggle('active'); - const searchTerm = searchInput ? searchInput.value.toLowerCase() : ''; - filterCards(searchTerm, getActiveFilters()); - }); - }); - - function getActiveFilters() { - return Array.from(document.querySelectorAll('.filter-chip.active')) - .map(filter => filter.textContent.toLowerCase()); - } - - function filterCards(searchTerm, activeFilters) { - searchableCards.forEach(card => { - const cardText = card.textContent.toLowerCase(); - const cardTags = Array.from(card.querySelectorAll('.badge')) - .map(badge => badge.textContent.toLowerCase()); - - const matchesSearch = searchTerm === '' || cardText.includes(searchTerm); - const matchesFilters = activeFilters.length === 0 || - activeFilters.some(filter => cardTags.includes(filter)); - - if (matchesSearch && matchesFilters) { - card.style.display = 'block'; - card.classList.remove('hidden'); - } else { - card.style.display = 'none'; - card.classList.add('hidden'); - } - }); - - updateSearchResults(); - } - - function updateSearchResults() { - const visibleCards = document.querySelectorAll('.speaker-card:not(.hidden), .volunteer-card:not(.hidden), .meetup-card:not(.hidden)'); - const resultsCount = document.querySelector('.search-results-count'); - - if (resultsCount) { - resultsCount.textContent = `${visibleCards.length} resultados encontrados`; - } - } - - // Botón de limpiar filtros - const clearFiltersBtn = document.querySelector('.clear-filters'); - if (clearFiltersBtn) { - clearFiltersBtn.addEventListener('click', function() { - searchFilters.forEach(filter => filter.classList.remove('active')); - if (searchInput) { - searchInput.value = ''; - } - filterCards('', []); - }); - } - - // Animaciones suaves para las tarjetas + // Animaciones suaves para las tarjetas de voluntarios const observerOptions = { threshold: 0.1, rootMargin: '0px 0px -50px 0px' @@ -101,7 +33,8 @@ document.addEventListener('DOMContentLoaded', function() { }); }, observerOptions); - searchableCards.forEach(card => { + const volunteerCards = document.querySelectorAll('.volunteer-card'); + volunteerCards.forEach(card => { card.style.opacity = '0'; card.style.transform = 'translateY(20px)'; card.style.transition = 'opacity 0.6s ease, transform 0.6s ease'; @@ -165,20 +98,6 @@ document.addEventListener('DOMContentLoaded', function() { wrapper.appendChild(table); }); - // Mejoras de formularios - const formInputs = document.querySelectorAll('input, textarea, select'); - formInputs.forEach(input => { - input.addEventListener('focus', function() { - this.parentElement.classList.add('focused'); - }); - - input.addEventListener('blur', function() { - if (!this.value) { - this.parentElement.classList.remove('focused'); - } - }); - }); - // Botones de acción mejorados const actionButtons = document.querySelectorAll('.btn-action'); actionButtons.forEach(button => { @@ -203,36 +122,14 @@ document.addEventListener('DOMContentLoaded', function() { }); }); - // Mejoras de breadcrumbs - const breadcrumbLinks = document.querySelectorAll('.breadcrumb-item a'); - breadcrumbLinks.forEach(link => { - link.addEventListener('click', function(e) { - // Añadir indicador de navegación - this.style.position = 'relative'; - const indicator = document.createElement('span'); - indicator.style.position = 'absolute'; - indicator.style.bottom = '-2px'; - indicator.style.left = '0'; - indicator.style.width = '0'; - indicator.style.height = '2px'; - indicator.style.backgroundColor = 'var(--python-green)'; - indicator.style.transition = 'width 0.3s ease'; - this.appendChild(indicator); - - setTimeout(() => { - indicator.style.width = '100%'; - }, 100); - }); - }); - - // Optimización de rendimiento + // Optimización de rendimiento en resize let resizeTimer; window.addEventListener('resize', function() { clearTimeout(resizeTimer); resizeTimer = setTimeout(function() { // Recalcular posiciones después del resize cardObserver.disconnect(); - searchableCards.forEach(card => { + volunteerCards.forEach(card => { cardObserver.observe(card); }); }, 250); @@ -269,18 +166,8 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // Mejoras de carga de página + // Animar elementos de la página al cargar window.addEventListener('load', function() { - // Ocultar loader si existe - const loader = document.querySelector('.page-loader'); - if (loader) { - loader.style.opacity = '0'; - setTimeout(() => { - loader.style.display = 'none'; - }, 300); - } - - // Animar elementos de la página const animatedElements = document.querySelectorAll('.hero-section, .stats-grid, .features-grid'); animatedElements.forEach((element, index) => { setTimeout(() => { @@ -290,37 +177,6 @@ document.addEventListener('DOMContentLoaded', function() { }); }); - // Mejoras de SEO y analytics - const trackEvent = function(category, action, label) { - if (typeof gtag !== 'undefined') { - gtag('event', action, { - 'event_category': category, - 'event_label': label - }); - } - }; - - // Track clicks en botones de acción - actionButtons.forEach(button => { - button.addEventListener('click', function() { - const action = this.textContent.trim(); - trackEvent('engagement', 'button_click', action); - }); - }); - - // Track búsquedas - if (searchInput) { - let searchTimeout; - searchInput.addEventListener('input', function() { - clearTimeout(searchTimeout); - searchTimeout = setTimeout(() => { - if (this.value.length > 2) { - trackEvent('search', 'search_performed', this.value); - } - }, 1000); - }); - } - // Mejoras de UX para móviles if (window.innerWidth <= 768) { // Optimizar navegación móvil @@ -352,7 +208,7 @@ document.addEventListener('DOMContentLoaded', function() { console.log('Python CDMX Custom JavaScript loaded successfully!'); }); -// Estilos CSS adicionales para las mejoras de JavaScript +// Estilos CSS para los efectos de JavaScript const additionalStyles = ` .button-ripple { position: absolute; @@ -379,35 +235,10 @@ const additionalStyles = ` pointer-events: none; } - .search-results-count { - text-align: center; - color: var(--python-gray); - font-size: 0.9rem; - margin: 1rem 0; - } - - .page-loader { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: white; - display: flex; - align-items: center; - justify-content: center; - z-index: 9999; - transition: opacity 0.3s ease; - } - .scrolling * { animation-play-state: paused !important; } - .focused { - transform: translateY(-2px); - } - @media (max-width: 768px) { .button-ripple { display: none; diff --git a/docs/meetups/2023/index.md b/docs/meetups/2023/index.md index 1aa5b85..8a3c9de 100644 --- a/docs/meetups/2023/index.md +++ b/docs/meetups/2023/index.md @@ -12,11 +12,11 @@ | **Fecha** | **Charla** | **Ponente** | **Lugar** | **Detalles** | |:---|:---|:---|:---|:---| -| **14 Noviembre 2023** | GitOps: Automatizando el despliegue de aplicaciones | Carlos Reyes | Wizeline México | [Ver detalles](202311-noviembre.md) | -| **14 Noviembre 2023** | Historia de Python: De Guido van Rossum a la actualidad | Gustavo Vera | Wizeline México | [Ver detalles](202311-noviembre.md) | -| **10 Octubre 2023** | Jupyter a Web: De notebooks a aplicaciones web | Gustavo Vera | Wizeline México | [Ver detalles](202310-octubre.md) | -| **12 Septiembre 2023** | Metaprogramación en Python | David Sol | Wizeline México | [Ver detalles](202309-septiembre.md) | -| **12 Septiembre 2023** | AWS AI: Servicios de IA en la nube | Gustavo Vera | Wizeline México | [Ver detalles](202309-septiembre.md) | +| **14 Noviembre 2023** | GitOps: Automatizando el despliegue de aplicaciones | Carlos Reyes | Wizeline México | [Ver detalles](202311-noviembre/) | +| **14 Noviembre 2023** | Historia de Python: De Guido van Rossum a la actualidad | Gustavo Vera | Wizeline México | [Ver detalles](202311-noviembre/) | +| **10 Octubre 2023** | Jupyter a Web: De notebooks a aplicaciones web | Gustavo Vera | Wizeline México | [Ver detalles](202310-octubre/) | +| **12 Septiembre 2023** | Metaprogramación en Python | David Sol | Wizeline México | [Ver detalles](202309-septiembre/) | +| **12 Septiembre 2023** | AWS AI: Servicios de IA en la nube | Gustavo Vera | Wizeline México | [Ver detalles](202309-septiembre/) | --- @@ -56,4 +56,4 @@ --- -¿Te gustaría aparecer aquí? Conoce a nuestros [ponentes y voluntarios reconocidos](../../comunidad/como-contribuir.md). +¿Te gustaría aparecer aquí? Conoce a nuestros [ponentes y voluntarios reconocidos](../../comunidad/como-contribuir/). diff --git a/docs/meetups/2024/index.md b/docs/meetups/2024/index.md index 666e809..39b65bf 100644 --- a/docs/meetups/2024/index.md +++ b/docs/meetups/2024/index.md @@ -12,21 +12,21 @@ | **Fecha** | **Charla** | **Ponente** | **Lugar** | **Detalles** | |:---|:---|:---|:---|:---| -| **12 Noviembre 2024** | Mejora tu código: Evita anti-patrones en Python | Alejandro Lopez | Wizeline México | [Ver detalles](202411-noviembre.md) | -| **12 Noviembre 2024** | Exactamente qué y (sobre todo) por qué ChatGPT | Hugo Ramírez | Wizeline México | [Ver detalles](202411-noviembre.md) | -| **08 Octubre 2024** | Contribuir a Open Source | Alex Callejas | Wizeline México | [Ver detalles](202410-octubre.md) | -| **10 Septiembre 2024** | Protocolos en Python | Diego Barriga | Wizeline México | [Ver detalles](202409-septiembre.md) | -| **10 Septiembre 2024** | Guía Open Source | David Sol | Wizeline México | [Ver detalles](202409-septiembre.md) | -| **13 Agosto 2024** | ETLs con Python | Gustavo Vera | Wizeline México | [Ver detalles](202408-agosto.md) | -| **13 Agosto 2024** | GIL: Global Interpreter Lock | Fer Perales | Wizeline México | [Ver detalles](202408-agosto.md) | -| **09 Julio 2024** | Regresión Lineal | Konstantin Spirin | Wizeline México | [Ver detalles](202407-julio.md) | -| **09 Julio 2024** | Fonética con Python | Hugo Ramirez | Wizeline México | [Ver detalles](202407-julio.md) | -| **11 Junio 2024** | Regresión Lineal | Geovanni Zepeda Martínez | Wizeline México | [Ver detalles](202406-junio.md) | -| **14 Mayo 2024** | Pydantic | Charly Román | Wizeline México | [Ver detalles](202405-mayo.md) | -| **09 Abril 2024** | Contenedores | David Sol | Wizeline México | [Ver detalles](202404-abril.md) | -| **12 Marzo 2024** | Flask APIs | Alejandro López | Wizeline México | [Ver detalles](202403-marzo.md) | -| **13 Febrero 2024** | Entornos Virtuales | Gustavo Vera | Wizeline México | [Ver detalles](202402-febrero.md) | -| **09 Enero 2024** | PyPI | David Sol | Wizeline México | [Ver detalles](202401-enero.md) | +| **12 Noviembre 2024** | Mejora tu código: Evita anti-patrones en Python | Alejandro Lopez | Wizeline México | [Ver detalles](202411-noviembre/) | +| **12 Noviembre 2024** | Exactamente qué y (sobre todo) por qué ChatGPT | Hugo Ramírez | Wizeline México | [Ver detalles](202411-noviembre/) | +| **08 Octubre 2024** | Contribuir a Open Source | Alex Callejas | Wizeline México | [Ver detalles](202410-octubre/) | +| **10 Septiembre 2024** | Protocolos en Python | Diego Barriga | Wizeline México | [Ver detalles](202409-septiembre/) | +| **10 Septiembre 2024** | Guía Open Source | David Sol | Wizeline México | [Ver detalles](202409-septiembre/) | +| **13 Agosto 2024** | ETLs con Python | Gustavo Vera | Wizeline México | [Ver detalles](202408-agosto/) | +| **13 Agosto 2024** | GIL: Global Interpreter Lock | Fer Perales | Wizeline México | [Ver detalles](202408-agosto/) | +| **09 Julio 2024** | Regresión Lineal | Konstantin Spirin | Wizeline México | [Ver detalles](202407-julio/) | +| **09 Julio 2024** | Fonética con Python | Hugo Ramirez | Wizeline México | [Ver detalles](202407-julio/) | +| **11 Junio 2024** | Regresión Lineal | Geovanni Zepeda Martínez | Wizeline México | [Ver detalles](202406-junio/) | +| **14 Mayo 2024** | Pydantic | Charly Román | Wizeline México | [Ver detalles](202405-mayo/) | +| **09 Abril 2024** | Contenedores | David Sol | Wizeline México | [Ver detalles](202404-abril/) | +| **12 Marzo 2024** | Flask APIs | Alejandro López | Wizeline México | [Ver detalles](202403-marzo/) | +| **13 Febrero 2024** | Entornos Virtuales | Gustavo Vera | Wizeline México | [Ver detalles](202402-febrero/) | +| **09 Enero 2024** | PyPI | David Sol | Wizeline México | [Ver detalles](202401-enero/) | --- @@ -66,4 +66,4 @@ --- -¿Te gustaría aparecer aquí? Conoce a nuestros [ponentes y voluntarios reconocidos](../../comunidad/como-contribuir.md). +¿Te gustaría aparecer aquí? Conoce a nuestros [ponentes y voluntarios reconocidos](../../comunidad/como-contribuir/). diff --git a/docs/meetups/2025/index.md b/docs/meetups/2025/index.md index b923d64..f3b5ee1 100644 --- a/docs/meetups/2025/index.md +++ b/docs/meetups/2025/index.md @@ -12,19 +12,19 @@ | **Fecha** | **Charla** | **Ponente** | **Lugar** | **Detalles** | |:---|:---|:---|:---|:---| -| **12 Agosto 2025** | Cómo preparar una ambiente de desarrollo con Python desde zero | Juan Guillermo Gómez | Jardin Chapultepec | [Ver detalles](202508-agosto.md) | -| **08 Julio 2025** | Cómo preparar una ambiente de desarrollo con Python desde zero | David Sol | Clara | [Ver detalles](202507-julio.md) | -| **10 Junio 2025** | Usando Python y software libre para crear nuevas herramientas: Traductor de voz español-inglés | Carlos Cesar Caballero | Wizeline México | [Ver detalles](202506-junio.md) | -| **13 Mayo 2025** | Construyendo un paquete en Python y publicándolo en PyPI | Javier Novoa | Wizeline México | [Ver detalles](202505-mayo.md) | -| **25 Abril 2025** | portafolio.py: Como hacer un portafolio web sin saber diseño web | Daniel Paredes | UNAM Facultad de Ciencias | [Ver detalles](202504-unam.md) | -| **25 Abril 2025** | Programar en tiempos del Vibe-Coding | Charly Roman | UNAM Facultad de Ciencias | [Ver detalles](202504-unam.md) | -| **08 Abril 2025** | El para que cosa de Quien. Kubernetes y AI | Carlos Reyes | Wizeline México | [Ver detalles](202504-abril.md) | -| **11 Marzo 2025** | Mi Primer Agente de Inteligencia Artificial con Python | Erik Rivera | Wizeline México | [Ver detalles](202503-marzo.md) | -| **11 Maro 2025** | Interfases gráficas con Pyside6 | David Sol | Wizeline México | [Ver detalles](202503-marzo.md) | -| **11 Febrero 2025** | Lecciones del Advent of Code 2024 | Manuel Rábade | Wizeline México | [Ver detalles](202502-febrero.md) | -| **11 Febrero 2025** | Embeddings: El lenguaje como las máquinas entienden el lenguaje humano | Juan Guillermo Gómez | Wizeline México | [Ver detalles](202502-febrero.md) | -| **14 Enero 2025** | Crea extensiones para LibreOffice con Python | elMau (Mauricio B.) | Wizeline México | [Ver detalles](202501-enero.md) | -| **14 Enero 2025** | Seguridad y cumplimiento de Python: Garantizar el cumplimiento de PCI DSS | Mauro Parra | Wizeline México | [Ver detalles](202501-enero.md) | +| **12 Agosto 2025** | Cómo preparar una ambiente de desarrollo con Python desde zero | Juan Guillermo Gómez | Jardin Chapultepec | [Ver detalles](202508-agosto/) | +| **08 Julio 2025** | Cómo preparar una ambiente de desarrollo con Python desde zero | David Sol | Clara | [Ver detalles](202507-julio/) | +| **10 Junio 2025** | Usando Python y software libre para crear nuevas herramientas: Traductor de voz español-inglés | Carlos Cesar Caballero | Wizeline México | [Ver detalles](202506-junio/) | +| **13 Mayo 2025** | Construyendo un paquete en Python y publicándolo en PyPI | Javier Novoa | Wizeline México | [Ver detalles](202505-mayo/) | +| **25 Abril 2025** | portafolio.py: Como hacer un portafolio web sin saber diseño web | Daniel Paredes | UNAM Facultad de Ciencias | [Ver detalles](202504-unam/) | +| **25 Abril 2025** | Programar en tiempos del Vibe-Coding | Charly Roman | UNAM Facultad de Ciencias | [Ver detalles](202504-unam/) | +| **08 Abril 2025** | El para que cosa de Quien. Kubernetes y AI | Carlos Reyes | Wizeline México | [Ver detalles](202504-abril/) | +| **11 Marzo 2025** | Mi Primer Agente de Inteligencia Artificial con Python | Erik Rivera | Wizeline México | [Ver detalles](202503-marzo/) | +| **11 Maro 2025** | Interfases gráficas con Pyside6 | David Sol | Wizeline México | [Ver detalles](202503-marzo/) | +| **11 Febrero 2025** | Lecciones del Advent of Code 2024 | Manuel Rábade | Wizeline México | [Ver detalles](202502-febrero/) | +| **11 Febrero 2025** | Embeddings: El lenguaje como las máquinas entienden el lenguaje humano | Juan Guillermo Gómez | Wizeline México | [Ver detalles](202502-febrero/) | +| **14 Enero 2025** | Crea extensiones para LibreOffice con Python | elMau (Mauricio B.) | Wizeline México | [Ver detalles](202501-enero/) | +| **14 Enero 2025** | Seguridad y cumplimiento de Python: Garantizar el cumplimiento de PCI DSS | Mauro Parra | Wizeline México | [Ver detalles](202501-enero/) | --- @@ -65,4 +65,4 @@ --- -¿Te gustaría aparecer aquí? Conoce a nuestros [ponentes y voluntarios reconocidos](../../comunidad/como-contribuir.md). +¿Te gustaría aparecer aquí? Conoce a nuestros [ponentes y voluntarios reconocidos](../../comunidad/como-contribuir/). diff --git a/presnetacion.md b/docs/templates/presentaciones/2025-06-junio.md similarity index 100% rename from presnetacion.md rename to docs/templates/presentaciones/2025-06-junio.md diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 0000000..15b2fcb --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,33 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negation pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +*tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +# Lock files (commit these to ensure consistent provider versions) +# .terraform.lock.hcl diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl new file mode 100644 index 0000000..cdc1668 --- /dev/null +++ b/terraform/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 0000000..8b99777 --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,474 @@ +# Python CDMX - Terraform Infrastructure + +Este directorio contiene la infraestructura como código (IaC) para el sitio web de Python CDMX usando AWS. + +## 📋 Arquitectura + +``` + ┌──────────────────────┐ + │ Route53 Hosted │ + │ pythoncdmx.org │ + └──────────┬───────────┘ + │ + ┌──────────────┴──────────────┐ + │ │ + ┌───────────▼──────────┐ ┌───────────▼──────────┐ + │ pythoncdmx.org │ │ staging.pythoncdmx.org│ + │ www.pythoncdmx.org │ │ (Testing) │ + └───────────┬──────────┘ └───────────┬───────────┘ + │ │ + │ HTTPS (TLS 1.2+) │ HTTPS (TLS 1.2+) + │ │ + ▼ ▼ + ┌─────────────────────┐ ┌─────────────────────┐ + │ CloudFront PROD │ │ CloudFront STAGING │ + │ - ACM Certificate │ │ - ACM Certificate │ + │ - Cache: 1h-24h │ │ - Cache: 5min-2h │ + │ - Gzip/Brotli │ │ - Shorter TTL │ + └──────────┬──────────┘ └──────────┬──────────┘ + │ │ + │ OAC (SigV4) │ OAC (SigV4) + │ │ + ▼ ▼ + ┌──────────────────────┐ ┌──────────────────────┐ + │ S3: pythoncdmx- │ │ S3: pythoncdmx- │ + │ website (PROD) │ │ website-staging │ + │ - Private │ │ - Private │ + │ - Versioning │ │ - Versioning │ + │ - AES256 │ │ - AES256 │ + └──────────────────────┘ └──────────────────────┘ +``` + +## 🚀 Recursos Creados + +### 🌐 Route53 (DNS) +- **Hosted Zone**: pythoncdmx.org (existente, solo se agregan registros) +- **Registros Production**: + - A/AAAA: `pythoncdmx.org` → CloudFront PROD + - A/AAAA: `www.pythoncdmx.org` → CloudFront PROD +- **Registros Staging**: + - A/AAAA: `staging.pythoncdmx.org` → CloudFront STAGING +- **Validación ACM**: Registros CNAME automáticos para certificados + +### 📦 Production Environment + +#### 1. **S3 Bucket (Production)** +- **Nombre**: `pythoncdmx-website` +- **Acceso**: Privado (solo CloudFront puede acceder) +- **Características**: + - ✅ Versioning habilitado + - ✅ Encriptación AES256 + - ✅ Lifecycle: 90 días versiones antiguas + - ✅ CORS configurado + +#### 2. **CloudFront Distribution (Production)** +- **Dominios**: pythoncdmx.org, www.pythoncdmx.org +- **Características**: + - ✅ Certificado SSL/TLS (ACM) + - ✅ HTTP/2 y HTTP/3 habilitado + - ✅ Compresión Gzip/Brotli + - ✅ Cache optimizado (HTML: 10min, Assets: 24h, Images: 7d) + - ✅ IPv6 habilitado + - ✅ Origin Access Control (OAC) + +#### 3. **ACM Certificate (Production)** +- **Región**: us-east-1 (requerido para CloudFront) +- **Validación**: DNS automática vía Route53 +- **Dominios cubiertos**: + - pythoncdmx.org + - www.pythoncdmx.org + +### 🧪 Staging Environment + +#### 4. **S3 Bucket (Staging)** +- **Nombre**: `pythoncdmx-website-staging` +- **Acceso**: Privado (solo CloudFront puede acceder) +- **Características**: + - ✅ Versioning habilitado + - ✅ Encriptación AES256 + - ✅ Lifecycle: 30 días versiones antiguas (más agresivo) + - ✅ CORS configurado + +#### 5. **CloudFront Distribution (Staging)** +- **Dominio**: staging.pythoncdmx.org +- **Características**: + - ✅ Certificado SSL/TLS (ACM) + - ✅ Cache más corto (HTML: 1min, Assets: 30min, Images: 1h) + - ✅ IPv6 habilitado + - ✅ Banner "STAGING" en todas las páginas + +#### 6. **ACM Certificate (Staging)** +- **Región**: us-east-1 +- **Validación**: DNS automática vía Route53 +- **Dominio**: staging.pythoncdmx.org + +### 🔒 Security & State + +#### 7. **IAM Role (GitHub Actions)** +- **OIDC Provider**: GitHub Actions sin credenciales long-lived +- **Permisos**: + - S3: Read/Write en ambos buckets (prod y staging) + - CloudFront: Invalidación de cache en ambas distribuciones + - Scope: Repositorio PythonMexico/pythonCDMX + +#### 8. **Backend State** +- **S3 Bucket**: `pythoncdmx-terraform-state` +- **DynamoDB**: `pythoncdmx-terraform-locks` +- **Encriptación**: Habilitada + +## 📦 Prerequisitos + +1. **Terraform** >= 1.0 + ```bash + brew install terraform # macOS + ``` + +2. **AWS CLI** configurado + ```bash + aws configure + ``` + +3. **Route53 Hosted Zone** ya creada para `pythoncdmx.org` + ```bash + # Verificar hosted zone existente + aws route53 list-hosted-zones + ``` + +4. **Permisos AWS requeridos**: + - S3: Crear/modificar buckets + - CloudFront: Crear/modificar distribuciones + - ACM: Solicitar/validar certificados + - Route53: Crear/modificar registros DNS + - IAM: Crear roles y políticas + +## 🔧 Configuración Inicial + +### 1. Crear Backend de Terraform (Una sola vez) + +```bash +# Crear bucket para Terraform state +aws s3 mb s3://pythoncdmx-terraform-state --region us-east-1 + +# Habilitar versioning +aws s3api put-bucket-versioning \ + --bucket pythoncdmx-terraform-state \ + --versioning-configuration Status=Enabled + +# Habilitar encriptación +aws s3api put-bucket-encryption \ + --bucket pythoncdmx-terraform-state \ + --server-side-encryption-configuration '{ + "Rules": [{ + "ApplyServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + }] + }' + +# Crear tabla DynamoDB para locks +aws dynamodb create-table \ + --table-name pythoncdmx-terraform-locks \ + --attribute-definitions AttributeName=LockID,AttributeType=S \ + --key-schema AttributeName=LockID,KeyType=HASH \ + --billing-mode PAY_PER_REQUEST \ + --region us-east-1 +``` + +### 2. Validar Dominio en ACM + +**✅ AUTOMÁTICO**: La validación DNS se hace automáticamente vía Route53. Terraform crea los registros CNAME necesarios y espera la validación. + +Si necesitas verificar el estado: + +```bash +# Ver certificados y su estado +aws acm list-certificates --region us-east-1 + +# Ver detalles de validación +terraform output certificate_validation_records +``` + +**Tiempo de validación**: 5-30 minutos (automático) + +## 🏗️ Despliegue + +### Inicializar Terraform + +```bash +cd terraform +terraform init +``` + +### Plan de Cambios + +```bash +terraform plan +``` + +### Aplicar Infraestructura + +```bash +terraform apply +``` + +Terraform creará: +- ✅ 2 S3 buckets privados (production + staging) +- ✅ 2 CloudFront distributions +- ✅ 2 Certificados ACM (validación DNS automática) +- ✅ Registros DNS en Route53 +- ✅ IAM Role con OIDC para GitHub Actions +- ✅ Políticas de acceso +- ✅ Cache behaviors optimizados + +**⏱️ Tiempo estimado**: 15-30 minutos (mayoría es validación de certificados) + +### Obtener Outputs + +```bash +terraform output +``` + +Outputs importantes: +- **Production**: + - `website_url`: https://pythoncdmx.org + - `cloudfront_distribution_id`: ID para invalidación de cache + - `website_bucket_name`: pythoncdmx-website +- **Staging**: + - `staging_website_url`: https://staging.pythoncdmx.org + - `staging_cloudfront_distribution_id`: ID para invalidación de cache staging + - `staging_bucket_name`: pythoncdmx-website-staging +- **Route53**: + - `hosted_zone_id`: ID de la hosted zone + - `hosted_zone_name_servers`: Name servers (para verificación) + +## 🔐 Configuración de GitHub Actions + +El deploy automático requiere configurar secretos en GitHub: + +### 1. Crear IAM Role para GitHub OIDC + +```bash +# Ver terraform/iam-github.tf (crear este archivo) +terraform apply +``` + +### 2. Configurar Secrets en GitHub + +En el repositorio, ir a **Settings > Secrets and variables > Actions**: + +```bash +# Obtener valores de Terraform +terraform output + +# Configurar estos secrets: +AWS_ROLE_ARN: arn:aws:iam::123456789012:role/GitHubActionsDeployRole +CLOUDFRONT_DISTRIBUTION_ID: E1234ABCDEF567 (production) +CLOUDFRONT_DISTRIBUTION_ID_STAGING: E7890GHIJKL123 (staging) +``` + +### 3. Workflows Configurados + +**Production** (`.github/workflows/deploy-aws.yml`): +- **Trigger**: Push a `main` +- **Destino**: pythoncdmx.org +- **S3 Bucket**: pythoncdmx-website +- **Cache**: Agresivo (1h-24h) + +**Staging** (`.github/workflows/deploy-staging.yml`): +- **Trigger**: Push a `develop`/`staging` o PR a `main` +- **Destino**: staging.pythoncdmx.org +- **S3 Bucket**: pythoncdmx-website-staging +- **Cache**: Corto (1min-2h) +- **Banner**: "🚧 STAGING ENVIRONMENT" en todas las páginas + +## 📊 Gestión de Cache + +### Invalidar Cache Completo + +```bash +aws cloudfront create-invalidation \ + --distribution-id E1234ABCDEF567 \ + --paths "/*" +``` + +### Invalidar Paths Específicos + +```bash +aws cloudfront create-invalidation \ + --distribution-id E1234ABCDEF567 \ + --paths "/index.html" "/css/*" +``` + +### Verificar Estado de Invalidación + +```bash +aws cloudfront get-invalidation \ + --distribution-id E1234ABCDEF567 \ + --id I1234ABCDEF567 +``` + +## 🔄 Estrategia de Cache + +### HTML Files +- Cache: 10 minutos +- Header: `Cache-Control: public, max-age=600, must-revalidate` + +### Assets Estáticos (CSS/JS) +- Cache: 24 horas +- Header: `Cache-Control: public, max-age=86400` + +### Imágenes +- Cache: 7 días +- Header: `Cache-Control: public, max-age=604800` + +### Sitemap +- Cache: Sin cache +- Header: `Cache-Control: public, max-age=0, must-revalidate` + +## 💰 Costos Estimados + +### Free Tier (Primer Año) +- S3: 5GB de almacenamiento +- CloudFront: 50GB de transferencia +- ACM: Certificados gratuitos + +### Después de Free Tier (Estimado Mensual) +- S3: ~$0.50 (20GB) +- CloudFront: ~$2-5 (dependiendo del tráfico) +- **Total**: ~$3-6/mes + +## 🔍 Troubleshooting + +### Error: Certificate validation timeout + +**Problema**: El certificado ACM no se valida automáticamente. + +**Solución**: +1. Verifica que Route53 tenga los registros de validación: + ```bash + aws route53 list-resource-record-sets --hosted-zone-id ZXXXXX + ``` +2. Los registros deben ser tipo CNAME con nombres `_abc123.pythoncdmx.org` +3. Terraform crea estos automáticamente, pero puede tardar 5-30 minutos +4. Si persiste el error después de 45 minutos, revisar permisos de Route53 + +### Error: S3 bucket already exists + +**Problema**: El nombre del bucket ya está en uso. + +**Solución**: +```bash +# Cambiar nombre en variables.tf +bucket_name = "pythoncdmx-website-prod" +``` + +### Error: Access Denied al subir a S3 + +**Problema**: Permisos insuficientes del IAM role. + +**Solución**: +1. Verificar políticas del role: `terraform/iam-github.tf` +2. Confirmar trust relationship con GitHub OIDC +3. Revisar logs de CloudWatch + +### CloudFront muestra contenido antiguo + +**Problema**: Cache no invalidado. + +**Solución**: +```bash +aws cloudfront create-invalidation \ + --distribution-id $DISTRIBUTION_ID \ + --paths "/*" +``` + +## 🔒 Seguridad + +### Buenas Prácticas Implementadas + +✅ **S3 Bucket privado**: No acceso público directo +✅ **Origin Access Control**: CloudFront usa firma SigV4 +✅ **Encriptación en reposo**: AES256 en S3 +✅ **TLS 1.2+**: Protocolo mínimo seguro +✅ **Versioning**: Protección contra eliminación accidental +✅ **IAM Roles**: Sin credenciales hardcoded +✅ **OIDC GitHub**: Autenticación sin long-lived tokens + +### Checklist de Seguridad + +- [ ] Backend state encriptado +- [ ] S3 bucket policy restrictiva +- [ ] CloudFront usa HTTPS únicamente +- [ ] Certificado SSL válido +- [ ] IAM roles con least privilege +- [ ] Logs de acceso habilitados (opcional) +- [ ] AWS WAF configurado (opcional para producción) + +## 🛠️ Mantenimiento + +### Actualizar Infraestructura + +```bash +cd terraform +terraform plan +terraform apply +``` + +### Backup de State + +```bash +# Descargar state actual +aws s3 cp s3://pythoncdmx-terraform-state/website/terraform.tfstate ./backup-$(date +%Y%m%d).tfstate +``` + +### Destruir Infraestructura (PELIGRO) + +```bash +# ⚠️ Esto eliminará TODOS los recursos +terraform destroy +``` + +## 🌍 Gestión de Entornos + +### Diferencias Production vs Staging + +| Aspecto | Production | Staging | +|---------|-----------|---------| +| **Dominio** | pythoncdmx.org | staging.pythoncdmx.org | +| **S3 Bucket** | pythoncdmx-website | pythoncdmx-website-staging | +| **Cache HTML** | 10 minutos | 1 minuto | +| **Cache Assets** | 24 horas | 30 minutos | +| **Cache Images** | 7 días | 1 hora | +| **Lifecycle S3** | 90 días | 30 días | +| **Deploy Trigger** | Push a `main` | Push a `develop`/staging | +| **Banner** | No | Sí ("STAGING ENV") | + +### Flujo de Trabajo Recomendado + +1. **Desarrollo**: Crear branch de feature +2. **Testing**: Merge a `develop` → Deploy a staging +3. **QA**: Probar en https://staging.pythoncdmx.org +4. **Producción**: PR a `main` → Review → Merge → Deploy automático + +## 📚 Referencias + +- [Terraform AWS Provider](https://registry.terraform.io/providers/hashicorp/aws/latest/docs) +- [CloudFront con S3](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/GettingStarted.SimpleDistribution.html) +- [ACM Certificate Validation](https://docs.aws.amazon.com/acm/latest/userguide/dns-validation.html) +- [Route53 Records](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-values.html) +- [GitHub OIDC con AWS](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services) + +## 📞 Soporte + +Para problemas con la infraestructura: +1. Revisar esta documentación +2. Consultar logs de CloudWatch +3. Verificar estado de recursos: `terraform show` +4. Abrir issue en el repositorio +5. Contactar al equipo de infraestructura + +--- + +**Última actualización**: 2025-01-11 +**Mantenido por**: Equipo Python CDMX +**Versión**: 2.0 (Route53 + Staging Environment) diff --git a/terraform/acm-staging.tf b/terraform/acm-staging.tf new file mode 100644 index 0000000..f2bb745 --- /dev/null +++ b/terraform/acm-staging.tf @@ -0,0 +1,31 @@ +# ACM certificate for CloudFront staging (must be in us-east-1) +resource "aws_acm_certificate" "website_staging" { + provider = aws.us_east_1 + + domain_name = var.staging_subdomain + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } + + tags = merge( + var.tags, + { + Name = "PythonCDMX Website Certificate - Staging" + Environment = "staging" + } + ) +} + +# Certificate validation for staging +resource "aws_acm_certificate_validation" "website_staging" { + provider = aws.us_east_1 + + certificate_arn = aws_acm_certificate.website_staging.arn + validation_record_fqdns = [for record in aws_route53_record.certificate_validation_staging : record.fqdn] + + timeouts { + create = "45m" + } +} diff --git a/terraform/acm.tf b/terraform/acm.tf new file mode 100644 index 0000000..84ebc7d --- /dev/null +++ b/terraform/acm.tf @@ -0,0 +1,30 @@ +# ACM certificate for CloudFront (must be in us-east-1) +resource "aws_acm_certificate" "website" { + provider = aws.us_east_1 + + domain_name = var.domain_name + subject_alternative_names = var.alternative_domain_names + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } + + tags = merge( + var.tags, + { + Name = "PythonCDMX Website Certificate" + } + ) +} + +# Certificate validation +resource "aws_acm_certificate_validation" "website" { + provider = aws.us_east_1 + + certificate_arn = aws_acm_certificate.website.arn + + timeouts { + create = "45m" + } +} diff --git a/terraform/cloudfront-staging.tf b/terraform/cloudfront-staging.tf new file mode 100644 index 0000000..3504eb0 --- /dev/null +++ b/terraform/cloudfront-staging.tf @@ -0,0 +1,144 @@ +# Origin Access Control for S3 Staging +resource "aws_cloudfront_origin_access_control" "website_staging" { + name = "pythoncdmx-oac-staging" + description = "OAC for PythonCDMX staging website" + origin_access_control_origin_type = "s3" + signing_behavior = "always" + signing_protocol = "sigv4" +} + +# CloudFront distribution for staging +resource "aws_cloudfront_distribution" "website_staging" { + enabled = true + is_ipv6_enabled = true + default_root_object = "index.html" + price_class = "PriceClass_100" # Use only North America and Europe + comment = "PythonCDMX Community Website - Staging" + + aliases = [var.staging_subdomain] + + origin { + domain_name = aws_s3_bucket.website_staging.bucket_regional_domain_name + origin_id = "S3-${var.staging_bucket_name}" + origin_access_control_id = aws_cloudfront_origin_access_control.website_staging.id + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "S3-${var.staging_bucket_name}" + + forwarded_values { + query_string = false + headers = ["Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"] + + cookies { + forward = "none" + } + } + + viewer_protocol_policy = "redirect-to-https" + min_ttl = 0 + default_ttl = 300 # 5 minutes - shorter cache for staging + max_ttl = 3600 # 1 hour - shorter cache for staging + compress = true + } + + # Cache behavior for static assets (CSS) - shorter cache for staging + ordered_cache_behavior { + path_pattern = "/css/*" + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "S3-${var.staging_bucket_name}" + + forwarded_values { + query_string = false + cookies { + forward = "none" + } + } + + viewer_protocol_policy = "redirect-to-https" + min_ttl = 0 + default_ttl = 1800 # 30 minutes + max_ttl = 7200 # 2 hours + compress = true + } + + # Cache behavior for static assets (JS) - shorter cache for staging + ordered_cache_behavior { + path_pattern = "/js/*" + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "S3-${var.staging_bucket_name}" + + forwarded_values { + query_string = false + cookies { + forward = "none" + } + } + + viewer_protocol_policy = "redirect-to-https" + min_ttl = 0 + default_ttl = 1800 # 30 minutes + max_ttl = 7200 # 2 hours + compress = true + } + + # Cache behavior for images - shorter cache for staging + ordered_cache_behavior { + path_pattern = "/images/*" + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "S3-${var.staging_bucket_name}" + + forwarded_values { + query_string = false + cookies { + forward = "none" + } + } + + viewer_protocol_policy = "redirect-to-https" + min_ttl = 0 + default_ttl = 3600 # 1 hour + max_ttl = 86400 # 24 hours + compress = true + } + + # Custom error responses for SPA-like behavior + custom_error_response { + error_code = 404 + response_code = 404 + response_page_path = "/404.html" + } + + custom_error_response { + error_code = 403 + response_code = 404 + response_page_path = "/404.html" + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = aws_acm_certificate.website_staging.arn + ssl_support_method = "sni-only" + minimum_protocol_version = "TLSv1.2_2021" + } + + tags = merge( + var.tags, + { + Name = "PythonCDMX Website Distribution - Staging" + Environment = "staging" + } + ) + + depends_on = [aws_acm_certificate_validation.website_staging] +} diff --git a/terraform/cloudfront.tf b/terraform/cloudfront.tf new file mode 100644 index 0000000..8e7da3c --- /dev/null +++ b/terraform/cloudfront.tf @@ -0,0 +1,141 @@ +# Origin Access Control for S3 +resource "aws_cloudfront_origin_access_control" "website" { + name = "pythoncdmx-oac" + description = "OAC for PythonCDMX website" + origin_access_control_origin_type = "s3" + signing_behavior = "always" + signing_protocol = "sigv4" +} + +# CloudFront distribution +resource "aws_cloudfront_distribution" "website" { + enabled = true + is_ipv6_enabled = true + default_root_object = "index.html" + price_class = "PriceClass_100" # Use only North America and Europe + comment = "PythonCDMX Community Website" + + aliases = concat([var.domain_name], var.alternative_domain_names) + + origin { + domain_name = aws_s3_bucket.website.bucket_regional_domain_name + origin_id = "S3-${var.bucket_name}" + origin_access_control_id = aws_cloudfront_origin_access_control.website.id + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "S3-${var.bucket_name}" + + forwarded_values { + query_string = false + headers = ["Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"] + + cookies { + forward = "none" + } + } + + viewer_protocol_policy = "redirect-to-https" + min_ttl = 0 + default_ttl = 3600 # 1 hour + max_ttl = 86400 # 24 hours + compress = true + } + + # Cache behavior for static assets (images, CSS, JS) + ordered_cache_behavior { + path_pattern = "/css/*" + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "S3-${var.bucket_name}" + + forwarded_values { + query_string = false + cookies { + forward = "none" + } + } + + viewer_protocol_policy = "redirect-to-https" + min_ttl = 0 + default_ttl = 86400 # 24 hours + max_ttl = 31536000 # 1 year + compress = true + } + + ordered_cache_behavior { + path_pattern = "/js/*" + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "S3-${var.bucket_name}" + + forwarded_values { + query_string = false + cookies { + forward = "none" + } + } + + viewer_protocol_policy = "redirect-to-https" + min_ttl = 0 + default_ttl = 86400 # 24 hours + max_ttl = 31536000 # 1 year + compress = true + } + + ordered_cache_behavior { + path_pattern = "/images/*" + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "S3-${var.bucket_name}" + + forwarded_values { + query_string = false + cookies { + forward = "none" + } + } + + viewer_protocol_policy = "redirect-to-https" + min_ttl = 0 + default_ttl = 604800 # 7 days + max_ttl = 31536000 # 1 year + compress = true + } + + # Custom error responses for SPA-like behavior + custom_error_response { + error_code = 404 + response_code = 404 + response_page_path = "/404.html" + } + + custom_error_response { + error_code = 403 + response_code = 404 + response_page_path = "/404.html" + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = aws_acm_certificate.website.arn + ssl_support_method = "sni-only" + minimum_protocol_version = "TLSv1.2_2021" + } + + tags = merge( + var.tags, + { + Name = "PythonCDMX Website Distribution" + } + ) + + depends_on = [aws_acm_certificate_validation.website] +} diff --git a/terraform/iam-github.tf b/terraform/iam-github.tf new file mode 100644 index 0000000..70ce23f --- /dev/null +++ b/terraform/iam-github.tf @@ -0,0 +1,136 @@ +# IAM role for GitHub Actions OIDC +# This allows GitHub Actions to authenticate with AWS without long-lived credentials + +data "aws_caller_identity" "current" {} + +# OIDC Provider for GitHub Actions +resource "aws_iam_openid_connect_provider" "github" { + url = "https://token.actions.githubusercontent.com" + + client_id_list = [ + "sts.amazonaws.com" + ] + + thumbprint_list = [ + "6938fd4d98bab03faadb97b34396831e3780aea1", + "1c58a3a8518e8759bf075b76b750d4f2df264fcd" + ] + + tags = merge( + var.tags, + { + Name = "GitHub Actions OIDC Provider" + } + ) +} + +# IAM Role for GitHub Actions +resource "aws_iam_role" "github_actions" { + name = "GitHubActionsDeployRole" + description = "Role for GitHub Actions to deploy to S3 and invalidate CloudFront" + assume_role_policy = data.aws_iam_policy_document.github_actions_assume_role.json + + tags = merge( + var.tags, + { + Name = "GitHub Actions Deploy Role" + } + ) +} + +# Trust policy for GitHub OIDC +data "aws_iam_policy_document" "github_actions_assume_role" { + statement { + effect = "Allow" + actions = ["sts:AssumeRoleWithWebIdentity"] + + principals { + type = "Federated" + identifiers = [aws_iam_openid_connect_provider.github.arn] + } + + condition { + test = "StringEquals" + variable = "token.actions.githubusercontent.com:aud" + values = ["sts.amazonaws.com"] + } + + condition { + test = "StringLike" + variable = "token.actions.githubusercontent.com:sub" + values = ["repo:PythonMexico/pythonCDMX:*"] + } + } +} + +# Policy for S3 access (production and staging) +data "aws_iam_policy_document" "github_actions_s3" { + statement { + sid = "AllowS3AccessProduction" + effect = "Allow" + + actions = [ + "s3:PutObject", + "s3:PutObjectAcl", + "s3:GetObject", + "s3:DeleteObject", + "s3:ListBucket" + ] + + resources = [ + aws_s3_bucket.website.arn, + "${aws_s3_bucket.website.arn}/*" + ] + } + + statement { + sid = "AllowS3AccessStaging" + effect = "Allow" + + actions = [ + "s3:PutObject", + "s3:PutObjectAcl", + "s3:GetObject", + "s3:DeleteObject", + "s3:ListBucket" + ] + + resources = [ + aws_s3_bucket.website_staging.arn, + "${aws_s3_bucket.website_staging.arn}/*" + ] + } +} + +# Policy for CloudFront invalidation (production and staging) +data "aws_iam_policy_document" "github_actions_cloudfront" { + statement { + sid = "AllowCloudFrontInvalidation" + effect = "Allow" + + actions = [ + "cloudfront:CreateInvalidation", + "cloudfront:GetInvalidation", + "cloudfront:ListInvalidations" + ] + + resources = [ + aws_cloudfront_distribution.website.arn, + aws_cloudfront_distribution.website_staging.arn + ] + } +} + +# Attach S3 policy to role +resource "aws_iam_role_policy" "github_actions_s3" { + name = "S3AccessPolicy" + role = aws_iam_role.github_actions.id + policy = data.aws_iam_policy_document.github_actions_s3.json +} + +# Attach CloudFront policy to role +resource "aws_iam_role_policy" "github_actions_cloudfront" { + name = "CloudFrontAccessPolicy" + role = aws_iam_role.github_actions.id + policy = data.aws_iam_policy_document.github_actions_cloudfront.json +} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..e0879fe --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,43 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + backend "s3" { + bucket = "bucket-terraform-a8ab" + key = "pythoncdmx/terraform.tfstate" + region = "us-east-1" + encrypt = true + } +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = "PythonCDMX" + Environment = var.environment + ManagedBy = "Terraform" + } + } +} + +# Provider for ACM certificates (must be in us-east-1 for CloudFront) +provider "aws" { + alias = "us_east_1" + region = "us-east-1" + + default_tags { + tags = { + Project = "PythonCDMX" + Environment = var.environment + ManagedBy = "Terraform" + } + } +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000..18001f7 --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,88 @@ +output "website_bucket_name" { + description = "Name of the S3 bucket hosting the website" + value = aws_s3_bucket.website.id +} + +output "website_bucket_arn" { + description = "ARN of the S3 bucket hosting the website" + value = aws_s3_bucket.website.arn +} + +output "cloudfront_distribution_id" { + description = "ID of the CloudFront distribution" + value = aws_cloudfront_distribution.website.id +} + +output "cloudfront_domain_name" { + description = "Domain name of the CloudFront distribution" + value = aws_cloudfront_distribution.website.domain_name +} + +output "certificate_arn" { + description = "ARN of the ACM certificate" + value = aws_acm_certificate.website.arn +} + +output "certificate_validation_records" { + description = "DNS validation records for the certificate" + value = [ + for dvo in aws_acm_certificate.website.domain_validation_options : { + name = dvo.resource_record_name + type = dvo.resource_record_type + value = dvo.resource_record_value + } + ] +} + +output "website_url" { + description = "URL of the website (production)" + value = "https://${var.domain_name}" +} + +# ============================================================================ +# STAGING ENVIRONMENT OUTPUTS +# ============================================================================ + +output "staging_bucket_name" { + description = "Name of the S3 bucket hosting the staging website" + value = aws_s3_bucket.website_staging.id +} + +output "staging_bucket_arn" { + description = "ARN of the S3 bucket hosting the staging website" + value = aws_s3_bucket.website_staging.arn +} + +output "staging_cloudfront_distribution_id" { + description = "ID of the CloudFront distribution (staging)" + value = aws_cloudfront_distribution.website_staging.id +} + +output "staging_cloudfront_domain_name" { + description = "Domain name of the CloudFront distribution (staging)" + value = aws_cloudfront_distribution.website_staging.domain_name +} + +output "staging_certificate_arn" { + description = "ARN of the ACM certificate (staging)" + value = aws_acm_certificate.website_staging.arn +} + +output "staging_website_url" { + description = "URL of the staging website" + value = "https://${var.staging_subdomain}" +} + +# ============================================================================ +# ROUTE53 OUTPUTS +# ============================================================================ + +output "hosted_zone_id" { + description = "ID of the Route53 hosted zone" + value = data.aws_route53_zone.main.zone_id +} + +output "hosted_zone_name_servers" { + description = "Name servers for the hosted zone" + value = data.aws_route53_zone.main.name_servers +} diff --git a/terraform/required-permissions.json b/terraform/required-permissions.json new file mode 100644 index 0000000..06a9d53 --- /dev/null +++ b/terraform/required-permissions.json @@ -0,0 +1,113 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ACMCertificateManagement", + "Effect": "Allow", + "Action": [ + "acm:RequestCertificate", + "acm:DescribeCertificate", + "acm:ListCertificates", + "acm:DeleteCertificate", + "acm:AddTagsToCertificate", + "acm:ListTagsForCertificate" + ], + "Resource": "*" + }, + { + "Sid": "CloudFrontManagement", + "Effect": "Allow", + "Action": [ + "cloudfront:CreateDistribution", + "cloudfront:GetDistribution", + "cloudfront:GetDistributionConfig", + "cloudfront:UpdateDistribution", + "cloudfront:DeleteDistribution", + "cloudfront:TagResource", + "cloudfront:CreateOriginAccessControl", + "cloudfront:GetOriginAccessControl", + "cloudfront:UpdateOriginAccessControl", + "cloudfront:DeleteOriginAccessControl", + "cloudfront:CreateInvalidation", + "cloudfront:GetInvalidation", + "cloudfront:ListInvalidations" + ], + "Resource": "*" + }, + { + "Sid": "Route53DNSManagement", + "Effect": "Allow", + "Action": [ + "route53:GetHostedZone", + "route53:ListHostedZones", + "route53:ListResourceRecordSets", + "route53:ChangeResourceRecordSets", + "route53:GetChange", + "route53:ListTagsForResource" + ], + "Resource": "*" + }, + { + "Sid": "S3BucketManagement", + "Effect": "Allow", + "Action": [ + "s3:CreateBucket", + "s3:DeleteBucket", + "s3:ListBucket", + "s3:GetBucketLocation", + "s3:GetBucketPolicy", + "s3:PutBucketPolicy", + "s3:DeleteBucketPolicy", + "s3:GetBucketVersioning", + "s3:PutBucketVersioning", + "s3:GetBucketPublicAccessBlock", + "s3:PutBucketPublicAccessBlock", + "s3:GetBucketCORS", + "s3:PutBucketCORS", + "s3:GetEncryptionConfiguration", + "s3:PutEncryptionConfiguration", + "s3:GetLifecycleConfiguration", + "s3:PutLifecycleConfiguration", + "s3:GetBucketTagging", + "s3:PutBucketTagging", + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject", + "s3:PutObjectAcl" + ], + "Resource": [ + "arn:aws:s3:::pythoncdmx-website", + "arn:aws:s3:::pythoncdmx-website/*", + "arn:aws:s3:::pythoncdmx-website-staging", + "arn:aws:s3:::pythoncdmx-website-staging/*" + ] + }, + { + "Sid": "IAMRoleManagement", + "Effect": "Allow", + "Action": [ + "iam:CreateOpenIDConnectProvider", + "iam:GetOpenIDConnectProvider", + "iam:DeleteOpenIDConnectProvider", + "iam:TagOpenIDConnectProvider", + "iam:CreateRole", + "iam:GetRole", + "iam:DeleteRole", + "iam:UpdateAssumeRolePolicy", + "iam:AttachRolePolicy", + "iam:DetachRolePolicy", + "iam:PutRolePolicy", + "iam:GetRolePolicy", + "iam:DeleteRolePolicy", + "iam:ListRolePolicies", + "iam:ListAttachedRolePolicies", + "iam:TagRole", + "iam:ListRoleTags" + ], + "Resource": [ + "arn:aws:iam::700463753979:oidc-provider/token.actions.githubusercontent.com", + "arn:aws:iam::700463753979:role/GitHubActionsDeployRole" + ] + } + ] +} diff --git a/terraform/route53.tf b/terraform/route53.tf new file mode 100644 index 0000000..ad6f463 --- /dev/null +++ b/terraform/route53.tf @@ -0,0 +1,131 @@ +# Data source for existing hosted zone +data "aws_route53_zone" "main" { + name = var.domain_name + private_zone = false +} + +# ============================================================================ +# PRODUCTION ENVIRONMENT - pythoncdmx.org +# ============================================================================ + +# A record for root domain (pythoncdmx.org) pointing to CloudFront +resource "aws_route53_record" "website_root" { + zone_id = data.aws_route53_zone.main.zone_id + name = var.domain_name + type = "A" + + alias { + name = aws_cloudfront_distribution.website.domain_name + zone_id = aws_cloudfront_distribution.website.hosted_zone_id + evaluate_target_health = false + } +} + +# AAAA record for IPv6 support (root domain) +resource "aws_route53_record" "website_root_ipv6" { + zone_id = data.aws_route53_zone.main.zone_id + name = var.domain_name + type = "AAAA" + + alias { + name = aws_cloudfront_distribution.website.domain_name + zone_id = aws_cloudfront_distribution.website.hosted_zone_id + evaluate_target_health = false + } +} + +# A record for www subdomain pointing to CloudFront +resource "aws_route53_record" "website_www" { + zone_id = data.aws_route53_zone.main.zone_id + name = "www.${var.domain_name}" + type = "A" + + alias { + name = aws_cloudfront_distribution.website.domain_name + zone_id = aws_cloudfront_distribution.website.hosted_zone_id + evaluate_target_health = false + } +} + +# AAAA record for IPv6 support (www subdomain) +resource "aws_route53_record" "website_www_ipv6" { + zone_id = data.aws_route53_zone.main.zone_id + name = "www.${var.domain_name}" + type = "AAAA" + + alias { + name = aws_cloudfront_distribution.website.domain_name + zone_id = aws_cloudfront_distribution.website.hosted_zone_id + evaluate_target_health = false + } +} + +# ============================================================================ +# STAGING ENVIRONMENT - staging.pythoncdmx.org +# ============================================================================ + +# A record for staging subdomain pointing to CloudFront staging distribution +resource "aws_route53_record" "website_staging" { + zone_id = data.aws_route53_zone.main.zone_id + name = var.staging_subdomain + type = "A" + + alias { + name = aws_cloudfront_distribution.website_staging.domain_name + zone_id = aws_cloudfront_distribution.website_staging.hosted_zone_id + evaluate_target_health = false + } +} + +# AAAA record for IPv6 support (staging subdomain) +resource "aws_route53_record" "website_staging_ipv6" { + zone_id = data.aws_route53_zone.main.zone_id + name = var.staging_subdomain + type = "AAAA" + + alias { + name = aws_cloudfront_distribution.website_staging.domain_name + zone_id = aws_cloudfront_distribution.website_staging.hosted_zone_id + evaluate_target_health = false + } +} + +# ============================================================================ +# ACM CERTIFICATE VALIDATION RECORDS +# ============================================================================ + +# Validation records for production certificate +resource "aws_route53_record" "certificate_validation" { + for_each = { + for dvo in aws_acm_certificate.website.domain_validation_options : dvo.domain_name => { + name = dvo.resource_record_name + record = dvo.resource_record_value + type = dvo.resource_record_type + } + } + + allow_overwrite = true + name = each.value.name + records = [each.value.record] + ttl = 60 + type = each.value.type + zone_id = data.aws_route53_zone.main.zone_id +} + +# Validation records for staging certificate +resource "aws_route53_record" "certificate_validation_staging" { + for_each = { + for dvo in aws_acm_certificate.website_staging.domain_validation_options : dvo.domain_name => { + name = dvo.resource_record_name + record = dvo.resource_record_value + type = dvo.resource_record_type + } + } + + allow_overwrite = true + name = each.value.name + records = [each.value.record] + ttl = 60 + type = each.value.type + zone_id = data.aws_route53_zone.main.zone_id +} diff --git a/terraform/s3-staging.tf b/terraform/s3-staging.tf new file mode 100644 index 0000000..731313d --- /dev/null +++ b/terraform/s3-staging.tf @@ -0,0 +1,111 @@ +# S3 bucket for staging website content +resource "aws_s3_bucket" "website_staging" { + bucket = var.staging_bucket_name + + tags = merge( + var.tags, + { + Name = "PythonCDMX Website Staging" + Environment = "staging" + } + ) +} + +# Enable versioning for staging bucket +resource "aws_s3_bucket_versioning" "website_staging" { + bucket = aws_s3_bucket.website_staging.id + + versioning_configuration { + status = "Enabled" + } +} + +# Block public access at bucket level (CloudFront OAC will handle access) +resource "aws_s3_bucket_public_access_block" "website_staging" { + bucket = aws_s3_bucket.website_staging.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +# Bucket policy to allow CloudFront access +resource "aws_s3_bucket_policy" "website_staging" { + bucket = aws_s3_bucket.website_staging.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowCloudFrontServicePrincipal" + Effect = "Allow" + Principal = { + Service = "cloudfront.amazonaws.com" + } + Action = "s3:GetObject" + Resource = "${aws_s3_bucket.website_staging.arn}/*" + Condition = { + StringEquals = { + "AWS:SourceArn" = aws_cloudfront_distribution.website_staging.arn + } + } + } + ] + }) + + depends_on = [aws_cloudfront_distribution.website_staging] +} + +# Enable server-side encryption +resource "aws_s3_bucket_server_side_encryption_configuration" "website_staging" { + bucket = aws_s3_bucket.website_staging.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +# Configure lifecycle rules - more aggressive cleanup for staging +resource "aws_s3_bucket_lifecycle_configuration" "website_staging" { + bucket = aws_s3_bucket.website_staging.id + + rule { + id = "delete-old-versions" + status = "Enabled" + + filter {} + + noncurrent_version_expiration { + noncurrent_days = 30 # Shorter retention for staging + } + } + + rule { + id = "delete-incomplete-uploads" + status = "Enabled" + + filter {} + + abort_incomplete_multipart_upload { + days_after_initiation = 3 + } + } +} + +# CORS configuration for website assets +resource "aws_s3_bucket_cors_configuration" "website_staging" { + bucket = aws_s3_bucket.website_staging.id + + cors_rule { + allowed_headers = ["*"] + allowed_methods = ["GET", "HEAD"] + allowed_origins = [ + "https://${var.staging_subdomain}" + ] + expose_headers = ["ETag"] + max_age_seconds = 3600 + } +} diff --git a/terraform/s3.tf b/terraform/s3.tf new file mode 100644 index 0000000..241313a --- /dev/null +++ b/terraform/s3.tf @@ -0,0 +1,111 @@ +# S3 bucket for website content +resource "aws_s3_bucket" "website" { + bucket = var.bucket_name + + tags = merge( + var.tags, + { + Name = "PythonCDMX Website" + } + ) +} + +# Enable versioning for backup purposes +resource "aws_s3_bucket_versioning" "website" { + bucket = aws_s3_bucket.website.id + + versioning_configuration { + status = "Enabled" + } +} + +# Block public access at bucket level (CloudFront OAC will handle access) +resource "aws_s3_bucket_public_access_block" "website" { + bucket = aws_s3_bucket.website.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +# Bucket policy to allow CloudFront access +resource "aws_s3_bucket_policy" "website" { + bucket = aws_s3_bucket.website.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowCloudFrontServicePrincipal" + Effect = "Allow" + Principal = { + Service = "cloudfront.amazonaws.com" + } + Action = "s3:GetObject" + Resource = "${aws_s3_bucket.website.arn}/*" + Condition = { + StringEquals = { + "AWS:SourceArn" = aws_cloudfront_distribution.website.arn + } + } + } + ] + }) + + depends_on = [aws_cloudfront_distribution.website] +} + +# Enable server-side encryption +resource "aws_s3_bucket_server_side_encryption_configuration" "website" { + bucket = aws_s3_bucket.website.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +# Configure lifecycle rules +resource "aws_s3_bucket_lifecycle_configuration" "website" { + bucket = aws_s3_bucket.website.id + + rule { + id = "delete-old-versions" + status = "Enabled" + + filter {} + + noncurrent_version_expiration { + noncurrent_days = 90 + } + } + + rule { + id = "delete-incomplete-uploads" + status = "Enabled" + + filter {} + + abort_incomplete_multipart_upload { + days_after_initiation = 7 + } + } +} + +# CORS configuration for website assets +resource "aws_s3_bucket_cors_configuration" "website" { + bucket = aws_s3_bucket.website.id + + cors_rule { + allowed_headers = ["*"] + allowed_methods = ["GET", "HEAD"] + allowed_origins = [ + "https://${var.domain_name}", + "https://www.${var.domain_name}" + ] + expose_headers = ["ETag"] + max_age_seconds = 3600 + } +} diff --git a/terraform/terraform.tfvars copy.example b/terraform/terraform.tfvars copy.example new file mode 100644 index 0000000..14e4bf9 --- /dev/null +++ b/terraform/terraform.tfvars copy.example @@ -0,0 +1,27 @@ +# Example Terraform variables file +# Copy this file to terraform.tfvars and customize as needed + +# AWS Region +aws_region = "us-east-1" + +# Environment +environment = "production" + +# Domain configuration +domain_name = "pythoncdmx.org" +alternative_domain_names = [ + "www.pythoncdmx.org" +] +staging_subdomain = "staging.pythoncdmx.org" + +# S3 bucket names +bucket_name = "pythoncdmx-website" +staging_bucket_name = "pythoncdmx-website-staging" +terraform_state_bucket = "pythoncdmx-terraform-state" +terraform_locks_table = "pythoncdmx-terraform-locks" + +# Additional tags +tags = { + Team = "Infrastructure" + Contact = "infra@pythoncdmx.org" +} diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..81c997a --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,59 @@ +variable "aws_region" { + description = "AWS region for the infrastructure" + type = string + default = "us-east-1" +} + +variable "environment" { + description = "Environment name (production, staging)" + type = string + default = "production" +} + +variable "domain_name" { + description = "Primary domain name for the website" + type = string + default = "pythoncdmx.org" +} + +variable "alternative_domain_names" { + description = "Alternative domain names (e.g., www subdomain)" + type = list(string) + default = ["www.pythoncdmx.org"] +} + +variable "staging_subdomain" { + description = "Subdomain for staging environment" + type = string + default = "staging.pythoncdmx.org" +} + +variable "bucket_name" { + description = "S3 bucket name for website hosting (production)" + type = string + default = "pythoncdmx-website" +} + +variable "staging_bucket_name" { + description = "S3 bucket name for staging website" + type = string + default = "pythoncdmx-website-staging" +} + +variable "terraform_state_bucket" { + description = "S3 bucket name for Terraform state" + type = string + default = "pythoncdmx-terraform-state" +} + +variable "terraform_locks_table" { + description = "DynamoDB table for Terraform state locks" + type = string + default = "pythoncdmx-terraform-locks" +} + +variable "tags" { + description = "Additional tags for resources" + type = map(string) + default = {} +}