diff --git a/content/guides/CLAUDE.md b/content/guides/CLAUDE.md new file mode 100644 index 000000000000..b1cc42567e87 --- /dev/null +++ b/content/guides/CLAUDE.md @@ -0,0 +1,105 @@ +--- +title: "Pulumi Guides Content Guidelines" +meta_desc: "Mandatory guidelines for creating and editing Pulumi Guides content with code sourcing requirements" +draft: true +_build: + render: never + list: never +--- + +# Pulumi Guides Content Guidelines + +**CRITICAL REQUIREMENT: NO SELF-GENERATED CODE** + +This file contains mandatory guidelines for creating and editing Pulumi Guides content. + +## Code Examples: The Golden Rule + +**ALL code examples in guides MUST come from verified sources. NEVER generate code examples yourself.** + +### Approved Sources for Code Examples + +1. **Pulumi Registry** (PRIMARY SOURCE) + - Location: https://www.pulumi.com/registry/ + - All examples in the registry are tested and production-ready + - Search pattern: `/registry/packages/{provider}/api-docs/{resource}/` + +2. **Pulumi Documentation** + - Location: https://www.pulumi.com/docs/ + +3. **Existing Tested Examples** + - Location: `/static/programs/` in this repository + +### Code Review Checklist + +Before submitting a guide: + +- [ ] Code examples are sourced from Pulumi Registry +- [ ] ALL 5 languages are present (TypeScript, Python, Go, C#, Java) +- [ ] Registry source URL is documented +- [ ] No self-generated or modified code is included + +## Guide Structure + +### Required Frontmatter + +```yaml +--- +title: "Short title (max 60 characters)" +meta_desc: "Brief description (max 160 characters)" +canonical_url: "https://www.pulumi.com/guides/[provider]/[guide-name]" +date: YYYY-MM-DD +category: "[Compute|Database|Storage|Networking|Security]" +tags: ["provider", "service", "feature"] +faq: + - question: Question text + answer: Answer text +--- +``` + +### Content Structure + +1. **H1**: Question format - "How do I [accomplish task]?" +2. **Answer snippet**: Bold opening sentence with concise answer +3. **Code examples**: Using `{{}}` and `{{%/* choosable */%}}` shortcodes +4. **Key configuration details**: Explain important parameters +5. **FAQ section**: 5 questions with schema.org markup support + +### Tags + +Follow the same tagging philosophy as blogs (see `/BLOGGING.md`): + +- **Cloud providers**: `aws`, `azure`, `google-cloud`, `kubernetes` +- **Services**: Specific service names like `lambda`, `rds`, `s3`, `eks` +- **Scenarios**: `serverless`, `containers`, `networking`, `security` +- **Keep minimal**: Reuse existing tags, don't create new ones unnecessarily + +Tags should be **interactive** - clicking a tag navigates to `/blog/tag/[tagname]/` + +## Layout Guidelines + +### Page Width + +- Match blog and docs page widths for consistency +- Avoid arbitrary max-width constraints + +### Schema Markup + +- Use the integrated schema system (see `/layouts/partials/schema/collectors/guides-entity.html`) +- Do NOT create custom schema - use the existing automation +- FAQ data in front matter automatically generates schema.org JSON-LD + +### Links + +- Use Hugo's `relref` shortcode: `[Text]({{}})` +- Link to registry: `/registry/packages/[provider]/api-docs/[resource]/` +- Link to relevant docs pages + +## Enforcement + +These guidelines are **mandatory**. Code reviews will reject any guides that: + +- Contain self-generated code examples +- Are missing any of the 5 required languages +- Don't cite registry sources +- Have incorrect schema markup diff --git a/content/guides/_index.md b/content/guides/_index.md new file mode 100644 index 000000000000..9b0a9c50c5bf --- /dev/null +++ b/content/guides/_index.md @@ -0,0 +1,22 @@ +--- +title: "Pulumi Guides" +meta_desc: "Quick, practical solutions to common cloud infrastructure problems using verified code from the Pulumi Registry." +--- + +Copy-paste infrastructure solutions that actually work. Every guide uses tested code from the [Pulumi Registry](/registry/)—just customize and deploy. + +## What makes a guide? + +- **Focused on one problem** - Each guide solves a specific, real-world challenge +- **Tested code** - All code examples are verified and production-ready +- **Clear guidance** - Includes when to use it, important notes, and considerations +- **Quick to implement** - Copy, customize, and deploy in minutes + +## How to use guides + +1. Find a guide that matches your problem +2. Copy the code example +3. Customize it for your specific needs +4. Deploy with `pulumi up` + +Browse guides by category below, or explore the full [Pulumi Registry](/registry/) for comprehensive API documentation. diff --git a/content/guides/ec2-auto-scaling.md b/content/guides/ec2-auto-scaling.md new file mode 100644 index 000000000000..5b53767f750d --- /dev/null +++ b/content/guides/ec2-auto-scaling.md @@ -0,0 +1,219 @@ +--- +title: "Auto-Scale EC2 Instances" +meta_desc: "Scale EC2 instances based on demand using AWS Auto Scaling Groups with launch templates and Pulumi." +canonical_url: "https://www.pulumi.com/guides/aws/ec2-auto-scaling" +date: 2025-10-08 +category: "Compute" +tags: ["aws", "ec2", "auto-scaling", "high-availability", "elasticity"] +faq: + - question: What is the difference between launch templates and launch configurations? + answer: Launch templates are the newer, recommended approach. They support all the latest EC2 features including T2/T3 unlimited mode, spot instances, and more instance types. Launch configurations are deprecated and lack these features. Always use launch templates for new deployments. + - question: How does Auto Scaling maintain high availability? + answer: Auto Scaling continuously monitors the health of instances. When an instance fails health checks, Auto Scaling automatically terminates it and launches a replacement to maintain your desired capacity. Distributing instances across multiple availability zones provides additional resilience. + - question: What is the cooldown period and why does it matter? + answer: The cooldown period (default 300 seconds) is the time between scaling activities. It prevents Auto Scaling from launching or terminating instances too quickly when metrics fluctuate. Adjust this based on your application's startup time - longer for slow-starting apps. + - question: How do I add scaling policies to respond to load? + answer: Use aws.autoscaling.Policy resources to define target tracking or step scaling policies. Target tracking (recommended) automatically adjusts capacity to maintain a metric like CPU utilization at 70%. This example shows the basic group; scaling policies are added separately. + - question: Can I use Auto Scaling with a load balancer? + answer: Yes, and it's recommended for production. Attach your Auto Scaling Group to an Application Load Balancer or Network Load Balancer. The load balancer distributes traffic across healthy instances, and you can configure health checks to let Auto Scaling know when to replace unhealthy instances. +--- + +## How do I auto-scale EC2 instances with AWS Auto Scaling Groups? + +**To automatically scale EC2 instances based on demand**, create an Auto Scaling Group with a launch template that defines your instance configuration. Auto Scaling maintains your desired capacity, replaces unhealthy instances, and can scale up or down in response to load. The following example shows how to set up basic auto-scaling in TypeScript, Python, Go, C#, and Java. + +{{< chooser language "typescript,python,go,csharp,java" >}} +{{% choosable language typescript %}} +```typescript +import * as pulumi from "@pulumi/pulumi"; +import * as aws from "@pulumi/aws"; + +const example = new aws.ec2.LaunchTemplate("example", { + namePrefix: "example", + imageId: exampleAwsAmi.id, + instanceType: "c5.large", +}); + +const exampleGroup = new aws.autoscaling.Group("example", { + availabilityZones: ["us-east-1a"], + desiredCapacity: 1, + maxSize: 1, + minSize: 1, + launchTemplate: { + id: example.id, + version: "$Latest", + }, +}); +``` +{{% /choosable %}} + +{{% choosable language python %}} +```python +import pulumi +import pulumi_aws as aws + +example = aws.ec2.LaunchTemplate("example", + name_prefix="example", + image_id=example_aws_ami["id"], + instance_type="c5.large") + +example_group = aws.autoscaling.Group("example", + availability_zones=["us-east-1a"], + desired_capacity=1, + max_size=1, + min_size=1, + launch_template=aws.autoscaling.GroupLaunchTemplateArgs( + id=example.id, + version="$Latest", + )) +``` +{{% /choosable %}} + +{{% choosable language go %}} +```go +package main + +import ( + "github.com/pulumi/pulumi-aws/sdk/v7/go/aws/autoscaling" + "github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + example, err := ec2.NewLaunchTemplate(ctx, "example", &ec2.LaunchTemplateArgs{ + NamePrefix: pulumi.String("example"), + ImageId: pulumi.Any(exampleAwsAmi.Id), + InstanceType: pulumi.String("c5.large"), + }) + if err != nil { + return err + } + + _, err = autoscaling.NewGroup(ctx, "example", &autoscaling.GroupArgs{ + AvailabilityZones: pulumi.StringArray{ + pulumi.String("us-east-1a"), + }, + DesiredCapacity: pulumi.Int(1), + MaxSize: pulumi.Int(1), + MinSize: pulumi.Int(1), + LaunchTemplate: &autoscaling.GroupLaunchTemplateArgs{ + Id: example.ID(), + Version: pulumi.String("$Latest"), + }, + }) + if err != nil { + return err + } + return nil + }) +} +``` +{{% /choosable %}} + +{{% choosable language csharp %}} +```csharp +using System.Collections.Generic; +using Pulumi; +using Aws = Pulumi.Aws; + +return await Deployment.RunAsync(() => +{ + var example = new Aws.Ec2.LaunchTemplate("example", new() + { + NamePrefix = "example", + ImageId = exampleAwsAmi.Id, + InstanceType = "c5.large", + }); + + var exampleGroup = new Aws.AutoScaling.Group("example", new() + { + AvailabilityZones = new[] + { + "us-east-1a", + }, + DesiredCapacity = 1, + MaxSize = 1, + MinSize = 1, + LaunchTemplate = new Aws.AutoScaling.Inputs.GroupLaunchTemplateArgs + { + Id = example.Id, + Version = "$Latest", + }, + }); +}); +``` +{{% /choosable %}} + +{{% choosable language java %}} +```java +package generated_program; + +import com.pulumi.Context; +import com.pulumi.Pulumi; +import com.pulumi.aws.ec2.LaunchTemplate; +import com.pulumi.aws.ec2.LaunchTemplateArgs; +import com.pulumi.aws.autoscaling.Group; +import com.pulumi.aws.autoscaling.GroupArgs; +import com.pulumi.aws.autoscaling.inputs.GroupLaunchTemplateArgs; + +public class App { + public static void main(String[] args) { + Pulumi.run(App::stack); + } + + public static void stack(Context ctx) { + var example = new LaunchTemplate("example", LaunchTemplateArgs.builder() + .namePrefix("example") + .imageId(exampleAwsAmi.id()) + .instanceType("c5.large") + .build()); + + var exampleGroup = new Group("example", GroupArgs.builder() + .availabilityZones("us-east-1a") + .desiredCapacity(1) + .maxSize(1) + .minSize(1) + .launchTemplate(GroupLaunchTemplateArgs.builder() + .id(example.id()) + .version("$Latest") + .build()) + .build()); + } +} +``` +{{% /choosable %}} +{{< /chooser >}} + +## Key configuration details + +**Launch template**: Defines the instance configuration including AMI, instance type, security groups, and user data. Using `namePrefix` instead of `name` allows AWS to generate unique names when creating new versions. + +**Desired capacity**: The number of instances Auto Scaling attempts to maintain. Auto Scaling replaces failed instances to maintain this count. Set this based on your baseline load. + +**Min and max size**: Define the boundaries for scaling. `minSize` ensures you always have at least this many instances running. `maxSize` prevents runaway scaling costs. + +**Availability zones**: Specifies which zones to launch instances in. For production, use multiple zones (e.g., `["us-east-1a", "us-east-1b", "us-east-1c"]`) to ensure high availability across zone failures. + +**Launch template version**: Using `$Latest` always launches instances with the most recent template version. You can specify a specific version number for more control over deployments. + +**Health checks**: By default, Auto Scaling uses EC2 status checks. For applications behind load balancers, add `healthCheckType: "ELB"` to use load balancer health checks instead. + +**Scaling policies not included**: This example creates a static group. Add scaling policies separately to automatically adjust capacity based on metrics like CPU utilization or request count. + +## Frequently asked questions + +**What is the difference between launch templates and launch configurations?** +Launch templates are the newer, recommended approach. They support all the latest EC2 features including T2/T3 unlimited mode, spot instances, and more instance types. Launch configurations are deprecated and lack these features. Always use launch templates for new deployments. + +**How does Auto Scaling maintain high availability?** +Auto Scaling continuously monitors the health of instances. When an instance fails health checks, Auto Scaling automatically terminates it and launches a replacement to maintain your desired capacity. Distributing instances across multiple availability zones provides additional resilience. + +**What is the cooldown period and why does it matter?** +The cooldown period (default 300 seconds) is the time between scaling activities. It prevents Auto Scaling from launching or terminating instances too quickly when metrics fluctuate. Adjust this based on your application's startup time - longer for slow-starting apps. + +**How do I add scaling policies to respond to load?** +Use aws.autoscaling.Policy resources to define target tracking or step scaling policies. Target tracking (recommended) automatically adjusts capacity to maintain a metric like CPU utilization at 70%. This example shows the basic group; scaling policies are added separately. + +**Can I use Auto Scaling with a load balancer?** +Yes, and it's recommended for production. Attach your Auto Scaling Group to an Application Load Balancer or Network Load Balancer. The load balancer distributes traffic across healthy instances, and you can configure health checks to let Auto Scaling know when to replace unhealthy instances. diff --git a/content/guides/eks-cluster-basic.md b/content/guides/eks-cluster-basic.md new file mode 100644 index 000000000000..12e4461786b5 --- /dev/null +++ b/content/guides/eks-cluster-basic.md @@ -0,0 +1,356 @@ +--- +title: "Deploy a Managed Kubernetes Cluster with EKS" +meta_desc: "Deploy a managed Kubernetes cluster on AWS using EKS with Pulumi for container orchestration." +canonical_url: "https://www.pulumi.com/guides/aws/eks-cluster-basic" +date: 2025-10-08 +category: "Compute" +tags: ["aws", "eks", "kubernetes", "containers", "orchestration"] +faq: + - question: Do I need to create a node group to run applications on EKS? + answer: Yes, this example creates only the EKS control plane. To run actual workloads, you need to add node groups (managed or self-managed EC2 instances) or use AWS Fargate for serverless container execution. The control plane alone cannot run your pods. + - question: How much does an EKS cluster cost? + answer: AWS charges $0.10 per hour (approximately $73 per month) for each EKS cluster control plane, regardless of the number of nodes or workloads. You also pay for the EC2 instances (node groups) or Fargate pods that run your applications separately. + - question: Which Kubernetes version should I use? + answer: Specify an explicit version like 1.31 to avoid unexpected upgrades. AWS supports the latest three minor Kubernetes versions. When a new version is released, the oldest supported version is deprecated after about 14 months. Plan for regular upgrades. + - question: What VPC requirements does EKS have? + answer: Your VPC must have at least two subnets in different availability zones for high availability. These subnets must be properly tagged for EKS to discover them for load balancers and other resources. Private subnets are recommended for nodes. + - question: How do I access my EKS cluster after creation? + answer: Use kubectl with the AWS CLI to authenticate. Run 'aws eks update-kubeconfig --name your-cluster-name' to add the cluster to your kubeconfig. You may also need to configure the aws-auth ConfigMap to grant access to additional IAM users or roles. +--- + +## How do I create an AWS EKS Kubernetes cluster? + +**To deploy a managed Kubernetes cluster on AWS**, create an EKS cluster with the necessary IAM roles and VPC configuration. EKS manages the Kubernetes control plane for you, providing high availability and automatic upgrades. The following example shows how to create an EKS cluster in TypeScript, Python, Go, C#, and Java. + +{{< chooser language "typescript,python,go,csharp,java" >}} +{{% choosable language typescript %}} +```typescript +import * as pulumi from "@pulumi/pulumi"; +import * as aws from "@pulumi/aws"; + +const cluster = new aws.iam.Role("cluster", { + name: "eks-cluster-example", + assumeRolePolicy: JSON.stringify({ + Version: "2012-10-17", + Statement: [{ + Action: [ + "sts:AssumeRole", + "sts:TagSession", + ], + Effect: "Allow", + Principal: { + Service: "eks.amazonaws.com", + }, + }], + }), +}); + +const clusterAmazonEKSClusterPolicy = new aws.iam.RolePolicyAttachment("cluster_AmazonEKSClusterPolicy", { + policyArn: "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy", + role: cluster.name, +}); + +const example = new aws.eks.Cluster("example", { + name: "example", + accessConfig: { + authenticationMode: "API", + }, + roleArn: cluster.arn, + version: "1.31", + vpcConfig: { + subnetIds: [ + az1.id, + az2.id, + az3.id, + ], + }, +}, { + dependsOn: [clusterAmazonEKSClusterPolicy], +}); +``` +{{% /choosable %}} + +{{% choosable language python %}} +```python +import pulumi +import pulumi_aws as aws +import json + +cluster = aws.iam.Role("cluster", + name="eks-cluster-example", + assume_role_policy=json.dumps({ + "Version": "2012-10-17", + "Statement": [{ + "Action": [ + "sts:AssumeRole", + "sts:TagSession", + ], + "Effect": "Allow", + "Principal": { + "Service": "eks.amazonaws.com", + }, + }], + })) + +cluster_amazon_eks_cluster_policy = aws.iam.RolePolicyAttachment("cluster_AmazonEKSClusterPolicy", + policy_arn="arn:aws:iam::aws:policy/AmazonEKSClusterPolicy", + role=cluster.name) + +example = aws.eks.Cluster("example", + name="example", + access_config=aws.eks.ClusterAccessConfigArgs( + authentication_mode="API", + ), + role_arn=cluster.arn, + version="1.31", + vpc_config=aws.eks.ClusterVpcConfigArgs( + subnet_ids=[ + az1.id, + az2.id, + az3.id, + ], + ), + opts=pulumi.ResourceOptions(depends_on=[cluster_amazon_eks_cluster_policy])) +``` +{{% /choosable %}} + +{{% choosable language go %}} +```go +package main + +import ( + "encoding/json" + + "github.com/pulumi/pulumi-aws/sdk/v7/go/aws/eks" + "github.com/pulumi/pulumi-aws/sdk/v7/go/aws/iam" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + assumeRolePolicyJSON, err := json.Marshal(map[string]interface{}{ + "Version": "2012-10-17", + "Statement": []map[string]interface{}{ + { + "Action": []string{ + "sts:AssumeRole", + "sts:TagSession", + }, + "Effect": "Allow", + "Principal": map[string]interface{}{ + "Service": "eks.amazonaws.com", + }, + }, + }, + }) + if err != nil { + return err + } + + cluster, err := iam.NewRole(ctx, "cluster", &iam.RoleArgs{ + Name: pulumi.String("eks-cluster-example"), + AssumeRolePolicy: pulumi.String(string(assumeRolePolicyJSON)), + }) + if err != nil { + return err + } + + clusterAmazonEKSClusterPolicy, err := iam.NewRolePolicyAttachment(ctx, "cluster_AmazonEKSClusterPolicy", &iam.RolePolicyAttachmentArgs{ + PolicyArn: pulumi.String("arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"), + Role: cluster.Name, + }) + if err != nil { + return err + } + + _, err = eks.NewCluster(ctx, "example", &eks.ClusterArgs{ + Name: pulumi.String("example"), + AccessConfig: &eks.ClusterAccessConfigArgs{ + AuthenticationMode: pulumi.String("API"), + }, + RoleArn: cluster.Arn, + Version: pulumi.String("1.31"), + VpcConfig: &eks.ClusterVpcConfigArgs{ + SubnetIds: pulumi.StringArray{ + az1.Id, + az2.Id, + az3.Id, + }, + }, + }, pulumi.DependsOn([]pulumi.Resource{clusterAmazonEKSClusterPolicy})) + if err != nil { + return err + } + return nil + }) +} +``` +{{% /choosable %}} + +{{% choosable language csharp %}} +```csharp +using System.Collections.Generic; +using System.Text.Json; +using Pulumi; +using Aws = Pulumi.Aws; + +return await Deployment.RunAsync(() => +{ + var cluster = new Aws.Iam.Role("cluster", new() + { + Name = "eks-cluster-example", + AssumeRolePolicy = JsonSerializer.Serialize(new Dictionary + { + ["Version"] = "2012-10-17", + ["Statement"] = new[] + { + new Dictionary + { + ["Action"] = new[] + { + "sts:AssumeRole", + "sts:TagSession", + }, + ["Effect"] = "Allow", + ["Principal"] = new Dictionary + { + ["Service"] = "eks.amazonaws.com", + }, + }, + }, + }), + }); + + var clusterAmazonEKSClusterPolicy = new Aws.Iam.RolePolicyAttachment("cluster_AmazonEKSClusterPolicy", new() + { + PolicyArn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy", + Role = cluster.Name, + }); + + var example = new Aws.Eks.Cluster("example", new() + { + Name = "example", + AccessConfig = new Aws.Eks.Inputs.ClusterAccessConfigArgs + { + AuthenticationMode = "API", + }, + RoleArn = cluster.Arn, + Version = "1.31", + VpcConfig = new Aws.Eks.Inputs.ClusterVpcConfigArgs + { + SubnetIds = new[] + { + az1.Id, + az2.Id, + az3.Id, + }, + }, + }, new CustomResourceOptions + { + DependsOn = new[] + { + clusterAmazonEKSClusterPolicy, + }, + }); +}); +``` +{{% /choosable %}} + +{{% choosable language java %}} +```java +package generated_program; + +import com.pulumi.Context; +import com.pulumi.Pulumi; +import com.pulumi.aws.iam.Role; +import com.pulumi.aws.iam.RoleArgs; +import com.pulumi.aws.iam.RolePolicyAttachment; +import com.pulumi.aws.iam.RolePolicyAttachmentArgs; +import com.pulumi.aws.eks.Cluster; +import com.pulumi.aws.eks.ClusterArgs; +import com.pulumi.aws.eks.inputs.ClusterAccessConfigArgs; +import com.pulumi.aws.eks.inputs.ClusterVpcConfigArgs; +import com.pulumi.resources.CustomResourceOptions; +import static com.pulumi.codegen.internal.Serialization.*; + +public class App { + public static void main(String[] args) { + Pulumi.run(App::stack); + } + + public static void stack(Context ctx) { + var cluster = new Role("cluster", RoleArgs.builder() + .name("eks-cluster-example") + .assumeRolePolicy(serializeJson( + jsonObject( + jsonProperty("Version", "2012-10-17"), + jsonProperty("Statement", jsonArray(jsonObject( + jsonProperty("Action", jsonArray( + "sts:AssumeRole", + "sts:TagSession" + )), + jsonProperty("Effect", "Allow"), + jsonProperty("Principal", jsonObject( + jsonProperty("Service", "eks.amazonaws.com") + )) + ))) + ))) + .build()); + + var clusterAmazonEKSClusterPolicy = new RolePolicyAttachment("cluster_AmazonEKSClusterPolicy", RolePolicyAttachmentArgs.builder() + .policyArn("arn:aws:iam::aws:policy/AmazonEKSClusterPolicy") + .role(cluster.name()) + .build()); + + var example = new Cluster("example", ClusterArgs.builder() + .name("example") + .accessConfig(ClusterAccessConfigArgs.builder() + .authenticationMode("API") + .build()) + .roleArn(cluster.arn()) + .version("1.31") + .vpcConfig(ClusterVpcConfigArgs.builder() + .subnetIds( + az1.id(), + az2.id(), + az3.id()) + .build()) + .build(), CustomResourceOptions.builder() + .dependsOn(clusterAmazonEKSClusterPolicy) + .build()); + } +} +``` +{{% /choosable %}} +{{< /chooser >}} + +## Key configuration details + +**IAM role for EKS**: The cluster requires an IAM role that allows the EKS service to assume it. This role must have the `AmazonEKSClusterPolicy` attached, which grants permissions to manage AWS resources on behalf of Kubernetes. + +**Cluster version**: Specify an explicit Kubernetes version (e.g., `1.31`) to control when upgrades happen. AWS supports the latest three minor versions and deprecates older versions approximately 14 months after a new version is released. + +**VPC configuration**: The `vpcConfig` block specifies which subnets the cluster uses. You need at least two subnets in different availability zones for high availability. These subnets must be tagged appropriately for EKS. + +**Authentication mode**: Setting `authenticationMode: "API"` enables the EKS API for cluster authentication. You can also use `CONFIG_MAP` for legacy aws-auth ConfigMap authentication or `API_AND_CONFIG_MAP` for both. + +**Node groups not included**: This example creates only the control plane. You must separately create managed node groups, self-managed node groups, or configure Fargate profiles to run actual workloads. + +**Cluster dependencies**: The `dependsOn` option ensures the IAM policy attachment completes before creating the cluster. This prevents authorization failures during cluster creation. + +## Frequently asked questions + +**Do I need to create a node group to run applications on EKS?** +Yes, this example creates only the EKS control plane. To run actual workloads, you need to add node groups (managed or self-managed EC2 instances) or use AWS Fargate for serverless container execution. The control plane alone cannot run your pods. + +**How much does an EKS cluster cost?** +AWS charges $0.10 per hour (approximately $73 per month) for each EKS cluster control plane, regardless of the number of nodes or workloads. You also pay for the EC2 instances (node groups) or Fargate pods that run your applications separately. + +**Which Kubernetes version should I use?** +Specify an explicit version like 1.31 to avoid unexpected upgrades. AWS supports the latest three minor Kubernetes versions. When a new version is released, the oldest supported version is deprecated after about 14 months. Plan for regular upgrades. + +**What VPC requirements does EKS have?** +Your VPC must have at least two subnets in different availability zones for high availability. These subnets must be properly tagged for EKS to discover them for load balancers and other resources. Private subnets are recommended for nodes. + +**How do I access my EKS cluster after creation?** +Use kubectl with the AWS CLI to authenticate. Run 'aws eks update-kubeconfig --name your-cluster-name' to add the cluster to your kubeconfig. You may also need to configure the aws-auth ConfigMap to grant access to additional IAM users or roles. diff --git a/content/guides/lambda-vpc-access.md b/content/guides/lambda-vpc-access.md new file mode 100644 index 000000000000..3323f983f508 --- /dev/null +++ b/content/guides/lambda-vpc-access.md @@ -0,0 +1,251 @@ +--- +title: "Connect Lambda to Private VPC Resources" +meta_desc: "Deploy Lambda functions that access private VPC resources like RDS, ElastiCache, or internal APIs with Pulumi." +canonical_url: "https://www.pulumi.com/guides/aws/lambda-vpc-access" +date: 2025-10-08 +category: "Compute" +tags: ["aws", "lambda", "vpc", "networking", "rds", "compute", "serverless"] +faq: + - question: How do I find the correct subnet IDs for my VPC? + answer: You can use aws.ec2.getSubnets to query subnets by VPC ID and filter by tags, or use the AWS Console to find subnet IDs in your VPC configuration. Typically, Lambda functions should be placed in private subnets. + - question: Does Lambda in a VPC have internet access by default? + answer: No, Lambda functions in private subnets need a NAT Gateway for outbound internet access. Public subnets can use an Internet Gateway, but it's recommended to use private subnets for Lambda. + - question: How much does a NAT Gateway cost? + answer: AWS NAT Gateways cost approximately $0.045/hour (about $32/month) plus $0.045/GB for data processed. This is required if your Lambda needs to access external APIs or AWS services outside your VPC. + - question: Why are my Lambda cold starts slower with VPC? + answer: VPC-enabled Lambda functions need to create Elastic Network Interfaces (ENIs) on cold starts, adding 1-2 seconds of latency. AWS's Hyperplane ENIs have improved this significantly in recent years. + - question: Can I use the same security group for Lambda and RDS? + answer: No, it's better practice to use separate security groups. Configure your RDS security group to allow inbound traffic from the Lambda security group on the database port (e.g., 5432 for PostgreSQL). +--- + +## How do I connect AWS Lambda to private VPC resources? + +**To enable AWS Lambda to access private resources in your VPC**, configure the function's `vpcConfig` with subnet IDs and security group IDs. The following example shows how to deploy a VPC-connected Lambda function in TypeScript, Python, Go, C#, and Java. + +{{< chooser language "typescript,python,go,csharp,java" >}} +{{% choosable language typescript %}} +```typescript +import * as pulumi from "@pulumi/pulumi"; +import * as aws from "@pulumi/aws"; + +const example = new aws.lambda.Function("example", { + code: new pulumi.asset.FileArchive("function.zip"), + name: "example_vpc_function", + role: exampleAwsIamRole.arn, + handler: "app.handler", + runtime: aws.lambda.Runtime.Python3d12, + memorySize: 1024, + timeout: 30, + vpcConfig: { + subnetIds: [ + examplePrivate1.id, + examplePrivate2.id, + ], + securityGroupIds: [exampleLambda.id], + ipv6AllowedForDualStack: true, + }, + ephemeralStorage: { + size: 5120, + }, + snapStart: { + applyOn: "PublishedVersions", + }, +}); +``` +{{% /choosable %}} + +{{% choosable language python %}} +```python +import pulumi +import pulumi_aws as aws + +example = aws.lambda_.Function("example", + code=pulumi.FileArchive("function.zip"), + name="example_vpc_function", + role=example_aws_iam_role["arn"], + handler="app.handler", + runtime=aws.lambda_.Runtime.PYTHON3D12, + memory_size=1024, + timeout=30, + vpc_config=aws.lambda_.FunctionVpcConfigArgs( + subnet_ids=[ + example_private1["id"], + example_private2["id"], + ], + security_group_ids=[example_lambda["id"]], + ipv6_allowed_for_dual_stack=True, + ), + ephemeral_storage=aws.lambda_.FunctionEphemeralStorageArgs( + size=5120, + ), + snap_start=aws.lambda_.FunctionSnapStartArgs( + apply_on="PublishedVersions", + )) +``` +{{% /choosable %}} + +{{% choosable language go %}} +```go +package main + +import ( + "github.com/pulumi/pulumi-aws/sdk/v7/go/aws/lambda" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + _, err := lambda.NewFunction(ctx, "example", &lambda.FunctionArgs{ + Code: pulumi.NewFileArchive("function.zip"), + Name: pulumi.String("example_vpc_function"), + Role: pulumi.Any(exampleAwsIamRole.Arn), + Handler: pulumi.String("app.handler"), + Runtime: pulumi.String(lambda.RuntimePython3d12), + MemorySize: pulumi.Int(1024), + Timeout: pulumi.Int(30), + VpcConfig: &lambda.FunctionVpcConfigArgs{ + SubnetIds: pulumi.StringArray{ + examplePrivate1.Id, + examplePrivate2.Id, + }, + SecurityGroupIds: pulumi.StringArray{ + exampleLambda.Id, + }, + Ipv6AllowedForDualStack: pulumi.Bool(true), + }, + EphemeralStorage: &lambda.FunctionEphemeralStorageArgs{ + Size: pulumi.Int(5120), + }, + SnapStart: &lambda.FunctionSnapStartArgs{ + ApplyOn: pulumi.String("PublishedVersions"), + }, + }) + if err != nil { + return err + } + return nil + }) +} +``` +{{% /choosable %}} + +{{% choosable language csharp %}} +```csharp +using System.Collections.Generic; +using Pulumi; +using Aws = Pulumi.Aws; + +return await Deployment.RunAsync(() => +{ + var example = new Aws.Lambda.Function("example", new() + { + Code = new FileArchive("function.zip"), + Name = "example_vpc_function", + Role = exampleAwsIamRole.Arn, + Handler = "app.handler", + Runtime = Aws.Lambda.Runtime.Python3d12, + MemorySize = 1024, + Timeout = 30, + VpcConfig = new Aws.Lambda.Inputs.FunctionVpcConfigArgs + { + SubnetIds = new[] + { + examplePrivate1.Id, + examplePrivate2.Id, + }, + SecurityGroupIds = new[] + { + exampleLambda.Id, + }, + Ipv6AllowedForDualStack = true, + }, + EphemeralStorage = new Aws.Lambda.Inputs.FunctionEphemeralStorageArgs + { + Size = 5120, + }, + SnapStart = new Aws.Lambda.Inputs.FunctionSnapStartArgs + { + ApplyOn = "PublishedVersions", + }, + }); +}); +``` +{{% /choosable %}} + +{{% choosable language java %}} +```java +package generated_program; + +import com.pulumi.Context; +import com.pulumi.Pulumi; +import com.pulumi.aws.lambda.Function; +import com.pulumi.aws.lambda.FunctionArgs; +import com.pulumi.aws.lambda.inputs.FunctionVpcConfigArgs; +import com.pulumi.aws.lambda.inputs.FunctionEphemeralStorageArgs; +import com.pulumi.aws.lambda.inputs.FunctionSnapStartArgs; +import com.pulumi.asset.FileArchive; + +public class App { + public static void main(String[] args) { + Pulumi.run(App::stack); + } + + public static void stack(Context ctx) { + var example = new Function("example", FunctionArgs.builder() + .code(new FileArchive("function.zip")) + .name("example_vpc_function") + .role(exampleAwsIamRole.arn()) + .handler("app.handler") + .runtime("python3.12") + .memorySize(1024) + .timeout(30) + .vpcConfig(FunctionVpcConfigArgs.builder() + .subnetIds( + examplePrivate1.id(), + examplePrivate2.id()) + .securityGroupIds(exampleLambda.id()) + .ipv6AllowedForDualStack(true) + .build()) + .ephemeralStorage(FunctionEphemeralStorageArgs.builder() + .size(5120) + .build()) + .snapStart(FunctionSnapStartArgs.builder() + .applyOn("PublishedVersions") + .build()) + .build()); + } +} +``` +{{% /choosable %}} +{{< /chooser >}} + +## Key configuration details + +**VPC Configuration**: The `vpcConfig` block specifies which subnets and security groups the Lambda function uses. Using multiple private subnets across availability zones provides high availability. + +**Security Groups**: The `securityGroupIds` define network access rules. Ensure your security group allows outbound traffic to your VPC resources and that target resources (like RDS) allow inbound traffic from the Lambda security group. + +**Subnet Selection**: Deploy Lambda to private subnets. If the function needs internet access, these subnets should route traffic through a NAT Gateway. + +**Enhanced Networking**: The `ipv6AllowedForDualStack` option enables dual-stack networking for both IPv4 and IPv6. + +**Ephemeral Storage**: The example sets ephemeral storage to 5120 MB (5 GB). The default is 512 MB, and the maximum is 10,240 MB. + +**SnapStart**: This feature improves cold start performance for Java functions by reusing initialized snapshots. It only applies to Java runtimes. + +## Frequently asked questions + +**How do I find the correct subnet IDs for my VPC?** +You can use aws.ec2.getSubnets to query subnets by VPC ID and filter by tags, or use the AWS Console to find subnet IDs in your VPC configuration. Typically, Lambda functions should be placed in private subnets. + +**Does Lambda in a VPC have internet access by default?** +No, Lambda functions in private subnets need a NAT Gateway for outbound internet access. Public subnets can use an Internet Gateway, but it's recommended to use private subnets for Lambda. + +**How much does a NAT Gateway cost?** +AWS NAT Gateways cost approximately $0.045/hour (about $32/month) plus $0.045/GB for data processed. This is required if your Lambda needs to access external APIs or AWS services outside your VPC. + +**Why are my Lambda cold starts slower with VPC?** +VPC-enabled Lambda functions need to create Elastic Network Interfaces (ENIs) on cold starts, adding 1-2 seconds of latency. AWS's Hyperplane ENIs have improved this significantly in recent years. + +**Can I use the same security group for Lambda and RDS?** +No, it's better practice to use separate security groups. Configure your RDS security group to allow inbound traffic from the Lambda security group on the database port (e.g., 5432 for PostgreSQL). diff --git a/content/guides/rds-secrets-manager.md b/content/guides/rds-secrets-manager.md new file mode 100644 index 000000000000..a089ec7559b0 --- /dev/null +++ b/content/guides/rds-secrets-manager.md @@ -0,0 +1,174 @@ +--- +title: "Secure RDS Passwords with AWS Secrets Manager" +meta_desc: "Automatically manage RDS passwords with AWS Secrets Manager for improved security and compliance using Pulumi." +canonical_url: "https://www.pulumi.com/guides/aws/rds-secrets-manager" +date: 2025-10-08 +category: "Database" +tags: ["aws", "rds", "secrets-manager", "security", "database"] +faq: + - question: How do I retrieve the password from Secrets Manager? + answer: The password is stored in AWS Secrets Manager automatically. Use the AWS SDK or Pulumi's aws.secretsmanager.getSecretVersion function to retrieve it programmatically. The secret ARN is available in the RDS instance's masterUserSecret output. + - question: Does enabling Secrets Manager integration cost extra? + answer: Yes, AWS Secrets Manager charges approximately $0.40 per month per secret, plus $0.05 per 10,000 API calls. This is a small cost for significantly improved security and automatic password rotation capabilities. + - question: Can I use this with existing RDS instances? + answer: Yes, you can enable manageMasterUserPassword on existing RDS instances. AWS will generate a new password and store it in Secrets Manager. Make sure to update your application connection strings to retrieve the password from Secrets Manager. + - question: How does automatic password rotation work? + answer: When you enable manageMasterUserPassword, AWS can automatically rotate the password based on a schedule you configure in Secrets Manager. The rotation is performed without downtime, and applications using the Secrets Manager SDK automatically receive the new password. + - question: What happens to my current password when I enable this feature? + answer: When you enable manageMasterUserPassword on an existing instance, AWS generates a new random password and stores it in Secrets Manager. Your old password is no longer valid. Ensure your applications are configured to use Secrets Manager before enabling this feature. +--- + +## How do I secure RDS passwords with AWS Secrets Manager? + +**To secure RDS database passwords using AWS Secrets Manager**, enable the `manageMasterUserPassword` option when creating or updating your RDS instance. AWS automatically generates a strong password, stores it securely in Secrets Manager, and can rotate it on a schedule. The following example shows how to deploy this configuration in TypeScript, Python, Go, C#, and Java. + +{{< chooser language "typescript,python,go,csharp,java" >}} +{{% choosable language typescript %}} +```typescript +import * as pulumi from "@pulumi/pulumi"; +import * as aws from "@pulumi/aws"; + +const _default = new aws.rds.Instance("default", { + allocatedStorage: 10, + dbName: "mydb", + engine: "mysql", + engineVersion: "8.0", + instanceClass: aws.rds.InstanceType.T3_Micro, + manageMasterUserPassword: true, + username: "foo", + parameterGroupName: "default.mysql8.0", +}); +``` +{{% /choosable %}} + +{{% choosable language python %}} +```python +import pulumi +import pulumi_aws as aws + +default = aws.rds.Instance("default", + allocated_storage=10, + db_name="mydb", + engine="mysql", + engine_version="8.0", + instance_class=aws.rds.InstanceType.T3_MICRO, + manage_master_user_password=True, + username="foo", + parameter_group_name="default.mysql8.0") +``` +{{% /choosable %}} + +{{% choosable language go %}} +```go +package main + +import ( + "github.com/pulumi/pulumi-aws/sdk/v7/go/aws/rds" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + _, err := rds.NewInstance(ctx, "default", &rds.InstanceArgs{ + AllocatedStorage: pulumi.Int(10), + DbName: pulumi.String("mydb"), + Engine: pulumi.String("mysql"), + EngineVersion: pulumi.String("8.0"), + InstanceClass: pulumi.String(rds.InstanceType_T3_Micro), + ManageMasterUserPassword: pulumi.Bool(true), + Username: pulumi.String("foo"), + ParameterGroupName: pulumi.String("default.mysql8.0"), + }) + if err != nil { + return err + } + return nil + }) +} +``` +{{% /choosable %}} + +{{% choosable language csharp %}} +```csharp +using System.Collections.Generic; +using Pulumi; +using Aws = Pulumi.Aws; + +return await Deployment.RunAsync(() => +{ + var @default = new Aws.Rds.Instance("default", new() + { + AllocatedStorage = 10, + DbName = "mydb", + Engine = "mysql", + EngineVersion = "8.0", + InstanceClass = Aws.Rds.InstanceType.T3_Micro, + ManageMasterUserPassword = true, + Username = "foo", + ParameterGroupName = "default.mysql8.0", + }); +}); +``` +{{% /choosable %}} + +{{% choosable language java %}} +```java +package generated_program; + +import com.pulumi.Context; +import com.pulumi.Pulumi; +import com.pulumi.aws.rds.Instance; +import com.pulumi.aws.rds.InstanceArgs; + +public class App { + public static void main(String[] args) { + Pulumi.run(App::stack); + } + + public static void stack(Context ctx) { + var default_ = new Instance("default", InstanceArgs.builder() + .allocatedStorage(10) + .dbName("mydb") + .engine("mysql") + .engineVersion("8.0") + .instanceClass("db.t3.micro") + .manageMasterUserPassword(true) + .username("foo") + .parameterGroupName("default.mysql8.0") + .build()); + } +} +``` +{{% /choosable %}} +{{< /chooser >}} + +## Key configuration details + +**Automatic password generation**: Setting `manageMasterUserPassword: true` instructs AWS to automatically generate a strong, random password and store it in AWS Secrets Manager. You never need to handle the password in your infrastructure code. + +**Master user secret**: The RDS instance exposes a `masterUserSecret` output containing the ARN of the Secrets Manager secret. Use this ARN to retrieve the password programmatically in your applications. + +**Database engine support**: This feature is supported for MySQL, PostgreSQL, MariaDB, Oracle, and SQL Server engines. Ensure your engine version supports managed passwords. + +**Application integration**: Your applications must be configured to retrieve the password from Secrets Manager using the AWS SDK. Connection strings should reference the secret ARN rather than hardcoded passwords. + +**Automatic rotation**: After enabling Secrets Manager integration, you can configure automatic password rotation policies in Secrets Manager. AWS handles rotation without application downtime. + +**Security benefits**: This approach eliminates hardcoded passwords in your code, provides audit trails of password access, and meets compliance requirements for credential management. + +## Frequently asked questions + +**How do I retrieve the password from Secrets Manager?** +The password is stored in AWS Secrets Manager automatically. Use the AWS SDK or Pulumi's aws.secretsmanager.getSecretVersion function to retrieve it programmatically. The secret ARN is available in the RDS instance's masterUserSecret output. + +**Does enabling Secrets Manager integration cost extra?** +Yes, AWS Secrets Manager charges approximately $0.40 per month per secret, plus $0.05 per 10,000 API calls. This is a small cost for significantly improved security and automatic password rotation capabilities. + +**Can I use this with existing RDS instances?** +Yes, you can enable manageMasterUserPassword on existing RDS instances. AWS will generate a new password and store it in Secrets Manager. Make sure to update your application connection strings to retrieve the password from Secrets Manager. + +**How does automatic password rotation work?** +When you enable manageMasterUserPassword, AWS can automatically rotate the password based on a schedule you configure in Secrets Manager. The rotation is performed without downtime, and applications using the Secrets Manager SDK automatically receive the new password. + +**What happens to my current password when I enable this feature?** +When you enable manageMasterUserPassword on an existing instance, AWS generates a new random password and stores it in Secrets Manager. Your old password is no longer valid. Ensure your applications are configured to use Secrets Manager before enabling this feature. diff --git a/content/guides/s3-cloudfront-cdn.md b/content/guides/s3-cloudfront-cdn.md new file mode 100644 index 000000000000..80c34a1d66d0 --- /dev/null +++ b/content/guides/s3-cloudfront-cdn.md @@ -0,0 +1,299 @@ +--- +title: "Serve S3 Content Through CloudFront CDN" +meta_desc: "Distribute static website content globally with low latency using AWS CloudFront CDN and S3 bucket as origin with Pulumi in TypeScript, Python, Go, C#, or Java." +canonical_url: "https://www.pulumi.com/guides/aws/s3-cloudfront-cdn" +date: 2025-10-08 +category: "Storage" +tags: ["aws", "s3", "cloudfront", "cdn", "static-website"] +faq: + - question: What is the difference between Origin Access Control and Origin Access Identity? + answer: Origin Access Control (OAC) is the newer, recommended method to secure S3 origins. OAC supports all S3 buckets, SSE-KMS encryption, and more AWS Regions compared to the legacy Origin Access Identity (OAI). AWS recommends migrating from OAI to OAC for new distributions. + - question: How long does it take for CloudFront to deploy? + answer: CloudFront distributions typically take 15-20 minutes to fully deploy and propagate to all edge locations worldwide. During this time, the distribution status will show as 'InProgress'. You can access your content once the status changes to 'Deployed'. + - question: How do I update content that's already cached? + answer: You can create a CloudFront invalidation to immediately remove content from edge caches. Use the aws.cloudfront.Invalidation resource in Pulumi. Note that the first 1,000 invalidation paths per month are free, with $0.005 per path after that. + - question: Can I use my custom domain with CloudFront? + answer: Yes, you can add custom domain names (CNAMEs) to your CloudFront distribution. You'll need to create an ACM certificate in us-east-1 region for your domain and configure DNS records to point to the CloudFront distribution domain name. + - question: How much does CloudFront cost compared to serving directly from S3? + answer: CloudFront costs approximately $0.085/GB for the first 10TB in the US (cheaper for higher volumes), compared to S3's $0.09/GB for data transfer out to the internet. CloudFront is usually cost-effective for frequently accessed content due to caching, and provides better performance globally. +--- + +## How do I serve S3 content through CloudFront CDN? + +**To distribute S3 content globally with low latency**, create a CloudFront distribution with your S3 bucket as the origin. CloudFront caches your content at edge locations worldwide, reducing latency for users and offloading traffic from your S3 bucket. The following example shows how to configure this in TypeScript, Python, Go, C#, and Java. + +{{< chooser language "typescript,python,go,csharp,java" >}} +{{% choosable language typescript %}} +```typescript +import * as pulumi from "@pulumi/pulumi"; +import * as aws from "@pulumi/aws"; + +const s3OriginId = "myS3Origin"; + +const s3Distribution = new aws.cloudfront.Distribution("s3_distribution", { + origins: [{ + domainName: b.bucketRegionalDomainName, + originAccessControlId: _default.id, + originId: s3OriginId, + }], + enabled: true, + isIpv6Enabled: true, + comment: "Some comment", + defaultRootObject: "index.html", + defaultCacheBehavior: { + allowedMethods: [ + "DELETE", "GET", "HEAD", "OPTIONS", + "PATCH", "POST", "PUT" + ], + cachedMethods: ["GET", "HEAD"], + targetOriginId: s3OriginId, + forwardedValues: { + queryString: false, + cookies: { forward: "none" }, + }, + viewerProtocolPolicy: "allow-all", + minTtl: 0, + defaultTtl: 3600, + maxTtl: 86400, + }, +}); +``` +{{% /choosable %}} + +{{% choosable language python %}} +```python +import pulumi +import pulumi_aws as aws + +s3_origin_id = "myS3Origin" + +s3_distribution = aws.cloudfront.Distribution("s3_distribution", + origins=[aws.cloudfront.DistributionOriginArgs( + domain_name=b.bucket_regional_domain_name, + origin_access_control_id=default.id, + origin_id=s3_origin_id, + )], + enabled=True, + is_ipv6_enabled=True, + comment="Some comment", + default_root_object="index.html", + default_cache_behavior=aws.cloudfront.DistributionDefaultCacheBehaviorArgs( + allowed_methods=[ + "DELETE", "GET", "HEAD", "OPTIONS", + "PATCH", "POST", "PUT" + ], + cached_methods=["GET", "HEAD"], + target_origin_id=s3_origin_id, + forwarded_values=aws.cloudfront.DistributionDefaultCacheBehaviorForwardedValuesArgs( + query_string=False, + cookies=aws.cloudfront.DistributionDefaultCacheBehaviorForwardedValuesCookiesArgs( + forward="none", + ), + ), + viewer_protocol_policy="allow-all", + min_ttl=0, + default_ttl=3600, + max_ttl=86400, + )) +``` +{{% /choosable %}} + +{{% choosable language go %}} +```go +package main + +import ( + "github.com/pulumi/pulumi-aws/sdk/v7/go/aws/cloudfront" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + s3OriginId := "myS3Origin" + + _, err := cloudfront.NewDistribution(ctx, "s3_distribution", &cloudfront.DistributionArgs{ + Origins: cloudfront.DistributionOriginArray{ + &cloudfront.DistributionOriginArgs{ + DomainName: b.BucketRegionalDomainName, + OriginAccessControlId: _default.Id, + OriginId: pulumi.String(s3OriginId), + }, + }, + Enabled: pulumi.Bool(true), + IsIpv6Enabled: pulumi.Bool(true), + Comment: pulumi.String("Some comment"), + DefaultRootObject: pulumi.String("index.html"), + DefaultCacheBehavior: &cloudfront.DistributionDefaultCacheBehaviorArgs{ + AllowedMethods: pulumi.StringArray{ + pulumi.String("DELETE"), pulumi.String("GET"), pulumi.String("HEAD"), + pulumi.String("OPTIONS"), pulumi.String("PATCH"), pulumi.String("POST"), + pulumi.String("PUT"), + }, + CachedMethods: pulumi.StringArray{ + pulumi.String("GET"), + pulumi.String("HEAD"), + }, + TargetOriginId: pulumi.String(s3OriginId), + ForwardedValues: &cloudfront.DistributionDefaultCacheBehaviorForwardedValuesArgs{ + QueryString: pulumi.Bool(false), + Cookies: &cloudfront.DistributionDefaultCacheBehaviorForwardedValuesCookiesArgs{ + Forward: pulumi.String("none"), + }, + }, + ViewerProtocolPolicy: pulumi.String("allow-all"), + MinTtl: pulumi.Int(0), + DefaultTtl: pulumi.Int(3600), + MaxTtl: pulumi.Int(86400), + }, + }) + if err != nil { + return err + } + return nil + }) +} +``` +{{% /choosable %}} + +{{% choosable language csharp %}} +```csharp +using System.Collections.Generic; +using Pulumi; +using Aws = Pulumi.Aws; + +return await Deployment.RunAsync(() => +{ + var s3OriginId = "myS3Origin"; + + var s3Distribution = new Aws.CloudFront.Distribution("s3_distribution", new() + { + Origins = new[] + { + new Aws.CloudFront.Inputs.DistributionOriginArgs + { + DomainName = b.BucketRegionalDomainName, + OriginAccessControlId = @default.Id, + OriginId = s3OriginId, + }, + }, + Enabled = true, + IsIpv6Enabled = true, + Comment = "Some comment", + DefaultRootObject = "index.html", + DefaultCacheBehavior = new Aws.CloudFront.Inputs.DistributionDefaultCacheBehaviorArgs + { + AllowedMethods = new[] + { + "DELETE", "GET", "HEAD", "OPTIONS", + "PATCH", "POST", "PUT", + }, + CachedMethods = new[] + { + "GET", "HEAD", + }, + TargetOriginId = s3OriginId, + ForwardedValues = new Aws.CloudFront.Inputs.DistributionDefaultCacheBehaviorForwardedValuesArgs + { + QueryString = false, + Cookies = new Aws.CloudFront.Inputs.DistributionDefaultCacheBehaviorForwardedValuesCookiesArgs + { + Forward = "none", + }, + }, + ViewerProtocolPolicy = "allow-all", + MinTtl = 0, + DefaultTtl = 3600, + MaxTtl = 86400, + }, + }); +}); +``` +{{% /choosable %}} + +{{% choosable language java %}} +```java +package generated_program; + +import com.pulumi.Context; +import com.pulumi.Pulumi; +import com.pulumi.aws.cloudfront.Distribution; +import com.pulumi.aws.cloudfront.DistributionArgs; +import com.pulumi.aws.cloudfront.inputs.DistributionOriginArgs; +import com.pulumi.aws.cloudfront.inputs.DistributionDefaultCacheBehaviorArgs; +import com.pulumi.aws.cloudfront.inputs.DistributionDefaultCacheBehaviorForwardedValuesArgs; +import com.pulumi.aws.cloudfront.inputs.DistributionDefaultCacheBehaviorForwardedValuesCookiesArgs; + +public class App { + public static void main(String[] args) { + Pulumi.run(App::stack); + } + + public static void stack(Context ctx) { + var s3OriginId = "myS3Origin"; + + var s3Distribution = new Distribution("s3_distribution", DistributionArgs.builder() + .origins(DistributionOriginArgs.builder() + .domainName(b.bucketRegionalDomainName()) + .originAccessControlId(default_.id()) + .originId(s3OriginId) + .build()) + .enabled(true) + .isIpv6Enabled(true) + .comment("Some comment") + .defaultRootObject("index.html") + .defaultCacheBehavior(DistributionDefaultCacheBehaviorArgs.builder() + .allowedMethods( + "DELETE", "GET", "HEAD", "OPTIONS", + "PATCH", "POST", "PUT") + .cachedMethods("GET", "HEAD") + .targetOriginId(s3OriginId) + .forwardedValues(DistributionDefaultCacheBehaviorForwardedValuesArgs.builder() + .queryString(false) + .cookies(DistributionDefaultCacheBehaviorForwardedValuesCookiesArgs.builder() + .forward("none") + .build()) + .build()) + .viewerProtocolPolicy("allow-all") + .minTtl(0) + .defaultTtl(3600) + .maxTtl(86400) + .build()) + .build()); + } +} +``` +{{% /choosable %}} +{{< /chooser >}} + +## Key configuration details + +**Origins configuration**: The `origins` array specifies your S3 bucket as the content source. Use `bucketRegionalDomainName` instead of `bucketDomainName` to avoid redirect issues in certain regions. + +**Origin Access Control**: The `originAccessControlId` secures your S3 bucket by allowing only CloudFront to access it. Create an Origin Access Control resource separately and reference its ID here. This replaces the legacy Origin Access Identity. + +**Cache behavior**: The `defaultCacheBehavior` defines how CloudFront caches and serves content. The example caches GET and HEAD requests for 1 hour (3600 seconds) by default. + +**TTL settings**: Time-to-Live values control cache duration. `minTtl` (0), `defaultTtl` (3600), and `maxTtl` (86400) set the minimum, default, and maximum cache durations in seconds. + +**IPv6 support**: Setting `isIpv6Enabled: true` allows CloudFront to serve content over IPv6, improving accessibility for IPv6-only clients. + +**Viewer protocol policy**: The `allow-all` policy permits both HTTP and HTTPS. For production, consider using `redirect-to-https` or `https-only` for better security. + +**Default root object**: Specifies the file (e.g., `index.html`) to serve when users request the root URL of your distribution. + +## Frequently asked questions + +**What is the difference between Origin Access Control and Origin Access Identity?** +Origin Access Control (OAC) is the newer, recommended method to secure S3 origins. OAC supports all S3 buckets, SSE-KMS encryption, and more AWS Regions compared to the legacy Origin Access Identity (OAI). AWS recommends migrating from OAI to OAC for new distributions. + +**How long does it take for CloudFront to deploy?** +CloudFront distributions typically take 15-20 minutes to fully deploy and propagate to all edge locations worldwide. During this time, the distribution status will show as 'InProgress'. You can access your content once the status changes to 'Deployed'. + +**How do I update content that's already cached?** +You can create a CloudFront invalidation to immediately remove content from edge caches. Use the aws.cloudfront.Invalidation resource in Pulumi. Note that the first 1,000 invalidation paths per month are free, with $0.005 per path after that. + +**Can I use my custom domain with CloudFront?** +Yes, you can add custom domain names (CNAMEs) to your CloudFront distribution. You'll need to create an ACM certificate in us-east-1 region for your domain and configure DNS records to point to the CloudFront distribution domain name. + +**How much does CloudFront cost compared to serving directly from S3?** +CloudFront costs approximately $0.085/GB for the first 10TB in the US (cheaper for higher volumes), compared to S3's $0.09/GB for data transfer out to the internet. CloudFront is usually cost-effective for frequently accessed content due to caching, and provides better performance globally. diff --git a/layouts/guides/list.html b/layouts/guides/list.html new file mode 100644 index 000000000000..dbf25e195d56 --- /dev/null +++ b/layouts/guides/list.html @@ -0,0 +1,87 @@ +{{ define "hero" }} +
+
+

{{ .Title }}

+

{{ .Params.meta_desc }}

+ +
+
+{{ end }} + +{{ define "main" }} +
+
+
+ {{ .Content }} +
+ + {{ if .Pages }} + {{ $categories := dict }} + {{ range .Pages }} + {{ $cat := .Params.category | default "General" }} + {{ $list := index $categories $cat | default slice }} + {{ $categories = merge $categories (dict $cat ($list | append .)) }} + {{ end }} + + {{ range $category, $pages := $categories }} +
+

+ {{ if eq $category "Compute" }} + + {{ else if eq $category "Database" }} + + {{ else if eq $category "Storage" }} + + {{ else if eq $category "Networking" }} + + {{ else }} + + {{ end }} + {{ $category }} +

+ + +
+ {{ end }} + {{ end }} + +
+

Ready to get started?

+

+ Deploy your infrastructure in minutes using Pulumi's modern infrastructure as code platform. +

+ + Start your journey + +
+
+
+{{ end }} \ No newline at end of file diff --git a/layouts/guides/single.html b/layouts/guides/single.html new file mode 100644 index 000000000000..4e584a1316d8 --- /dev/null +++ b/layouts/guides/single.html @@ -0,0 +1,150 @@ +{{ define "hero" }} +
+
+ + + + +
+

+ {{ .Title }} +

+

+ {{ .Params.meta_desc }} +

+ + + {{ if .Params.tags }} +
+ {{ range .Params.tags }} + + {{ . }} + + {{ end }} +
+ {{ end }} +
+
+
+{{ end }} + +{{ define "main" }} +
+
+
+ + + + + +
+
+
+ + +{{ end }} \ No newline at end of file diff --git a/layouts/partials/schema/collectors/guides-entity.html b/layouts/partials/schema/collectors/guides-entity.html new file mode 100644 index 000000000000..37f671a51730 --- /dev/null +++ b/layouts/partials/schema/collectors/guides-entity.html @@ -0,0 +1,54 @@ +{{/* Returns HowTo entity with FAQ questions for guide pages */}} + +{{/* Initialize HowTo schema for guides - these are instructional content */}} +{{ $schema := dict + "@type" "HowTo" + "@id" "#main-content" + "url" .Permalink + "name" (or .Params.title_tag .Title) + "inLanguage" "en-US" +}} + +{{/* Add description if available */}} +{{ with (or .Params.meta_desc .Summary) }} + {{ $schema = merge $schema (dict "description" .) }} +{{ end }} + +{{/* Build FAQ questions from front matter */}} +{{ if .Params.faq }} + {{ $questions := slice }} + + {{ range .Params.faq }} + {{ $qa := dict + "@type" "Question" + "name" .question + "acceptedAnswer" (dict + "@type" "Answer" + "text" (.answer | plainify) + ) + }} + {{ $questions = $questions | append $qa }} + {{ end }} + + {{/* Add questions as mainEntity if we have any */}} + {{ if $questions }} + {{ $schema = merge $schema (dict "mainEntity" $questions) }} + {{ end }} +{{ end }} + +{{/* Add publisher reference */}} +{{ $schema = merge $schema (dict + "publisher" (dict "@id" "https://www.pulumi.com/#organization") +) }} + +{{/* Add breadcrumb reference */}} +{{ $schema = merge $schema (dict + "breadcrumb" (dict "@id" (printf "%s#breadcrumb" .Permalink)) +) }} + +{{/* Add keywords from tags */}} +{{ with .Params.tags }} + {{ $schema = merge $schema (dict "keywords" (delimit . ", ")) }} +{{ end }} + +{{ return $schema }} diff --git a/layouts/partials/schema/collectors/main-entity.html b/layouts/partials/schema/collectors/main-entity.html index 3e26ebdf5869..72cd750eee26 100644 --- a/layouts/partials/schema/collectors/main-entity.html +++ b/layouts/partials/schema/collectors/main-entity.html @@ -69,6 +69,10 @@ {{/* Case studies get Article schema */}} {{ $entity = partial "schema/collectors/article-entity.html" . }} + {{ else if eq .Section "guides" }} + {{/* Guide pages get HowTo schema with FAQ questions */}} + {{ $entity = partial "schema/collectors/guides-entity.html" . }} + {{ else if .IsHome }} {{/* Homepage doesn't have a main content entity, handled separately in graph-builder */}} {{ $entity = dict }} diff --git a/layouts/taxonomy/tag.html b/layouts/taxonomy/tag.html index 819ab43a094c..9a23c565c8cd 100644 --- a/layouts/taxonomy/tag.html +++ b/layouts/taxonomy/tag.html @@ -1,13 +1,53 @@ {{ define "main" }} -
-
-
- {{ partial "blog/series-list-sidebar.html" . }} + {{ $guidePosts := where .Data.Pages "Section" "guides" }} + {{ $blogPosts := where .Data.Pages "Section" "blog" }} + + {{ if gt (len $guidePosts) 0 }} + {{/* Guide Tag Page */}} +
+
+

+ {{ .Data.Term }} +

+

+ {{ len $guidePosts }} guide{{ if ne (len $guidePosts) 1 }}s{{ end }} found +

+
+
+ + + {{ else if gt (len $blogPosts) 0 }} + {{/* Blog Tag Page - Original Code */}} +